the facts, for looking up
Reference
Every command, config key, environment variable, and metric.
Commands
corvo <command> [options]
With no command, corvo prints usage and exits nonzero. corvo version prints the version and commit. corvo help prints usage.
Common options
Every command that loads config accepts these. They override the YAML file and the CORVO_* environment.
corvo run
corvo run
Plugin mode. Reads audit lines from stdin, regroups them, writes the queue, and POSTs to the endpoint. This is what auditd calls, via /etc/audit/plugins.d/corvo.conf. It is the default command in the plugin file's args.
Logs go to stderr and, when available, to syslog. Never to stdout, which belongs to the auditd plugin protocol.
Responds to two signals:
SIGHUP: reload config. auditd sends this when it reloads plugins. See what reloads.
SIGTERM or SIGINT: clean shutdown. Stops reading stdin, drains the reassembler, tries to empty the queue within shutdown.drain_timeout, syncs. Whatever does not fit stays on disk.
corvo tail
corvo tail [-json] [-log-level LEVEL]
Reads audit events over the multicast netlink socket and prints them. Coexists with auditd. Lossy: the kernel drops events on this socket under load with no backpressure and no warning. Debugging only. Needs CAP_AUDIT_READ or root.
-json prints the full event object instead of a one-line summary.
tail ignores corvo.yaml's filter, on purpose. It is how you find out what record types your machine generates before writing the filter.
corvo replay
corvo replay <file>
corvo replay - # read from stdin
Reads an audit.log from disk and sends it through the same pipeline, queue, and retry policy as plugin mode. Uses for this:
- Backfill after a gap in collection.
- Resend a batch from
queue/dead/.
- Test a new endpoint with real data before pointing the plugin at it.
corvo check
corvo check [--offline]
Validates config and environment before production. Exits nonzero on any failure, so it works in ExecStartPre and in CI. Checks:
- The YAML parses and validates.
- The token file exists, is readable, and is not world-readable.
- The current session's
auid and which rule it matches.
- Whether auditd is in
ENRICHED mode.
- Which SELinux context
check itself runs in.
- TLS file paths.
- Queue depth and the contents of
dead/.
- Endpoint DNS, TLS, auth, and the body contract, by sending one synthetic event. Skipped with
--offline.
Example:
$ corvo check
ok config https://logs.example.com/v1/audit
ok token /etc/corvo/token (0600)
ok queue /var/lib/corvo/queue: 0 pending event(s), 0 B on disk, 1 segment(s)
ok dead-letter empty
ok dns logs.example.com -> 10.0.0.5
ok endpoint HTTP 200 in 42ms
All good.
corvo reload
corvo reload
Sends SIGHUP to every running corvo process and reports how many it reached. Same effect as pkill -HUP -x corvo, without needing pkill installed. systemctl reload auditd does not work, the auditd unit refuses it.
Configuration keys
corvo builds its config from three layers, each overriding the last:
/etc/corvo/corvo.yaml (or --config)
CORVO_* environment variables (reference)
- command-line flags
A missing default config file is tolerated and corvo runs on defaults plus environment. An explicit --config pointing nowhere is an error. An unknown key in the YAML, or an unknown CORVO_* variable, stops the process at startup. A misspelled CORVO_ENDPIONT_URL has to fail loudly, not quietly keep sending to the old endpoint.
Durations are strings like 10s, 5m, 1h.
endpoint
endpoint.tls
With cert_file and key_file set, corvo does not send Authorization.
batch
Sending triggers on the first limit to overflow.
retry
queue
queue.on_full
queue.fsync
filter
Picks which events ship, by record_type. See Pick which events ship.
A record type that auparse does not recognize stops the process at startup. An empty list item does too.
shutdown
metrics
log
What reloads on SIGHUP
auditd sends SIGHUP when it reloads its plugins. corvo reload sends it directly. An invalid new config is rejected whole and the previous one stays, because a plugin that dies over a typo is worse than one with a stale config.
For the three that do not reload, corvo logs a warning that the new value was not applied, instead of letting you believe it was. The queue is excluded because swapping its directory live would abandon events already written in the old one. Metrics and shutdown are excluded because the server and the deadline are assembled once, at startup.
Environment variables
Every CORVO_* variable maps to one config key. They override the YAML file and are overridden by command-line flags. A CORVO_* variable that matches no key stops the process at startup, with the list of valid names in the error.
The name is the key's path, uppercased, joined by underscores. List values are comma-separated (syscall,user_login); an empty string clears the list, which lets the environment turn off a filter set in the YAML.
endpoint.headers has no variable. Arbitrary header names have no fixed mapping, so headers come only from the YAML.
Setting them for the plugin
auditd's plugin file (corvo.conf) has a two-argument limit and no way to pass environment. To set a variable for the plugin, use a systemd drop-in on the auditd unit:
# /etc/systemd/system/auditd.service.d/corvo.conf
[Service]
Environment=CORVO_LOG_LEVEL=debug
Then systemctl daemon-reload and restart auditd. For most settings the YAML file is the better place.
Metrics
Off by default. With metrics.enabled: true, corvo serves GET /metrics in Prometheus text format and GET /healthz on metrics.listen (127.0.0.1:9909 by default). Enabling needs an auditd restart, not a SIGHUP. See Turn on metrics.
/healthz returns ok while the process is alive and serving. It does not probe the endpoint. corvo is healthy with the endpoint down, that is the queue's job.
Pipeline
Queue
Sink
Build
What to alert on
corvo_events_lost_total > 0: kernel-side loss, the audit buffer is too small.
rate(corvo_queue_depth_bytes) positive for several minutes: the endpoint is slow or down. This fires before anything is lost.
corvo_events_dropped_total > 0: the queue filled and drop_oldest ran.
corvo_events_dead_lettered_total > 0: a batch was permanently rejected, inspect dead/.
Audit rules and the plugin file
The plugin file
Installed at /etc/audit/plugins.d/corvo.conf (0640). auditd reads it on start and on reload.
active = yes
direction = out
path = /usr/bin/corvo
type = always
args = run
format = string
path points at the binary. A source install rewrites this to $PREFIX/bin/corvo.
args = run is the whole command line. man audit-plugins warns of a two-argument limit, so the config path comes from the default or from CORVO_*, not from here.
format = string: auditd converts each event to text before writing it to the pipe, which is what corvo's parser expects. binary would hand over the raw netlink record and add work for nothing.
On AlmaLinux 9 and other audit 3.x systems, audispd was merged into auditd and plugins live at this path. RHEL 7 and 8 tutorials show /etc/audisp/plugins.d/, which is wrong here.
The rules
Installed at /etc/audit/rules.d/corvo.rules, loaded with augenrules --load.
-a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=unset -k cmds
-a always,exit -F arch=b32 -S execve -F auid>=1000 -F auid!=unset -k cmds
-a always,exit -F arch=b64 -S execve -F auid=0 -k cmds-root
-a always,exit -F arch=b32 -S execve -F auid=0 -k cmds-root
#-a always,exit -F arch=b64 -S execve -k cmds-all
The -F filters run inside the kernel, before the record exists. That is why they matter for cost: they cut at the source instead of generating a record and discarding it.
System daemons are not swept in. systemd and what it starts have auid=unset, which does not match auid=0.
The auid=unset case
What sets auid is pam_loginuid, present in AlmaLinux 9's /etc/pam.d/login and /etc/pam.d/sshd. An interactive session that shows up with auid=unset (hypervisor console, agetty with no PAM, docker exec) is not fixed by a rule. With no auid the event holds nobody accountable. Fix it in PAM.
To check the current session:
corvo check # shows the session's auid and which rule matches
cat /proc/self/loginuid # the raw value
auditd.conf settings that matter
This page orients you. The full specification for whoever writes the receiving server is design/api-contract.md, kept honest by a golden file that a test compares byte for byte on every build.
corvo POSTs a JSON array. Each element has four fields:
{
"v": 1,
"id": "srv-web-01-2841-1756938610123000000",
"host": "srv-web-01",
"event": { }
}
This is go-libaudit's aucoalesce.Event, serialized as-is. Fields you can count on: @timestamp, sequence, category, record_type, session, summary, user, ecs. Fields that appear only when populated: result, tags, file, source, destination, network, data, paths.
summary, user, process, and ecs are Go structs, so they show up in the JSON even when empty ({}). Test process.exe for being populated, not "process" in event.
A full worked example, id -u run as root, is in testdata/execve-completo.json.
record_type is the record that originated the event. category is the family go-libaudit derives from it. The full catalogue, with what each type carries, is in design/api-contract.md.
Two rules to know up front:
- Some types never arrive alone.
execve, path, cwd, proctitle, sockaddr, bprm_fcaps are fragments. corvo merges them into the main record, where they become process.args, paths, process.cwd, process.title, source, destination. Seeing one as an event's record_type means the rest never arrived.
- In a composite event the type comes from the first record, not from
SYSCALL. An execve that trips a SELinux denial arrives as avc, with the syscall's paths and data attached. So filtering by syscall alone misses those commands.
process.title is cut at 128 bytes by the kernel, mid-word, no marker. process.args carries the full argv. Index and search on args, use title for display only.
@timestamp has variable fractional precision, zero to nine decimal places within one batch. Use a real RFC 3339 parser, not a fixed format string.
- Almost everything is a string, including
pid, uid, inode, mode, port. audit delivers those in mixed formats and the library does not risk converting them. Only v and sequence are integers.
data and paths are open maps whose keys vary by syscall. On a store with strict mapping, use flattened or dynamic: false or a jsonb column.
- Delivery is at-least-once. A repeated
id must be idempotent on the server, this is a hard requirement of the contract.