KRAKEN wiki

The audit log

What the Panel records about every change, the colour rule that tells you whose fault a row is, the download-token and rate-limit rows, how long entries are kept, and why the source column is worth nothing until you name your proxy.

on this page

Every request that changes something is written to the audit log in Postgres. Reads are not, deliberately: a log where a page refresh is the most common entry is a log nobody reads.

What a row holds

field what it is
time when the request was handled
actor the username, or anonymous for a pre-authentication event
action the method plus the route pattern, plus a detail when the handler added one
method · path the HTTP method and the path as requested
target_type · target_id server, node, spec, user or auth, and the {id} from the route
status the response status the request actually ended on
ip the resolved client address
forwarded_for the raw X-Forwarded-For as received — only when ip identifies nobody, and never trusted

The action field uses the route pattern, not the concrete path, so rows group: POST /servers/{id}/power is one action whichever server it was, and the id is in target_id where it belongs.

GET, HEAD and OPTIONS never write a row. Three routes that sit outside the authenticated group audit themselves instead, because they have something worth recording and no session to attribute it to: login, Agent enrollment, and the loopback local-enroll on first run.

The colour rule

A status is coloured by its family, never by a verdict the UI invents:

  • 2xx takes the status gold.
  • 4xx takes caution violet. The caller got it wrong.
  • 5xx takes crisis magenta. We did.

A 4xx is never painted crisis, because a mistyped password is not an outage. It is a small rule and it does a lot of work on a filtered view: a screen of violet is somebody fumbling, a screen of magenta is something to go and fix.

the audit log filtered to failures: 4xx rows in violet where the caller got it wrong, 5xx in magenta where the Panel could not reach a node
the audit log filtered to failures: 4xx rows in violet where the caller got it wrong, 5xx in magenta where the Panel could not reach a node

Download tokens leave two rows

A tokenised file download writes a mint row and a redemption row:

  • POST /servers/{id}/files/download-token — mint download token (zip, 3 paths)
  • GET /servers/{id}/files/raw — download token redeemed (raw, 1 path)

The path count, not the paths. The token itself appears in no log line at all. A redemption that was refused reads download token refused (…), because the row carries the request's real outcome rather than an assumed 200: a download the node could not serve is on the record with the status it got — a 404 for a file that is not there, a 409 for one another process holds, a 503 when the node could not be reached.

A redemption rejected before it got that far, an expired or forged token, is not an audit row. It is a Debug log line and a counter, kraken_download_tokens_rejected_total, so an unauthenticated caller with a bad token cannot amplify writes into the audit table. Turn KRAKEN_LOG_LEVEL to debug when you want to watch them.

Rate limiting leaves one row per window

A login refused by the rate limiter never reaches the handler that would audit a failed attempt, so the limiter writes the row itself. Two limiters guard that route, on different keys, and each leaves its own row:

POST /auth/login — rate limited (too many attempts from this address)

as anonymous, when one client address has exhausted its twenty a minute, and

POST /auth/login — rate limited (too many failed attempts for this username)

as the username, when one account has taken ten failed guesses in a minute. The second one is the finding: this account is being worked on. It is written even when the Panel cannot tell one caller from another, which is the whole reason that limiter exists. POST /auth/change-password writes the same row when the current password is fumbled past the same budget.

Either row is written once per key per window rather than once per request. That is the difference between a signal and a flood: somebody hammering login produces one row a minute, not one row a request.

Reading the source column

ip is whatever the Panel resolved the client to be, and the default is the real TCP peer with no forwarding header believed at all.

Behind a reverse proxy or a Cloudflare Tunnel, that default is wrong, and it is wrong in a way that looks like it is working: the peer is the proxy on every request, so every row records the proxy. The same resolution feeds both rate limiters, which collapse into one shared bucket a single stranger can exhaust for everybody, and the /setup/* internal-network gate, which then sees a loopback address for the entire internet.

Name your proxy's CIDR in KRAKEN_TRUSTED_PROXIES and the Panel takes the rightmost X-Forwarded-For hop that is not itself trusted. Rightmost, because the list is appended hop by hop and only what a trusted proxy appended can be believed. X-Forwarded-For is the only header consulted; CF-Connecting-IP looks more direct and is not, since every proxy other than Cloudflare passes a client-supplied one through untouched.

An entry that does not parse is a startup error, not a warning, for exactly this reason: a typo that silently emptied the list would leave you with a Panel that looks fine and an audit log that records nothing useful.

The whole configuration, with the working examples, is on behind a reverse proxy.

when the address identifies nobody

Some topologies cannot be fixed by naming a proxy — Docker Desktop rewrites the source of every published-port connection to its own gateway, and the real address is gone before the Panel sees a byte. For those rows the Panel keeps the raw X-Forwarded-For chain alongside the resolved address, and the audit log marks the source cell · fwd with the chain on hover. It does this only when the resolved address is private or is one you exempted with KRAKEN_RATE_LIMIT_IP_SKIP; a row with a real client address does not need it.

Read it as forensics, never as proof. The caller wrote that header. Behind Cloudflare → a proxy → a published port it is where the visitor's true address survives and the one place you can go looking for it, but nothing in the Panel resolves to it, limits on it or gates anything behind it, and neither should you.

How long entries are kept

The Panel keeps 90 days by default and deletes what is older:

KRAKEN_AUDIT_RETENTION_DAYS=90   # 0 keeps every entry forever

A background job runs the sweep a minute after startup and every 24 hours after that. It deletes in batches of 5000 rather than in one statement, so a log that has been accumulating since the Panel was installed is drained over a few short deletes instead of one long lock held against the audit writes still arriving. A pass that removed something logs a single line with the count; a pass that found nothing says nothing.

Set the window to 0 when something else owns retention — an external log shipper, or a compliance rule that says the trail outlives the database's convenience. Nothing is deleted then, and the console says retained indefinitely instead of naming a window.

The value has to be a whole number of days, 0 or more. Anything else stops the Panel at startup: deletion is not recoverable, and a Panel that fell back to the default after a typo would be pruning on a schedule nobody chose.

Getting at it

GET /api/v1/audit returns the most recent entries, newest first, and needs the audit.view permission, which Owner and Admin hold. The response also carries retention_days, which is where the console's footnote gets its number — the screen quotes what the Panel is actually keeping to rather than a figure typed into the copy.