feat: Structured audit layer middleware - #934
Conversation
b0e545e to
e7f9a8d
Compare
a63f4cf to
14b35bd
Compare
|
Note: needs 2 reviewers |
a750d9b to
7463432
Compare
| superseded_by: "" | ||
| --- | ||
|
|
||
| # ADR-0003: Tamper-Evident JSONL Audit Log — Single-Writer Architecture |
There was a problem hiding this comment.
To put it simply:
- JsonL is one of the most cummonly use formats for logs
- the "L" (one entry by line) avoids entirely every issue with formatters and such, while keeping the nested nature of Json that extractors need
- No overhead, No boilerplates, Just logs and some delimiters
- breakages (exp: incomplete line) can be trivially detected
- already ingested by Splunk or even a simple
grepfor indicident response. Also Given that Splunk-UF-on-raw-JSONL is the only production export path we currently have, storing JSONL means the forwarder reads the authoritative file directly. No transform, no second copy to keep in sync. Any other format will oblige us to either disable splunk export and forward it to a new PR or add a synchro macanism that will make the system slower and error prone
For the syslog question of the screenshot, yes the standard exists, but it's dead, and the living implementation is proprietary and doesn't do what we'd be doing anyway.
There was a problem hiding this comment.
And you do confirm that JSONL format is consumable by Splunk, Datadog, etc.?
There was a problem hiding this comment.
And you do confirm that JSONL format is consumable by Splunk, Datadog, etc.?
To my understanding there is a problem with this question, please correct me if I am mixing things:our local file where we store data and "what we push to external systems" are two separate matters.
First matter
The first is what this PR is about and is our decision that is have justified by these arguments above
Second matter
The second point is to be done here: #937
Storing our data in JsonL does not have inference to how we will export it to Splunk or friends, here is a doc example :
Currently, an non live export to those guys is technically possible
Thanks to a small hack I have documented, we can technically start live exporting now, but that's not yet a feature of the KMS. It's rather a hack smart operators can pull out - and that I preferred to document 'to expect loss' rather than wait for people to do it and get errors (we can control the KMS, but not what users do with their terminal)
HatemMn
left a comment
There was a problem hiding this comment.
Sorry for the late posting, I forgot to post this last week
| superseded_by: "" | ||
| --- | ||
|
|
||
| # ADR-0003: Tamper-Evident JSONL Audit Log — Single-Writer Architecture |
There was a problem hiding this comment.
To put it simply:
- JsonL is one of the most cummonly use formats for logs
- the "L" (one entry by line) avoids entirely every issue with formatters and such, while keeping the nested nature of Json that extractors need
- No overhead, No boilerplates, Just logs and some delimiters
- breakages (exp: incomplete line) can be trivially detected
- already ingested by Splunk or even a simple
grepfor indicident response. Also Given that Splunk-UF-on-raw-JSONL is the only production export path we currently have, storing JSONL means the forwarder reads the authoritative file directly. No transform, no second copy to keep in sync. Any other format will oblige us to either disable splunk export and forward it to a new PR or add a synchro macanism that will make the system slower and error prone
For the syslog question of the screenshot, yes the standard exists, but it's dead, and the living implementation is proprietary and doesn't do what we'd be doing anyway.
cbcd326 to
06dc5a7
Compare
2ea2349 to
eba855e
Compare
f5cfbf2 to
4cc0def
Compare
4cc0def to
a9886b8
Compare
fix: fixes fix: commit fix: manual edits fix: another fix
feat: WIP on feat/audit_and_siem chore: adr feat: fix the limits doc: doc is hard test: integration
feat: fix final flaws feat: fix tests that didn't pass fix: ci fix feat: 2 feat: ci fear: update reverse proxy docs fix: iris fix: another fix fix: restaure iris
This commit basically hashes usernames
- UserId.username is now a typed UserId (not String) after develop's UserId refactor: use Display instead of clone(). - AuditFailureMode::default() disambiguated from Default::default() per clippy::default_trait_access (pedantic).
a9886b8 to
ff57fb1
Compare

About merging
To keep everything consistent, the following PRs should be rebased on top of each other in this order
Specs (optional) :
Artifacts :
For testing purposes, these debug artifacts are shared :
no_auth_audit.toml
The following commands (all assume you already ran a KMS) :
Normally produce kms-audit.jsonl :
{"id":0,"timestamp":"2026-07-20T19:02:25.605113314Z","operation":"Create","user":"admin","object_uid":"359019d8-1543-4e2e-9d96-674dd64fcffc","algorithm":"AES","client_ip":"127.0.0.1","result":"Success","duration_ms":4,"request_id":"4b2e6994-0ef9-4dfe-9804-0da4c5edde4b","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","row_hash":"641c442edc2bb075e9770a905c843fdadfb85503464e67c1658549624d44fb00"} {"id":1,"timestamp":"2026-07-20T19:02:36.704410987Z","operation":"Encrypt","user":"admin","object_uid":"359019d8-1543-4e2e-9d96-674dd64fcffc","algorithm":"AES","client_ip":"127.0.0.1","result":"Success","duration_ms":12,"request_id":"3618ade8-5db7-4635-9d05-5af6a7614d52","prev_hash":"641c442edc2bb075e9770a905c843fdadfb85503464e67c1658549624d44fb00","row_hash":"637cd569144c90f0ee73564286f8a75c56084cf76f7375b3d31266161b3de6a3"} {"id":2,"timestamp":"2026-07-20T19:02:43.474676849Z","operation":"kmip","user":"admin","object_uid":null,"algorithm":null,"client_ip":"127.0.0.1","result":{"Failure":"422 Unprocessable Entity"},"duration_ms":0,"request_id":"029b157a-4d66-4aa8-a504-dab433baa374","prev_hash":"637cd569144c90f0ee73564286f8a75c56084cf76f7375b3d31266161b3de6a3","row_hash":"3b00ac7a2880cfe79c30763b3e6e18c37a8019dbe1687d6a1e4936ddd4772695"} {"id":3,"timestamp":"2026-07-20T19:02:50.496997982Z","operation":"Create","user":"admin","object_uid":"021c29e9-8017-42d6-9887-bebb5c413e74","algorithm":"AES","client_ip":"127.0.0.1","result":"Success","duration_ms":2,"request_id":"2e5fcc19-2021-45a3-822c-9ecf4e38de85","prev_hash":"3b00ac7a2880cfe79c30763b3e6e18c37a8019dbe1687d6a1e4936ddd4772695","row_hash":"b75759b556141c03e2e4c9b6cfb4e8992f3de09338bb32b46eb549b0e1a6f30c"} {"id":4,"timestamp":"2026-07-20T19:03:00.273344336Z","operation":"Encrypt","user":"admin","object_uid":"021c29e9-8017-42d6-9887-bebb5c413e74","algorithm":"AES","client_ip":"127.0.0.1","result":"Success","duration_ms":2,"request_id":"330d8643-91a4-4869-af8c-849ee50d5643","prev_hash":"b75759b556141c03e2e4c9b6cfb4e8992f3de09338bb32b46eb549b0e1a6f30c","row_hash":"c849c7efd68569ced3b63af96af2ad5d77a82051e36c64c2ef9f5be7eeea17e9"} {"id":5,"timestamp":"2026-07-20T19:03:00.685874574Z","operation":"Decrypt","user":"admin","object_uid":"021c29e9-8017-42d6-9887-bebb5c413e74","algorithm":"AES","client_ip":"127.0.0.1","result":"Success","duration_ms":0,"request_id":"29df4906-b8c2-47fb-b88b-b8ca3a0c6ceb","prev_hash":"c849c7efd68569ced3b63af96af2ad5d77a82051e36c64c2ef9f5be7eeea17e9","row_hash":"fbca602d562d8205315085ddf9a5493eee14be73180ff79c023061027ea6cde9"} {"id":6,"timestamp":"2026-07-20T19:03:01.069844169Z","operation":"Decrypt","user":"admin","object_uid":"00000000-0000-0000-0000-000000000000","algorithm":"AES","client_ip":"127.0.0.1","result":{"Failure":"422 Unprocessable Entity"},"duration_ms":1,"request_id":"7a297435-64f3-41ef-a0b2-df64146915de","prev_hash":"fbca602d562d8205315085ddf9a5493eee14be73180ff79c023061027ea6cde9","row_hash":"ad83e2e8730ed5556bf51f2d8b301167cbe54b1314a2a1ce720a274b35f84594"} {"id":7,"timestamp":"2026-07-20T19:03:01.079329139Z","operation":"kmip","user":"admin","object_uid":null,"algorithm":null,"client_ip":"127.0.0.1","result":{"Failure":"422 Unprocessable Entity"},"duration_ms":0,"request_id":"236a3050-940d-4005-8cbd-f25a4a33627a","prev_hash":"ad83e2e8730ed5556bf51f2d8b301167cbe54b1314a2a1ce720a274b35f84594","row_hash":"a361c07f62b719755b3e5ff687049db728e0c1cdb722a8e3060369f17dd01610"}Then you can run :
cargo run -p ckms --features non-fips -- audit verify --path #your fileOther technical considerations
Reverse proxy usage :
This parameter seems over engibneered, but :
Threat without allowlist, two bad options:
Trust XFF always → any attacker sends fake X-Forwarded-For: 1.2.3.4 header directly to KMS → audit log records wrong [client_ip]. Forensic/compliance trail poisoned — attacker hides real origin or frames innocent IP. This is audit-log spoofing (CWE-117-ish), works via any protocol (UI, CLI, curl), no UI needed.
Never trust XFF → when real proxy present, [client_ip] always = proxy's own IP → useless for forensics regardless of caller.
CIDR allowlist fixes both: trust XFF only from known proxy IPs, so external attacker can't inject fake header, but real proxy's forwarded value still recorded.
What's audited, what's not
Audited
POST/GET /kmip—kmip::kmip_2_1_json(JSON) andkmip::kmip(binary TTLV)access::*— grant/revoke/list access rights, list owned objects, get current userget_server_info,get_hsm_status,/download-cliNot audited
start_socket_server→handle_ttlv_bytes(start_kms_server.rs). Full KMIP protocol, noAuditMiddlewarepossible (not Actix), no manual audit call either./v1/cryptoREST Native Crypto API (jose/) —POST /encrypt,/decrypt,/sign,/verify,/mac,POST /keys(create),DELETE /keys/{kid}(destroy),POST /keys/unwrap, tag add/remove/list./v1/{transit_mount}(create_transit_key,configure_transit_key,delete_transit_key,sign_with_transit_key, ...) and/v1/{pki_mount}(sign_intermediate), gated byvault_api_enabled./v1/authproxy scope also unaudited (pure forward, no key ops)./tokenize(non-fips only) —hash,noise,word_mask,word_tokenize,word_pattern_mask,aggregate_number,aggregate_date,scale_number./google_cse,/ms_dke,/aws(XKS),/azureekm— already decided (SEC-04, ejected).Not audited, but by design (no key operations)
/.well-known/jwks.json— public key exporthealth,get_version, swagger/OpenAPI routes,root_redirect