- Replace all docker-compose.yml/docker-compose.monitoring.yml references with compose.yaml/compose.monitoring.yaml (modern Compose Spec naming) - Replace all `docker-compose` CLI commands with `docker compose` (plugin syntax) - Update Dockerfile stage descriptions: node:18-alpine → node:20.11-bookworm-slim, built-in node user → explicit nodeapp:1001 non-root user - Update image version references: postgres:14-alpine → postgres:14.12-alpine3.19, redis:7-alpine → redis:7.2-alpine3.19 - Externalize postgres credentials: hardcoded values → POSTGRES_USER/PASSWORD/DB env vars - Externalize Grafana admin password: hardcoded 'agentidp' → GF_ADMIN_PASSWORD env var - Add Docker Compose Variables section to environment-variables.md (POSTGRES_*, GF_ADMIN_PASSWORD) - Update local-development.md Step 3: cp .env.example .env, document POSTGRES_* purpose - Update quick-start.md: cp .env.example .env, use awk/sed for JWT key injection - Update 07-dev-setup.md: remove 'no .env.example' claim, reference cp .env.example - Update docker-compose.yml key file description in 04-codebase-structure.md - Update monitoring overlay launch commands across all docs (compose.yaml + compose.monitoring.yaml) - Update volume names to kebab-case: postgres_data → postgres-data, redis_data → redis-data - Fix compliance encryption-runbook: docker-compose restart agentidp → docker compose restart app All docs now consistent with compose.yaml in repo root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.5 KiB
Encryption Key Rotation Runbook — SentryAgent.ai AgentIdP
Control: SOC 2 CC6.1 — Encryption at Rest
Service: src/services/EncryptionService.ts
Vault path: Configured via ENCRYPTION_KEY_VAULT_PATH env var (default: secret/data/agentidp/encryption-key)
Overview
AgentIdP uses AES-256-CBC column-level encryption for sensitive PostgreSQL columns.
The encryption key is a 64-character hex string (32 bytes) stored in HashiCorp Vault.
The EncryptionService fetches the key once and caches it in process memory.
Encrypted format: base64(IV):base64(ciphertext) where IV is 16 random bytes per encryption call.
Key Rotation Procedure
Prerequisites
- Access to HashiCorp Vault with write permissions to the encryption key path
- Access to the production application environment (to trigger restart)
- At least one backup of the current key stored securely offline
Step 1: Generate a New Key
Generate a cryptographically strong 32-byte (64-character hex) key:
openssl rand -hex 32
# Example output: a1b2c3d4e5f6... (64 hex chars)
Record the new key securely.
Step 2: Backup the Current Key
Before overwriting, read and securely store the current key:
vault kv get -field=encryptionKey secret/agentidp/encryption-key > /secure/backup/encryption-key-$(date +%Y%m%d).txt
Store in a hardware security module (HSM) or offline key store.
Step 3: Write the New Key to Vault
vault kv put secret/agentidp/encryption-key encryptionKey="<new-64-char-hex-key>"
Verify the write:
vault kv get secret/agentidp/encryption-key
Confirm the encryptionKey field contains exactly 64 hex characters.
Step 4: Restart the Application
The EncryptionService caches the key in process memory. A restart forces a re-fetch from Vault:
# Kubernetes rolling restart
kubectl rollout restart deployment/agentidp
# Docker Compose
docker compose restart app
# PM2
pm2 restart agentidp
Step 5: Verify Key Pick-Up
Check the application logs for:
[AgentIdP] EncryptionService enabled — sensitive columns encrypted at rest (SOC 2 CC6.1)
Call the compliance controls endpoint to confirm the control is passing:
curl -s https://api.sentryagent.ai/v1/compliance/controls | jq '.controls[] | select(.id == "CC6.1")'
Expected output:
{ "id": "CC6.1", "name": "Encryption at Rest", "status": "passing", "lastChecked": "..." }
Step 6: Re-encryption of Existing Rows
Existing rows encrypted with the old key will fail to decrypt after key rotation. Re-encryption happens lazily: the next time each row is read and re-written (e.g. credential rotation, webhook update), the application will decrypt with the old key and re-encrypt with the new one.
For immediate full re-encryption, use the re-encryption script:
# Run the re-encryption migration script (reads old key from backup, encrypts with new key)
# Note: This script requires both old and new keys to be available
ts-node scripts/reencrypt-columns.ts --old-key-file /secure/backup/encryption-key-<date>.txt
Emergency Rollback
If the new key causes issues (e.g. test failures, decryption errors), roll back:
Step 1: Restore Old Key to Vault
vault kv put secret/agentidp/encryption-key encryptionKey="<old-64-char-hex-key-from-backup>"
Step 2: Restart the Application
kubectl rollout restart deployment/agentidp
Step 3: Verify Recovery
curl -s https://api.sentryagent.ai/v1/compliance/controls | jq '.controls[] | select(.id == "CC6.1")'
Step 4: Investigate Root Cause
Review application logs for AES-256-CBC decryption failed errors and audit the cause before
reattempting rotation.
Troubleshooting
| Symptom | Likely Cause | Resolution |
|---|---|---|
Invalid encryption key ... expected a 64-character hex string |
Key in Vault is wrong length or encoding | Re-write correct key to Vault, restart |
AES-256-CBC decryption failed — possible key mismatch |
Key rotated but rows still encrypted with old key | Rollback to old key, then migrate properly |
CC6.1 status shows unknown |
Vault unreachable, key fetch failed | Check Vault connectivity, VAULT_ADDR, VAULT_TOKEN |
Audit Evidence
After rotation, record the following for SOC 2 evidence:
- Date of rotation
- Who performed the rotation (approver + executor)
- Vault audit log entry confirming the key write
- Application log confirming EncryptionService initialised with new key
GET /compliance/controlsresponse showing CC6.1 = passing