Key Management Policy
Key Management Policy
Project /Organization:ALAI Holding ASBilko —DropBalkanPaymentAccountingAppSaaS Policy Number: POL-SEC-KM-001 Version: 1.0 Date: 2026-02-23 Author:Security ArchitectCTO Status: Draft Reviewers:CISO,DPO,CTO,EngineeringDPONext Review:2027-02-23Lead Classification: Confidential — RestrictedDistribution
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Initial |
1. Purpose & Scope
Purpose: This policy defines the lifecycle management requirements for all cryptographic keys and secrets used by ALAIBilko. HoldingIt AScovers for the Drop payment app, includingkey generation, distribution, storage, usage, rotation, revocation, and destruction.
Regulatory basis:
GDPR Art. 32 — appropriate technical measures for personal dataPersonopplysningsloven (LOV-2018-06-15-38) § 28IKT-forskriften (FOR-2003-05-21-630) §§ 5-6DORA (EU) 2022/2554 Art. 9(4)(d) — encryption key managementHvitvaskingsloven (LOV-2018-06-01-23) § 30 — KYC data protection at rest
Scope: All cryptographicBilko keysproduction and secretsstaging inenvironments. useAll across:personnel with access to Railway environment variables or Vaultwarden.
Production,encryptionstaging,scope: FIELD_ENCRYPTION_KEY and AWS infrastructure (KMS, Secrets Manager, S3, App Runner)All employeeJMBG andcontractorOIBworkstationsfieldshandlingonly.ConfidentialPIB,orJIB,RestrictedanddataIBAN Allarethird-partyNOTintegrationssubjectwheretoALAIfield-levelHoldingencryptionASperholdsADR-014keys
Field-level developmentFIELD_HMAC_KEY environmentsapply forto Drop
Policy Owner: CISO§2 ([email protected])Tier Operational2 Owner:controls Security— teamdisk-level encryption only).
2. Key Inventory
2.1 Complete Key Taxonomy
| Key ID | Key Type | Purpose | Rotation Period | Owner | |||
|---|---|---|---|---|---|---|---|
JWT_PRIVATE_KEY |
|||||||
JWT_PUBLIC_KEY |
|||||||
REFRESH_TOKEN_SECRET |
|||||||
FIELD_ENCRYPTION_KEY |
|||||||
FIELD_HMAC_KEY |
Org-scoped HMAC- |
||||||
| |||||||
| |||||||
| |||||||
| |
2.2 Secrets Inventory (AWS Secrets Manager)
| |||||
| |||||
| |||||
DATABASE_URL |
PostgreSQL connection string with credentials | Database access | Railway secret | On |
CTO |
SEF_API_KEY |
DB (encrypted) per org | Per SEF portal policy | Per organization | ||
| FINA_CERT | X.509 certificate + private key | HR-FISK e-invoice signing (FINA PKI) | DB (encrypted) per org | Per FINA PKI (1-3 years) | Per organization |
| SENTRY_DSN | DSN string | Error tracking | Railway secret / env var | On compromise | CTO |
3. Key Hierarchy
AWSgraph KMSTD
ROOT["Root ofSecrets\n(CTO Trustpersonal (eu-north-1Vaultwarden primaryvault)"]
—RAILWAY["Railway Stockholm)Environment Secrets\n(production / staging / 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"| +RAILWAY
RAILWAY -->|"Load drop-national-id-at boot"| APP
ROOT -->|"Rotation authority"| ORG_SECRETS
ORG_SECRETS -->|"Decrypt on request"| APP
Principle: No key (AES-256-GCMmaterial —is annualever rotation)committed |to +--source DEK-{user_id}-{timestamp}:code. Per-recordNo envelopekey DEKsis | +-- Encrypts: foedselsnummer fieldstored in usersplaintext tableoutside |Railway +--secrets Stored:or 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 / 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)
Vaultwarden.
4. Key LifecycleGeneration Standards
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 CSPRNGNEVER use user-supplied passphrases, timestamps, UUIDs, or predictable values as keysNEVER generate keys in application code for Restricted data — use AWS KMSGenerateKeyorGenerateDataKey
Approved key generation methods:
| Key Type | Generation Method | |
|---|---|---|
openssl |
||
openssl |
||
|
||
openssl |
||
Generation environment:Commands:
# 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
KMS-managedproduction keys stored as Railway environment variables - Railway EU West region — encrypted at rest by Railway (AES-256)
- Access: CTO + one designated backup (CEO) only
- Two-factor authentication mandatory for
RestrictedRailwaydata:account - Railway
withinaccountAWSusesKMSALAIHSMsSSO / strong password (≥20 chars, in Vaultwarden)
Staging/Dev Keys
- Separate Railway project (staging) — different keys from production
- Dev:
.env.localfiles excluded from git via.gitignore - Dev keys may use weaker entropy but must still be valid format
Vaultwarden (Backup & Documentation)
- URL: https://vault.basicconsulting.no
- Stores: production key material
neverasleavessecureAWSnotes (encrypted) JWT_SECRET:Access:generatedCTOusing+CEOcrypto.randomBytes(32)then(break-glassstoredaccess)- Purpose: Recovery if Railway secrets are lost; rotation documentation
Per-Organization Secrets (SEF API Keys, FINA Certificates)
- Stored in
AWSPostgreSQLSecretsOrganizationSecretManagertable AllValuekeyencryptedgenerationwitheventsFIELD_ENCRYPTION_KEYloggedbeforeinstorage- Decrypted
CloudTrailin-memory only when needed for API call - FINA private keys additionally protected with password (stored separately)
6. Key Rotation Procedures
4.36.1 DistributionAnnual Rotation (Standard)
Principles:Schedule: First Monday of each calendar year.
NEVER transmit keys in plaintext over any channelNEVER include keys in source code,.envfiles committed to Git, or log outputNEVER send keys via email, Slack, or any messaging platformAlways fetch secrets at application runtime from AWS Secrets Manager or KMS API
DistributionFIELD_ENCRYPTION_KEY methods:
| ||
4.4 Storage
Storage hierarchy for Drop:re-encryption):
Level1. 1Generate —new AWS KMS HSMsFIELD_ENCRYPTION_KEY (FIPSopenssl 140-2rand Level-hex 3)32)
+--2. drop-national-id-Deploy a migration job that:
a. Reads each encrypted field with old key
(foedselsnummerb. encryption)Decrypts
+--c. drop-db-master-Re-encrypts with new key
(databased. TDEWrites — 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 3back to diskDB
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) keys
ProhibitedJWT storagekey locations:pair rotation (zero-downtime):
1. SourceGenerate codenew orRSA configkey filespair
2. Add new public key to JWKS endpoint alongside old (support both during rotation window)
3. Begin issuing new tokens signed with new private key
4. Wait for all old tokens to expire (15 minutes max)
5. Remove old public key from JWKS
6. Update JWT_PRIVATE_KEY and JWT_PUBLIC_KEY in GitRailway
7. Invalidate all refresh tokens (users will re-login)
6.2 Emergency Rotation (On Compromise)
If a key is suspected compromised:
- Immediately invalidate: all user sessions (clear RefreshToken table)
ApplicationGeneratelogsneworkeyerrorwithinmessages15(Sentry, BetterStack)minutesUnencryptedUpdatedatabaseRailwaycolumnssecretEmailDeployattachmentsneworapplicationSlackinstancemessages(Railway auto-deploys on env var change)BrowserDocumentlocalStorageinorVaultwarden:sessionStorageold key, date of compromise, date of rotationContainerAssessimagewhetherlayersbreach 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
4.57. Usage
Key Access control principles:
| |
KMS key policy roles:
| ||
| | |
| | |
| | |
| |
4.6 Rotation Schedule
Control
| Key | ||||
|---|---|---|---|---|
JWT_PRIVATE_KEY |
||||
FIELD_ENCRYPTION_KEY |
||||
DATABASE_URL |
||||
SEF API keys |
||||
FINA certificates |
||||
|
JWT_SECRETAccess rotationlog: overlapAll procedure:
Generate newJWT_SECRETvalueDeploy newRailway secrettoviewsAWS Secrets ManagerRolling 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-authenticateDelete 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 accessSystem compromise where key waslogged inuseRailway Detectionauditoftrail.unauthorizedAnydecryptionaccessinoutsideCloudTrailnormallogsdeployment
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 protectedreviewed 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 users must re-authenticate
Step 5: Document in incident log
4.8 Destruction
When destruction is required:
Key rotated out and overlap period expiredCrypto-shredding: deleting user foedselsnummer by destroying envelope DEK (GDPR Art. 17)System decommissioned
Destruction methods:
| ||
| ||
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 databaseThe ciphertext (foedselsnummer) becomes permanently unreadableAML 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
| ||
| ||
| ||
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 to drop-compliance-decrypt role. Application code cannot decrypt foedselsnummer except during explicit compliance workflows.CTO.
8. AuditEscrow Logging& forRecovery
FIELD_ENCRYPTION_KEY OperationsEscrow (Critical)
The FIELD_ENCRYPTION_KEY is the most critical key — loss means permanent loss of all L4 Restricted field data (tax IDs, IBAN).
AllEscrow key operations logged in AWS CloudTrail, including:procedure:
KeyFIELD_ENCRYPTION_KEYcreationstored(actor,inkeyVaultwardenID,securekeynotetype,accessibletimestamp)to: CTO, CEOEncryption/decryptionVaultwarden has its own backup (actor,seekeysystemID,infrastructurecontext, timestamp)docs)- Key material noted with: creation date, rotation
(actor,date,old key version, new key version, timestamp) Key disabling / deletion scheduling (actor, key ID, reason, timestamp)Failed access attempts (actor, key ID, reason, timestamp)description
LogIf destination:FIELD_ENCRYPTION_KEY is lost and not recoverable: AWSAll CloudTrailencrypted →field S3data (immutable,is append-only)permanently +unreadable. BetterStackThis aggregationis Loga retention:catastrophic 5data yearsloss (AMLevent. complianceContact —legal Hvitvaskingslovencounsel §and 30)affected Alertsupervisory on:authorities.
Railway Account Recovery
DecryptRailwayoperationsrootbyaccount:unexpected[email protected]IAM(passwordrolein Vaultwarden)Failed2FAKMSrecoveryaccesscodes:attemptsVaultwarden>secure3 in 5 minutesnoteOff-hoursDesignated backup access: CEO has view access todrop-national-id-keyordrop-kyc-keyAnyScheduleKeyDeletioneventRailway (immediate alert)read-only)
9. ExceptionKey ProcessDestruction
ExceptionsWhen nota permittedkey for:is Restrictedretired (L4)superseded databy rotation):
- Remove from Railway environment variables
- Remove from active Vaultwarden entries
- Archive to Vaultwarden secure note: "Retired Keys" with date and reason
- Old FIELD_ENCRYPTION_KEY versions: retained for 3 months after rotation (
foedselsnummer,inKYCcasedocuments)rollback—needed),nothenexceptionspermanently deleted from Vaultwarden
10. Securion Sign-off Requirement
No changes to FIELD_ENCRYPTION_KEY, FIELD_HMAC_KEY, or field-level encryption.encryption implementation may be deployed to production without written approval from Parisa Tabriz (Securion).
Exception request process:Process:
SubmitAllrequestfieldto:encryption[email protected]code changes must complete Securion security reviewRequired:Reviewsystem,conducteddataagainstclassification,checklist:key being excepted, business justification, risk assessment, compensating controls, duration (max 12 months)docs/security/FieldEncryptionSecurionChecklist.mdApproval:Sign-offCISOevidence file required beforemc.js doneon any field encryption taskLoggedEvidence file stored in:compliance registerReview: Quarterly — exceptionsdocs/security/securion-approvals/YYYY-MM-DD-<task-id>12 months require board-level approval.md
ActiveScope: exceptions:This requirement applies to:
10. Emergency Procedures — Key Compromise
Immediate response checklistimplementation (executeMC within#9966)
1Any 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: 72hchanges to DatatilsynetFieldEncryption.kt, ->FieldHmac.kt, Finanstilsynet notification if payment data affected
□ Document everything in incident log
□ Post-mortem within 48 hours
Re-encryption priority:
Foedselsnummer (drop-national-id-key) — Restricted (L4), highest priorityKYC documents (drop-kyc-key) — Restricted (L4)FieldEncryptionRotationScript.kt- Database
backupmigrationfileschanges(drop-backup-key)affecting—jmbg,Restrictedoib,(L4)jmbg_hash, oib_hash columns DatabaseKeymasterrotation procedures- Addition of new encrypted fields
Exemptions: Changes to non-cryptographic code (drop-db-master-key)UI —masking, PhaseAPI 2response
Approval
| Role | Name | |||
|---|---|---|---|---|
| Author | 2026-02-23 | |||
| CEO |