Skip to content

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.

Terminal window
./bin/tellstone --enable-audit --audit-log-path /var/log/tellstone --audit-events all
FlagEnv varDefaultMeaning
--enable-auditTSD_ENABLE_AUDITfalseEnable structured audit logging
--audit-log-pathTSD_AUDIT_LOG_PATHstdoutAudit destination: a directory of rotating files, or stdout
--audit-eventsTSD_AUDIT_EVENTSauth,aclComma-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.

EventFires onFields
connectTCP connection opened, either protocolremote_addr, protocol, shard_id
disconnectConnection closedremote_addr, protocol
auth_successSuccessful AUTHuser, remote_addr, protocol
auth_failureRejected AUTH — wrong password, unknown user, malformed requestuser, reason, remote_addr, protocol
acl_denyCommand blocked by RBAC (-NOPERM)user, command, key, remote_addr, protocol
commandEvery dispatched data/admin commandcommand, key, user, remote_addr, protocol

--audit-events takes a comma-separated list of tokens. Unknown tokens are silently ignored, so the flag stays forward-compatible with newer event types.

TokenRecords
authauth_success + auth_failure
aclacl_deny
connectconnect
disconnectdisconnect
commandcommand
allevery 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.

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"}

--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 0600 permissions.
  • 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.

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.

  • Zero cost when disabled. Without --enable-audit the engine is a no-op whose Record() 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() and Close() 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.

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]
FieldSizeValues
Magic4 bytesTSDA — distinguishes headed files from legacy headerless ones
Version1 byteFormat version (1) — bumped when the layout changes
Key mode1 byte0 = Simple (records sealed directly with the pass-through key), 1 = Envelope (records sealed with a per-instance DEK wrapped by the KEK)
Fingerprint16 bytesBLAKE3 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.

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.

Terminal window
tellstone audit decrypt /var/log/tellstone/1786912322586083246_3f54f64d_233946_tsd.log \
--encryption-key "$(cat /etc/tellstone/key | base64)"
FlagEnv varMeaning
--encryption-keyTSD_ENCRYPTION_KEYBase64-encoded 32-byte key used to decrypt the records
--encryption-key-fileTSD_ENCRYPTION_KEY_FILEPath to a file holding the raw 32-byte key; mutually exclusive with --encryption-key
--outputWrite decrypted output to this file instead of stdout

The two key sources are mutually exclusive — setting both is rejected.

The decrypt tool resolves the decryption engine by examining the file header:

  1. KeyModeEnvelope, fingerprint matches supplied key — records were sealed directly with the operator’s key (non-envelope mode where NewLogEngine still writes KeyModeEnvelope). No DEK unwrapping needed.
  2. KeyModeEnvelope, fingerprint differs — the header carries the DEK fingerprint; the operator supplied the KEK. The tool loads audit.env from the file’s parent directory, verifies the KEK fingerprint, unwraps the DEK, and builds a fresh engine from it.
  3. KeyModeSimple — records are plaintext; the fingerprint is validated against the supplied key but no decryption is performed.
  4. 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:

Terminal window
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.

  • Missing key. If no --encryption-key or --encryption-key-file is 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.
Terminal window
# Decrypt to stdout
tellstone audit decrypt 233946_tsd.log --encryption-key "$KEY"
# Decrypt to file
tellstone audit decrypt 233946_tsd.log --encryption-key "$KEY" --output decrypted.jsonl
# Decrypt with key from file
tellstone audit decrypt 233946_tsd.log --encryption-key-file /etc/tellstone/key
# Pipe through jq
tellstone 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:

Terminal window
# Key from env — no flag needed
tellstone audit decrypt 233946_tsd.log --output decrypted.jsonl
# Key file from env
tellstone audit decrypt 233946_tsd.log | jq '.event'