feat(phase-3): workstream 6 — SOC 2 Type II Preparation
Implements all 22 WS6 tasks completing Phase 3 Enterprise. Column-level encryption (AES-256-CBC, Vault-backed key) via EncryptionService applied to credentials.secret_hash, credentials.vault_path, webhook_subscriptions.vault_secret_path, and agent_did_keys.vault_key_path. Backward-compatible: isEncrypted() guard skips decryption for existing plaintext rows until next read-write cycle. Audit chain integrity (CC7.2): AuditRepository computes SHA-256 Merkle hash on every INSERT (hash = SHA-256(eventId+timestamp+action+outcome+agentId+orgId+prevHash)). AuditVerificationService walks the full chain verifying hash continuity. AuditChainVerificationJob runs hourly; sets agentidp_audit_chain_integrity Prometheus gauge to 1 (pass) or 0 (fail). TLS enforcement (CC6.7): TLSEnforcementMiddleware registered as first middleware in Express stack; 301 redirect on non-https X-Forwarded-Proto in production. SecretsRotationJob (CC9.2): hourly scan for credentials expiring within 7 days; increments agentidp_credentials_expiring_soon_total. ComplianceController + routes: GET /audit/verify (auth+audit:read scope, 30/min rate-limit); GET /compliance/controls (public, Cache-Control 60s). ComplianceStatusStore: module-level map updated by jobs, consumed by controller. Prometheus: 2 new metrics (agentidp_credentials_expiring_soon_total, agentidp_audit_chain_integrity); 6 alerting rules in alerts.yml. Compliance docs: soc2-controls-matrix.md, encryption-runbook.md, audit-log-runbook.md, incident-response.md, secrets-rotation.md. Tests: 557 unit tests passing (35 suites); 26 new tests (EncryptionService, AuditVerificationService); 19 compliance integration tests. TypeScript clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
142
docs/compliance/secrets-rotation.md
Normal file
142
docs/compliance/secrets-rotation.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Secrets Rotation Runbook — SentryAgent.ai AgentIdP
|
||||
|
||||
**Control:** SOC 2 CC9.2 — Secrets Rotation
|
||||
**Last updated:** 2026-03-31
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
AgentIdP manages three categories of secrets that require periodic rotation:
|
||||
|
||||
1. **Agent client secrets** — Per-credential client secrets used for OAuth 2.0 token issuance
|
||||
2. **OIDC signing keys** — RSA/EC keys used to sign ID tokens
|
||||
3. **AES-256-CBC encryption key** — Column-level database encryption key (see `encryption-runbook.md`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Agent Credential (Client Secret) Rotation
|
||||
|
||||
### API endpoint
|
||||
|
||||
```
|
||||
POST /api/v1/agents/:agentId/credentials/:credentialId/rotate
|
||||
```
|
||||
|
||||
Requires Bearer token with `agents:write` scope.
|
||||
|
||||
### Procedure
|
||||
|
||||
```bash
|
||||
# 1. List active credentials for the agent
|
||||
curl -s -H "Authorization: Bearer <token>" \
|
||||
"https://api.sentryagent.ai/v1/agents/<agentId>/credentials?status=active"
|
||||
|
||||
# 2. Rotate the credential (generate new secret)
|
||||
curl -s -X POST \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"expiresAt": "2027-03-31T00:00:00.000Z"}' \
|
||||
"https://api.sentryagent.ai/v1/agents/<agentId>/credentials/<credentialId>/rotate"
|
||||
|
||||
# Response includes the new clientSecret — store it immediately; it is never shown again
|
||||
```
|
||||
|
||||
### Key points
|
||||
|
||||
- The new `clientSecret` is returned **once only** — store it securely before the response is discarded
|
||||
- The agent's previous secret is immediately invalidated (Vault KV v2 version overwritten)
|
||||
- An audit event `credential.rotated` is logged to the immutable audit chain
|
||||
- A `credential.rotated` webhook event is dispatched to all active subscriptions
|
||||
|
||||
### Recommended rotation schedule
|
||||
|
||||
| Credential type | Recommended rotation interval |
|
||||
|---|---|
|
||||
| Production agent credentials | 90 days |
|
||||
| Staging / development credentials | 180 days |
|
||||
| Service account credentials | 365 days (annual) |
|
||||
| Credentials involved in a security incident | Immediately |
|
||||
|
||||
### Automated expiry detection
|
||||
|
||||
`SecretsRotationJob` runs hourly and queries credentials expiring within 7 days.
|
||||
Prometheus alert `CredentialExpiryApproaching` fires immediately when any are detected.
|
||||
Respond to this alert by rotating the flagged credential(s) before the expiry date.
|
||||
|
||||
---
|
||||
|
||||
## 2. OIDC Signing Key Rotation
|
||||
|
||||
### Overview
|
||||
|
||||
OIDC signing keys are managed by `OIDCKeyService` (`src/services/OIDCKeyService.ts`).
|
||||
Keys are stored in the `oidc_keys` PostgreSQL table. The current active key is used to
|
||||
sign all new ID tokens; public keys are exposed via `GET /.well-known/jwks.json`.
|
||||
|
||||
### When to rotate
|
||||
|
||||
- Key compromise or suspected exposure
|
||||
- Scheduled rotation (recommended every 90 days for production)
|
||||
- Algorithm upgrade (e.g. RS256 → ES256)
|
||||
|
||||
### Rotation procedure
|
||||
|
||||
OIDC key rotation is handled automatically by `OIDCKeyService.ensureCurrentKey()`:
|
||||
|
||||
```bash
|
||||
# Force generation of a new signing key by calling the internal rotate endpoint
|
||||
# (or trigger by redeploying with OIDC_FORCE_KEY_ROTATION=true)
|
||||
|
||||
# 1. Mark current key as inactive (if manual rotation is required)
|
||||
psql "$DATABASE_URL" -c "
|
||||
UPDATE oidc_keys
|
||||
SET active = false
|
||||
WHERE active = true;"
|
||||
|
||||
# 2. Restart the application — ensureCurrentKey() will generate a new key on startup
|
||||
kubectl rollout restart deployment/agentidp
|
||||
```
|
||||
|
||||
### JWKS update behavior
|
||||
|
||||
- Old public keys remain in `GET /.well-known/jwks.json` for **24 hours** after rotation
|
||||
(grace period for in-flight tokens)
|
||||
- After the grace period, old keys are removed from the JWKS endpoint
|
||||
- Redis JWKS cache TTL is configured by `JWKS_CACHE_TTL_SECONDS` (default: 3600)
|
||||
|
||||
### Impact on existing tokens
|
||||
|
||||
Existing valid tokens signed with the old key **continue to work** until they expire,
|
||||
as long as the old public key remains in JWKS. After the grace period, old tokens
|
||||
will fail verification.
|
||||
|
||||
---
|
||||
|
||||
## 3. Encryption Key Rotation
|
||||
|
||||
See `docs/compliance/encryption-runbook.md` for the full AES-256-CBC encryption key rotation procedure.
|
||||
|
||||
**Summary:** Generate new 32-byte hex key → write to Vault at `ENCRYPTION_KEY_VAULT_PATH` → restart app → existing rows re-encrypted lazily on next read-write cycle.
|
||||
|
||||
---
|
||||
|
||||
## Schedule Recommendations
|
||||
|
||||
| Secret Type | Production Interval | Staging Interval | Trigger for Immediate Rotation |
|
||||
|---|---|---|---|
|
||||
| Agent client secrets | 90 days | 180 days | Credential suspected compromised |
|
||||
| OIDC signing keys | 90 days | 180 days | Key file exposed, algorithm upgrade |
|
||||
| AES-256-CBC encryption key | 365 days (annual) | On demand | Key exposed, Vault breach, compliance audit requirement |
|
||||
| Webhook HMAC secrets | Per customer policy | N/A | Webhook endpoint compromised |
|
||||
|
||||
---
|
||||
|
||||
## Compliance Evidence
|
||||
|
||||
For SOC 2 CC9.2 evidence collection:
|
||||
|
||||
- Prometheus metric history: `agentidp_credentials_expiring_soon_total`
|
||||
- Audit log entries with `action: credential.rotated` — query via `GET /audit?action=credential.rotated`
|
||||
- Key rotation records from Vault audit log
|
||||
- This runbook + sign-off from Security Engineering
|
||||
Reference in New Issue
Block a user