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-k8sskill — container hardening.
Table of Contents
- Threat Model Baseline (STRIDE)
- Authentication
- Authorization
- Current User Port
- Secrets Management
- Input Validation
- Injection Prevention
- Cryptography
- Transport Security
- Logging & PII
- Rate Limiting & DoS
- CORS & CSRF
- Audit Trail
- Dependency & Supply Chain
- Container & Runtime
- Multi-tenancy Isolation
- Error Handling & Information Disclosure
- Compliance Hooks
- Security Testing
- 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, orscope). - Enforce via
@PreAuthorizeon inbound port adapters (controllers, consumers), never on use cases. - Domain has no concept of “roles” — it accepts a
Uservalue 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 Securityclass CreateInvoiceUseCase { @PreAuthorize("hasRole('ADMIN')") // ❌ framework leak into application override fun execute(command: CreateInvoiceCommand): Invoice = ...}
// RIGHT — authorization at the adapter, use case is framework-free@RestControllerclass 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 domainfun interface CurrentUserPort { fun get(): AuthenticatedUser}
data class AuthenticatedUser( val subject: UserId, val tenantId: TenantId, val roles: Set<Role>)@Componentclass 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 secretplatform: 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 boundarydata 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 — invariantsdata 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@JsonTypeInfoon anObject/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
ObjectInputStreamon untrusted bytes.
7. Injection Prevention
7.1 SQL Injection
- Always parameterized queries. Spring Data JDBC
@Querywith:nameplaceholders. - 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).
// WRONGval 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
ProcessBuilderwithcommand(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:utexton 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:
Argon2PasswordEncoderorBCryptPasswordEncoder.
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
SecureRandomfor tokens, IDs, salts. NeverMath.random()orRandomfor 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: truerequires 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=Stricton all auth cookies.Secureflag 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
LATESTor+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
latesttag. - 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: truein K8s securityContext). - Drop all capabilities; add back only what’s required.
- No
.envfiles copied in.
FROM eclipse-temurin:21-jre-alpine AS runtimeRUN addgroup -g 10001 app && adduser -D -u 10001 -G app appUSER appWORKDIR /appCOPY --chown=app:app --from=build /workspace/build/libs/*.jar app.jarENTRYPOINT ["java", "-jar", "/app/app.jar"]15.2 K8s
- Pod Security Standards:
restrictedprofile. NetworkPolicydeny-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
tenantIdclaim 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, noX-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
@Testfun `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:
- Contain — revoke tokens / API keys / sessions; isolate affected service; block traffic at WAF if needed.
- Preserve — snapshot logs (audit, app, infra), DB state, container images; freeze affected resources.
- Communicate — internal IR channel; legal/compliance per regulation; regulatory disclosure (GDPR ≤72h).
- Eradicate — patch vulnerability, rotate credentials, redeploy, invalidate all sessions / tokens of impacted scope.
- Recover — restore from clean state, validate, gradual traffic re-enable.
- 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:
@PreAuthorizeon 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).