Sync docs and fix flaky renewal e2e test

- ARCHITECTURE.md: add Vault Endpoints section, /unseal UI page,
  vault_sealed/vault_unsealed audit events, sealed interceptor in
  gRPC chain
- openapi.yaml: add /v1/vault/{status,unseal,seal} endpoints, update
  /v1/health sealed-state docs, add VaultSealed response component,
  add vault audit event types and Admin — Vault tag
- web/static/openapi.yaml: kept in sync with root
- test/e2e: increase renewal test token lifetime from 2s to 10s
  (sleep 6s) to eliminate race between token expiry and HTTP round-trip

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 00:39:41 -07:00
parent d87b4b4042
commit b1b52000c4
3 changed files with 164 additions and 9 deletions

View File

@@ -307,6 +307,18 @@ components:
error: rate limit exceeded
code: rate_limited
VaultSealed:
description: |
The vault is sealed. The server is running but has no key material.
Unseal via `POST /v1/vault/unseal` before retrying.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: vault is sealed
code: vault_sealed
paths:
# ── Public ────────────────────────────────────────────────────────────────
@@ -314,12 +326,17 @@ paths:
/v1/health:
get:
summary: Health check
description: Returns `{"status":"ok"}` if the server is running. No auth required.
description: |
Returns server health status. Always returns HTTP 200, even when the
vault is sealed. No auth required.
When the vault is sealed, `status` is `"sealed"` and most other
endpoints return 503. When healthy, `status` is `"ok"`.
operationId: getHealth
tags: [Public]
responses:
"200":
description: Server is healthy.
description: Server is running (check `status` for sealed state).
content:
application/json:
schema:
@@ -327,6 +344,7 @@ paths:
properties:
status:
type: string
enum: [ok, sealed]
example: ok
/v1/keys/public:
@@ -369,6 +387,121 @@ paths:
description: Base64url-encoded public key bytes.
example: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo
/v1/vault/status:
get:
summary: Vault seal status
description: |
Returns whether the vault is currently sealed. Always accessible,
even when sealed. No auth required.
Clients should poll this after startup or after a 503 `vault_sealed`
response to determine when to attempt an unseal.
operationId: getVaultStatus
tags: [Public]
responses:
"200":
description: Current vault seal state.
content:
application/json:
schema:
type: object
required: [sealed]
properties:
sealed:
type: boolean
example: false
/v1/vault/unseal:
post:
summary: Unseal the vault
description: |
Provide the master passphrase to derive the encryption key, decrypt
the Ed25519 signing key, and unseal the vault. Once unsealed, all
other endpoints become available.
Rate limited to 3 requests per second per IP (burst 5) to limit
brute-force attempts against the passphrase.
The passphrase is never logged. A generic `"unseal failed"` error
is returned for any failure (wrong passphrase, vault already unsealed
mid-flight, etc.) to avoid leaking information.
operationId: unsealVault
tags: [Public]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [passphrase]
properties:
passphrase:
type: string
description: Master passphrase used to derive the encryption key.
example: correct-horse-battery-staple
responses:
"200":
description: Vault unsealed (or was already unsealed).
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [unsealed, already unsealed]
example: unsealed
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Wrong passphrase or key decryption failure.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: unseal failed
code: unauthorized
"429":
$ref: "#/components/responses/RateLimited"
/v1/vault/seal:
post:
summary: Seal the vault (admin)
description: |
Zero all key material in memory and transition the server to the
sealed state. After this call:
- All subsequent requests (except health, vault status, and unseal)
return 503 `vault_sealed`.
- The caller's own JWT is immediately invalidated because the public
key needed to verify it is no longer held in memory.
- The server can be unsealed again via `POST /v1/vault/unseal`.
This is an emergency operation. Use it to protect key material if a
compromise is suspected. It does **not** restart the server or wipe
the database.
operationId: sealVault
tags: [Admin — Vault]
security:
- bearerAuth: []
responses:
"200":
description: Vault sealed (or was already sealed).
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [sealed, already sealed]
example: sealed
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/v1/auth/login:
post:
summary: Login
@@ -1148,7 +1281,7 @@ paths:
`pgcred_accessed`, `pgcred_updated`, `pgcred_access_granted`,
`pgcred_access_revoked`, `tag_added`, `tag_removed`,
`policy_rule_created`, `policy_rule_updated`, `policy_rule_deleted`,
`policy_deny`.
`policy_deny`, `vault_sealed`, `vault_unsealed`.
operationId: listAudit
tags: [Admin — Audit]
security:
@@ -1530,3 +1663,5 @@ tags:
description: Requires admin role.
- name: Admin — Policy
description: Requires admin role. Manage policy rules and account tags.
- name: Admin — Vault
description: Requires admin role. Emergency vault seal operation.