a73x

docs/credential-revocation.md

Ref:   Size: 3.2 KiB   History

# Host credential compromise & revocation

Each agent authenticates its QUIC session with a bearer credential:

```
<host_id>.<generation>.<issued_unix>.<hmac-sha256>
```

The HMAC (keyed by the server's `host_secret`) covers all three fields. The
`generation` is compared against the host row's `cred_generation` on every
Hello **and every report (~10s)**, so revocation takes effect on live
sessions within one tick.

## Single credential leaked (the common case)

```
curl -X POST -H "Authorization: Bearer $EITRI_TOKEN" \
  http://server:8080/api/v1/hosts/<host_id>/revoke-credential
```

`$EITRI_TOKEN` is a personal access token (console → Settings); the call
revokes a credential on one of your own hosts.

- Bumps that host's generation: the leaked credential is dead fleet-wide
  within ~10s; every other host is untouched.
- The host's VMs keep running (the agent reconciles autonomously); the host
  is dark to the control plane until re-enrolled.
- Re-enroll: mint a join token, `eitri-agent join <blob>` on the host,
  restart the agent daemon.
- The revocation is recorded in the audit log (`host.credential.revoke`).

## Server `host_secret` leaked (the disaster case)

Rotate `host_secret` in `server.json` and restart—this revokes **every**
host credential at once; re-enroll each host as above.

## Optional max-age

`credential_max_age` in `server.json` (Go duration, e.g. `"2160h"`) rejects
credentials older than the window. It is checked at Hello and, like revocation,
on every report tick, so a live session cannot outlive the window. Off by default:
there is no automatic renewal channel yet, so expiry trades credential
lifetime against operator toil. Generation revocation is the primary
mechanism.

## SSH user certificates

Guest SSH access uses short-lived certificates self-signed with a tenant's own
user CA (see [connecting.md](connecting.md))—eitri holds no user signing key.
The short validity you sign with (`eitri ssh` uses 30 minutes) is the
first line of defense: a leaked cert expires on its own.

Before it does, a specific cert can be revoked at the gate by serial
(tenant-scoped, idempotent):

```
POST /api/v1/ssh-certs/revoke   {"serial": N}  or  {"certificate": "<cert line>"}
GET  /api/v1/ssh-certs/revoked
```

The by-line form extracts the serial from a pasted cert. The gate rejects
revoked serials at auth, and the revocation is recorded in the audit log
(`ssh-cert.revoke`).

A compromised **tenant user CA** is the bigger event: the tenant's CA must be
replaced and its VMs re-seeded to drop trust in the old one—fast CA-level
revocation is an open follow-up.

## The audit log is pruned

Both records above—`host.credential.revoke` and `ssh-cert.revoke`—live in the
server's append-only audit log, and that log does not grow forever.
`audit_retention` in `server.json` (Go duration) is the window, default
`"2160h"` (90 days); the server prunes older rows at startup and once a day
after that. `"0"` disables pruning and keeps the log indefinitely. A negative
value is refused at startup rather than silently ignored.

## Related

- [connecting.md](connecting.md)—how user and host certs work
- [cert-rotation.md](cert-rotation.md)—rotating the server's QUIC identity