Files
sentryagent-idp/docs/compliance/encryption-runbook.md
SentryAgent.ai Developer f9a6a8aafb docs(devops): update all documentation for DockerSpec compliance
- 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>
2026-04-08 08:27:37 +00:00

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/controls response showing CC6.1 = passing