corvo.rda.run
git.rda.run/corvo/corvo
Install
how it works

The path of one event, stage by stage

The whole path first, then the five stages between the kernel and PostgreSQL. Each stage exists because of a known failure mode, and each one has a declared cost. What the pair does not do is at the bottom, in the same words we would use after an incident.

on this page 01 reassembly
02 queue on disk
03 backoff and Retry-After
04 dead-letter
05 idempotency
06 honest limits
the whole path

From the kernel to the web UI, with no gap

contract v1 · at-least-once
on the audited machine
kernel Linux 5 records per command
→
auditd owner of the rules and the buffer
→
corvo (plugin)
reassembles · resolves uid→name
→
queue on disk 16 MiB JSONL segments
https → POST · gzip
on the server
ninho
2xx acknowledges · 503 when it is down
→
PostgreSQL monthly partitions
→
web UI embedded in the binary

Diagram description: the kernel hands the audit records to auditd, which writes each line to corvo stdin. corvo reassembles the fragments of one command, resolves the numeric identifiers into names and writes the event to the queue on disk. The queue delivers over HTTPS to ninho, which acknowledges with 2xx, persists to PostgreSQL and serves the web UI. Only after that acknowledgement does the offset move.

01
reassembly

Five records, one event

The reassembler groups the lines by sequence number, resolves uid, gid and auid into names with a 60 s TTL, and closes the event when the final record arrives. A 500 ms ticker closes whatever stayed incomplete, so that no event is stuck in there forever.

internal flow
stdin (auditd)  →  line parser (1 MiB buffer)
                →  reassembler keyed by sequence
                     ├─ final record arrived    →  close the event
                     └─ 500 ms ticker fired     →  close what it has
                →  resolve uid→name (cache, 60 s TTL)
                →  queue on disk
The line buffer is not decorative
The 64 KiB default in bufio.Scanner fails silently when it overflows, and what overflows first is exactly the long EXECVE, the most interesting record in an audit. corvo reads with 1 MiB.
02
queue on disk

A directory, not a database

16 MiB JSONL segments plus one offset file. It survives a process restart and a machine reboot, and repairs itself on open if a crash cut a line in half. During an incident you read the queue with the tools you already have.

queue on disk48.0 MiB / 512.0 MiB
3 segment(s) · rotates at 16 MiB
/var/lib/corvo/queue
$ ls /var/lib/corvo/queue
dead/  offset  seg-00000040.jsonl  seg-00000041.jsonl  seg-00000042.jsonl

$ tail -1 seg-00000042.jsonl | jq -r '.event.process.args | join(" ")'
sudo systemctl restart auditd

$ cat offset
seg-00000042.jsonl 11534336
drop_oldest · default

When the queue fills, it deletes the oldest segment. It loses the past and preserves the present.

block

It stops consuming stdin. No event is dropped by corvo, but any eventual loss becomes the kernel's loss and shows up in the metric. Strict compliance is worth that trade, and the cost is real.

03
backoff

When the endpoint goes down

Exponential backoff with jitter, with no attempt limit by default, because giving up means losing audit. The jitter keeps a whole fleet from hitting the endpoint together when it comes back.

1 s → 2 s → 4 s → 8 s → … → 5 min (ceiling) ±30 % jitter on every wait
responsewhat corvo doeswhy
2xxacknowledges and advances the offsetthe only way a batch leaves the queue
429 · 503retry honoring Retry-Afterinfrastructure busy or down, the data is fine
401 · 403retry for 15 min, then dead-letterusually an expired token, not bad data
400 · 404 · 413 · 422immediate dead-letterdoes not improve on repeat and would freeze the queue
no responseinfinite retry with jitterthe network comes back, and giving up means losing audit
04
dead-letter

What retry cannot fix

A malformed batch in infinite retry would freeze the queue forever. It moves to queue/dead/ with the server response beside it, and comes back once the cause is fixed.

resending a dead-letter batch
$ ls /var/lib/corvo/queue/dead
batch-20260904T123010Z.jsonl  batch-20260904T123010Z.response.txt

$ cat batch-20260904T123010Z.response.txt
HTTP 422 · field "event.sequence" missing in 3 of 128 events

$ corvo replay /var/lib/corvo/queue/dead/batch-20260904T123010Z.jsonl
401 and 403 are the exception
Those two retry for 15 minutes before anything else, because they are usually an expired token, not bad data. Sending valid audit to dead-letter over a stale token is losing events by mistake.
05
idempotency

At-least-once is the contract

The offset can lag behind, and then the same batch is sent twice. Every event carries a stable id, and the receiver has to treat a repeated id as idempotent. The contract requires it.

event id
srv-web-01-2841-1756938610123000000
└── host ──┘ └seq┘ └── timestamp in ns ──┘
deliveryat-least-once
keyid (host + sequence + ts)
in ninhoON CONFLICT (id) DO NOTHING
resendoffset behind, never ahead
06
honest limits

What corvo does not do

ambiguous silence

corvo only POSTs when it has an event to deliver. On an idle machine the silence is legitimate, and the server cannot tell legitimate silence from a killed agent.

disk volume

The queue is a second copy on disk, so the written volume roughly doubles. It keeps uncompressed JSONL even with gzip: true. That tradeoff is deliberate, and the disk cost is real.

tls

corvo does not terminate TLS, and neither does ninho. The reverse proxy in front does it. Out of scope in ninho: user accounts, multi-tenancy, caching and an alerting system.

auditd → https · nothing gets lost corvo (Portuguese for raven) is the agent, ninho (nest) is the server. Both speak the same versioned contract.
docs how it works corvo (agent) ninho (server)
ninho docs tutorials how-to guides reference explanation
corvo docs tutorials how-to guides reference explanation
reference API contract install metrics and queue
code git.rda.run/corvo/corvo git.rda.run/corvo/ninho MIT license