Skip to content

Architecture Security

Tiana_ edited this page May 30, 2026 · 1 revision

Architecture - Security

Authentication, authorization, secrets, encryption, OWASP coverage, threat-model summary, secure coding practices. Companion to Architecture-Overview, Threat-Modeling, Architecture-Resilience. Detailed threat-model per service in Threat-Modeling.


Security principles

  1. Zero trust - every request authenticated, every call authorized, even within the cluster.
  2. Defense in depth - multiple layers (TLS + auth + authz + invariants + audit + SIEM).
  3. Least privilege - services and humans get exactly the permissions they need, nothing more.
  4. Auditable by default - every state-changing action logged with actor, timestamp, intent.
  5. Secrets out of code - never in repos, never in env vars, always via secret store.
  6. Cryptographic agility - algorithms are config, rotation is built-in.
  7. PII minimization - store only what's required, encrypt what's stored, log only references.

1. Authentication: Keycloak + OIDC

Identity Provider: Keycloak 26.6.1 bundled in OSS docker-compose; production deployments use external OIDC (Auth0, Okta, Cognito, internal Keycloak HA).

1.1. Realm configuration

Realm: fincore

Clients:

Client Type Use
fincore-gateway confidential bearer-only Internal validation of JWTs
fincore-cli public CLI tool (PKCE flow)
fincore-dashboard public Operator UI (PKCE flow)
fincore-api-client confidential Adopter machine-to-machine (client_credentials)

Authentication flows:

  • OAuth2 client_credentials: machine-to-machine API access
  • OAuth2 authorization_code + PKCE: human users via dashboard/CLI
  • OAuth2 refresh_token: long-lived sessions

1.2. JWT structure

Access tokens: RS256-signed JWTs with TTL = 5 minutes:

{
  "iss": "https://keycloak.example.com/realms/fincore",
  "aud": ["fincore-api"],
  "sub": "01HX...user-id",
  "iat": 1714050000,
  "exp": 1714050300,
  "azp": "fincore-api-client",
  "scope": "openid ledger:read ledger:write payments:initiate",
  "realm_access": {
    "roles": ["LEDGER_WRITER", "PAYMENTS_INITIATOR"]
  },
  "resource_access": {
    "fincore-api": {
      "roles": ["LEDGER_WRITER"]
    }
  },
  "tenant": null,
  "actor_id": "01HX...service-id"
}

Refresh tokens: TTL 30 days, rotation on each use.

1.3. Token validation

Performed at the API Gateway, cached per request to avoid duplicate work:

@Component
class JwtValidator(
    private val jwksClient: JwksClient,
    @Qualifier("jwksCache") private val jwksCache: Cache<String, JWK>,
) {
    suspend fun validate(token: String): Principal {
        val jwt = SignedJWT.parse(token)
        val kid = jwt.header.keyID
        val jwk = jwksCache.getIfPresent(kid)
            ?: jwksClient.fetchKey(kid).also { jwksCache.put(kid, it) }

        val verifier = RSASSAVerifier(jwk.toRSAKey().toRSAPublicKey())
        require(jwt.verify(verifier)) { "JWT signature invalid" }

        val claims = jwt.jwtClaimsSet
        require(claims.expirationTime.after(Date())) { "JWT expired" }
        require(claims.issuer == EXPECTED_ISSUER) { "JWT issuer wrong" }
        require(EXPECTED_AUDIENCE in claims.audience) { "JWT audience wrong" }

        return Principal(
            subject = claims.subject,
            scopes = claims.getStringClaim("scope").split(" "),
            roles = claims.getStringListClaim("realm_access.roles"),
            actorId = claims.getStringClaim("actor_id"),
        )
    }
}

1.4. JWKS rotation

  • Keycloak rotates signing keys every 30 days
  • Gateway pre-fetches keys with KID; expired keys evicted from cache
  • During rotation: both old and new keys present in JWKS, no downtime
  • Compromised key revocation: realm-level rollover, all tokens invalidated within 5 min (TTL)

2. Authorization (RBAC + scope-based)

2.1. Role catalog

Roles map to business functions, not individual permissions:

Role Allowed actions
LEDGER_READER GET accounts, balances, transactions, entries
LEDGER_WRITER LEDGER_READER + create/update accounts
LEDGER_POSTER LEDGER_READER + post transactions
LEDGER_REVERSER LEDGER_READER + reverse transactions
LEDGER_ADMIN All ledger actions
PAYMENTS_INITIATOR Initiate payments
PAYMENTS_VIEWER View payments
PAYMENTS_ADMIN Cancel, manual override
COMPLIANCE_VIEWER View KYC sessions, AML alerts, cases
COMPLIANCE_RESOLVER + resolve cases
COMPLIANCE_ADMIN + manage rules, approve manual overrides
DECISION_VIEWER View rules, evaluations
DECISION_ADMIN Manage rules + activate
WEBHOOK_ADMIN Manage subscriptions
SYSTEM_OPERATOR Operational endpoints (replay, reconciliation, runbooks)
SYSTEM_AUDITOR Read-only access to all logs and audit trails (cannot modify state)

2.2. Scope-based delegation

For machine-to-machine (M2M) calls, OAuth2 scopes refine permissions:

Scope Purpose
ledger:read GET ledger resources
ledger:write All ledger writes (combined with role check)
payments:initiate Initiate payments
compliance:resolve Resolve cases
decision:evaluate Call decision engine
decision:admin Manage rules
webhook:subscribe Manage own subscriptions

Both role AND scope must match for an endpoint to authorize. This is intentional: scopes limit what a token can do, roles limit what a user may do, and a third-party with a delegated token can't escalate.

2.3. Spring Security configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http
        .csrf { it.disable() }              // Stateless API, no session
        .cors { it.configurationSource(corsConfig()) }
        .authorizeHttpRequests { auth ->
            auth
                .requestMatchers("/v1/health/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
                .requestMatchers("/v1/webhooks/**").permitAll() // signature-checked separately
                .anyRequest().authenticated()
        }
        .oauth2ResourceServer { oauth2 ->
            oauth2.jwt { jwt ->
                jwt.jwtAuthenticationConverter(JwtAuthenticationConverter().also {
                    it.setJwtGrantedAuthoritiesConverter(rolesConverter())
                })
            }
        }
        .sessionManagement { it.sessionCreationPolicy(STATELESS) }
        .headers { headers ->
            headers
                .frameOptions { it.deny() }
                .contentSecurityPolicy { it.policyDirectives(CSP_POLICY) }
                .strictTransportSecurity { it.maxAgeInSeconds(31536000).preload(true) }
                .xssProtection { it.disable() }     // CSP supersedes
                .referrerPolicy { it.policy(STRICT_ORIGIN_WHEN_CROSS_ORIGIN) }
        }
        .build()
}

Method-level checks:

@RestController
class TransactionController(private val service: TransactionService) {

    @PostMapping("/v1/transactions")
    @PreAuthorize("hasRole('LEDGER_POSTER') and hasAuthority('SCOPE_ledger:write')")
    suspend fun post(@Valid @RequestBody req: PostTransactionRequest): ResponseEntity<TransactionResponse> { ... }

    @PostMapping("/v1/transactions/{id}/reverse")
    @PreAuthorize("hasRole('LEDGER_REVERSER') and hasAuthority('SCOPE_ledger:write')")
    suspend fun reverse(@PathVariable id: UUID, @Valid @RequestBody req: ReverseRequest): ResponseEntity<TransactionResponse> { ... }
}

3. Network security

3.1. TLS everywhere

Boundary Protocol Notes
Public internet ↔ Ingress TLS 1.3 (mandatory), ECDSA P-256 cert Let's Encrypt or commercial CA
Ingress ↔ Gateway TLS 1.2+ (in-cluster) mTLS recommended via service mesh
Gateway ↔ Services TLS optional in v0.1, mTLS in v1.0+ service mesh (Istio) recommended
Services ↔ DB TLS mandatory self-signed CA OK if cluster-internal
Services ↔ Kafka SASL_SSL (Kafka native) TLS + SASL/SCRAM authentication
Services ↔ Redis TLS (rediss://) mandatory in production
Services ↔ External providers TLS 1.2+ + cert pinning where supported per-provider config

3.2. Cipher policy

  • TLS 1.3 only for new connections (where supported)
  • TLS 1.2 fallback with whitelisted ciphers: ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-GCM-SHA256
  • No SSL 3.0, no TLS 1.0/1.1, no static RSA, no MD5, no SHA-1, no CBC ciphers
  • HSTS header: max-age=31536000; includeSubDomains; preload

3.3. Network policies (Kubernetes)

Default-deny, with explicit allows:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-default
  namespace: fincore-engine
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ledger-allow
  namespace: fincore-engine
spec:
  podSelector:
    matchLabels: { app: ledger-service }
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: api-gateway }
      ports: [{ port: 8080 }]
    - from:
        - podSelector:
            matchLabels: { app: payment-service }
      ports: [{ port: 8080 }]
  egress:
    - to:
        - podSelector:
            matchLabels: { app: postgres-primary }
      ports: [{ port: 5432 }]
    - to:
        - namespaceSelector:
            matchLabels: { name: kube-system }
      ports: [{ port: 53, protocol: UDP }] # DNS

4. Secrets management

4.1. Storage tiers

Tier Use
Vault / AWS Secrets Manager / GCP Secret Manager / K8s Secrets with KMS All production secrets
.env files Dev only, gitignored
Plain env vars Forbidden in production for sensitive values
Code constants Forbidden always

4.2. Secret types in FinCore

Secret Where Rotation
DB credentials Vault 30 days
Kafka SASL credentials Vault 90 days
Redis password Vault 90 days
Keycloak admin password Vault, sealed manual, audited
Keycloak client secrets Vault, per-client 90 days
Webhook subscription HMAC keys DB (encrypted column with KMS-DEK) client-controlled
External provider API keys (KYC, bank, sanctions, LLM) Vault per provider policy
TLS private keys cert-manager + cluster issuer 90 days (Let's Encrypt)

4.3. Spring integration

spring:
  cloud:
    vault:
      authentication: KUBERNETES
      kubernetes:
        role: fincore-ledger
      uri: ${VAULT_ADDR}
      kv:
        enabled: true
        backend: secret
        default-context: fincore/ledger

# values pulled by name
spring.datasource.password: ${vault.fincore.ledger.db_password}

4.4. Encryption at rest

  • Postgres: TDE via filesystem-level encryption (LUKS) and/or column-level encryption for PII
  • Keycloak DB: same as above
  • Kafka topic data: Redpanda native encryption-at-rest enabled
  • Redis: encryption-at-rest if backed by ElastiCache/MemoryStore; minimum TLS in transit

4.5. PII column encryption

For columns containing PII (KYC evidence references, customer notes, encrypted API tokens):

@Entity
@Table(name = "kyc_documents")
class KycDocumentRecord(
    @Id val id: UUID,
    @Column(name = "external_ref")
    @Convert(converter = AesGcmConverter::class)
    val externalRef: String,           // encrypted at rest with DEK from Vault
    val createdAt: Instant,
)

AesGcmConverter uses AES-256-GCM, DEK fetched from Vault per service, KEK rotated quarterly.


5. OWASP Top 10 (2021) coverage

# OWASP FinCore mitigation
A01 Broken Access Control RBAC + scope-based + method-level @PreAuthorize + integration tests asserting deny
A02 Cryptographic Failures TLS 1.3, AES-256-GCM, RS256 JWT, no MD5/SHA-1, secrets in Vault
A03 Injection JPA prepared statements only (no native string concat), input validation via Jakarta Bean Validation, CSP headers
A04 Insecure Design Threat-modeling done per service (Threat-Modeling), invariants enforced at multiple layers
A05 Security Misconfiguration Security headers via Spring Security, TLS-only, default-deny NetworkPolicy, non-root containers, distroless base images
A06 Vulnerable Components Dependabot weekly, OWASP Dependency-Check in CI, Trivy scan on Docker images, monthly base image refresh
A07 Auth & Session Failures Stateless JWT, short TTLs, refresh rotation, no plaintext credentials, account lockout via Keycloak
A08 Software & Data Integrity Failures Signed Docker images (cosign), signed git commits, SBOM (CycloneDX), npm/Maven dependency lock
A09 Logging & Monitoring Failures Structured logging, MDC correlation IDs, Prometheus metrics, alert on auth failures, audit log retention 7 years
A10 Server-Side Request Forgery Outbound calls only to allowlisted domains, no user-supplied URLs in /v1/* (except webhook subscriptions which are explicitly user-owned and rate-limited)

5.1. Webhook subscription URL validation (anti-SSRF)

Subscriptions allow user-supplied URLs. To prevent SSRF:

fun validateWebhookUrl(url: HttpUrl): Boolean {
    require(url.scheme == "https") { "https only" }
    val host = url.host

    // Block private IPs
    val ip = InetAddress.getByName(host)
    require(!ip.isLoopback && !ip.isSiteLocalAddress && !ip.isAnyLocalAddress && !ip.isLinkLocalAddress) {
        "private IP not allowed"
    }
    require(!isMulticast(ip)) { "multicast not allowed" }

    // Block metadata services
    require(host !in BLOCKED_METADATA_HOSTS) { "metadata host blocked" }
    require(!ip.hostAddress.startsWith("169.254.")) { "metadata IP blocked" }

    // Optional: allowlist if extra paranoid
    return true
}

val BLOCKED_METADATA_HOSTS = setOf(
    "metadata.google.internal",
    "instance-data.ec2.internal",
    "169.254.169.254",
)

6. Input validation

6.1. Layers

  1. Bean validation at controller (Jakarta Bean Validation):
data class PostTransactionRequest(
    @field:NotBlank @field:Size(max = 255) val reference: String,
    @field:Size(max = 2048) val description: String?,
    @field:NotEmpty @field:Size(min = 2, max = 1000)
    val entries: List<EntryRequest>,
    @field:Valid val metadata: Map<String, String>?,
)

data class EntryRequest(
    @field:NotNull val accountId: UUID,
    @field:NotNull @field:DecimalMin("-1000000000000") @field:DecimalMax("1000000000000")
    val amount: BigDecimal,
    @field:NotBlank @field:Pattern(regexp = "^[A-Z]{3}$") val currency: String,
    @field:NotNull val direction: EntryDirection,
)
  1. Domain invariant in entity constructor (last line of defense):
class Transaction(...) {
    init {
        require(reference.length in 1..255)
        require(entries.size in 2..1000)
        // SUM=0 check ...
    }
}
  1. DB constraints: NOT NULL, CHECK, FK, deferred trigger

6.2. Special cases

  • JSON depth limit: max 10 nested levels (anti-billion-laughs)
  • Request body size: max 1 MB (configurable per route)
  • Query string length: max 8 KB
  • Header size: max 32 KB
  • Header count: max 100

Spring Boot defaults adjusted:

server:
  max-http-header-size: 32KB
spring:
  servlet:
    multipart:
      max-file-size: 1MB
      max-request-size: 1MB
  mvc:
    async:
      request-timeout: 30000

7. Rate limiting

7.1. Tiers

Tier Limit Per Storage
Per-IP global 100 req/sec, 1000 burst IP Redis
Per-authenticated-user 1000 req/sec user_id Redis
Per-endpoint global varies endpoint+IP Redis
Per-IP webhook 10 req/sec IP Redis

7.2. Algorithms

  • Token bucket for normal endpoints (smoother)
  • Sliding window log for sensitive endpoints (auth, webhooks)

7.3. Spring Cloud Gateway integration

spring:
  cloud:
    gateway:
      default-filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 100
            redis-rate-limiter.burstCapacity: 200
            redis-rate-limiter.requestedTokens: 1
            key-resolver: "#{@principalKeyResolver}"

7.4. Response when limited

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714050300
Content-Type: application/problem+json

{
  "type": "https://docs.fincore.dev/errors/rate-limit",
  "title": "Too many requests",
  "status": 429,
  "detail": "Rate limit exceeded for principal abc-123"
}

8. Audit & forensics

8.1. What we audit

Event Where stored Retention
All state-changing API calls structured log + audit_events table 7 years
All authentication events (success + failure) Keycloak audit + log 1 year
All authorization decisions (allow/deny) log 1 year
All decision engine evaluations decision_logs 7 years
All compliance case actions case_notes + compliance_events 7 years
All webhook deliveries webhook_deliveries 1 year
All admin actions (rule changes, manual overrides) audit_events table forever
All ledger writes transactions + entries (immutable journal) forever

8.2. Audit event format

{
  "id": "audit_01HX...",
  "occurredAt": "2026-04-25T10:00:00Z",
  "actor": {
    "type": "USER",
    "id": "01HX...",
    "displayName": "user@example.com",
    "ipAddress": "203.0.113.42",
    "userAgent": "Mozilla/5.0..."
  },
  "action": "transaction.posted",
  "resourceType": "Transaction",
  "resourceId": "tx_01HX...",
  "result": "SUCCESS",
  "context": {
    "correlationId": "01HX...",
    "requestId": "01HX...",
    "method": "POST",
    "path": "/v1/transactions",
    "statusCode": 201
  },
  "diff": {
    "before": null,
    "after": {
      "id": "tx_01HX...",
      "reference": "demo-001",
      "..."
    }
  }
}

8.3. SIEM integration

All security-relevant events ship to SIEM (Splunk, Sumo Logic, Elastic SIEM, in-cluster Wazuh):

  • Authentication failures
  • Authorization denials
  • Rate limit hits beyond threshold
  • Anomalous transaction patterns
  • Admin actions
  • Network policy violations (from Falco/Cilium)

9. Container & supply chain security

9.1. Base images

  • Distroless Java image (gcr.io/distroless/java21-debian12:nonroot)
  • No shell, no package manager, no curl, minimal CVE surface
  • USER nonroot (UID 65532)
  • Read-only root filesystem (writable mounts only for tmp, logs)

9.2. Multi-stage Dockerfile pattern

FROM gradle:8.14-jdk21 AS builder
WORKDIR /app
COPY --chown=gradle:gradle . .
RUN ./gradlew :services:ledger:bootJar --no-daemon

FROM gcr.io/distroless/java21-debian12:nonroot
WORKDIR /app
COPY --from=builder /app/services/ledger/build/libs/*.jar /app/app.jar
EXPOSE 8080
USER nonroot
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

9.3. Image signing & SBOM

  • cosign signs every published image (keyless via Sigstore OIDC)
  • CycloneDX SBOM generated and attached to image
  • Trivy scan in CI fails on HIGH/CRITICAL vulnerabilities

9.4. Pod security

spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 65532
    fsGroup: 65532
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: ledger
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: [ALL]
      resources:
        requests: { memory: "512Mi", cpu: "200m" }
        limits:   { memory: "2Gi",   cpu: "2"   }

PSA-restricted enforced at namespace level.


10. Webhook security

10.1. Inbound (provider → us)

  • Endpoint published per provider (/v1/webhooks/payments/{providerId})
  • HMAC-SHA256 signature in X-Provider-Signature header
  • Per-provider shared secret in Vault, rotated quarterly
  • Replay protection: timestamp in signed payload, reject events older than 5 min
  • processed_webhooks table dedups by provider_event_id

10.2. Outbound (us → subscriber)

  • Subscriber-supplied secret stored encrypted at rest (column-level KEK from Vault)
  • HMAC-SHA256 signature: X-Signature: sha256=<hex> over raw body
  • Timestamp header: X-Timestamp: <unix ms> for replay protection
  • Subscriber re-computes HMAC and verifies (sample code in API-Webhooks)

10.3. Webhook URL allowlist (configurable)

Production deployments can require webhook URLs match allowlist:

fincore:
  webhook:
    outbound:
      url-allowlist:
        - "https://*.example.com/**"
        - "https://api.partner.example.com/**"

If allowlist set, URLs not matching are rejected at subscription creation.


11. Compliance-relevant security commitments

11.1. PCI DSS adjacency

FinCore is not a PCI DSS environment by itself (no card data processing).

  • Card data never enters FinCore - adopters using card processors handle PCI at their tokenization boundary.
  • We support PCI Self-Assessment Questionnaire A (SAQ-A) for the rare adopter that uses redirect-based card collection.
  • For SAQ-D adopters, network segregation is the adopter's responsibility (FinCore in non-CDE).

11.2. SOC 2 Type II readiness (Y1 H2 target)

The architecture supports SOC 2 trust services criteria:

Criterion FinCore mechanism
Security All of Section 1-10 above
Availability SLOs (Architecture-SLA-SLI-SLO), DR plan (Architecture-Resilience), incident response runbook
Processing Integrity Ledger invariants, idempotency, outbox correctness, audit trail
Confidentiality Encryption at rest, encryption in transit, RBAC, secret management
Privacy PII minimization, encryption, data deletion (right-to-be-forgotten in Y1 H2)

11.3. GDPR readiness

GDPR right FinCore mechanism
Right of access GET /v1/users/{id}/data-export (Y1 H2)
Right to rectification Update endpoints with audit log
Right to erasure Soft delete + DEK rotation (data unrecoverable, audit-preserved) - Y1 H2
Right to data portability Same as access, JSON/CSV formats - Y1 H2
Right to restriction Account FROZEN state
Audit log of consent consent_log table (Y1 H2)

11.4. AML / KYC compliance ownership

FinCore provides the infrastructure for AML/KYC compliance - rule engine, case management, audit logs, screening orchestration. The regulatory ownership sits with the adopter / their banking partner / their compliance team. We are not a registered AML reporting entity.


12. Threat model summary

Detailed threat model per service in Threat-Modeling. Top threats and mitigations:

Threat Mitigation
Compromised JWT signing key Short TTL (5 min), JWKS rotation, revocation via realm rollover
Compromised service account Vault-issued short-lived creds, audit on credential use
Insider with prod DB access Audit log of every DB query, DB user audit, separate roles for app vs DBA
Webhook subscriber URL pivot to internal services URL validation (anti-SSRF), allowlist option
Replay attack on webhook Timestamp + dedup table
MITM on external provider call TLS + cert pinning where supported
DoS via expensive endpoints Rate limiting + circuit breaker on downstream
Data exfiltration via API Per-user rate limiting, pagination caps, audit log review
Cache poisoning Cache key includes principal context where relevant; signed cache entries for sensitive data
Race condition exploit Optimistic locking + invariant triggers + idempotency

13. Vulnerability disclosure

Security policy in SECURITY.md:

We accept reports via:
- security@fincore.dev (PGP key: ...)
- GitHub Security Advisories (private)

Out of scope:
- Findings on dependencies (report to upstream)
- Issues in sandbox/dev configurations
- DoS via volumetric attacks
- Social engineering of maintainers

Response SLA:
- Acknowledgment within 48 hours
- Triage within 5 business days
- Fix within 30 days for HIGH/CRITICAL, 90 days for MEDIUM
- Bounties: case-by-case via GitHub Sponsors

90-day public disclosure timeline if unfixed.


14. Security-relevant dependencies

We pin and audit:

  • Spring Boot - bi-weekly Dependabot + manual major version review
  • Keycloak - monthly version review
  • PostgreSQL JDBC driver - version-locked, tested
  • Hibernate ORM - bumped with Spring Boot
  • Liquibase - manually reviewed before bump
  • Kafka client - version-locked with broker
  • jackson-databind - Dependabot daily
  • Apache Commons - known offender, minimal use, locked

Daily Dependabot scan + weekly OWASP Dependency-Check in CI.


15. Penetration testing & red team

  • Annual external pentest (Y1 H2 target - recommended cadence for production fintech adopters)
  • Quarterly internal red team (Y2+)
  • Continuous fuzzing of API surface (planned via Schemathesis from OpenAPI spec - Y1 H2)

Findings ranked CVSS, tracked as security issues with strict SLA.


16. Operator security

For operator UI / admin actions:

  • Mandatory MFA for all admin roles (enforced at Keycloak level)
  • Session TTL 30 minutes for admin UI; auto-logout
  • Re-authentication for sensitive actions (rule activation, case escalation, production secret rotation)
  • Privileged Access Management (PAM) integration for credentials with full DB access
  • Audit log of every admin click - operator UI tracks UI actions in addition to API actions

17. Pre-release security checklist

Before tagging any v0.x.0:

  • No secrets in repo (gitleaks scan)
  • All dependencies up to date or pinned with reason
  • Trivy scan clean (no HIGH/CRITICAL CVEs)
  • CodeQL scan clean
  • OWASP Top 10 coverage matrix updated
  • All endpoints have @PreAuthorize (or permitAll with explicit reason)
  • All controller methods have integration test asserting auth/authz
  • All entities with PII columns have @Convert encryption
  • No println, no debug logs in production code paths
  • No hardcoded URLs to production services
  • SBOM generated and attached
  • Image signed with cosign
  • SECURITY.md current (PGP key valid)
  • Threat model reviewed for any new aggregate
  • Penetration test done before v1.0

Related reading

Clone this wiki locally