Audit Logging
Tellstone can write a structured audit trail of security-relevant events —
connections, AUTH results, and RBAC denials — as one JSON object per line.
Every line carries "level": "AUDIT", so log aggregators can separate the
audit trail from operational INFO/WARN/ERROR output without custom parsing.
Audit logging is off by default and adds no overhead when disabled.
Enabling
Section titled “Enabling”./bin/tellstone --enable-audit --audit-log-path /var/log/tellstone --audit-events all| Flag | Env var | Default | Meaning |
|---|---|---|---|
--enable-audit | TSD_ENABLE_AUDIT | false | Enable structured audit logging |
--audit-log-path | TSD_AUDIT_LOG_PATH | stdout | Audit destination: a directory of rotating files, or stdout |
--audit-events | TSD_AUDIT_EVENTS | auth,acl | Comma-separated event types to record |
The audit trail is independent of the operational log level: a server running
at --log-level fatal still emits a full audit trail when --enable-audit is
set.
Event types
Section titled “Event types”| Event | Fires on | Fields |
|---|---|---|
connect | TCP connection opened, either protocol | remote_addr, protocol, shard_id |
disconnect | Connection closed | remote_addr, protocol |
auth_success | Successful AUTH | user, remote_addr, protocol |
auth_failure | Rejected AUTH — wrong password, unknown user, malformed request | user, reason, remote_addr, protocol |
acl_deny | Command blocked by RBAC (-NOPERM) | user, command, key, remote_addr, protocol |
command | Every dispatched data/admin command | command, key, user, remote_addr, protocol |
Filtering events
Section titled “Filtering events”--audit-events takes a comma-separated list of tokens. Unknown tokens are
silently ignored, so the flag stays forward-compatible with newer event types.
| Token | Records |
|---|---|
auth | auth_success + auth_failure |
acl | acl_deny |
connect | connect |
disconnect | disconnect |
command | command |
all | every event type |
An event type can also be named directly (e.g.
--audit-events auth_success,command). The default is auth,acl — the
security-relevant events compliance frameworks require — while
connect, disconnect, and command are high-volume and must be opted into
explicitly. command is the only event type with per-command dispatch
overhead.
Log format
Section titled “Log format”Each record is one JSON object with a time, level: "AUDIT", event, and
msg, plus the event’s fields:
{"event":"acl_deny","level":"AUDIT","msg":"command denied by rbac policy","protocol":"binary","remote_addr":"127.0.0.1:51642","command":"SET","key":"config:key","user":"reader","time":"2026-08-05T10:40:32.908569649+02:00"}Destination
Section titled “Destination”--audit-log-path is stdout (the default) or a directory. In directory mode
the server creates rotating files named
<unix-nanoseconds>_<8-hex-directory-hash>_<pid>_tsd.log:
- File names are generated, never supplied — the timestamp is nanosecond precision so two rotations in the same second cannot collide, and the directory hash separates instances sharing a directory.
- Files are created with
0600permissions. - Once a file reaches 50 MiB the writer closes it and switches to a fresh file in the same directory. Rotation never truncates or renames history, so a completed file is safe to ingest or inspect in one piece.
- If the directory cannot be opened, the server logs an error and falls back
to
stdout.
Encryption
Section titled “Encryption”When --enable-encryption is set, every record is sealed with the crypto
engine before it is flushed. A plaintext 4-byte
big-endian length prefix is prepended to each sealed blob, making every record
self-delimiting: a completed file decodes sequentially without knowing
plaintext lengths or reading between records.
In envelope mode the audit
log seals records with a DEK of its own — never the operator’s KEK, never a
shard’s DEK — wrapped by the KEK and stored as an audit.env envelope beside
the records. A stdout destination is never persisted, so records stay
plaintext.
Operational notes
Section titled “Operational notes”- Zero cost when disabled. Without
--enable-auditthe engine is a no-op whoseRecord()returns on a single boolean comparison — no writer, no encoder, no allocation — and the listeners call it unconditionally. - Zero-copy on the hot path. Command and key strings alias the gnet event buffer and are consumed synchronously by the encoder before the frame is discarded, so enabling audit events adds no allocation to the dispatch path.
- Concurrency-safe.
Record()andClose()are serialized by a mutex, so an event loop never races file rotation or shutdown. The engine is closed only after both listeners are stopped. - Fail-fast sink. A broken writer is reported by
Close(); subsequent records are dropped rather than masking the first failure.
Related
Section titled “Related”- Security — TLS, mTLS, and password AUTH
- At-Rest Encryption — the crypto engine that seals audit records
- RBAC — the role policy behind
acl_denyevents - Configuration — full flag reference
File header format
Section titled “File header format”Every audit file written by a current Tellstone release starts with a self-describing 22-byte header:
[TSDA:4][version:1][keyMode:1][fingerprint:16]| Field | Size | Values |
|---|---|---|
| Magic | 4 bytes | TSDA — distinguishes headed files from legacy headerless ones |
| Version | 1 byte | Format version (1) — bumped when the layout changes |
| Key mode | 1 byte | 0 = Simple (records sealed directly with the pass-through key), 1 = Envelope (records sealed with a per-instance DEK wrapped by the KEK) |
| Fingerprint | 16 bytes | BLAKE3 fingerprint of the key that sealed the file |
Files written before the header was introduced are legacy headerless — they contain only length-prefixed sealed blobs with no framing metadata. The decrypt tool handles both formats transparently.
Decrypting audit logs
Section titled “Decrypting audit logs”tellstone audit decrypt reads a single audit file, parses the header,
resolves the correct decryption key, and writes every decrypted JSON record
to stdout or a file. This is an offline CLI tool — it does not require a
running server.
tellstone audit decrypt /var/log/tellstone/1786912322586083246_3f54f64d_233946_tsd.log \ --encryption-key "$(cat /etc/tellstone/key | base64)"| Flag | Env var | Meaning |
|---|---|---|
--encryption-key | TSD_ENCRYPTION_KEY | Base64-encoded 32-byte key used to decrypt the records |
--encryption-key-file | TSD_ENCRYPTION_KEY_FILE | Path to a file holding the raw 32-byte key; mutually exclusive with --encryption-key |
--output | — | Write decrypted output to this file instead of stdout |
The two key sources are mutually exclusive — setting both is rejected.
Key resolution
Section titled “Key resolution”The decrypt tool resolves the decryption engine by examining the file header:
- KeyModeEnvelope, fingerprint matches supplied key — records were sealed
directly with the operator’s key (non-envelope mode where
NewLogEnginestill writesKeyModeEnvelope). No DEK unwrapping needed. - KeyModeEnvelope, fingerprint differs — the header carries the DEK
fingerprint; the operator supplied the KEK. The tool loads
audit.envfrom the file’s parent directory, verifies the KEK fingerprint, unwraps the DEK, and builds a fresh engine from it. - KeyModeSimple — records are plaintext; the fingerprint is validated against the supplied key but no decryption is performed.
- No header (legacy) — records are decrypted directly with the supplied key, assuming the same framing format.
A fingerprint mismatch at any step returns an error — partial output from a wrong key is never produced.
Pass - as the file argument to read from stdin:
cat audit.log | tellstone audit decrypt - --encryption-key "$KEY"Stdin input works for non-envelope files. Envelope-encrypted files require a
file path on disk because the tool must locate audit.env in the file’s
parent directory.
Error handling
Section titled “Error handling”- Missing key. If no
--encryption-keyor--encryption-key-fileis provided, the tool exits with an error. - Wrong key. A fingerprint mismatch returns an error before any records are decrypted.
- Truncated tail. A file whose last record was cut short by a process crash returns every complete record and stops cleanly.
- Malformed frame. A zero-length blob or a length prefix exceeding the remaining bytes is returned as an error.
Examples
Section titled “Examples”# Decrypt to stdouttellstone audit decrypt 233946_tsd.log --encryption-key "$KEY"
# Decrypt to filetellstone audit decrypt 233946_tsd.log --encryption-key "$KEY" --output decrypted.jsonl
# Decrypt with key from filetellstone audit decrypt 233946_tsd.log --encryption-key-file /etc/tellstone/key
# Pipe through jqtellstone audit decrypt 233946_tsd.log --encryption-key "$KEY" | jq '.event'If TSD_ENCRYPTION_KEY or TSD_ENCRYPTION_KEY_FILE is already set in your
environment (e.g. from the server startup config), the flags can be omitted:
# Key from env — no flag neededtellstone audit decrypt 233946_tsd.log --output decrypted.jsonl
# Key file from envtellstone audit decrypt 233946_tsd.log | jq '.event'