Security API

Security status, tamper-evident audit trail, and the credential vault.

Endpoints

MethodPathActionDescription
GET/security/statusSecurityReadSecurity status summary
GET/security/auditSecurityAuditQuery recent audit entries
GET/security/verifySecurityReadVerify the audit hash-chain integrity
POST/audit/appendAuditWriteAppend a manual audit entry
GET/vaultVaultReadList credential vault key names + timestamps
PUT/vault/{key}VaultManageStore/rotate a secret
DELETE/vault/{key}VaultManageRemove a secret

SecurityRead is admin/operator/viewer/auditor; SecurityAudit is admin/auditor only; AuditWrite and both vault actions (VaultRead, VaultManage) are admin-only — no other role, including auditor, can read vault key names or write/delete a secret (issue #475, same bar as KeyManagement).

There is deliberately no GET /vault/{key} — the credential vault is write/rotate/delete only from the API. There is no read-back of a stored secret value over HTTP, by design (issue #475 non-goal / acceptance criterion #8). GET /vault returns key names and timestamps only, never values.


GET /security/status

curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  http://localhost:4200/security/status

Response (200)

{
  "audit_enabled": true,
  "audit_entries": 1247,
  "auth_mode": "api-key",
  "tls_enabled": true
}

auth_mode is "none", "api-key", or "oidc" (derived from the daemon's AuthMode, not the raw config string). There is no active_keys, ip_allowlist_configured, or audit_integrity field in this payload.


GET /security/audit

curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  "http://localhost:4200/security/audit?limit=5&category=DataAccess&actor=ci-deploy-bot"

Query Parameters

ParamTypeDefaultDescription
limitinteger20Max entries to return
categorystringnoneFilter by audit category
actorstringnoneFilter by identity
sincestringnoneISO 8601 timestamp lower bound

Response (200) — AuditEntry[]

{
  "entries": [
    {
      "id": 1247,
      "seq": 1247,
      "timestamp": "2026-06-04T10:15:02Z",
      "identity": "ci-deploy-bot",
      "role": "operator",
      "action": "POST /agents/550e8400.../invoke",
      "target": "550e8400-e29b-41d4-a716-446655440000",
      "result": "success",
      "hash": "sha256:...",
      "metadata": null
    }
  ]
}

Returns {"entries": []} (not an error) when no audit store is configured.


GET /security/verify

Verifies the audit trail's tamper-evident hash chain — each entry's hash covers the previous hash plus every core field (length-prefixed concatenation, not delimiter-joined, so no field's content can be mistaken for a boundary — issue #529).

curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  http://localhost:4200/security/verify

Response (200 — audit enabled)

{
  "valid": true,
  "entry_count": 1247,
  "chain_anchor": "sha256:genesis...",
  "tip_hash": "sha256:...",
  "algorithm": "sha256(\"hoziron-audit-chain-v2\" || ...)",
  "integrity_covers": "timestamp, identity, role, action, target, result, metadata (via metadata_hash)",
  "integrity_excludes": "none — metadata is covered via metadata_hash, not hashed inline",
  "verification_note": "..."
}

Response (200 — audit disabled)

{"valid": null, "status": "disabled", "reason": "audit disabled"}

Note this endpoint never returns HTTP error status for a broken chain — valid: false (with entry_count/hashes still populated) is itself the signal, not a 4xx/5xx.


POST /audit/append

Manually append an audit entry (admin-only). Routed through the platform, per ADR-036 ("core owns audit writes").

curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_admin..." \
  -H "Content-Type: application/json" \
  -d '{"event_type": "manual.override", "agent_id": "550e8400-...", "detail": "operator manually reviewed"}' \
  http://localhost:4200/audit/append

Response (201)

{"status": "recorded", "event_type": "manual.override"}

GET /vault

curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_admin..." \
  http://localhost:4200/vault

Response (200)

[
  {"key": "ANTHROPIC_API_KEY", "stored_at": "2026-06-01T10:00:00Z"}
]

Note this is a bare JSON array, not wrapped in {"entries": [...]}. Never includes the secret value.


PUT /vault/{key}

curl -X PUT -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_admin..." \
  -H "Content-Type: application/json" \
  -d '{"value": "sk-ant-..."}' \
  http://localhost:4200/vault/ANTHROPIC_API_KEY

Request Body

FieldTypeRequiredDescription
valuestringYesThe secret value to store (zeroized in memory after use)

Response (200)

{"key": "ANTHROPIC_API_KEY", "stored_at": "2026-06-04T10:00:00Z"}

Storing to an existing key rotates it live — the next credential resolution (e.g. the next LLM call) observes the new value with no daemon restart required.


DELETE /vault/{key}

Response (204 No Content)


  • auth.md — API keys and OIDC, KeyManagement (same admin-only bar as the vault)
  • README.md — error envelope and RBAC role table