Auth, RBAC & audit — design
Class:
design· Function:auth(gatesserve's write path). The whole-system view isdesign.md; the model rationale + proposals are inauth-rbac-design.md(reasoning). This file states the wired posture.
The write gate (reads are not gated)
A write goes: agent → authenticate → authorize (RBAC) → route → cap → backend.put(). authorize_write was added only to the write path, so a read never runs auth.
Authenticate — server/lib/src/auth.rs
validate_request() accepts either:
- mTLS CN — the client-cert chain is verified once, at the TLS handshake by rustls
WebPkiClientVerifier(server/lib/src/exposed/tls.rs,ringprovider pinned process-wide). Per request only the leaf CN is parsed (leaf_common_name, x509-parser), and only on writes. ALPN negotiates HTTP/2, so many requests multiplex over one long-lived connection (one handshake, N requests). This is the fast path. - OIDC bearer token — validated against the issuer's
/userinfothrough one shared pooledreqwest::Client(OnceLock) + a TTL token cache keyed by(issuer, blake3(token))(default 120 s,NORNIR_OIDC_USERINFO_CACHE_TTL_SECS; the JWKS-refetch TTL is the separateNORNIR_OIDC_JWKS_CACHE_TTL_SECS, default 3600 s). This TTL is the revocation window — until it expires a token the IdP has already revoked is still accepted, so it is the knob to shorten when hardening. The reader lives inedda/crates/nornir-auth-oidc, NOT in holger:server/lib/src/auth.rsis a 21-line re-export since the policy layer was promoted, and the variable was RENAMED in that move. These docs named the pre-renameHOLGER_OIDC_CACHE_TTL_SECSfor months while no code read it, so an operator who set it got no error, no warning and the full 120 s window — a security control failing silently OPEN. The legacy name is now honoured with a loud deprecation warning rather than ignored. The raw token is never stored — only its blake3 digest. A repeat write with the same token returns from cache with zero network calls.
An empty method list = open access; a configured-but-invalid credential = 401.
Authorize — RBAC (Role = Reader | Writer | Admin)
Both write paths run the role check: gRPC put_artifact → authorize_write (grpc.rs) and the HTTP/OCI gateway → authorize_http_write (exposed/http.rs).
- Role model — ordered least→most.
role_for: explicitrolesmap →default_role→Reader. When RBAC is on, DELETE requiresadmin; every other write requireswriter-or-admin. - Fail-closed — policy configured but no authenticated identity ⇒ denied.
- Opt-in —
rbac_enabled()is false until arolesordefault_rolepolicy is set, so an existing config keeps authN-only behaviour (any valid identity may write). With noauth.methodsat all, writes proceed asanonymous. - Policy lives in RON, alongside repos/endpoints/storage — git-tracked, diffable, and immutable in the sealed static flavor. Not a mutable KV store.
auth: Some((
methods: [ Oidc(( issuer_url: "https://idp/realm" )), Mtls(( ca_cert: "..." )) ],
roles: { "alice@corp": Admin, "CN=build-bot": Writer },
default_role: Some(Reader),
))
Caps, audit & hardening
- Ingest / body cap —
ron_max_body_byteson theExposedEndpoint(default 1 GiB, one knob) caps both the HTTP-gateway body and the gRPCputingest; oversized payloads are rejected (413 /ResourceExhausted) before the backend sees them. - Audit — on by default: an append-only Arrow-IPC segment log records identity (
sub/CN) + repo + artifact + action + source IP + timestamp on every write (and read). Replay tolerates a corrupt/truncated segment (logged and skipped), so a crashed write can't poison offline review of the rest of the trail. - SSRF guards (agent side) — the Nexus connector refuses to attach credentials to a
downloadUrlthat isn't same-origin with the configured base URL; the world/pip connector restricts downloads to an https host allow-list. - Injection-safe responses — attacker-influenced JSON (docker
tags/list, nuget versions) is built withserde_json; the PEP 503 HTML is escaped;file-ocitags/list404s an unknown repo instead of masking it as 200 + empty.
Known gaps (flagged in code)
Per-method mTLS ca_cert verification is flagged open — trust is currently delegated to the TLS client_ca; the per-method chain check needs cert plumbing. Reads are not yet gated. Offline OIDC (JWT-signature verify against an offline JWKS, no /userinfo) is the airgap auth path and is now WIRED (auth.rs::validate_oidc_bearer → verify_jwt_offline against a cached / pre-provisioned JWKS; see auth-rbac-design.md §W1-airgap). Production air-gapped CA issuance/rotation does not exist (only the mannequin test PKI).