Skip to main content

Security Architecture

Security Architecture Document

Project: {{PROJECT_NAME}}Drop — PSD2 Pass-Through Payment App Version: {{VERSION}}1.0 Date: {{DATE}}2026-02-23 Author: {{AUTHOR}}Security Architect Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}}CTO, DPO, Engineering Lead Classification: Confidential

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}Security Architect Initial draft from security audit + hardening implementation

1. Security Architecture Overview

Security Owner: {{SECURITY_OWNER_ROLE}}CISO / Security Architect ({{CONTACT_EMAIL}})[email protected]) Last Security Review: {{DATE}}2026-02-13 (post-hardening) Next Scheduled Review: {{DATE}}2026-08-23 Compliance Targets: GDPR |/ PCI-DSSpersonopplysningsloven | SOC2PSD2 Type/ IIbetalingstjenesteloven | ISOhvitvaskingsloven 27001(AML) | HIPAAIKT-forskriften / DORA | {{OTHER}}Finanstilsynet licensing

Architecture Model: Drop is a PSD2 pass-through payment app (AISP + PISP). It never holds customer money. AISP reads bank balances via Open Banking. PISP initiates payments from the user's bank account. Cards are a future feature gated behind feature flags (all default to false).

Defense-in-Depth Overview

flowchart TB
    subgraph Perimeter["Perimeter Security"]
        DNS[DNS / DDoS Protection\nCloudflare / AWS Shield]WAF]
        WAF[Web Application Firewall]Firewall\nCloudflare WAF Rules]
        CDN[CDN — Edge TLS Termination]1.3 Termination\nCloudflare]
    end

    subgraph Network["Network Security"]
        LB[LoadAWS Balancer\App Runner\nTLS 1.3]
        SG[Security Groups / NACLs]
        VPC[Private VPC — NoVPC\nNo public subnet\nforsubnet for data layer]
    end

    subgraph Application["Application Security"]
        BANKID[BankID OIDC\nNorwegian eID — Level 4]
        AUTH[AuthenticationJWT Auth Service\nJWTnHS256, +httpOnly MFA + Session]cookies]
        AUTHZ[Authorization Layer\nRBAC user/merchant/admin + PolicyAND Engine]user_id=?]
        VALID[Input Validation\nSchemanZod schemas + Sanitization]sanitizeText()]
        RATE[Rate Limiting\nPernSQLite-backed, IP + Per user]per-IP]
    end

    subgraph Data["Data Security"]
        ENCRYPT[Encryption at Rest\nAES-256]
        TRANSIT[Encryption in Transit\nTLS 1.3 / mTLS]3]
        MASK[PII Masking\nField-levelnLast-4 encryption]only for cards/bank accounts]
        VAULT[Secrets Management\n{{VAULT_TOOL}}]nAWS Secrets Manager + JWT_SECRET env]
    end

    subgraph Monitoring["Security Monitoring"]
        SIEM[SIEMSENTRY[Error /Monitoring\nSentry]
        BETTERSTACK[Uptime + Log Aggregation]
        IDS[Intrusion Detection]Monitoring\nBetterStack]
        ALERTS[Security Alerts\nPagerDuty]nPagerDuty escalation]
        AUDIT[Audit Trail\nImmutablenSession table + structured logs]
    end

    DNS --> WAF --> CDN --> LB
    LB --> SG --> VPC
    VPC --> BANKID --> AUTH --> AUTHZ --> VALID
    VALID --> RATE
    RATE --> Data
    Data --> Monitoring

2. Authentication Flows

2.1 StandardBankID OIDC Authentication (Production Target)

sequenceDiagram
    autonumber
    actor User
    participant App as Drop App
    participant API as Drop API
    participant BankID as BankID OIDC Provider
    participant DB as User Store (PostgreSQL)

    User->>App: Tap "Login with BankID"
    App->>API: GET /auth/bankid/authorize
    API->>BankID: Redirect with PKCE code_challenge, scope=openid+profile+nnin
    User->>BankID: Authenticate with BankID (Level 4 — possession + knowledge + inherence)
    BankID->>API: Redirect with authorization_code
    API->>BankID: Exchange code for tokens (PKCE code_verifier)
    BankID-->>API: {id_token, access_token} — contains fødselsnummer, name, birthdate
    API->>API: Validate id_token signature + iss + aud + exp + nonce
    API->>API: Extract name, fødselsnummer; verify age >= 18 (from birthdate)
    API->>DB: Upsert user (name, phone, bankid_ref); check KYC status
    API->>API: Generate JWT (HS256, 24h), create session record
    API-->>App: Set httpOnly cookie (JWT) + 200 OK
    App->>App: Render dashboard

2.2 Current Email/Password Login /(MVP Registration— pre-BankID)

sequenceDiagram
    autonumber
    actor User
    participant FE as Drop Frontend
    participant GWAPI as Drop API Gateway
    participant AUTH as Auth Service(src/drop-app)
    participant DB as UserSQLite Store/ participant EMAIL as Email ServicePostgreSQL

    User->>FE: Enter email + password
    FE->>GW:API: POST /api/auth/login {email, password}
    GW-API->>GW:API: Rate limit check (510 attempts/15min/IP)req/60s GW-per IP — middleware.ts)
    API->>AUTH:API: ValidateCSRF credentialsOrigin AUTH-header validation
    API->>DB: SELECT user WHERE email = $1?
    DB-->>AUTH:API: User record
    AUTH-API->>AUTH:API: Verifybcrypt.compare(password, bcrypt/Argon2hash) hash— cost factor 12
    alt Invalid credentials
        AUTH-->>GW: 401 Unauthorized
        GW->>AUTH: Log failed attempt (for lockout)
        GW-API-->>FE: 401 Invalid credentials
        Note over FE: Generic message"Invalid credentials" (no user enumerationenumeration)
    end
    alt MFA enabled
        AUTH-API->>FE:DB: 200INSERT {mfa_required:INTO true,sessions session_id}(id, User-user_id, token_hash, expires_at, revoked=0)
    API->>FE: Enter TOTP code
        FE->>AUTH: POST /auth/mfa {session_id, totp_code}
        AUTH->>AUTH: Verify TOTP
    end
    AUTH->>AUTH:API: Generate JWT accessHS256 + refreshsetIssuedAt(), tokens24h AUTH->>DB:expiry
    UPDATE last_login_at
    AUTH-->>GW: {access_token, refresh_token, expires_in}
    GW-API-->>FE: Set HttpOnlyhttpOnly=true, secure=true, sameSite=strict cookie (refresh) + access_token in body
    FE->>FE: StoreRender access_tokendashboard (no token in memory onlylocalStorage)

2.23 Token Refresh Flow& Session Revocation

sequenceDiagram
    autonumber
    actor FE as Drop Frontend
    participant GWAPI as Drop API Gateway
    participant AUTH as Auth Service
    participant DB as TokenSession Store

    FE->>GW:API: POSTAny /auth/refreshauthenticated request (HttpOnlyhttpOnly cookie:cookie refresh_token)sent GW-automatically)
    API->>AUTH:API: ValidateVerify refreshJWT tokensignature AUTH-+ expiry (jose library)
    API->>DB: SELECT tokensessions WHERE valuetoken_hash = hash($1)SHA256(jwt) AND revoked = false
    DB-->>AUTH: Token record
    AUTH->>AUTH: Check expiry + rotation window0
    alt TokenSession revoked or expired
        or revoked
        AUTH-API-->>FE: 401 — Re-authenticateForce re-login
    end
    AUTH-API->>API: Extract userId, proceed with request

    Note over FE,DB: On logout — all sessions revoked
    FE->>API: POST /api/auth/logout
    API->>DB: RevokeUPDATE oldsessions refreshSET tokenrevoked=1 (rotation)WHERE AUTH->>AUTH:user_id Generate= new?
    access_token + refresh_token
    AUTH->>DB: Store new refresh token (hashed)
    AUTH-API-->>FE: {access_token}Clear + Set new HttpOnlyhttpOnly cookie

2.34 Multi-FactorMFA — Dynamic Linking for PSD2 Compliance (Phase 2)

BankID inherently provides possession + knowledge (two factors). For PSD2 Strong Customer Authentication (MFA)

Methods supported:SCA):

MethodFactor Library/ProviderBankID Element Notes
TOTP (Authenticator apps)Knowledge {{TOTP_LIB}}BankID PIN / password RFCUser-memorized 6238, 30s window, 1 step tolerancesecret
SMS OTPPossession {{SMS_PROVIDER}}BankID app / hardware token FallbackRegistered only — SIM swap riskdevice
HardwareDynamic key (FIDO2/WebAuthn)linking {{WEBAUTHN_LIB}}Amount + payee shown in BankID signing dialog HighestPrevents security,transaction phishing-resistant
Recovery codesCustom10 codes, one-time use, bcrypt-hashedsubstitution
sequenceDiagram

Current autonumber actor User participant AUTH as Auth Service participant DB as DB User->>AUTH: POST /auth/mfa/setupstate (authenticated)MVP): AUTH->>AUTH:Email/password Generateonly TOTP secretno (160-bitSCA entropy)compliance. AUTH->>User:BankID QRintegration codeis +Phase backup codes (shown ONCE) User->>AUTH: POST /auth/mfa/verify {totp_code} (confirm setup) AUTH->>AUTH: Verify TOTP code AUTH->>DB: UPDATE user SET mfa_secret = encrypt($secret), mfa_enabled = true AUTH->>DB: INSERT backup_codes (hashed, 10 codes) AUTH-->>User: MFA enabled

2.4 SSO / OAuth2 Flow

sequenceDiagram
    autonumber
    actor User
    participant FE as Frontend
    participant AUTH as Auth Service
    participant IDP as Identity Provider\n(Auth0 / Keycloak / Azure AD)

    User->>FE: Click "Sign in with {{PROVIDER}}"
    FE->>AUTH: GET /auth/sso/{{provider}}
    AUTH->>IDP: Redirect with PKCE code_challenge
    User->>IDP: Authenticate with IDP
    IDP->>AUTH: Redirect with authorization_code
    AUTH->>IDP: Exchange code for tokens (PKCE code_verifier)
    IDP-->>AUTH: {id_token, access_token}
    AUTH->>AUTH: Validate id_token signature + claims
    AUTH->>AUTH: Find or create local user account
    AUTH-->>FE: Issue local JWT (same flow as login)


3. Authorization Model

Model: RBAC (Role-Based Access Control) with resource-levelmandatory conditionsuser-scoped data isolation

3.1 Roles & Permissions Matrix

all
Permission Owneruser Adminmerchant MemberViewerAPIadmin
ManageView tenantown settings
Manage billing
Invite / remove users
Create {{RESOURCE_1}}
Read {{RESOURCE_1}} (own)transactions
ReadInitiate {{RESOURCE_1}} (all)remittance
UpdateInitiate {{RESOURCE_1}}QR payment
Manage recipients (own)
View merchant dashboard
View merchant transactions✓ (own)✓ (all)
Manage merchant settings✓ (own)
Admin functions
DeleteView {{RESOURCE_1}} users

KYC Status Gates (required for financial operations):

KYC StatusRemittanceQR PaymentBalance Read
pendingBlockedBlockedBlocked
Export dataapproved Allowed Allowed Allowed
View audit logsrejected Blocked Blocked Blocked

3.2 Resource-Level Conditions (IDOR Protection)

All data access queries include mandatory user scoping. Source: src/drop-app/src/lib/:

// All recipient queries — IDOR prevention
db.prepare("SELECT * FROM recipients WHERE id = ? AND user_id = ?").get(id, userId);

// All transaction queries
db.prepare("SELECT * FROM transactions WHERE id = ? AND user_id = ?").get(id, userId);

// All bank account queries
db.prepare("SELECT * FROM bank_accounts WHERE user_id = ?").all(userId);

Merchant endpoints additionally verify:

  1. User has role = 'merchant'
  2. Merchant record belongs to authenticated user

3.3 Permission Hierarchy

SUPER_ADMINadmin (platform level — bypasses tenant isolation)
  └── OWNER (tenant platform-level — full control within tenant)access)
  └── ADMINmerchant (cantenant-level manage users,own notdata billing)+ merchant dashboard)
        └── MEMBERuser (standard CRUD on own resources)data └── VIEWER (read-only)
                          └── API (scoped to specific permissions)

3.3 Resource-Level Conditions

Rule: Member can UPDATE {{resource}} IF:
  resource.tenant_id == user.tenant_id AND
  (resource.created_by == user.id OR user.role IN ['admin', 'owner'])

Rule: API key can READ {{resource}} IF:
  api_key.tenant_id == resource.tenant_id AND
  api_key.scopes CONTAINS '{{resource}}:read'

4. Data Encryption

4.1 Encryption at Rest

KMSkey
Data Store Encryption Method Key Management
SQLite (current MVP)OS-level disk encryptionPlatform
PostgreSQL (Phase 2) AES-256256-GCM (transparentTDE storagevia encryption)cloud provider) CloudAWS KMS
RedisPostgreSQL — fødselsnummer AES-256256-GCM (cloudapplication-layer provider)field encryption CloudAWS KMS — separate key
PostgreSQL — KYC documentsAES-256-GCMAWS KMS
ObjectAWS StorageS3 (S3/Blob)backups SSE-KMS (AES-256) CloudAWS KMS
BackupsAES-256-GCMSeparate backup KMS key
PIIBankID fields (field-level)certificates AES-256-GCMAWS (applicationCertificate layer)Manager ApplicationAWS-managed
JWT_SECRETAWS Secrets ManagerAWS-managed

4.2 Encryption in Transit

TLS Configurationconfiguration (minimumCloudflare TLS+ 1.2,AWS preferApp TLS 1.3)Runner):

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
add_headerTLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
Strict-Transport-SecuritySecurity: "max-age=63072000; includeSubDomains; preload" always;preload

mTLS for internal service communication:

  • All service-to-serviceAPI callstraffic: withinHTTPS the cluster use mTLS
  • Certificates managed by {{CERT_TOOL}}only (e.g.,HTTP cert-manager, Istio,301 Linkerd)
  • redirect).
  • CertificatehttpOnly rotation:cookies Automaticwith everysecure: {{ROTATION_PERIOD}}
  • true
in production.

4.3 Field-Level Encryption for PII

Fields encrypted at application layer (before DB write):
- users.mfa_secretFødselsnummer → AES-256-GCM, key:encrypted USER_MFA_KEYimmediately -on users.{{PII_FIELD}}receipt from AES-256-GCM,BankID
  key:Key: USER_PII_KEYNATIONAL_ID_KEY -(AWS {{OTHER_SENSITIVE_FIELD}}KMS  AES-256-GCM,separate key:from {{KEY_NAME}}database Envelopemaster encryptionkey)
  pattern:Stored: Data →Only encrypted with DEK (Data Encryption Key)
  DEK → encrypted with KEK (Key Encryption Key, stored in KMS)
  Only DEK ciphertext stored in database
  Read: Decrypted only for KYC checks, by compliance role only

Bank account numbers → masked in API responses (last 4 digits only)
  Source: utils-server.ts:23-26 maskAccountNumber()

Card numbers → last_four + token_ref only (full PAN never stored)
  Note: Cards feature gated behind feature flags, all default false

5. Network Security

5.1 Network Architecture

Internet
  → Cloudflare DDoS Protection ({{PROVIDER}})+ WAFWAF ({{WAF_TOOL}}) — blocks OWASP Top 10
  →Cloudflare CDN / Edge (TLS termination)1.3 termination at edge)LoadAWS BalancerApp Runner (re-encryptsencrypts, toTLS app — TLS)1.3)PublicApplication Subnetcontainers (only LB + NAT Gateway)Next.js)Private SubnetDatabase (Application servers)SQLiteDataPostgreSQL Subnetin (DB,private Cache — no internet access)subnet)

5.2 Security Groups / Firewall Rules

Source Destination Port Protocol Action
Internet LoadCloudflare Balanceredge 443 HTTPS ALLOW
Internet Any 80 HTTP REDIRECT → 443
Load BalancerCloudflare AWS App serversRunner {{APP_PORT}}443 TCPHTTPS ALLOW
App serversRunner PostgreSQL (Phase 2) 5432 TCP ALLOW
App serversRedis6379TCPALLOW
App serversMessage Queue{{MQ_PORT}}TCPALLOW
Any DataPostgreSQL Subnetdirect Any Any DENY (default)
InternalAny (unauthenticated) Internal/api/auth/* Any443 AnyHTTPS mTLSRate onlylimited (10/60s)
Any (unauthenticated)/api/transactions/*443HTTPSRate limited (10/60s)

5.3 WAF Rules (Cloudflare)

Rule Action Notes
OWASP Core Rule Set Block ModSecurityManaged CRSCloudflare 3.3+ruleset
SQL injection Block Including encoded variants
XSS Block Including DOM-based patterns
Path traversalBlock../ patterns
Known bad IPsBlockThreat intelligence feed
Bot traffic Challenge (CAPTCHA) Suspicious patterns
Rate limiting Block > {{RATE}}120 req/min from single IP to /api/rates
Geo-blocking BlockLog (review) Countries:Sanctioned {{BLOCKED_COUNTRIES}}countries (ifper applicable)OFAC/EU list

6. API Security

6.1 Rate Limiting Strategy

Source: src/drop-app/src/lib/middleware.ts:6-31

TierEndpoint Type Limit WindowImplementation
AnonymousAuth routes (login, register) {{N}}10 req/minrequests Per60 secondsSQLite-backed, per-IP
AuthenticatedTransaction routes (remittance, qr-payment) {{N}}10 req/minrequests Per60 tokensecondsSQLite-backed, per-IP
AdminRates endpoint (/api/rates) {{N}}120 req/minrequests Per60 tokensecondsSQLite-backed, per-IP

Note (Medium finding M3): X-Forwarded-For header is trusted from Cloudflare. Must validate only from trusted proxy IPs in production.

6.2 Input Validation

Source: src/drop-app/src/lib/middleware/validation.ts

ALL// All text inputs validatedsanitized
at:sanitizeText(input) 1. APIremoves GatewayHTML leveltags, control schemachars, typetrims, checkingenforces 2.maxLength

Controller// level — DTOFinancial validation
(class-validatorvalidateAmount(amount) → positive, finite, max 2 decimal places
  Remittance: 100 NOK ≤ amount ≤ 50,000 NOK
  QR Payment: 1 NOK ≤ amount ≤ 100,000 NOK
  Number.isFinite() prevents NaN/Infinity injection

/ Zod / joi)
3. Service level — business ruleIdentity validation
4.validateIBAN(iban) Database level — constraintsformat + checkchecksum
constraintsvalidatePIN(pin) Never trustexactly client-supplied4 data.digits
Always:validateCurrency(currency) - Whitelistwhitelist: allowedEUR, charactersUSD, whereGBP, possibleBAM, -CHF, EnforcePLN, maxNOK, lengthRSD, onTRY, allPKR
stringvalidateLanguage(lang) fields -whitelist: Validatenb, fileen, typesbs, bysq

magic// bytes,SQL: notALL extension24 -endpoints Useuse parameterized queries — NEVERzero string concatenation for SQL

6.3 CORS Policy

// Production CORS config
{
  origin: ['https://app.{{DOMAIN}}'getdrop.no', 'https://{{DOMAIN}}'app.getdrop.no'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
  exposedHeaders: ['X-Request-ID', 'X-RateLimit-Remaining'],
  credentials: true,   // Required for HttpOnlyhttpOnly cookies
  maxAge: 86400              // 24h preflight cache
}

6.4 Content Security Policy

Source: src/drop-app/next.config.ts:6-46

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{NONCE}';
  style-srcunsafe-inline' 'self' 'nonce-{NONCE}';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.{{DOMAIN}}unsafe-eval';
  frame-ancestors 'none';
  form-action[remaining 'self';
  upgrade-insecure-requests;headers...]

Known issue (Medium M1): unsafe-inline and unsafe-eval required for Next.js. Production target: nonce-based CSP.


7. OWASP Top 10 Mitigation Matrix

OWASP Risk Mitigation Implementation Status
A01: Broken Access Control RBAC + resourceuser_id ownership checksAuth middlewarescoping on every endpointqueryAND user_id = ? in all data queries; middleware.ts
A02: Cryptographic Failures TLS 1.3 + AES-256-GCM256 + Argon2bcrypt(12) Seeauth.ts, §4next.config.ts, IKT-sikkerhetspolicy
A03: Injection Parameterized queries + ORM + WAFexclusively NeverAll raw24 SQLAPI interpolationendpoints — zero string concatenation
A04: Insecure Design ThreatPSD2 modelingpass-through +model security reviewno stored funds Per-featureArchitecture securitydecision review requiredDrop never holds money Partial
A05: Security Misconfiguration IaCHardened security scanningheaders + hardenedfeature defaultsflags Checkovnext.config.ts /headers, tfsec in CIfeature-flags.ts  (CSP partial)
A06: Vulnerable Components SCARecent scanningdependency versions Snykjose /^6.1.3, Dependabotbcryptjs in^3.0.3, CInext + weekly scans16.1.6
A07: Auth Failures MFA + rateRate limiting + accountsession lockoutrevocation + bcrypt Seemiddleware.ts, §2sessions table, auth.ts
A08: Software Integrity Signed commits + container image signingCI/CD SigstoreGitHub /Actions cosignpipeline Partial
A09: Logging Failures Structured audit logsSentry + SIEMBetterStack structured logs SeeError §9tracking + uptime monitoring Partial (no audit_log table yet)
A10: SSRF AllowlistInput validation + allowlist for outbound HTTP + metadata blocking DenyvalidateIBAN, internalexternal IPAPI rangesallowlist Partial

8. Security Headers Checklist

Source: src/drop-app/next.config.ts

Header Value Status
Strict-Transport-Security max-age=63072000; includeSubDomains; preload  / TODO
Content-Security-Policy Seedefault-src §6.4'self'; frame-ancestors 'none'; ... /(unsafe-inline/eval TODOTODO)
X-Content-Type-Options nosniff  / TODO
X-Frame-Options DENY  / TODO
Referrer-Policy strict-origin-when-cross-origin  / TODO
Permissions-Policy geolocation=camera=()self), microphone=(), camera=geolocation=()self)  / TODO
Cache-Control (auth responses) no-store ✓ / TODO
X-XSS-Protection 0 (deprecated, rely on CSP) ✓ / TODO

9. Dependency Vulnerability Management

Current dependency status (audit date: 2026-02-12):

PackageVersionRiskNotes
jose^6.1.3LowJWT library — well-maintained
bcryptjs^3.0.3LowPure JS bcrypt — no native compilation
better-sqlite3^12.6.2LowParameterized queries
next16.1.6LowRecent version
react19.2.3LowLatest major
radix-ui^1.4.3LowUI components only

Scanning tools:tools (planned):

  • SAST: {{SAST_TOOL}}Semgrep / CodeQL (runs on every PR)
  • SCA: Snyk / Dependabot + npm audit (runs on every PR + daily on main)
  • ContainerContainer: scanning: {{CONTAINER_SCANNER}}Trivy (runs on Docker image build)

Remediation SLAs:

Severity SLA Owner
Critical (CVSS ≥ 9.0) 24 hours Security + affected team
High (CVSS 7.0-8.9) 7 days Affected team
Medium (CVSS 4.0-6.9) 30 days Affected team
Low (CVSS < 4.0) 90 days Next sprint

10. Security Logging & Audit Trail

Current

Securitymonitoring Eventsstack:

Always
    Logged

  • Sentry: Error tracking, exception monitoring, performance
  • BetterStack: Uptime monitoring, log aggregation, alerting
  • Sessions table: Token revocation, session lifecycle
  • Planned (pre-production):

    Event Data Logged Retention Alert?
    Login success user_id, ip, user_agent, timestamp 1 year No
    Login failure ip, email_hash, attempt_count, timestamp 1 year Yes (> 5 failures)
    MFABankID verificationauthentication user_id, method,bankid_ref, success/failure 1 year Yes (failure)
    Password change user_id, ip, timestamp 1 year Yes
    PermissionSession deniedrevocation user_id, resource, action,session_ids, timestamp 1 year Aggregate alertNo
    AccountTransaction lockoutcreated user_id, ip,amount, corridor, timestamp 15 yearyearsYes (>50,000 NOK)
    KYC status changeuser_id, old_status, new_status, operator5 years Yes
    DataAML exportflag triggered user_id, export_type,rule, record_counttransaction_id 25 years Yes (large exports)always)
    Admin action actor_id, action, target, changes 2 years Yes
    API key created/revokeduser_id, key_id, timestamp2 yearsNo
    Secret accessservice, secret_name, timestamp2 yearsYes (unusual access)

    Log format:retention (hvitvaskingsloven § 30): JSONMinimum structured,5 immutableyears for KYC and transaction data.

    SIEM integration (append-only),Phase shipped to SIEM SIEM:2): {{SIEM_TOOL}}BetterStack dashboard:Sentry {{LINK}} TamperPagerDuty protection:escalation Logs signed with {{SIGNING_KEY}}, stored in write-once bucketchain.


    Approval

    Role Name Date Signature
    Author Security Architect 2026-02-23
    CISO / Security Lead
    DPO
    CTO