Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Admin API and RBAC

The admin API is a JSON REST surface mounted at /api/admin on the admin listener (server.admin_listen) or, when no admin listener is configured, on the OCSP listener. The web UI at /ui/ is a client of this API and has no capabilities beyond it. See TLS and Mutual TLS for putting the listener behind TLS; do not expose it in cleartext.

Operators and roles

Operators are defined statically in configuration. There are no built-in accounts and no self-registration.

[server.admin]
session_ttl_secs = 3600

[[server.admin.operators]]
name          = "alice"
password_hash = "$2b$12$…"      # bcrypt cost 12; see "Generating a password hash"
role          = "administrator"

[[server.admin.operators]]
name          = "noc"
password_hash = "$2b$12$…"
role          = "viewer"          # default when omitted
RoleCanCannot
viewerRead status, bundles, certificates, rotation state, gossip membership, effective config, anti-rollback state; run OCSP queries; extract a single entry from a bundleChange anything
operatorEverything a viewer can, plus reload bundles, inspect/verify/diff bundles, apply deltas, trigger on-demand signingRotate keys
administratorEverything—

Roles are strictly ordered (viewer < operator < administrator); a route’s minimum role is checked on every request.

Generating a password hash

hoike does not yet ship a hashing subcommand. Any bcrypt tool works; cost 12 is the value the server’s timing decoy assumes:

# Apache htpasswd (httpd-tools)
htpasswd -nbBC 12 "" 'correct horse battery staple' | tr -d ':\n'

# Python
python3 -c 'import bcrypt,getpass; print(bcrypt.hashpw(getpass.getpass().encode(), bcrypt.gensalt(12)).decode())'

Never commit a hash from the documentation examples; the server should be treated as compromised if a documented hash is found in a live configuration.

Session lifecycle

sequenceDiagram
    participant C as Client
    participant A as Admin API
    C->>A: POST /session {name, password}
    A-->>C: 200 {session_token, role, expires_in_secs}
    C->>A: GET /status  (Authorization: Bearer <token>)
    A-->>C: 200 …
    C->>A: DELETE /session
    A-->>C: 204
  • Tokens are 32 random bytes, hex-encoded, presented as Authorization: Bearer <token>.
  • A session lives for session_ttl_secs (default 3600) from login. There is no idle timeout; the lifetime is fixed.
  • Logout (DELETE /session) invalidates the token immediately. Restarting the process invalidates all sessions; the session store is in memory only.
  • The web UI stores the token in browser sessionStorage, which is cleared when the tab closes.

Login limits

Login is deliberately expensive and bounded:

BoundValue
Request body4,096 bytes, 5-second read timeout
Concurrent password verifications4
Process-wide login attempts60 per minute (429 Too Many Requests beyond that)
Active sessions4,096 (login fails with 503 when full; expired sessions are pruned first)
Unknown operatorVerified against a decoy hash so timing does not reveal whether the name exists

There is no per-account lockout and no password-complexity enforcement in this release. Both are planned; see DISA STIG Guidance. Until then, put the admin listener behind mutual TLS and a management network.

Endpoints

All paths are relative to /api/admin. All require a bearer token except POST /session.

Session

MethodPathRoleDescription
POST/session—Log in; body {"name": "...", "password": "..."}
DELETE/sessionanyLog out

Read-only

MethodPathRoleDescription
GET/statusviewerProcess mode, uptime, loaded CAs, generation, freshness
GET/bundlesviewerLoaded bundles per CA with epoch, entry counts, algorithm
GET/bundles/{label}viewerDetail for one CA’s bundle
GET/certsviewerResponder and seal certificates with expiry
GET/rotationviewerKey-rotation monitor state per CA
GET/gossipviewerSWIM membership and last generation announcements
GET/configviewerEffective configuration with secrets redacted
GET/stateviewerAnti-rollback high-water marks
POST/queryviewerRun an OCSP query against the local responder; body carries serial and issuer hashes
POST/bundles/extractviewerReturn one entry from a bundle by CertID

Bundle operations

MethodPathRoleDescription
POST/bundles/reloadoperatorRe-read bundle_dir, applying seal trust and anti-rollback checks
POST/bundles/inspectoperatorParse a bundle and return its manifest
POST/bundles/verifyoperatorVerify a bundle’s seal against the configured trust policy
POST/bundles/diffoperatorCompare two generations
POST/bundles/applyoperatorMaterialize deltas; output is unsigned unless a seal key is available on this node

Signing and rotation (signer or combined mode only)

MethodPathRoleDescription
POST/sign/{label}operatorProduce a new generation for one CA now
POST/sign/alloperatorProduce new generations for every CA
POST/rotate/{label}administratorRun the configured rotation_command and reload key material for one CA

Signing routes share the signer mutex with scheduled production, so an on-demand run never interleaves with a scheduled one.

Errors

Errors are returned as short JSON strings with conventional status codes: 401 missing or expired token, 403 insufficient role, 409 signer busy, 422 malformed input, 429 login rate exceeded, 503 feature not built or capacity exhausted. Error bodies never include stack traces, file paths, or key material.

Audit trail

Bundle loads, signing runs, and rotation attempts emit audit events. Login success and failure are not yet audited and events do not yet carry the operator name; both are tracked for the next release.