Skip to main content

Security Architecture

Security Architecture Document

Project: DropBilkoPSD2Balkan Pass-ThroughAccounting Payment AppSaaS Version: 1.0 Date: 2026-02-23 Author: SecurityCompliance Architect Status: Draft Reviewers: CTO, DPO, Engineering Lead Classification: Confidential

Document History

Version Date Author Changes
0.1 2026-02-23 SecurityCompliance Architect Initial draft from— based on Bilko security auditarchitecture + hardening implementationspec

1. Security Architecture Overview

Security Owner: CISO / SecurityCompliance Architect ([email protected])[email protected]) Last Security Review: 2026-02-13 (post-hardening)23 Next Scheduled Review: 2026-08-23 Compliance Targets: GDPR /| personopplysningslovenZakon o zaštiti podataka o ličnosti RS (ZZPL) | PSD2Zakon /o betalingstjenestelovenzaštiti ličnih podataka BiH (ZZLP) | hvitvaskingslovenGDPR (AML)HR) | IKT-forskriftenZakon /o DORAračunovodstvu RS | FinanstilsynetZakon licensingo PDV BiH | Zakon o PDV HR

Architecture Model: DropBilko is a PSD2multi-tenant pass-throughcloud paymentaccounting app (AISP + PISP).SaaS. It neverprocesses holdsinvoices, customerexpenses, money.VAT AISPreturns, readsand bankfinancial balancesreports viaon Openbehalf Banking.of PISPorganizations initiatesin paymentsSerbia, fromBosnia & Herzegovina, and Croatia. Each organization's data is strictly scoped by organizationId at the user'sdatabase bank account. Cards are a future feature gated behind feature flags (all default to false).layer.

Defense-in-Depth Overview

flowchartgraph TBTD
    CLIENT["Client Browser / PWA"]

    subgraph Perimeter[NETWORK["PerimeterNetwork Security"Layer"]
        DNS[DNSCF["Cloudflare / DDoSWAF\nDDoS Protection\nCloudflare WAF]
        WAF[Web Application Firewall\nCloudflare WAF Rules]
        CDN[CDN — Edge TLSnTLS 1.3 Termination\nCloudflare]termination\nHSTS"]
    end

    subgraph Network[APP_LAYER["NetworkApplication Security"Layer"]
        LB[AWSHELMET["Helmet.js\nCSP App+ Runner\nTLSX-Frame 1.3]+ SG[SecurityHSTS\nno GroupsX-Powered-By"]
        CORS["CORS Whitelist\nbilko.io only\nno wildcard *"]
        RATE["Rate Limiter\nexpress-rate-limit\n5 req/15min auth\n100 req/15min general"]
        AUTH_MW["Auth Middleware\nJWT verify (15min access)\norg-scope injection"]
        RBAC_MW["RBAC Middleware\nowner / NACLs]admin VPC[Private/ VPC\nNoaccountant public/ subnetviewer"]
        forZOD["Zod dataValidation\nall layer]request bodies\ntype-safe parsing"]
    end

    subgraph Application[DATA_LAYER["ApplicationData Security"Layer"]
        BANKID[BankIDPRISMA_ORM["Prisma OIDC\nNorwegianORM\nparameterized eIDqueries\nno raw LevelSQL 4]for AUTH[JWTuser Authinput\norg-scoped Service\nHS256,WHERE httpOnly cookies]
        AUTHZ[Authorization Layer\nRBAC user/merchant/admin + AND user_id=?clauses"]
        VALID[InputPG_ENC["PostgreSQL Validation\nZod(Railway schemasEU)\nAES-256 +disk sanitizeText()encryption\nbackup encryption"]
        RATE[Rate Limiting\nSQLite-backed, per-IP]
    end

    subgraph Data[AUDIT["DataAudit Security"Layer"]
        ENCRYPT[EncryptionLOG["LoggedAction at Rest\nAES-256]
        TRANSIT[Encryption in Transit\nTLS 1.3]
        MASK[PII Masking\nLast-4 only for cards/bank accounts]
        VAULT[Secrets Management\nAWS Secrets Managertable\nAPPEND-ONLY\nIP + JWT_SECRETuser env]+ timestamp\nold/new values (changedFields)"]
    end

    subgraph Monitoring["Security Monitoring"]
        SENTRY[Error Monitoring\nSentry]
        BETTERSTACK[Uptime + Log Monitoring\nBetterStack]
        ALERTS[Security Alerts\nPagerDuty escalation]
        AUDIT[Audit Trail\nSession table + structured logs]
    end

    DNSCLIENT --> WAFCF --> CDNHELMET --> LB
    LB --> SG --> VPC
    VPC --> BANKID --> AUTH --> AUTHZ --> VALID
    VALIDCORS --> RATE RATE--> AUTH_MW --> Data
    DataRBAC_MW --> MonitoringZOD --> PRISMA_ORM --> PG_ENC
    PRISMA_ORM --> LOG

2. Authentication Flows

2.1 BankID 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: GenerateStrategy: JWT (HS256,JSON 24h)Web Tokens)

Why JWT:

  • Stateless — scales horizontally on Railway
  • Works with PWA/mobile
  • Industry standard for multi-tenant SaaS

2.2 Token Types

Access Token

  • Lifetime: 15 minutes
  • Storage: Authorization: Bearer <token> header (memory only — not localStorage)
  • Contains: user ID (sub), createorganization sessionID record(org), API-->>App:role
  • Set
  • Refresh: Automatic via refresh token rotation

Refresh Token

  • Lifetime: 7 days
  • Storage: httpOnly cookie (JWT)not +accessible 200to OKJavaScript)
  • App->>App:
  • Rotation: RenderNew dashboardrefresh
token issued on each use
  • Revocation: Stored hashed in database, invalidated on logout
  • 2.23 CurrentJWT Email/PasswordAuth Login (MVP — pre-BankID)Flow

    sequenceDiagram
        autonumber
        actor User
        participant FE as DropFrontend Frontend(bilko.io)
        participant API as DropExpress API (src/drop-app)api.bilko.io)
        participant DB as SQLitePostgreSQL /(Railway PostgreSQLEU)
    
        User->>FE: Enter email + password
        FE->>API: POST /api/v1/auth/login {email, password}
        API->>API: Rate limit check (10 req/60s per IP — middleware.ts)
        API->>API: CSRF Origin header validation
        API->>DB: SELECT user WHERE email = ? (parameterized)
        DB-->>API: User record (passwordHash)
        API->>API: bcrypt.compare(password, hash) — cost12 factor 12rounds
        alt InvalidPassword credentialsvalid
            API->>API: jwt.sign({sub, org, role}, JWT_SECRET, 15m)
            API->>API: jwt.sign({sub}, JWT_REFRESH_SECRET, 7d)
            API->>DB: INSERT refreshToken (hashed, expiresAt)
            API-->>FE: 200 { accessToken } + Set-Cookie: refreshToken (httpOnly, secure, sameSite=strict)
            FE->>FE: Store accessToken in memory only
        else Password invalid
            API-->>FE: 401 Unauthorized (generic message Generic "Invalid credentials" (no user enumeration)
        end
    
        API->>DB: INSERT INTO sessions (id, user_id, token_hash, expires_at, revoked=0)
        API->>API: Generate JWT HS256 — setIssuedAt(), 24h expiry
        API-->>FE: Set httpOnly=true, secure=true, sameSite=strict cookie
        FE->>FE: Render dashboard (no token in localStorage)
    

    2.3 Token Refresh & Session Revocation

    sequenceDiagram
        autonumber
        actor FE as Drop Frontend
        participant API as Drop API
        participant DB as Session Store
    
        FE->>API: Any authenticated request (httpOnly cookie sent automatically)
        API->>API: Verify JWT signature + expiry (jose library)
        API->>DB: SELECT sessions WHERE token_hash = SHA256(jwt) AND revoked = 0
        alt Session revoked or expired
            API-->>FE: 401 — Force re-login
        end
        API->>API: Extract userId, proceed with request
    
        Note over FE,DB:API: On15 logoutminutes laterallaccess sessionstoken revokedexpires
        FE->>API: POST /api/v1/auth/refresh (Cookie: refreshToken)
        API->>DB: SELECT refreshToken WHERE token = hash AND expiresAt > NOW()
        DB-->>API: Valid token record
        API->>API: Rotate: delete old, issue new refresh token
        API-->>FE: 200 { newAccessToken } + Set-Cookie: newRefreshToken
    
        Note over User,DB: Logout
        User->>FE: Click logout
        FE->>API: POST /api/v1/auth/logout
        API->>DB: UPDATEDELETE sessions SET revoked=1refreshToken WHERE user_iduserId = ?
        API-->>FE: 204 No Content
        FE->>FE: Clear httpOnlyaccessToken cookiefrom memory
    

    2.4 MFA — Dynamic Linking for PSD2 Compliance (Phase 2)

    BankID inherently provides possession + knowledge (two factors). For PSD2 Strong CustomerTwo-Factor Authentication (SCA):2FA)

    Method: TOTP (Time-based One-Time Password) — RFC 6238 Compatible apps: Google Authenticator, Authy, 1Password, Microsoft Authenticator

    Setup:

    1. POST /api/v1/auth/2fa/setup → QR code + base32 secret
    2. User scans QR code in authenticator app
    3. POST /api/v1/auth/2fa/verify { code } → 2FA enabled

    Login with 2FA:

    1. POST /api/v1/auth/login → { requires2FA: true, tempToken }
    2. POST /api/v1/auth/2fa/login { tempToken, code } → access + refresh tokens

    Backup codes: 10 single-use codes generated during setup, stored bcrypt-hashed.


    3. Authorization (RBAC)

    3.1 Role Permission Matrix

    Element
    FactorAction BankIDowner adminaccountantviewer
    Create invoice
    Edit invoice
    Delete invoice
    View invoice
    Approve expense
    Generate report
    Invite user
    Edit org settings
    Delete org

    3.2 Organization Scoping (IDOR Prevention)

    Every data query is scoped to the authenticated user's organizationId, injected by middleware:

    // Organization scope middleware — applied to all /api/v1/* routes
    app.use('/api/v1/*', (req, res, next) => {
      req.prismaWhere = { organizationId: req.user.organizationId };
      next();
    });
    
    // Applied to every Prisma query — no direct object reference possible
    await prisma.invoice.findMany({ where: { ...req.prismaWhere } });
    

    UUID primary keys throughout — no sequential IDs that enable enumeration.

    3.3 RBAC Middleware

    function requireRole(roles: string[]) {
      return (req: Request, res: Response, next: NextFunction) => {
        if (!roles.includes(req.user.role)) {
          return res.status(403).json({ error: 'Forbidden' });
        }
        next();
      };
    }
    
    app.post('/api/v1/invoices', requireRole(['owner', 'admin']), createInvoice);
    app.delete('/api/v1/invoices/:id', requireRole(['owner']), deleteInvoice);
    

    4. Encryption

    4.1 In Transit: TLS 1.3

    All traffic via HTTPS:

    • Frontend (Vercel): Automatic HTTPS, TLS 1.3
    • Backend (Railway): Automatic HTTPS, TLS 1.3
    • Cloudflare CDN: TLS 1.3 termination at edge, re-encrypted to origin
    • HSTS: Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

    4.2 At Rest: AES-256

    StoreMethod Notes
    KnowledgePostgreSQL (Railway EU West) BankID PIN / passwordUser-memorized secret
    PossessionBankID app / hardware tokenRegistered device
    Dynamic linkingAmount + payee shown in BankID signing dialogPrevents transaction substitution

    Current state (MVP): Email/password only — no SCA compliance. BankID integration is Phase 2.


    3. Authorization Model

    Model: RBAC (Role-Based Access Control) with mandatory user-scoped data isolation

    3.1 Roles & Permissions Matrix

    Permissionusermerchantadmin
    View own transactions
    Initiate remittance
    Initiate QR payment
    Manage recipients (own)
    View merchant dashboard
    View merchant transactions✓ (own)✓ (all)
    Manage merchant settings✓ (own)
    Admin functions
    View all users

    KYC Status Gates (required for financial operations):

    KYC StatusRemittanceQR PaymentBalance Read
    pendingBlockedBlockedBlocked
    approvedAllowedAllowedAllowed
    rejectedBlockedBlockedBlocked

    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

    admin (platform-level — full access)
      └── merchant (tenant-level — own data + merchant dashboard)
            └── user (standard — own data only)
    

    4. Data Encryption

    4.1 Encryption at Rest

    Data StoreEncryption MethodKey Management
    SQLite (current MVP)OS-levelAES-256 disk encryption (Railway default) PlatformFrankfurt or Paris region — EU data residency
    PostgreSQL (Phase 2)backups AES-256-GCM256 (TDERailway via cloud provider)automatic) AWS30-day KMSrolling backup retention
    PostgreSQLCloudflare R2 fødselsnummer(file storage) AES-256-GCM256 application-layer field encryptionserver-side AWSReceipts, KMSinvoice — separate key
    PostgreSQL — KYC documentsAES-256-GCMAWS KMS
    AWS S3 backupsSSE-KMS (AES-256)AWS KMS — backup key
    BankID certificatesAWS Certificate ManagerAWS-managed
    JWT_SECRETAWS Secrets ManagerAWS-managedPDFs

    4.23 EncryptionPassword in TransitSecurity

    • TLSAlgorithm: configurationbcrypt, (Cloudflare12 salt rounds
    • Requirements: Min 8 chars, uppercase + AWSlowercase App+ Runner):digit
    • Common password list:

      ssl_protocolsBlock TLSv1.3;top ssl_ciphers10K TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;known Strict-Transport-Security:weak max-age=63072000;passwords
    • includeSubDomains;
    • History: preloadPrevious 5 password hashes stored and blocked

    4.4 Financial Data Precision

    All APImonetary traffic:amounts HTTPSstored only (HTTP → 301 redirect). httpOnly cookies withas secure:NUMERIC(19,4) true— never float or JavaScript number. Exchange rates locked at transaction date. This prevents rounding errors in production.VAT and tax calculations.

    4.3 Field-Level Encryption for PII

    Fødselsnummer → AES-256-GCM, encrypted immediately on receipt from BankID
      Key: NATIONAL_ID_KEY (AWS KMS — separate from database master key)
      Stored: Only encrypted ciphertext 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 + WAF
      → Cloudflare CDN (TLS 1.3 termination at edge)
      → AWS App Runner (re-encrypts, TLS 1.3)
      → Application containers (Next.js)
      → Database (SQLite → PostgreSQL in private subnet)
    

    5.2 Firewall Rules

    SourceDestinationPortProtocolAction
    InternetCloudflare edge443HTTPSALLOW
    InternetAny80HTTPREDIRECT → 443
    CloudflareAWS App Runner443HTTPSALLOW
    App RunnerPostgreSQL (Phase 2)5432TCPALLOW
    AnyPostgreSQL directAnyAnyDENY
    Any (unauthenticated)/api/auth/*443HTTPSRate limited (10/60s)
    Any (unauthenticated)/api/transactions/*443HTTPSRate limited (10/60s)

    5.3 WAF Rules (Cloudflare)

    RuleActionNotes
    OWASP Core Rule SetBlockManaged Cloudflare ruleset
    SQL injectionBlockIncluding encoded variants
    XSSBlockIncluding DOM-based
    Bot trafficChallengeSuspicious patterns
    Rate limitingBlock>120 req/min from single IP to /api/rates
    Geo-blockingLog (review)Sanctioned countries per OFAC/EU list

    6. API Security

    6.1 Rate Limiting Strategy

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

    Endpoint TypeLimitWindowImplementation
    Auth routes (login, register)10 requests60 secondsSQLite-backed, per-IP
    Transaction routes (remittance, qr-payment)10 requests60 secondsSQLite-backed, per-IP
    Rates endpoint (/api/rates)120 requests60 secondsSQLite-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 text inputs sanitized
    sanitizeText(input) → removes HTML tags, control chars, trims, enforces maxLength
    
    // Financial validation
    validateAmount(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
    
    // Identity validation
    validateIBAN(iban) → format + checksum
    validatePIN(pin) → exactly 4 digits
    validateCurrency(currency) → whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR
    validateLanguage(lang) → whitelist: nb, en, bs, sq
    
    // SQL: ALL 24 endpoints use parameterized queries — zero string concatenation
    

    6.3 CORS Policy

    // Production CORS config
    {
      origin: ['https://getdrop.no', 'https://app.getdrop.no'],
      methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
      allowedHeaders: ['Content-Type', 'Authorization'],
      credentials: true,   // Required for httpOnly cookies
      maxAge: 86400
    }
    

    6.4 Content Security Policy

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

    Content-Security-Policy:
      default-src 'self';
      script-src 'self' 'unsafe-inline' 'unsafe-eval';
      frame-ancestors 'none';
      [remaining headers...]
    

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


    7. OWASP Top 10 Mitigation MatrixMitigations

    OWASP Risk MitigationImplementation Status
    A01: Broken Access Control RBAC + user_idorg-scoped scopingWHERE on every query + UUID PKs AND user_id = ? in all data queries; middleware.tsDesigned
    A02: Cryptographic Failures TLS 1.3 + AES-256 at rest + bcrypt(12) + no PII in JWTs auth.ts, next.config.ts, IKT-sikkerhetspolicyDesigned
    A03: Injection ParameterizedPrisma ORM parameterized queries exclusivelyAll 24 API endpoints — zero stringraw concatenationSQL for user input Designed
    A04: Insecure Design PSD2Multi-tenant pass-throughscoping modelat DB nolayer, storedimmutable fundsaudit trail Architecture decision — Drop never holds moneyDesigned
    A05: Security Misconfiguration HardenedHelmet.js headersheaders, +CORS featurewhitelist flags(no *), sanitized error messages next.config.ts headers, feature-flags.ts✓ (CSP partial)Designed
    A06: Vulnerable Components RecentDependabot dependencyalerts versions+ weekly npm audit + lock file committed jose ^6.1.3, bcryptjs ^3.0.3, next 16.1.6Planned
    A07: Auth Failures Rate limiting (5/15min auth) + sessionJWT revocationrotation + 2FA + bcrypt middleware.ts, sessions table, auth.tsDesigned
    A08: Software Integrity Signed commits + CI/CD pipeline + Dependabot GitHub Actions pipelinePartialPlanned
    A09: Logging Failures SentryImmutable LoggedAction audit trail + BetterStack structuredRailway logs + Sentry Error tracking + uptime monitoringPartial (no audit_log table yet)Designed
    A10: SSRF Input validation +(Zod schemas), allowlist for outboundvalidateIBAN, external API allowlistcalls (SEF, eRačun) PartialDesigned

    6. Rate Limiting

    EndpointLimitWindow
    POST /api/v1/auth/login5 requests15 minutes
    POST /api/v1/auth/register3 requests60 minutes
    POST /api/v1/auth/refresh10 requests15 minutes
    GET /api/v1/reports/*10 requests15 minutes
    All other /api/v1/*100 requests15 minutes

    Implementation: express-rate-limit, per-IP tracking, 429 Too Many Requests response.


    7. Input Validation

    All inputs validated with Zod schemas before reaching business logic:

    const createInvoiceSchema = z.object({
      customerId: z.string().uuid(),
      invoiceDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
      dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
      currencyCode: z.enum(['EUR', 'RSD', 'BAM']),
      items: z.array(z.object({
        description: z.string().min(1).max(500),
        quantity: z.number().positive(),
        unitPrice: z.number().nonnegative(),
        taxRate: z.number().min(0).max(100),
      })),
    });
    

    8. File Upload Security

    • Allowed types: JPG, PNG, PDF (receipts, invoice attachments)
    • Max size: 10 MB per file
    • Validation: MIME type + extension check
    • Storage: Cloudflare R2 (EU region), not served from app server
    • Planned (Phase 2): ClamAV virus scanning before R2 upload

    9. Audit Trail

    LoggedAction Table (Immutable — APPEND-ONLY)

    Every mutation logged:

    • tableName — which table was affected
    • action — INSERT / UPDATE / DELETE
    • userId — who performed the action
    • actionTimestamp — UTC timestamp
    • rowData — full row snapshot (before state)
    • changedFields — { field: { old: X, new: Y } }
    • clientIp — requester IP address

    Never delete from LoggedAction. On user data erasure (GDPR Art. 17), user ID is anonymized to "deleted-user" — the log entries themselves are retained for financial compliance.


    10. Security Headers Checklist(Helmet.js)

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

    Header ValueStatus
    Strict-Transport-Security max-age=63072000; includeSubDomains; preload
    Content-Security-Policy default-src 'self'; frame-ancestorsscript-src 'none'self' 'unsafe-inline'; ...img-src 'self' data: https:✓ (unsafe-inline/eval TODO)
    X-Content-Type-Options nosniff
    X-Frame-Options DENY
    X-Powered-ByRemoved
    Referrer-Policy strict-origin-when-cross-origin
    Permissions-Policycamera=(self), microphone=(), geolocation=(self)
    Cache-Control (auth responses)no-storeTODO
    X-XSS-Protection0 (rely on CSP)TODO

    9.11. DependencySecurity VulnerabilityPre-Launch ManagementChecklist

    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 (planned):

    • SAST: SemgrepJWT_SECRET / CodeQLgenerated (runs32+ chars, CSPRNG)
    •  JWT_REFRESH_SECRET generated (32+ chars, separate from JWT_SECRET)
    •  HTTPS enforced (no HTTP)
    •  CORS whitelist configured (only bilko.io)
    •  Rate limiting enabled and tested
    •  Helmet.js headers verified
    •  bcrypt rounds = 12
    •  All Prisma queries use org-scope WHERE
    •  Input validation on everyall PR)endpoints (Zod)
    • SCA: File upload restrictions in place
    •  LoggedAction audit trail active
    •  Error responses sanitized (no stack traces)
    • Dependabot +alerts npmenabled
    • audit
    • (runsPostgreSQL on everyRailway PREU +West daily on main)region
    • Container: TrivyDPAs signed (runsRailway, onVercel, DockerCloudflare, imageSendGrid)
    • build)
    •  Data deletion workflow tested

    Remediation SLAs:

    SeveritySLAOwner
    Critical (CVSS ≥ 9.0)24 hoursSecurity + affected team
    High (CVSS 7.0-8.9)7 daysAffected team
    Medium (CVSS 4.0-6.9)30 daysAffected team
    Low (CVSS < 4.0)90 daysNext sprint

    Current monitoring stack:

    Planned (pre-production):

    EventData LoggedRetentionAlert?
    Login successuser_id, ip, user_agent, timestamp1 yearNo
    Login failureip, email_hash, attempt_count, timestamp1 yearYes (> 5 failures)
    BankID authenticationuser_id, bankid_ref, success/failure1 yearYes (failure)
    Password changeuser_id, ip, timestamp1 yearYes
    Session revocationuser_id, session_ids, timestamp1 yearNo
    Transaction createduser_id, amount, corridor, timestamp5 yearsYes (>50,000 NOK)
    KYC status changeuser_id, old_status, new_status, operator5 yearsYes
    AML flag triggereduser_id, rule, transaction_id5 yearsYes (always)
    Admin actionactor_id, action, target, changes2 yearsYes

    Log retention (hvitvaskingsloven § 30): Minimum 5 years for KYC and transaction data.

    SIEM integration (Phase 2): BetterStack → Sentry → PagerDuty escalation chain.


    Approval

    Role Name Date Signature
    Author SecurityCompliance Architect 2026-02-23
    CISO / Security LeadCTO
    DPO
    CTOEngineering Lead