Key Management Policy
Key Management Policy
Project / Organization:
BilkoALAI Holding AS —BalkanDropAccountingPaymentSaaSApp Policy Number: POL-SEC-KM-001 Version: 1.0 Date: 2026-02-23 Author:CTOSecurity Architect Status: Draft Reviewers:DPO,CISO,EngineeringCTO,LeadDPO Next Review: 2027-02-23 Classification: Confidential — Restricted Distribution
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Initial draft — Drop key management |
1. Purpose & Scope
Purpose: This policy defines the lifecycle management requirements for all cryptographic keys and secrets used by Bilko.ALAI ItHolding coversAS keyfor 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 Bilkocryptographic productionkeys and stagingsecrets environments.in use across:
- Production, staging, and development environments for Drop
- AWS infrastructure (KMS, Secrets Manager, S3, App Runner)
- All
personnelemployeewithandaccesscontractortoworkstationsRailwayhandlingenvironment variablesConfidential orVaultwarden.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 | Algorithm | Purpose | Owner | Storage | ||||
|---|---|---|---|---|---|---|---|---|
drop-national-id-key |
AWS KMS (eu-north-1) | |||||||
drop-db-master-key |
AWS KMS (eu-north-1) | |||||||
drop-kyc-key |
AWS KMS (eu-north-1) | |||||||
drop-backup-key |
AWS KMS (eu-west-1) — separate region | |||||||
JWT_SECRET |
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 JWT signature verification | — | 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 envelope DEKs | Restricted (L4) | Application | Generated by AWS KMS GenerateDataKey |
2.2 Secrets Inventory (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 | |||
SENTRY_DSN |
|||||
3. Key Hierarchy
graphAWS TDKMS ROOT["Root Secrets\n(CTOof personalTrust Vaultwarden(eu-north-1 vault)"]primary RAILWAY["Railway— EnvironmentStockholm)
Secrets\n(production|
+-- drop-national-id-key (AES-256-GCM — annual rotation)
| +-- DEK-{user_id}-{timestamp}: Per-record envelope DEKs
| +-- Encrypts: foedselsnummer 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 REGION)
+-- 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 — annual)
+-- BANKID_CLIENT_SECRET (OIDC client secret)
+-- DATABASE_URL (connection string)
Cloudflare / stagingLet's /Encrypt
dev)"]
ORG_SECRETS["Per-Organization Secrets\n(DB encrypted, L4 Restricted)\nSEF API keys, FINA certs"]
APP["Application Runtime\n(keys loaded from env at startup)"]
ROOT +-->|"Provision"| RAILWAYTLS RAILWAYCertificate (ECDSA P-256 — 90-day automated rotation)
+-->|"Load atExternal boot"|HTTPS: APPgetdrop.no
ROOTBankID Norge AS CA
+-->|"Rotation authority"|BankID ORG_SECRETSCertificate ORG_SECRETS(RSA-2048 — per BankID renewal schedule)
+-->|"Decrypt onUsed request"|for: APPBankID JWT signature verification (Phase 2)
Principle: No key material is ever committed to source code. No key is stored in plaintext outside Railway secrets or Vaultwarden.
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 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 | |
|---|---|---|
KMS |
||
KMS |
||
|
||
protocol |
||
Commands:Generation environment:
# Generate JWT key pair
openssl genrsa -out jwt_private.pem 2048
openssl rsa -in jwt_private.pem -pubout -out jwt_public.pem
# Generate AES-256 field encryption key
openssl rand -hex 32
# Generate HMAC key
openssl rand -hex 32
All generated keys must be imported to Railway and Vaultwarden within 1 hour. Local files deleted securely after import.
5. Key Storage
Production Keys (Railway)
- All
productionKMS-managed keysstoredforasRestrictedRailwaydata:environmentgeneratedvariableswithin RailwayAWSEUKMSWest regionHSMs —encrypted at rest by Railway (AES-256)Access: CTO + one designated backup (CEO) onlyTwo-factor authentication mandatory for Railway accountRailway account uses ALAI SSO / strong password (≥20 chars, in Vaultwarden)
Staging/Dev Keys
Separate Railway project (staging) — different keys from productionDev:.env.localfiles excluded from git via.gitignoreDev keys may use weaker entropy but must still be valid format
Vaultwarden (Backup & Documentation)
URL: https://vault.basicconsulting.noStores: productionkey materialasneversecureleavesnotes (encrypted)AWSAccess:JWT_SECRET:CTOgenerated+usingCEOcrypto.randomBytes(32)(break-glassthenaccess)stored in AWS Secrets ManagerPurpose:AllRecoverykeyifgenerationRailwayeventssecrets are lost; rotation documentation
Per-Organization Secrets (SEF API Keys, FINA Certificates)
Storedlogged inPostgreSQLAWSOrganizationSecrettableValue encrypted with FIELD_ENCRYPTION_KEY before storageDecrypted in-memory only when needed for API callFINA private keys additionally protected with password (stored separately)CloudTrail
6.4.3 Key Rotation Procedures
6.1 Annual Rotation (Standard)Distribution
Schedule:Principles:
- NEVER
oftransmiteachkeyscalendarinyear.plaintext over any channel - NEVER include keys in source code,
.envfiles committed to Git, or log output - NEVER send keys via email, Slack, or any messaging platform
- Always fetch secrets at application runtime from AWS Secrets Manager or KMS API
FIELD_ENCRYPTION_KEYDistribution rotationmethods:
| Scenario | Method | Notes |
|---|---|---|
| Application runtime secrets ( |
AWS |
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 (scoped) | Rotated per project |
4.4 Storage
Storage hierarchy for Drop:
1.Level Generate1 new— FIELD_ENCRYPTION_KEYAWS KMS HSMs (opensslFIPS rand140-2 Level 3)
+-hex- 32)
2. Deploy a migration job that:
a. Reads each encrypted field with old drop-national-id-key b.(foedselsnummer Decryptsencryption)
c.+-- Re-encrypts with new drop-db-master-key d.(database WritesTDE back— 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
+-- JWT_SECRET during token signing/verification
NOTE: NEVER persist Level 3 to DB
3. Migration must be atomic per record (read old → write new in transaction)
4. Only after 100% migration: update Railway secret to new key
5. Delete old key from Vaultwarden (add to archive note with date)
6. Test: attempt decryption with both old (should fail) and new (should succeed) keysdisk
JWTProhibited storage locations:
- Source code or config files in Git
- Application logs or error messages (Sentry, BetterStack)
- Unencrypted database columns
- Email attachments or Slack messages
- Browser localStorage or sessionStorage
- Container image layers
4.5 Usage
Access control principles:
| Principle | Implementation |
|---|---|
| Least privilege | KMS key |
| 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 |
|---|---|---|
|
CreateKey, |
YES |
drop-app-encrypt |
kms:Encrypt, kms:GenerateDataKey |
No ( |
drop-compliance-decrypt |
kms:Decrypt for drop-national-id-key |
YES |
drop-key-auditor |
kms:ListKeys, kms:DescribeKey, CloudTrail read |
YES |
drop-app-runner |
kms:GenerateDataKey, kms:Decrypt ( |
No ( |
6.2 Emergency4.6 Rotation (On Compromise)Schedule
If a key is suspected compromised:
Immediatelyinvalidate: all user sessions (clear RefreshToken table)Generate new key within 15 minutesUpdate Railway secretDeploy new application instance (Railway auto-deploys on env var change)Document in Vaultwarden: old key, date of compromise, date of rotationAssess whether breach notification is required (see data-breach-response-plan.md)
6.3 FINA Certificate (HR-FISK) Rotation
FINA X.509 certificates for HR-FISK e-invoicing have a defined validity period (1-3 years per FINA PKI).
1. FINA certificate expiry alert fires 60 days before expiry
2. Organization admin is notified to renew via FINA portal
3. New certificate uploaded through Bilko settings → HR eRačun → Certificate
4. Old certificate archived (not deleted — needed to verify past submissions)
5. Test: submit a test e-invoice via HR-FISK test environment with new certificate
7. Key Access Control
| Key | Owner | Alert if Overdue | ||
|---|---|---|---|---|
drop-national-id-key |
Security |
Yes — 7 days overdue | ||
drop-db-master-key |
Security |
Yes — 7 days overdue | ||
drop-kyc-key |
Security |
Yes — 7 days overdue | ||
drop-backup-key |
Manual + |
Yes |
||
JWT_SECRET |
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 |
AccessJWT_SECRET log: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
- Delete old secret version from Secrets Manager
AWS KMS automatic rotation: AWS KMS generates new key material annually while retaining all prior versions for decryption of existing ciphertext.
4.7 Revocation Procedures
Trigger conditions for immediate revocation:
- Known or suspected key compromise (key material logged, unauthorized access)
- Employee departure with key management IAM role access
- System compromise where key was in use
- Detection of unauthorized decryption in CloudTrail logs
Emergency revocation procedure (target: < 1 hour):
Step 1: Alert Security Lead 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 (Restricted first)
Step 6: Update KMS key references in application configuration
Step 7: Deploy updated configuration
Step 8: Verify all systems using new key
Step 9: Audit CloudTrail: What decrypt operations used compromised key in last 30 days?
Step 10: 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
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 Railwayusers secretmust viewsre-authenticate
loggedStep 5: Document in Railwayincident log
4.8 Destruction
When destruction is required:
- Key rotated out and overlap period expired
- Crypto-shredding: deleting user foedselsnummer by destroying envelope DEK (GDPR Art. 17)
- System decommissioned
Destruction methods:
| Key Location | Destruction Method | Verification |
|---|---|---|
| AWS KMS key | ScheduleKeyDeletion (7-30 day waiting period) |
KMS + CloudTrail audit |
| 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 foedselsnummer is deleted from the database
- The ciphertext (foedselsnummer) becomes permanently unreadable
- AML retention: transaction records retained 5 years per Hvitvaskingsloven § 30
5. Key Management System
Primary KMS: AWS KMS (eu-north-1 — Stockholm region)
Backup 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)
6. Access Controls for Key Operations
| 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. Foedselsnummer Field Encryption — Implementation Pattern
Key: drop-national-id-key (AWS KMS — separate from database master key)
Stored: Only AES-256-GCM ciphertext — never plaintext foedselsnummer in database
Source: src/drop-app/src/lib/encrypt.ts (Phase 2 implementation)
// Envelope encryption for foedselsnummer
// Key: drop-national-id-key (AWS 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); // Zero DEK plaintext from memory after use
// Return: base64(encryptedDEK || iv || tag || ciphertext)
return Buffer.concat([dek.CiphertextBlob, iv, tag, ciphertext]).toString('base64');
}
Access restriction: Only the Compliance function has IAM access outsideto normaldrop-compliance-decrypt deploymentrole. isApplication reviewedcode bycannot CTO.decrypt foedselsnummer except during explicit compliance workflows.
8. EscrowAudit &Logging Recoveryfor Key Operations
FIELD_ENCRYPTION_KEY Escrow (Critical)
The FIELD_ENCRYPTION_KEY is the most criticalAll key —operations losslogged meansin permanentAWS lossCloudTrail, of all L4 Restricted field data (tax IDs, IBAN).
Escrow procedure:including:
FIELD_ENCRYPTION_KEYKeystoredcreationin(actor,VaultwardenkeysecureID,notekeyaccessibletype,to: CTO, CEOtimestamp)Vaultwarden has its own backupEncryption/decryption (seeactor,systemkeyinfrastructureID,docs)context, timestamp)- Key
material noted with: creation date,rotationdate,(actor,descriptionold key version, new key version, timestamp) - Key disabling / deletion scheduling (actor, key ID, reason, timestamp)
- Failed access attempts (actor, key ID, reason, timestamp)
IfLog FIELD_ENCRYPTION_KEY is lost and not recoverable:destination: AllAWS encryptedCloudTrail field→ dataS3 is(immutable, permanentlyappend-only) unreadable.+ ThisBetterStack isaggregation
aLog catastrophicretention: data5 lossyears event.(AML Contactcompliance legal— counselHvitvaskingsloven and§ affected30)
supervisoryAlert authorities.on:
Railway Account Recovery
RailwayDecryptrootoperationsaccount:by[email protected]unexpected(passwordIAM role- Failed KMS access attempts > 3 in
Vaultwarden)5 minutes 2FA recovery codes: Vaultwarden secure noteDesignated backup access: CEO has viewOff-hours access toRailwaydrop-national-id-keyordrop-kyc-key- Any
ScheduleKeyDeletionevent (read-only)immediate alert)
9. KeyException DestructionProcess
WhenExceptions anot keypermitted isfor: retiredRestricted (supersededL4) bydata rotation):(foedselsnummer, KYC documents) — no exceptions to field-level encryption.
Exception request process:
RemoveSubmitfromrequestRailwayto:environment variables[email protected]RemoveRequired:fromsystem,activedataVaultwardenclassification,entrieskey being excepted, business justification, risk assessment, compensating controls, duration (max 12 months)ArchiveApproval: CISO- Logged in: compliance register
- Review: Quarterly — exceptions > 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 only | 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 compromised key in last 30 days?
□ Assess breach notification requirement (data-breach-response-plan.md §5)
-> GDPR Art. 33 / Personopplysningsloven § 32: 72h to VaultwardenDatatilsynet
secure-> note:Finanstilsynet "Retirednotification Keys"if withpayment datedata andaffected
reason□ Document everything in incident log
□ Post-mortem within 48 hours
Re-encryption priority:
- Foedselsnummer (
drop-national-id-key) — Restricted (L4), highest priority OldKYCFIELD_ENCRYPTION_KEY versions: retained for 3 months after rotationdocuments (indrop-kyc-key)case—rollbackRestrictedneeded),(L4)- Database
permanentlybackupdeletedfilesfrom(drop-backup-key)Vaultwarden— Restricted (L4) - Database master (
drop-db-master-key) — Phase 2
Approval
| Role | Name | |||
|---|---|---|---|---|
| Author | Architect | 2026-02-23 | ||
| Management |