Key Management Policy
Key Management Policy
Project / Organization: ALAI Holding AS — Drop Payment App Policy Number: POL-SEC-KM-001 Version: 1.0 Date: 2026-02-23 Author: Security Architect Status: Draft Reviewers: CISO, CTO, DPO Next Review: 2027-02-23 Classification: Confidential — Restricted Distribution
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Security Architect | Initial draft — Drop key management for AWS KMS + Secrets Manager |
1. Purpose & Scope
Purpose: This policy defines the lifecycle management requirements for all cryptographic keys and secrets used by ALAI Holding AS for the Drop payment app, including generation, distribution, storage, usage, rotation, revocation, and destruction.
Regulatory basis:
- GDPR Art. 32 — appropriate technical measures for personal data
- Personopplysningsloven (LOV-2018-06-15-38) § 28
- IKT-forskriften (FOR-2003-05-21-630) §§ 5-6
- DORA (EU) 2022/2554 Art. 9(4)(d) — encryption key management
- Hvitvaskingsloven (LOV-2018-06-01-23) § 30 — KYC data protection at rest
Scope: All cryptographic keys and secrets in use across:
- Production, staging, and development environments for Drop
- AWS infrastructure (KMS, Secrets Manager, S3, App Runner)
- All employee and contractor workstations handling Confidential or Restricted data
- All third-party integrations where ALAI Holding AS holds keys
Policy Owner: CISO ([email protected]) Operational Owner: Security team
2. Key Inventory
2.1 Complete Key Taxonomy
| Key ID | Type | Algorithm | Purpose | Data Classification | Owner | Storage |
|---|---|---|---|---|---|---|
drop-national-id-key |
KEK |
AES-256-GCM | Restricted (L4) | Security team | AWS KMS (eu-north-1) | |
drop-db-master-key |
KEK (Database) | AES-256 XTS | PostgreSQL TDE — Phase 2 | Restricted (L4) | Security team | AWS KMS (eu-north-1) |
drop-kyc-key |
KEK (Object) | AES-256-GCM | KYC document encryption (S3 SSE-KMS) | Restricted (L4) | Security team | AWS KMS (eu-north-1) |
drop-backup-key |
KEK (Backup) | AES-256 | Database backup encryption | Restricted (L4) | Security team | AWS KMS (eu-west-1) — separate region |
JWT_SECRET |
Signing secret | HMAC-SHA-256 | JWT session token signing (HS256) | Confidential (L3) | Security team | AWS Secrets Manager |
TLS-EXT |
TLS Certificate | ECDSA P-256 | External HTTPS (getdrop.no) | — | Cloudflare / Let's Encrypt | Cloudflare managed |
BankID-cert |
TLS Certificate | RSA-2048 | BankID Norway |
— | BankID Norge AS CA | BankID managed |
SUMSUB-API-KEY |
API Key | HMAC-SHA-256 | Sumsub KYC API authentication | Confidential (L3) | Security team | AWS Secrets Manager |
DEK-* |
Data Encryption Keys | AES-256-GCM | Per-record |
Restricted (L4) | Application | Generated by AWS KMS GenerateDataKey |
2.2 Secrets Inventory (Non-KMS)AWS Secrets Manager)
| Secret Name | Purpose | Rotation | |
|---|---|---|---|
JWT_SECRET |
JWT token signing | Quarterly | |
SUMSUB_SECRET_KEY |
Sumsub KYC integration | Annual or on compromise | |
BANKID_CLIENT_SECRET |
BankID OIDC client secret | Per BankID schedule | |
DATABASE_URL |
PostgreSQL connection string | On DB credential rotation | |
SENTRY_DSN |
Sentry error monitoring | Annual |
3. Key Hierarchy
AWS KMS Root of Trust (eu-north-1 primary)primary │— ├──Stockholm)
|
+-- drop-national-id-key (AES-256-GCM — annual rotation)
│| └──+-- DEK-{user_id}-{timestamp}: Per-record envelope DEKs
│| └──+-- Encrypts: fødselsnummerfoedselsnummer field in users table
│| └──+-- Stored: base64(encryptedDEK || iv || tag || ciphertext)
│|
├──+-- drop-db-master-key (AES-256 XTS — annual rotation)
│| └──+-- Encrypts: PostgreSQL database at rest (Phase 2 — AWS RDS)
│|
├──+-- drop-kyc-key (AES-256-GCM — annual rotation)
│| └──+-- Encrypts: KYC document objects in AWS S3 (SSE-KMS)
│|
└──+-- AWS KMS Root of Trust (eu-west-1 — SEPARATE REGIONREGION)
for backups)
└──+-- drop-backup-key (AES-256 — annual rotation)
└──+-- Encrypts: All database backup files
AWS Secrets Manager ├──(eu-north-1)
+-- JWT_SECRET (HMAC-SHA-256 — quarterly rotation)
│| └──+-- Signs: Drop JWT session tokens (HS256 via jose ^6.1.3)
├──+-- SUMSUB_SECRET_KEY (API key — annualannual)
rotation)
├──+-- BANKID_CLIENT_SECRET (OIDC client secretsecret)
— BankID schedule)
└──+-- DATABASE_URL (connection string — on rotation)string)
Cloudflare / Let's Encrypt
└──+-- TLS Certificate (ECDSA P-256 — 90-day automated rotation)
└──+-- External HTTPS: getdrop.no
BankID Norge AS CA
└──+-- BankID Certificate (RSA-2048 — per BankID renewal schedule)
└──+-- Used for: BankID JWT signature verification (Phase 2)
4. Key Lifecycle
4.1 Lifecycle Overview
flowchart LR
GEN[Generation] -->|"AWS KMS CSPRNG"| DIST[Distribution]
DIST -->|"Runtime API call\nnever in env files"| STORE[Storage]
STORE -->|"KMS / Secrets Manager\naccess controlled"| USE[Usage]
USE -->|"Scheduled"| ROT[Rotation]
ROT -->|"New key active\noverlap period"| USE
ROT -->|"Old key retired"| ARCH[Archive / Revoke]
ARCH -->|"End of life"| DEST[Destruction]
REVOKE[Emergency Revocation] -->|"Compromise detected"| DEST
USE --> REVOKE
4.2 Generation
Entropy requirements:
- All keys MUST be generated using AWS KMS or a
cryptographically secure random number generator (CSPRNG)CSPRNG - NEVER use user-supplied passphrases, timestamps, UUIDs, or predictable values as keys
- NEVER generate keys in application code for Restricted data — use AWS KMS
GenerateKeyorGenerateDataKey
Approved key generation methods:
| Key Type | Generation Method | Tool |
|---|---|---|
| Symmetric (AES-256) KMS keys | KMS CreateKey API |
AWS KMS (FIPS 140-2 Level 3 HSMs) |
| Envelope DEKs | KMS GenerateDataKey API |
AWS KMS |
| JWT signing secret | crypto.randomBytes(32).toString('hex') |
Node.js CSPRNG |
| TLS certificates | ACME protocol | Cloudflare / Let's Encrypt |
| BankID certificates | BankID Norge AS CA | External CA |
Generation environment:
- All KMS-managed keys for Restricted data: generated within AWS KMS HSMs — key material never leaves AWS
- JWT_SECRET: generated using
crypto.randomBytes(32)then stored in AWS Secrets Manager - All key generation events logged in AWS CloudTrail
4.3 Distribution
Principles:
NeverNEVER transmit keys in plaintext over any channelNeverNEVER include keys in source code,.envfiles committed to Git,configuration files in VCS,or log outputNeverNEVER send keys via email, Slack, or any messaging platform- Always fetch secrets at application runtime from AWS Secrets Manager or KMS API
Distribution methods:
| Scenario | Method | Notes |
|---|---|---|
| Application runtime secrets (JWT_SECRET) | AWS Secrets Manager API at startup | Fatal error if JWT_SECRET missing |
| Envelope DEK generation | AWS KMS GenerateDataKey per encryption operation |
Key material decrypted in memory only |
| Developer access |
AWS IAM role + MFA | Least privilege |
| CI/CD pipeline secrets | GitHub Actions encrypted secrets ( |
Rotated per project |
4.4 Storage
Storage hierarchy for Drop:
Level 1 — AWS KMS HSMs (FIPS 140-2 Level 3)
├──+-- drop-national-id-key (fødselsnummerfoedselsnummer encryption)
├──+-- drop-db-master-key (database TDE — Phase 2)
├──+-- drop-kyc-key (KYC document S3 encryption)
└──+-- drop-backup-key (backup encryption — eu-west-1)
Level 2 — AWS Secrets Manager
├──+-- JWT_SECRET (HMAC signing key)
├──+-- SUMSUB_SECRET_KEY (API key)
├──+-- BANKID_CLIENT_SECRET (OIDC secret)
└──+-- DATABASE_URL (connection string)
Level 3 — Application memory (ephemeral only)
├──+-- Decrypted DEK during active encryption/decryption
operation
└──+-- JWT_SECRET during token signing/verification
NOTE: NEVER persist Level 3 to disk
Prohibited storage locations:
- Source code or config files in Git
(.env,config.ts,secrets.json) - Application logs or error messages (Sentry, BetterStack)
- Unencrypted database columns
- Email attachments or Slack messages
- Browser localStorage or sessionStorage
- Container image layers
(Dockerfile,.dockerignoreexclusions not sufficient)
4.5 Usage
Access control principles:
| Principle | Implementation |
|---|---|
| Least privilege | KMS key policies: encrypt-only for application, decrypt-only for compliance function |
| Separation of duties | key-admin cannot perform decryption; application service cannot create keys |
| Key purpose binding | drop-national-id-key used only for national ID encryption |
| Audit all access | Every KMS operation logged in AWS CloudTrail |
| Time-bound access | IAM role assumption with time-bound session tokens |
KMS key policy roles:
| IAM Role | Permissions | MFA Required |
|---|---|---|
drop-key-admin |
CreateKey, ScheduleKeyDeletion, RotateKey — NO Decrypt | YES |
drop-app-encrypt |
kms:Encrypt, kms:GenerateDataKey |
No (service account) |
drop-compliance-decrypt |
kms:Decrypt for drop-national-id-key only |
YES |
drop-key-auditor |
kms:ListKeys, kms:DescribeKey, CloudTrail read |
YES |
drop-app-runner |
kms:GenerateDataKey, kms:Decrypt (scoped per key) |
No (IAM |
4.6 Rotation Schedule
| Key | Rotation Period | Method | Owner | Alert if Overdue |
|---|---|---|---|---|
drop-national-id-key |
Annual | AWS KMS automatic key rotation | Security team | Yes — 7 days overdue |
drop-db-master-key |
Annual | AWS KMS automatic key rotation | Security team | Yes — 7 days overdue |
drop-kyc-key |
Annual | AWS KMS automatic key rotation | Security team | Yes — 7 days overdue |
drop-backup-key |
Annual | Manual + AWS KMS rotation | Security team | Yes — 7 days overdue |
JWT_SECRET |
Quarterly | Manual — generate new, deploy, rotate | Security team | Yes — 3 days overdue |
| TLS certificate (getdrop.no) | 90 days | Automated (Cloudflare / Let's Encrypt) | Platform | Yes — 14 days before expiry |
| BankID certificate | Per BankID schedule | Manual — BankID renewal process | Security team | Calendar reminder |
SUMSUB_SECRET_KEY |
Annual or on compromise | Manual via Sumsub dashboard | Security team | Yes — 7 days overdue |
JWT_SECRET rotation overlap procedure:
- Generate new
JWT_SECRETvalue - Deploy new secret to AWS Secrets Manager
- Rolling deploy of Drop app (picks up new secret)
- Wait for all existing sessions to expire (max 24h — JWT expiry)
- All sessions issued with old secret are now invalid
(— users re-authenticate)authenticate - Delete old secret version from Secrets Manager
AWS KMS automatic rotation: When enabled, AWS KMS generates new key material annually while retaining all prior versions for decryption of existing ciphertext. Re-encryption of existing data is required when performing cryptographic erasure (GDPR Art. 17).
4.7 Revocation Procedures
Trigger conditions for immediate revocation:
- Known or suspected key compromise (
e.g.,key material logged, unauthorized access) - Employee departure with
access tokey management IAMrolesrole access - System compromise where key was in use
Legal hold or regulatory requirement- Detection of unauthorized decryption
activityin CloudTrail logs
Emergency revocation procedure (target: < 1 hour):
Step 1: Alert Security Lead immediately via #security-incident Slack channel
Step 2: Identify scope — which data was accessible with compromised key?
Step 3: AWS KMS: Disable compromised key (prevents new encrypt/decrypt)
Step 4: Generate replacement key via KMS CreateKey
Step 5: Re-encrypt all data protected by compromised key (priority: Restricted first)
Step 6: Update KMS key references in application configuration
Step 7: Deploy updated application configuration
Step 8: Verify all systems using new key
Step 9: Audit CloudTrail: What decrypt operations used the compromised key in last 30 days?
Step 10: Assess breach notification requirement (see data-breach-response-plan.md §5)
-> GDPR Art. 33 / Personopplysningsloven § 32: 72h to Datatilsynet if personal data affected
-> Finanstilsynet notification if payment data affected
Step 11: Document in incident log
Step 12: Post-mortem within 48 hours
JWT_SECRET emergency rotation (session compromise):
Step 1: Generate new JWT_SECRET immediately
Step 2: Deploy to AWS Secrets Manager
Step 3: Rolling deploy Drop application — all in-flight sessions invalidated
Step 4: All users must re-authenticate
(acceptable security impact)
Step 5: Document in incident log
4.8 Destruction
When destruction is required:
- Key
has beenrotated out and overlap period expired(KMS: schedule deletion with 30-day waiting period) - Crypto-shredding: deleting user
datafoedselsnummer by destroyingtheenvelope DEKencrypted under(GDPR Art.drop-national-id-key17 erasure mechanism)17) - System decommissioned
Destruction methods:
| Key Location | Destruction Method | Verification |
|---|---|---|
| AWS KMS key | ScheduleKeyDeletion (7-30 day waiting period) |
KMS + CloudTrail audit log |
| AWS Secrets Manager | DeleteSecret (7-30 day waiting period) |
Secrets Manager audit log |
| Envelope DEK in database | Delete encrypted DEK field | Verify decryption fails |
Crypto-shredding for GDPR erasure (Art. 17): When a user requests account deletion:
- The envelope DEK for that user's
fødselsnummerfoedselsnummer is deleted from the database - The ciphertext (
fødselsnummer)foedselsnummer) becomes permanently unreadable - AML retention: transaction records retained 5 years
butperuser'sHvitvaskingslovenfødselsnummer§is unrecoverable30
5. Key Management System
Primary KMS: AWS KMS (eu-north-1 — Stockholm region)
Backup KMS for backups:KMS: AWS KMS (eu-west-1 — Ireland) for drop-backup-key only — separate region for disaster recovery
Secrets management: AWS Secrets Manager (eu-north-1)
Multi-region note: KMS keys are single-region by default. drop-backup-key is intentionally in a separate region (eu-west-1) so that a regional outage does not prevent backup restoration.
6. Access Controls for Key Operations
Separation of duties requirements:
| Operation | Required Roles | Approval Process |
|---|---|---|
| Create new KMS key | drop-key-admin + CISO approval |
AWS IAM + ticket |
| Rotate KMS key (manual) | drop-key-admin (proposer) + CISO (approver) |
Dual approval |
| Emergency key disable | Security Lead (any one) | Single — immediate incident ticket |
| Schedule key deletion | drop-key-admin + CISO |
Dual approval — minimum 7-day waiting period |
| Grant new service access |
Security Lead + system owner | IAM PR review + approval |
| Rotate JWT_SECRET | Security Lead | Single — with deployment coordination |
7. Audit Logging for Key Operations
All key operations logged in AWS CloudTrail, including:
Key creation (actor, key ID, key type, timestamp)Encryption/decryption (actor, key ID, context, timestamp)Key rotation (actor, old key version, new key version, timestamp)Key disabling / deletion scheduling (actor, key ID, reason, timestamp)Access grant/revoke (actor, grantee, key ID, permissions, timestamp)Failed access attempts (actor, key ID, reason, timestamp)
Log destination: AWS CloudTrail → S3 (immutable, append-only) + BetterStack aggregation
Log retention: 5 years (AML compliance minimum — Hvitvaskingsloven § 30)
Alert on:
Decrypt operations by unexpected IAM roleFailed KMS access attempts > 3 in 5 minutesOff-hours access todrop-national-id-keyordrop-kyc-keyAnyScheduleKeyDeletionevent (immediate alert)
8. FødselsnummerFoedselsnummer Field Encryption — Implementation Pattern
Key: drop-national-id-key (AWS KMS — separate from database master key)
Stored: Only AES-256-GCM ciphertext — never plaintext fødselsnummerfoedselsnummer in database
Source: src/drop-app/src/lib/encrypt.ts (Phase 2 implementation)
// Envelope encryption for fødselsnummer — src/drop-app/src/lib/encrypt.ts (Phase 2)foedselsnummer
// Key: drop-national-id-key (AWS KMS — separate from db master)KMS)
// Stored: base64(encryptedDEK || iv || tag || ciphertext)
async function encryptNationalId(fodselsnummer: string): Promise<string> {
const iv = crypto.randomBytes(12); // 96-bit random IV — never reused
const dek = await kmsClient.generateDataKey({
KeyId: process.env.NATIONAL_ID_KEY_ARN,
KeySpec: 'AES_256'
});
const cipher = crypto.createCipheriv('aes-256-gcm', dek.Plaintext, iv);
const ciphertext = Buffer.concat([cipher.update(fodselsnummer, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
dek.Plaintext.fill(0); // Securely zeroZero DEK plaintext from memory after use
dek.Plaintext.fill(0);
// Return: base64(encryptedDEK || iv || tag || ciphertext)
return Buffer.concat([dek.CiphertextBlob, iv, tag, ciphertext]).toString('base64');
}
async function decryptNationalId(encrypted: string): Promise<string> {
const buf = Buffer.from(encrypted, 'base64');
// AWS KMS CiphertextBlob is always 184 bytes for AES_256 DEK
const encryptedDek = buf.subarray(0, 184);
const iv = buf.subarray(184, 196); // 12 bytes
const tag = buf.subarray(196, 212); // 16 bytes
const ciphertext = buf.subarray(212);
const dek = await kmsClient.decrypt({ CiphertextBlob: encryptedDek });
const decipher = crypto.createDecipheriv('aes-256-gcm', dek.Plaintext, iv);
decipher.setAuthTag(tag);
const plaintext = decipher.update(ciphertext) + decipher.final('utf8');
dek.Plaintext.fill(0);
return plaintext;
}
Access restriction: Only the Compliance function (KYC verification, AML reporting) has IAM access to drop-compliance-decrypt role. Application code cannot decrypt fødselsnummerfoedselsnummer except during explicit compliance workflows.
8. Audit Logging for Key Operations
All key operations logged in AWS CloudTrail, including:
- Key creation (actor, key ID, key type, timestamp)
- Encryption/decryption (actor, key ID, context, timestamp)
- Key rotation (actor, old key version, new key version, timestamp)
- Key disabling / deletion scheduling (actor, key ID, reason, timestamp)
- Failed access attempts (actor, key ID, reason, timestamp)
Log destination: AWS CloudTrail → S3 (immutable, append-only) + BetterStack aggregation Log retention: 5 years (AML compliance — Hvitvaskingsloven § 30) Alert on:
- Decrypt operations by unexpected IAM role
- Failed KMS access attempts > 3 in 5 minutes
- Off-hours access to
drop-national-id-keyordrop-kyc-key - Any
ScheduleKeyDeletionevent (immediate alert)
9. Exception Process
Exceptions are not permitted for: Restricted (L4) data (fødselsnummer,foedselsnummer, KYC documents) — no exceptions to field-level encryption requirement.encryption.
Exception request process:
- Submit request to: [email protected]
- Required: system, data classification, key being excepted, business justification, risk assessment, compensating controls,
proposed exceptionduration (max 12 months) Approval required from:Approval: CISOExceptions loggedLogged in: compliance register- Review: Quarterly — exceptions
exceeding> 12 months require board-level approval
Active exceptions:
| System | Exception | Expiry | Compensating Controls |
|---|---|---|---|
| JWT signing (HS256 shared secret) | Shared-secret JWT instead of asymmetric RS256/EdDSA | Phase 2 migration | JWT_SECRET in AWS Secrets Manager; 24h session expiry; server-side revocation table |
| SQLite MVP (no column-level TDE) | Filesystem encryption |
Phase 2 migration | Full disk encryption; private network; no public DB access |
10. Emergency Procedures — Key Compromise
Immediate response checklist (execute within 1 hour of suspected compromise):
□ Activate incident response — notify: Security Lead + CISO + CTO
□ Identify scope: Which data was accessible with compromised key?
□ AWS KMS: Disable compromised key immediately
□ For JWT_SECRET compromise: rotate immediately (all sessions invalidated)
□ Generate replacement key
□ Re-encrypt all data protected by compromised key (Restricted data first)
□ Audit CloudTrail: What was decrypted using the compromised key in last 30 days?
□ Assess breach notification requirement (data-breach-response-plan.md §5)
→-> GDPR Art. 33 / Personopplysningsloven § 32: 72h to Datatilsynet
if personal data affected
→-> Finanstilsynet notification if payment data affected
□ Document everything in incident log
□ Post-mortem within 48 hours
Re-encryption priority:
FødselsnummerFoedselsnummer (drop-national-id-key) — Restricted (L4), highest priority- KYC documents (
drop-kyc-key) — Restricted (L4) - Database backup files (
drop-backup-key) — Restricted (L4) - Database master (
drop-db-master-key) — Phase 2
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Security Architect | 2026-02-23 | |
| CISO | |||
| CTO | |||
| DPO (GDPR relevance) | |||
| Management |