Outbound shipping

GET    /api/bng/audit/shipping/destinations            admin_only
POST   /api/bng/audit/shipping/destinations            admin_only
PATCH  /api/bng/audit/shipping/destinations/{name}     admin_only
DELETE /api/bng/audit/shipping/destinations/{name}     admin_only
POST   /api/bng/audit/shipping/destinations/{name}/test admin_only
GET    /api/bng/audit/shipping/status                  admin_only

The one place where the operational plane pushes rather than waits. A background
daemon tails the audit log from a persisted byte offset and fans each new line
into one on-disk spool per destination, each with its own delivery thread and
retry budget
— so a slow SIEM endpoint cannot stall an S3 archive.

event_log.jsonl ──tail──▶ shipper daemon
                              └─▶ spool/<destination>/queue.jsonl ──drain──▶ send(batch)

Three destination types are supported.

syslog speaks RFC 5424 over TCP, UDP, or TLS, using octet-counting framing
for the stream transports. Facility, severity, application name, TLS
verification, and a CA file are configurable.

http posts batched JSON to any endpoint — Splunk HEC, Datadog, Loki, a
Vector pipeline. A 2xx is success. A 5xx, a timeout, a 408, or a 429 is
retried. Other 4xx responses are treated as success and not retried: a
malformed batch that a server will reject forever is a poison pill, and retrying
it indefinitely would block every subsequent entry behind it.

s3 batches on an interval, defaulting to five minutes. When the batch window
has not elapsed the destination reports a distinct "not yet" outcome that the
daemon treats as skip and keep spooled rather than as a failure — a healthy S3
destination therefore does not accumulate a phantom failure count between
batches.

Retry is three attempts with exponential backoff. On persistent failure entries
remain spooled up to a hundred-megabyte cap, beyond which the oldest entries
drop first
. Log rotation is detected by offset comparison, and the newer
archives are shipped before resuming the live tail, so the pre-rotation tail is
never lost.

Destination secrets — bearer tokens and S3 secret keys — are encrypted at rest
under a box-local key and are never returned by the API. The list endpoint
redacts them and reports only a has_secret boolean. A PATCH that omits a
secret field preserves the stored one, so a partial update cannot accidentally
blank a credential.

The type of an existing destination is immutable; change it by deleting and
recreating.

The test endpoint sends one synthetic entry. Importantly, it builds the
destination from the stored configuration only
and ignores the request body —
this prevents the endpoint from being used to make the BNG issue arbitrary
outbound requests.

GET /shipping/status reports per-destination spool size, last success and last
attempt timestamps, consecutive failure count, batches sent, entries shipped, and
an up boolean. On a box with no daemon it returns a zero skeleton rather than an
error. These same values are published as Prometheus gauges, so shipping health
can be alarmed on without polling this endpoint.

Configuration Snapshots

The snapshot subsystem gives a BNG the configuration-archive semantics an
operator expects from carrier equipment: capture the whole box, verify it,
compare it against the running state, and put it back.