Configuration: observe and query¶
The keys of the indexer (audit-observe), the query service (audit-query) and its grants file. Shared blocks are in configuration.
Indexer¶
audit-observe follows the archive and writes the index (0062, 0066). It reads the archive, never writes it, and serves only /healthz and /readyz.
| Step | Behaviour |
|---|---|
| Pass | Lists records/<profile>/<tenant>/ from a cursor kept in Postgres. Indexes objects older than settle. Moves the cursor in the transaction that writes their rows |
| Wake-up | Set one of wake.nats and wake.sqs, or neither. A lost, repeated or reordered notification costs latency only |
| Latency without wake | Between settle and interval, at worst interval (five minutes with defaults) |
| Latency with wake | About settle, whatever interval is. Lower settle only as far as a put reliably finishes |
| Undecodable object | Skipped and counted (reason=unreadable) |
| Fetch or catalogue failure | Stops that tenant's cursor, retried by the next pass (reason=retry). Other tenants continue |
| Reset | audit reindex --reset-cursor rereads a profile from the start and changes nothing already indexed |
| Release | Run the same audit release as the writer or newer (upgrade to v1.74) |
| key | type | default | meaning |
|---|---|---|---|
listen |
listen |
:8080 |
the address /healthz and /readyz are served on. /healthz says the process is up (the liveness probe); /readyz says it can work now (the readiness probe): the index database answers and the archive's catalogues can be listed. A failing check is named in the 503, never its error |
deployment |
path, required | the profile configuration: its presets are the stores followed, and its profiles say which preset each profile is in |
|
archive |
archive |
none | what the process adds to the presets: stateRoot, sluisRoot, sluisDir and ca. It has no kmsKey: this process reads. The catalogues and their extension schemas are read from the archive too |
database |
database, required |
the index, as the indexer's own role (audit migrate --observe): read and write on the index and its cursors, nothing of the deduplication table, not the owner, and not the writer's or the query service's |
|
settle |
duration | 2m |
how far behind now the cursor stays. An object's key is fixed when its put starts and it is visible when it ends, so it must be longer than a put can take and than the clocks of the writers and of this process can disagree. It is the least time between a record's acknowledgement and its appearance in search |
interval |
duration | 5m |
the poll: how often a pass runs when nothing woke it. Without a wake an object is searchable after the longer of settle and the time to the next pass, so at worst after interval; with one the woken pass reschedules itself for when the object is old enough, the delay is about settle whatever interval is, and interval only bounds a lost notification |
readiness.failedPasses |
integer, at least 1 | 3 |
/readyz fails once this many passes in a row have failed |
readiness.staleIntervals |
integer, at least 1 | 3 |
/readyz fails once this many intervals have passed without a successful pass |
batch |
integer, at least 1 | 500 |
rows written in one transaction; a transaction ends at an object's end |
profiles |
list of strings, at least one, unique | every profile the archive has | the profiles to follow. Profiles and tenants are discovered by listing |
wake.nats.nats, wake.nats.subject |
nats (url, tokenFile), string |
a subject carrying the bucket's notifications. Their content is never read | |
wake.sqs |
sqs |
a queue of the bucket's notifications that is the indexer's own: each message wakes a pass and is deleted. Credentials are the SDK's ambient ones |
A failed pass is retried at the next poll. /readyz fails after readiness.failedPasses failed passes in a row, or readiness.staleIntervals intervals without a success. /healthz is unaffected, so a failing indexer does not crash-loop.
| Signal | Use |
|---|---|
audit_observe_passes_total, audit_observe_pass_since_success_seconds |
Back the chart alerts AuditIndexStalled and AuditIndexPassesFailing |
| Log line | Once per distinct error with the object and reason, then every half hour |
Query service¶
audit-query serves search, facets, get, export, tail and resolve behind the grants. The writer records every read it serves.
| key | type | default | meaning |
|---|---|---|---|
listen |
listen |
:8080 |
the address it is served on |
grants |
path, required | the grants file, below | |
sink |
sink, required |
the writer every read is recorded through: its url, or the ingest sqs queue of a writer that runs elsewhere (AWS) |
|
require |
logged, queued or archived |
none: checks nothing | the weakest durability the writer's acknowledgements may carry. Needs sink.expect at least as strong (durability) |
deployment |
path | none | the profile configuration. Required with archive and with s3scan. A grants preset needs it, because it turns roles into the deployment's own profiles |
searcher |
postgres or s3scan |
postgres |
postgres is the index; s3scan is the archive, within a budget, for a deployment with no database. The scan orders by occurred_at only and refuses recorded_at, so a deployment on it can search the trail but cannot follow it: there is no live tail (search) |
database |
database |
the index, as the query service's own role (audit migrate --reader): usage on the schema, select on the index's tables, not the owner. Tenant row-level security binds only a non-owner. Required unless searcher is s3scan |
|
archive |
archive |
none | what the process adds to the presets (stateRoot, sluisRoot, sluisDir, ca). Required with s3scan, which reads the stores of the deployment's presets, and where Get finds a record's object. Without it Get still answers, with where the copy is and seal_unknown set |
exports.bucket |
bucket, required with exports |
a separate bucket with no Object Lock, which clears it. Without exports the export operation is refused. It inherits nothing from the archive: name its endpoint, path style and credentialsSecret here |
|
exports.expiry |
duration | 168h |
how long an export is kept before the bucket clears it |
exports.linkValid |
duration | 1h |
how long a download link works |
keys |
keys |
none | the writer's key provider, which turns resolve on. Needs archive. Without keys there is nothing to resolve and the RPC is unimplemented. adapter: local reads the writer's rootFile, which must be shared; transit signs in as the query service's own identity, never the writer's |
The service's limits are fixed, not configured: filter 4 terms, sort 4, in 100 values, limit 1000. See the API reference.
Grants file¶
grants names the file (in the chart /etc/audit/grants.yaml, rendered from query.grants). It says who may authenticate and what each caller sees.
issuers:
- url: https://id.example.com # exactly as the tokens' iss claim says it
audience: audit # required
- url: https://customers.example.com
audience: audit
rules: # first match wins
- name: auditors
issuer: https://id.example.com # required once more than one issuer is trusted
claim: groups
value: all:audit:auditor
grant:
all_tenants: true
profiles: [security, operational]
operations: [search, facets, get, export]
- name: assessor-2026-q3 # an external assessor sees one period, not the archive
issuer: https://id.example.com
claim: groups
value: all:audit:assessor
grant:
all_tenants: true
profiles: [security]
operations: [search, get]
from: 2026-07-01T00:00:00Z # by when records happened; end exclusive
until: 2026-10-01T00:00:00Z
- name: acme-viewers
issuer: https://customers.example.com
claim: groups
value: acme:audit:viewer
grant:
tenants: [acme]
profiles: [security]
operations: [search, get]
A grants preset replaces a rule per group. It needs deployment in the query configuration, because it turns roles into the deployment's profiles.
presets:
- name: access-roster
issuer: https://id.example.com # required once more than one issuer is trusted
claim: groups # the default
The access-roster grants preset reads groups named <scope>:audit:<role>, the grant grammar from sluis. The name is a legacy identifier, renamed in v1.75–v1.76. The scope is all or an audit tenant id, byte for byte.
| Role | Profiles built from | Operations | all allowed |
|---|---|---|---|
viewer |
history |
search, facets, get | no: all:audit:viewer grants nothing |
security |
security, dora, pci-dss, nen-7513 |
search, facets, get, tail, export | yes |
auditor |
every profile built from no billing-* framework profile |
search, facets, get, export | yes |
billing |
billing-* |
search, facets, get, export | yes |
evidence |
evidence-etsi |
search, get, export | yes |
resolve comes from no group name: write an explicit rule naming the person.
| Rule | Behaviour |
|---|---|
| Union | Every matching rule and audit group counts, per request. On the profile asked for, the tenants of the covering grants are unioned. The read's record names all of them (acme:audit:viewer,all:audit:security) |
| Profiles | A union never crosses profiles |
| Windows | A window is never lent to another grant's tenants. A grant that another covers (its tenants over its window) is set aside. The grants left must share one window, or the request is denied |
The service refuses to start in these cases:
| Case | Reason |
|---|---|
| No issuer | Nobody could sign in |
| An issuer without an audience | A token minted for another service could be replayed |
| Several issuers and a rule naming none | A rule would match a group from the least-trusted issuer |
| A rule names an unlisted issuer or an unknown operation | Invalid reference |
A preset without deployment, or a nonexistent one |
The preset cannot resolve profiles |
It accepts a bearer token in Authorization and the access token the fleet gateway forwards. gateway-auth verifies it with one verifier per issuer. It checks signature, issuer, audience and expiry, refreshes keys in the background, and allows no clock skew.