Skip to content
My Guidelines
Guides
In this guide (79 items)

Security

Security Guidelines

MANDATORY FOR ALL AI AGENTS: Every endpoint, consumer, scheduler, and outbound call MUST apply the controls below. Security concerns belong in the infrastructure layer, NEVER in domain. Authentication, authorization, secrets, encryption, validation, rate limiting, and audit are cross-cutting and MUST be implemented as adapters/decorators per the Chain of Responsibility pattern. Domain code never reads SecurityContextHolder, environment variables, or HTTP headers directly.

Project-agnostic: code samples use placeholder package com.example.platform. Replace with your project’s actual package when applying patterns.

Companion guides:

  • hexagonal-architecture.md — layer placement, decorator/CoR pattern.
  • adr.md — capture security architecture decisions (auth model, crypto choice, secrets backend).
  • arch-doc-spec.md §STRIDE — threat model section of board deliverable.
  • observability.md — structured logging, never log PII/secrets.
  • spring-boot.md — Spring Security config patterns.
  • docker-k8s skill — container hardening.

Table of Contents

  1. Threat Model Baseline (STRIDE)
  2. Authentication
  3. Authorization
  4. Current User Port
  5. Secrets Management
  6. Input Validation
  7. Injection Prevention
  8. Cryptography
  9. Transport Security
  10. Logging & PII
  11. Rate Limiting & DoS
  12. CORS & CSRF
  13. Audit Trail
  14. Dependency & Supply Chain
  15. Container & Runtime
  16. Multi-tenancy Isolation
  17. Error Handling & Information Disclosure
  18. Compliance Hooks
  19. Security Testing
  20. Incident Response Checklist

1. Threat Model Baseline (STRIDE)

Every new external surface (REST endpoint, Kafka consumer, scheduled job ingesting external state, file upload, webhook) MUST be threat-modeled before merge. Use the /arch-threat command or arch-threat-modeler agent. Map each threat to existing or missing mitigations.

STRIDE Threat Default Mitigation
Spoofing Impersonation of user, service, message origin JWT verification, mTLS service-to-service, signed messages, idempotency-key
Tampering Modification of request, payload, persisted state TLS in transit, signed JWTs, DB integrity constraints, immutable audit log
Repudiation Actor denies performing action Append-only audit log, signed events, traceId + userId in every log
Information Disclosure Leak of PII, secrets, internal topology RBAC, field-level redaction, structured logging with allowlist, error mapping
Denial of Service Resource exhaustion Rate limiting, request size limits, circuit breakers, timeouts, pagination
Elevation of Privilege Gaining unauthorized capability RBAC enforced per method, no @PreAuthorize bypass, principle of least privilege on DB user

Output: THREAT-MODEL.md lives under docs/security/ per arch-doc-spec.md §STRIDE.


2. Authentication

2.1 Default: OAuth2 / OIDC with JWT

  • Issuer: external IdP (Keycloak, Auth0, Cognito, AzureAD). Never roll your own auth server.
  • Token format: JWT signed with RS256/ES256 (asymmetric). HS256 forbidden for cross-service tokens.
  • Validation MUST verify: signature, iss, aud, exp, nbf, iat.
  • Public keys via JWKS endpoint with caching (5–60 min TTL) and key rotation handled.
  • Tokens MUST be short-lived (≤15 min for access tokens). Refresh tokens with rotation + revocation.

2.2 Spring Security configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // enables @PreAuthorize (see §3)
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity, jwtDecoder: JwtDecoder): SecurityFilterChain =
http
.csrf { it.disable() } // stateless API
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests {
it.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/actuator/**").hasAuthority("SCOPE_actuator")
.anyRequest().authenticated()
}
.oauth2ResourceServer { rs ->
rs.jwt { jwt -> jwt.decoder(jwtDecoder).jwtAuthenticationConverter(rolesConverter()) }
}
.headers { h ->
h.contentSecurityPolicy { it.policyDirectives("default-src 'none'; frame-ancestors 'none'") }
.httpStrictTransportSecurity { it.maxAgeInSeconds(31536000).includeSubDomains(true) }
.frameOptions { it.deny() }
.referrerPolicy { it.policy(ReferrerPolicy.NO_REFERRER) }
}
.build()
}

2.3 Service-to-service

  • Internal cluster: mTLS (Istio, Linkerd, AWS App Mesh) or service-account JWTs (SPIFFE/SPIRE).
  • Outbound: client-credentials flow with scoped tokens. Never share user tokens across service boundaries.
  • Webhooks (inbound): HMAC signature verification with shared secret rotated quarterly.

2.4 API keys (when JWT is overkill)

  • ONLY for machine-to-machine, low-trust scenarios (public read-only APIs, partner integrations).
  • Store as bcrypt/argon2 hash. Compare in constant time.
  • Per-key rate limit + revocation list.
  • Never embed in URLs (query params). Header only: X-API-Key: <key>.

3. Authorization

3.1 RBAC default

  • Roles encoded as JWT claims (roles, permissions, or scope).
  • Enforce via @PreAuthorize on inbound port adapters (controllers, consumers), never on use cases.
  • Domain has no concept of “roles” — it accepts a User value object with capabilities resolved upstream.
// Controller (infrastructure/entry-points/rest-controller)
@RestController
@RequestMapping("/api/v1/invoices")
class InvoiceController(private val createInvoice: CreateInvoicePort) {
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_invoice:write')")
fun create(@Valid @RequestBody req: CreateInvoiceRequest, jwt: @AuthenticationPrincipal Jwt): InvoiceResponse =
createInvoice.execute(req.toCommand(jwt.subject)).toResponse()
}

3.2 Method security forbidden in use cases

// WRONG — use case depends on Spring Security
class CreateInvoiceUseCase {
@PreAuthorize("hasRole('ADMIN')") // ❌ framework leak into application
override fun execute(command: CreateInvoiceCommand): Invoice = ...
}
// RIGHT — authorization at the adapter, use case is framework-free
@RestController
class InvoiceController(private val createInvoice: CreateInvoicePort) {
@PostMapping
@PreAuthorize("hasRole('ADMIN')") // ✅ infrastructure concern stays in adapter
fun create(...) = createInvoice.execute(...)
}

3.3 ABAC / fine-grained

When RBAC is insufficient (per-resource ownership, attribute-based rules):

  • Express the rule as a domain policy (e.g., InvoicePolicy.canRead(viewer, invoice)).
  • Adapter loads the resource, calls the policy, returns 403 if denied.
  • Policy is pure — testable without Spring, no @PreAuthorize.

3.4 Defense in depth

  • DB row-level security (PostgreSQL RLS) for multi-tenant data, even if app filters.
  • Negative tests for every endpoint: anonymous, wrong role, wrong tenant.

4. Current User Port

Domain and use cases need the acting user’s identity (for ownership checks, audit, idempotency scoping) but MUST NOT import Spring Security.

// application/port/outbound — port belongs to APPLICATION, depends only on domain
fun interface CurrentUserPort {
fun get(): AuthenticatedUser
}
data class AuthenticatedUser(
val subject: UserId,
val tenantId: TenantId,
val roles: Set<Role>
)
infrastructure/driven-adapters/security
@Component
class SpringSecurityCurrentUserAdapter : CurrentUserPort {
override fun get(): AuthenticatedUser {
val auth = SecurityContextHolder.getContext().authentication
?: throw UnauthenticatedException()
val jwt = (auth.principal as? Jwt) ?: throw UnauthenticatedException()
return AuthenticatedUser(
subject = UserId(jwt.subject),
tenantId = TenantId(jwt.getClaimAsString("tenant_id")),
roles = jwt.getClaimAsStringList("roles").map(::Role).toSet()
)
}
}

Use cases inject CurrentUserPort like any other port. Tests provide a fake.


5. Secrets Management

5.1 Hard rules

  • NEVER hardcode secrets in source, configuration committed to git, Dockerfile, or application.yml.
  • NEVER log secrets, even at DEBUG.
  • NEVER pass secrets via CLI args (visible in process listings).
  • Pre-commit hook MUST scan for: sk-, ghp_, AKIA, xoxb-, password=, secret=, BEGIN PRIVATE KEY, BEGIN RSA. See ~/.claude/hooks/claude-secret-scan.sh.

5.2 Sources of truth (in order of preference)

Backend When
HashiCorp Vault On-prem, multi-cloud, dynamic DB credentials
AWS Secrets Manager / Parameter Store AWS-native, IAM-bound, KMS-encrypted
Kubernetes Secrets (sealed) K8s-native; pair with Sealed Secrets / External Secrets Operator
GCP Secret Manager / Azure Key Vault Cloud-native equivalents
OS environment variables Local dev only; production env vars must come from a secrets backend

5.3 Rotation

  • Database credentials: dynamic via Vault DB engine OR rotated ≤90 days.
  • API keys, JWT signing keys: rotated ≤90 days; emergency-rotatable in < 1h.
  • TLS certificates: automated via cert-manager / ACME; expiry alert at 30 days.

5.4 Spring Boot integration

// Use spring-cloud-config-vault, AWS Secrets Manager starter, or @Value with externalized config
@ConfigurationProperties(prefix = "platform.db")
data class DbProperties(val url: String, val username: String, val password: String)
// application.yml — references env, NEVER the literal secret
platform:
db:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}

6. Input Validation

6.1 Two-layer validation

Layer What How
DTO (entry adapter) Type, format, size, basic format Bean Validation (@NotNull, @NotBlank, @Size, @Pattern, @Email)
Domain (constructor / factory) Business invariants require(), factory method returning Result, sealed exception hierarchy
// DTO — superficial validation at adapter boundary
data class CreateInvoiceRequest(
@field:NotNull @field:Positive val amountCents: Long,
@field:NotBlank @field:Size(max = 3) @field:Pattern(regexp = "[A-Z]{3}") val currency: String,
@field:NotNull @field:FutureOrPresent val dueDate: LocalDate
)
// Domain — invariants
data class Money(val amountCents: Long, val currency: Currency) {
init {
require(amountCents >= 0) { "amount must be non-negative" }
}
}

6.2 Boundary rule

  • Trust nothing from outside. REST body, query params, headers, Kafka payloads, file uploads, webhook payloads — all hostile until validated.
  • Trust internal calls between layers. Domain → application boundary uses typed value objects, validation already done.
  • Validate ONCE at the entry boundary; do not re-validate the same constraint at every layer (causes drift).

6.3 File uploads

  • Whitelist content-type AND magic bytes (clients lie about content-type).
  • Cap max size (spring.servlet.multipart.max-file-size) AND reject early via reverse-proxy / API gateway.
  • Store outside the web root. Generate server-side filenames; never echo user input as a path component.
  • Scan for malware (ClamAV, S3 + Macie) for any untrusted upload.

6.4 Deserialization

  • Restrict Jackson polymorphic types: never enable default typing (activateDefaultTyping) and never put @JsonTypeInfo on an Object/interface base without a strict allowlist (PolymorphicTypeValidator) — an open type id lets an attacker name an arbitrary gadget class for deserialization.
  • Disable XML external entity (XXE) processing.
  • Never ObjectInputStream on untrusted bytes.

7. Injection Prevention

7.1 SQL Injection

  • Always parameterized queries. Spring Data JDBC @Query with :name placeholders.
  • NEVER string concatenation, String.format, or "WHERE x = '$value'".
  • Dynamic ORDER BY / column names: validate against an allowlist enum, never pass user input.
  • Use a least-privileged DB user per service (no SUPERUSER, no CREATE).
// WRONG
val sql = "SELECT * FROM invoices WHERE status = '$status'" // ❌ injection
// RIGHT
@Query("SELECT * FROM invoices WHERE status = :status")
fun findByStatus(@Param("status") status: String): List<Invoice>

7.2 NoSQL Injection

  • MongoDB: never pass user JSON directly to find(). Use typed repository methods.
  • Redis: prefer typed Lettuce/Jedis commands over raw EVAL with user input.

7.3 OS Command Injection

  • Forbidden in business code. If absolutely required, use ProcessBuilder with command(List) form (no shell expansion). Allowlist binaries by full path.

7.4 LDAP / XPath / Header Injection

  • Always escape user input per the target language (LDAPv3 RFC 4515, XPath 1.0 quoting).
  • HTTP header injection: reject CR/LF in any user-controlled value going into a header.

7.5 XSS (server-rendered HTML or JSON-with-HTML clients)

  • Default to JSON APIs. If returning HTML, Thymeleaf auto-escapes by default — never use th:utext on user input.
  • Set Content-Security-Policy, X-Content-Type-Options: nosniff.
  • For SSE / WebSocket: same input validation rules apply.

8. Cryptography

8.1 Hashing passwords

  • Argon2id (preferred) or bcrypt (cost ≥12).
  • NEVER MD5, SHA1, plain SHA-256 for passwords.
  • Spring Security: Argon2PasswordEncoder or BCryptPasswordEncoder.

8.2 Symmetric encryption

  • AES-256-GCM for data at rest. Never ECB, never CBC without explicit MAC.
  • Authenticated encryption (AEAD) only. Random 96-bit IV per message.
  • Key from KMS / Vault transit engine — never hardcoded.

8.3 Asymmetric

  • Ed25519 or ECDSA P-256 for signing.
  • RSA-2048+ when interop with legacy systems requires it; RSA-4096 for long-lived signing keys.

8.4 Hashing data integrity / fingerprints

  • SHA-256 or SHA-3-256. Never MD5 or SHA1.

8.5 Random

  • SecureRandom for tokens, IDs, salts. Never Math.random() or Random for security.
  • Token entropy ≥128 bits (≥22 base64 chars).

8.6 Key management

  • Keys live in KMS / Vault / HSM. Application receives data-encryption keys (DEKs) wrapped by a key-encryption key (KEK).
  • Key rotation ≤365 days. Re-encrypt only the DEKs; bulk data re-encryption is the rotation event for KEKs.

9. Transport Security

  • TLS 1.2 minimum, TLS 1.3 preferred. TLS 1.0/1.1 disabled at every termination point.
  • HSTS header on all HTTPS responses: max-age=31536000; includeSubDomains; preload.
  • Certificate validation enabled on all outbound clients. Never trustAllCerts.
  • Cipher suites: forward-secret (ECDHE) only. Disable RC4, 3DES, CBC-with-SHA1.
  • Pin only when you control both ends; otherwise rely on CA + CT logs.
  • gRPC: mTLS by default for internal calls.

10. Logging & PII

10.1 Never log

  • Passwords, tokens (access, refresh, API keys), session IDs.
  • Full credit cards, CVV, SSN, government IDs.
  • Full request/response bodies on authenticated endpoints (may contain PII).
  • Email + phone in production (depends on regulation; tokenize if needed).

10.2 Always log

  • traceId, spanId, userId (subject claim), tenantId, ip (truncated /24 for IPv4, /48 for IPv6).
  • Outcome (success, failure_reason).
  • Latency for performance correlation.

10.3 Structured logging

JSON output in production. Use MDC (Logback) / structured-logging library. See observability.md §8.

log.atInfo()
.addKeyValue("event", "invoice.created")
.addKeyValue("userId", user.subject.value)
.addKeyValue("tenantId", user.tenantId.value)
.addKeyValue("invoiceId", invoice.id.value)
.log("Invoice created")

10.4 Redaction

Centralized redactor at the logger appender level — strips known sensitive keys (password, token, secret, authorization) even if a caller accidentally passes them.


11. Rate Limiting & DoS

11.1 Layers

Layer Tool Granularity
Edge / CDN CloudFlare, AWS WAF, Akamai IP, ASN, geo
API Gateway Kong, Apigee, AWS API Gateway, Spring Cloud Gateway Per-route, per-API-key
Application Bucket4j, Resilience4j, Redis-backed Per-user, per-tenant, per-endpoint

11.2 Defaults

  • Anonymous endpoints: ≤10 req/min per IP.
  • Authenticated read: ≤100 req/min per user.
  • Authenticated write: ≤30 req/min per user.
  • Bulk / export: ≤5 req/hour per user, paginated.

11.3 Request size

  • JSON body ≤256 KB unless explicitly justified.
  • File uploads bounded per endpoint, never the default 10 MB.

11.4 Timeouts (every outbound call)

  • Connect: 2 s.
  • Read: 5 s (sync request path), 30 s (background).
  • Total per-request budget enforced via Resilience4j time-limiter.

11.5 Circuit breakers

  • Wrap every outbound HTTP / Kafka / DB call in Resilience4j circuit breaker.
  • Fallback returns degraded result OR fails fast (no infinite retry).

12. CORS & CSRF

12.1 CORS

  • Allowlist origins per environment, never * for credentialed requests.
  • Access-Control-Allow-Credentials: true requires explicit origin.
  • Preflight cache: Access-Control-Max-Age: 600.

12.2 CSRF

  • Stateless JWT APIs: CSRF disabled (no session cookie).
  • Cookie-based session APIs: CSRF token (double-submit or synchronizer token) MANDATORY.
  • SameSite=Strict on all auth cookies. Secure flag in production.

13. Audit Trail

13.1 What MUST be audited

  • Authentication events (login success / failure, token refresh, logout).
  • Authorization failures (403).
  • Privileged actions (role change, key rotation, data export, delete-all).
  • Data access on sensitive entities (PII read, financial transaction).

13.2 Append-only

  • Audit log is immutable — separate datastore or write-only DB role.
  • Use the transactional outbox to publish audit events atomically with the business write.
  • Retention ≥1 year (regulation-dependent: PCI 1y, SOX 7y, GDPR varies).

13.3 Fields

{
"auditId": "uuid",
"timestamp": "2026-06-10T12:00:00Z",
"actor": { "userId": "...", "tenantId": "...", "ip": "203.0.113.0/24" },
"action": "invoice.delete",
"resource": { "type": "invoice", "id": "..." },
"outcome": "success",
"metadata": { "reason": "...", "before": { ... }, "after": { ... } },
"traceId": "..."
}

14. Dependency & Supply Chain

14.1 Scanning

  • OWASP Dependency-Check or Snyk / Dependabot / Renovate in CI.
  • /arch-deps (skill: arch-deps-auditor) audits CVE, version drift, SNAPSHOT, BOM consistency.
  • Fail the build on CVSS ≥7.0.

14.2 Pinning

  • Pin all versions explicitly. Use BOM for transitive consistency.
  • NEVER LATEST or + in production.
  • SNAPSHOT only in pre-release branches; CI rejects SNAPSHOT in main.

14.3 Supply chain

  • Verify checksums (Gradle dependency verification, Maven <dependencyManagement> with PGP).
  • Sigstore / cosign for container images.
  • SBOM generated per build (CycloneDX or SPDX). Stored as build artifact.

14.4 License compliance

  • Approved licenses: Apache-2.0, MIT, BSD-2/3, MPL-2.0, EPL-2.0.
  • Banned: AGPL, SSPL, Commons Clause (unless legal approves).
  • Enforced via Gradle license plugin or FOSSA in CI.

15. Container & Runtime

15.1 Image hardening

  • Base: distroless or alpine (with security updates). Never latest tag.
  • Multi-stage build — runtime image has only JRE + app, no JDK, no shell where possible.
  • Run as non-root UID (USER 10001).
  • Read-only root filesystem (readOnlyRootFilesystem: true in K8s securityContext).
  • Drop all capabilities; add back only what’s required.
  • No .env files copied in.
FROM eclipse-temurin:21-jre-alpine AS runtime
RUN addgroup -g 10001 app && adduser -D -u 10001 -G app app
USER app
WORKDIR /app
COPY --chown=app:app --from=build /workspace/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

15.2 K8s

  • Pod Security Standards: restricted profile.
  • NetworkPolicy deny-by-default; explicit allow per service.
  • Secrets via External Secrets / CSI driver, not env vars in manifests.
  • ServiceAccount with minimal RBAC; no cluster-admin.

15.3 Image scanning

  • Trivy / Grype on every build.
  • Block deploy on CVSS ≥7.0 (configurable per environment).

16. Multi-tenancy Isolation

16.1 Tenant identification

  • tenantId claim in JWT; rejected if missing on tenant-scoped endpoints.
  • Tenant is a first-class value object in domain, not a string.

16.2 Data isolation strategies

Strategy When Enforcement
Shared DB, shared schema, tenant_id column Many small tenants RLS + app filter + ArchUnit test
Shared DB, schema-per-tenant Medium tenants, regulatory Connection-routing + RLS
DB-per-tenant Largest / regulated / sovereign Multi-datasource routing

16.3 Defense in depth

  • App layer — every query has explicit WHERE tenant_id = :tenantId.
  • DB layer — PostgreSQL RLS policy validates current_setting('app.tenant_id').
  • Tests — cross-tenant access tests fail by default.
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid);

17. Error Handling & Information Disclosure

17.1 Response shape

  • Production errors expose only a code + correlation ID. Never stack traces, SQL, internal paths.
  • Dev / staging may include details — controlled by profile.
{
"error": {
"code": "INVOICE_NOT_FOUND",
"message": "Resource not found.",
"traceId": "abc-123",
"timestamp": "2026-06-10T12:00:00Z"
}
}

17.2 Centralized @RestControllerAdvice

  • One mapper from domain exception → HTTP status + body.
  • 401 / 403 / 404 / 409 / 422 / 429 / 500 — never leak which one means “exists but forbidden” vs “doesn’t exist” for sensitive resources (uniform 404 for both).

17.3 Headers

  • Strip server fingerprinting: server.tomcat.add-server-header: false, no X-Powered-By.
  • Add X-Content-Type-Options: nosniff, X-Frame-Options: DENY.

18. Compliance Hooks

Not exhaustive — defer to legal/compliance for your jurisdiction. Common requirements:

Regulation Engineering hooks
GDPR Right-to-erasure endpoint, data export endpoint, consent record, processing log, EU data residency, DPA with sub-processors
PCI DSS Tokenize PAN, never store CVV, encrypt at rest, segregated cardholder data env, quarterly scans
HIPAA BAA, encryption in transit + at rest, audit log retention 6y, access reviews
SOX Immutable audit log 7y, change management approval gates, segregation of duties
SOC 2 Logging + monitoring + IR documented, vendor risk reviews, employee access reviews

Each compliance regime should be captured as an ADR describing scope, controls, evidence locations.


19. Security Testing

19.1 Required tests

Test type What it catches Tool
Negative auth tests Anonymous / wrong role / wrong tenant access JUnit + MockMvc / Karate
Input fuzzing Validation gaps, panic on malformed input Jazzer, jqwik, Schemathesis
Dependency scan Known CVEs in deps OWASP DC, Snyk, Dependabot
Static analysis Common vulns (CWE) Semgrep, SonarQube, SpotBugs + FindSecBugs
Container scan OS-level CVEs in base image Trivy, Grype
Secret scan Secrets in code / git history gitleaks, trufflehog
DAST Live endpoint vulns OWASP ZAP (pre-prod)
SBOM Supply-chain visibility CycloneDX Gradle/Maven plugin
Threat model Missing mitigations /arch-threat agent

19.2 In CI

  • Static analysis + dep scan + secret scan: blocking.
  • Container scan + DAST: blocking for staging promotion.
  • Threat model: blocking for any new external surface in PR review.

19.3 Negative test example

@Test
fun `should reject access to another tenant's invoice`() {
val attackerToken = jwt(userId = "attacker", tenantId = "tenant-A")
val victimInvoiceId = createInvoice(tenantId = "tenant-B")
mockMvc.get("/api/v1/invoices/$victimInvoiceId") {
header("Authorization", "Bearer $attackerToken")
}.andExpect {
status { isNotFound() } // uniform 404, NOT 403 — don't leak existence
}
}

20. Incident Response Checklist

When a security event is confirmed:

  1. Contain — revoke tokens / API keys / sessions; isolate affected service; block traffic at WAF if needed.
  2. Preserve — snapshot logs (audit, app, infra), DB state, container images; freeze affected resources.
  3. Communicate — internal IR channel; legal/compliance per regulation; regulatory disclosure (GDPR ≤72h).
  4. Eradicate — patch vulnerability, rotate credentials, redeploy, invalidate all sessions / tokens of impacted scope.
  5. Recover — restore from clean state, validate, gradual traffic re-enable.
  6. Post-mortem — blameless; root cause + timeline + remediation; new ADR if the architecture must change.

Track every incident in an internal register with severity, detection-to-containment latency, and remediation status.


Quick Reference — Pre-Merge Checklist

For every PR that adds an external surface or touches security-relevant code:

  • Threat model created/updated (/arch-threat) — STRIDE coverage.
  • AuthN: JWT validated (sig, iss, aud, exp). mTLS for internal calls.
  • AuthZ: @PreAuthorize on adapter, not use case. Negative tests present.
  • No secrets in code / config / logs. Secret-scan hook passed.
  • Inputs validated at adapter; domain invariants in constructors.
  • Parameterized queries; no string-built SQL.
  • Rate limit configured for new endpoint.
  • CORS allowlist updated (if cross-origin).
  • Structured logs include traceId, userId, tenantId. No PII leaked.
  • Audit event emitted for privileged action (via outbox).
  • Error responses use centralized handler; no stack traces in prod.
  • Dependency scan passes (no CVSS ≥7.0).
  • Container scan passes; runs as non-root; minimal base.
  • Tests: negative auth, cross-tenant denial, input fuzzing for new DTOs.
  • ADR drafted if security architecture changed (auth model, crypto, secrets backend).