Audit logs are newline-delimited JSON records.
Each line contains:
event: audit event payloadsignature: Ed25519 signature over the encodedevent
The signing key is TKeeper's integrity key. event.integrityKeyVersion tells the verifier which integrity public key version to use.
Example line, formatted for readability:
{
"event": {
"id": "01J9Y3J7F8H4B8N8H5M6Y2K3Q1",
"peerId": 1,
"integrityKeyVersion": 3,
"timestamp": 1760000000000,
"event": "keeper.sign",
"auth": {
"subject": "service:payments-api"
},
"context": {
"sid": "sign-01J9Y3K2H7G9M5N4"
},
"request": {
"method": "POST",
"path": "/v2/keeper/sign",
"remoteAddress": "10.0.12.44"
},
"crypto": {
"algo": "ECDSA",
"kid": "payments-hot",
"generation": 2
},
"digest": {
"purpose": "audit",
"hmacKeyVersion": 4,
"bodyHash": {
"alg": "HMAC_SHA256",
"value64": "3Aq9i3sM1c03eK1d8eAH7Q=="
}
},
"outcome": {
"statusCode": 200
},
"approvers": [
"Jq7P3Zx7T0Jmj5..."
],
"policy": {
"decision": "ALLOW",
"matches": [
{
"id": "small-payment",
"effect": "ALLOW"
}
]
},
"imposters": [],
"dead": []
},
"signature": "MEUCIQD3n7uN..."
}
Some fields depend on the operation. approvers appears only for approved operations. policy appears when a Verdict authority was evaluated. imposters and dead appear when a threshold protocol reports bad or unavailable peers.
The policy object is Verdict's PolicyEvaluation: decision plus matched rules.
File Sink
keeper.audit {
enabled = true
timeout = 1000
file {
directory = "/var/lib/tkeeper/audit"
extension = "ndjson"
prefix = "audit"
max-file-size-bytes = 67108864
roll-every = 1d
max-files = 10
retention-days = 30
gzip = false
fsync = false
}
}
Socket Sink
keeper.audit {
enabled = true
socket {
host = "audit.local"
port = 443
tls {
protocols = ["TLSv1.3", "TLSv1.2"]
verify-hostname = true
trust {
mode = "system"
}
}
}
}
Socket sinks expect an ack. If the sink accepts the line but never acks it, the operation waits until ack-timeout and then treats that sink as failed.
Verification
Verify one signed audit line:
POST /v1/keeper/audit/verify
Body:
{
"event": {
"id": "01J9Y3J7F8H4B8N8H5M6Y2K3Q1",
"peerId": 1,
"integrityKeyVersion": 3,
"timestamp": 1760000000000,
"event": "keeper.sign",
"auth": null,
"context": null,
"request": null,
"crypto": null,
"digest": null,
"outcome": null,
"approvers": null,
"policy": null,
"imposters": null,
"dead": null
},
"signature": "..."
}
Response:
{ "valid": true }
Verify a batch:
POST /v1/keeper/audit/verify/batch
Body:
{
"logs": [
{
"event": {
"id": "01J9Y3J7F8H4B8N8H5M6Y2K3Q1",
"peerId": 1,
"integrityKeyVersion": 3,
"timestamp": 1760000000000,
"event": "keeper.sign",
"auth": null,
"context": null,
"request": null,
"crypto": null,
"digest": null,
"outcome": null,
"approvers": null,
"policy": null,
"imposters": null,
"dead": null
},
"signature": "..."
}
]
}
Batch response is keyed by event id:
{
"01J9Y3J7F8H4B8N8H5M6Y2K3Q1": {
"valid": true
}
}
Required permission:
tkeeper.audit.log.verify
Rotate the integrity key:
POST /v1/keeper/integrity/rotate
Required permission:
tkeeper.integrity.rotate
Peers expose their current integrity public key on the internal API:
GET /v1/integrity/publicKey
Response:
{ "data": "base64-public-key" }
Failure Behavior
When audit is enabled, TKeeper checks sink availability before protected operations. At least one configured sink must be available.
When an event is written, the operation continues if at least one configured sink accepts the event before the audit timeout. If all configured sinks fail or miss the timeout, the operation fails with AUDIT_FAILED.
Frequent Problems
Audit sink is down
If at least one sink is alive, operations continue. If no sink is available, protected operations fail with AUDIT_NOT_AVAILABLE before the crypto session starts.
Verification fails
Check the line encoding, the Ed25519 signature, and event.integrityKeyVersion.