Admin access
How people and automation get into halo-server beyond the browser login. Design: ADR-0036.
Service-account tokens
Section titled “Service-account tokens”Automation (CI, halo mcp serve --server, scripts) uses a service-account token, not a console cookie copied from a browser. A token holds one halo-server role, always expires, and shows up in the audit log as its own principal, service-account:<name>:<id>.
export HALO_SERVER=https://halo.acme.exampleexport HALO_SESSION=... # your own interactive admin session, once, to mint the tokenhalo service-token create --name ci-killswitch --role operator --ttl 720h \ --owner platform-oncall --description "kill switch from the incident pipeline"# prints halo_sat_... once: put it in your secret manager nowThen, in the automation:
export HALO_SERVER=https://halo.acme.example HALO_TOKEN=halo_sat_...halo toggle kill sonnet-next-route --reason "SEV2: error spike"HALO_TOKEN=halo_sat_... halo mcp serve --policy-dir . --server "$HALO_SERVER"HALO_TOKEN wins over HALO_SESSION everywhere the CLI and the MCP server call halo-server. Neither is ever a flag or a tool argument.
| Property | Behaviour |
|---|---|
| Format | halo_sat_ + 80 hex digits. Add halo_sat_[0-9a-f]{80} to your secret scanner (GitHub custom pattern, gitleaks, trufflehog) |
| Role | One of viewer, operator, approver, auditor, admin; never more than the creator holds |
| Lifetime | --ttl, at least 1h; default and maximum --service-token-max-ttl (90 days unless configured, at most 365) |
| Storage | SHA-256 of the token only; the token is shown once |
| Who creates | An admin, from an interactive session. A token cannot create tokens; break-glass cannot either |
| Who revokes | Any admin principal: halo service-token revoke <id> --reason .... Holds on every replica at the token’s next request |
| What it cannot do | File, approve or deny access requests, or approve rollout steps: those need a person (403, audited as authz.deny with need: human). For every separation-of-duties check a token counts as its creator |
| Audit | Every action under the token’s principal; service-token.create, service-token.revoke; service-token.use (method, path) at most hourly per token per replica |
| Failed attempts | 10 per minute per client address (shared across replicas), then 429. An expired token is a 401 without a penalty |
Rotation: create the replacement, deploy it, then revoke the old one. halo service-token list shows status, expiry, owner and creator, never secrets.
API: POST /api/v1/service-tokens {"name","role","ttl","description","owner"} (201, returns token once), GET /api/v1/service-tokens, POST /api/v1/service-tokens/{id}/revoke {"reason"}, all admin. A request carries the token as Authorization: Bearer halo_sat_....
Session timeouts
Section titled “Session timeouts”Console sessions end at whichever comes first:
| Flag | Helm | Default | Meaning |
|---|---|---|---|
--session-ttl |
server.session.ttl |
12h |
Absolute lifetime from login (5m to 168h). Never extended |
--session-idle-timeout |
server.session.idleTimeout |
1h |
No request for this long ends the session (1m to the TTL; 0 turns it off). Sliding: the cookie is re-issued as you use it |
A HALO_SESSION copied from a browser now stops working an hour after the browser last used it. Move that automation to HALO_TOKEN; set --session-idle-timeout=0 only if you must keep the old behaviour.
Break-glass
Section titled “Break-glass”One local admin account for when the IdP is down or identity in policy is broken and nobody can log in. It is sealed (the endpoint answers 404) until you enable it, its session lasts 30 minutes and is never extended, and every login, failed attempt and request it makes is a high-severity audit entry (OCSF severity_id 4, syslog severity critical) on every SIEM sink. If the audit log cannot be written, the login is refused.
Set it up once (sealed)
Section titled “Set it up once (sealed)”# a human, on a trusted machineopenssl rand -base64 33 > bg-password # store in the vault, split custody if you canhalo-server breakglass-hash < bg-password > bg-hashkubectl -n halos create secret generic halos-breakglass --from-file=hash=bg-hashshred -u bg-hashserver: breakGlass: {enabled: false, existingSecret: halos-breakglass, key: hash, user: breakglass, allowedCIDRs: "10.8.0.0/16"}Without Helm: --breakglass-hash-file=/path/bg-hash --breakglass-allowed-cidrs=10.8.0.0/16 [--breakglass-user=breakglass]. The hash is PBKDF2-HMAC-SHA256 with 600,000 iterations; passwords under 16 characters are refused.
allowedCIDRs is required to enable break-glass: halo-server refuses to start, and the chart to render, without it. Set it to your VPN or bastion range. From anywhere else break-glass answers 404 as if sealed, before the password is read, and an existing break-glass session is refused, so outside attackers can neither guess the password nor trip the lockout. Ranges broader than IPv4 /8 or IPv6 /32 (0.0.0.0/0, ::/0) are refused by halo-server and the chart: they would let almost anyone spend the failure budget and keep break-glass locked. IPv4 /8 to /15 and IPv6 /32 to /47 are accepted with a startup warning; prefer the narrowest VPN or bastion range.
Twenty failed logins within 15 minutes from allowed addresses, on any replica, lock break-glass on every replica for 15 minutes; a correct password is refused while locked. The lockout is audited once as breakglass.lockout (high severity). An admin signed in through SSO lifts it without the locked path:
curl -X POST -b "halo_session=$HALO_SESSION" https://halo.acme.example/api/v1/breakglass/unlock # admin, a person (not a token)That resets the failure count on every replica and is audited as breakglass.unlock (high severity). If SSO is down too, wait out the 15 minutes from inside the allowed range.
Alert on these audit actions in your SIEM: breakglass.login, breakglass.login.failed, breakglass.lockout, breakglass.unlock, breakglass.request.
Use it (incident)
Section titled “Use it (incident)”-
Get approval under your incident process; record who and why.
-
Enable:
server.breakGlass.enabled: true(--breakglass-enabled;allowedCIDRsmust be set) and roll out. The server logsBREAK-GLASS ENABLEDat start. -
Log in and act:
Terminal window curl -sc jar -H 'Content-Type: application/json' https://halo.acme.example/auth/breakglass \-d "{\"user\":\"breakglass\",\"password\":\"$(cat bg-password)\"}"export HALO_SESSION=$(awk '$6=="halo_session"{print $7}' jar)halo freeze on --reason "SEV1: IdP outage" # or kill, revoke, ...The session is
breakglass:breakglasswith the admin role, for 30 minutes. It cannot create service tokens, delegate to agents, or decide access requests and rollout approvals. Every change it makes is written to the audit log before it happens; if the log cannot be written the change is refused (503).
Close it (rotate after every use)
Section titled “Close it (rotate after every use)”- Disable:
server.breakGlass.enabled: falseand roll out. Any open break-glass session ends at its next request. - Rotate: generate a new password and hash, update the Secret, roll out. A changed hash also ends old sessions.
- Review the audit log (
GET /api/v1/audit, or the SIEM) for everybreakglass.*entry of the incident and attach them to the post-incident review. - Revoke anything created during the incident that should not outlive it (
halo agents revocations,halo service-token list).
OIDC login requirements
Section titled “OIDC login requirements”Group claims, Entra ID groups overage and MFA requirements are policy (identity in halos.yaml): see Entra ID and Okta.