Security Posture and mTLS Decision¶
| Field | Value |
|---|---|
| Authority | ADR-011 (security foundation), NFR-05, NFR-06, NFR-12, NFR-18, ADR-021 |
| Status | MVP delivered (Sprints 04-13); reviewed in Sprint 14 (Feature 14.2); re-signed-off Phase 1, 2026-07-06 |
| Last updated | 2026-07-06 |
| Companion | docs/architecture/keycloak-and-auth.md (auth integration), 14.2-security-audit-report.md (findings) |
This document records the platform's security posture and the explicit, reviewed decision to defer
mutual TLS (mTLS) for internal service-to-service traffic in the MVP, with the production
recommendation. It is the authoritative posture reference; detailed auth integration lives in
keycloak-and-auth.md.
1. Trust boundaries¶
Browser / client
| HTTPS (TLS) + Authorization Code + PKCE
v
Keycloak (issuer) --JWT (access + refresh)--> client
|
v
API Gateway --validate JWT via realm JWKS--> strip client identity headers,
inject X-User-Id / X-User-Roles from verified token
| HTTP inside the cluster (gateway-behind-trust)
v
Downstream services --trust the forwarded identity headers--> @PreAuthorize / mediator AuthorizationRule
- Edge (untrusted -> trusted): the API Gateway. It is the only component that validates the JWT and the only source of the identity headers.
- Internal (trusted): downstream services trust
X-User-Id/X-User-Rolesbecause the gateway strips any client-supplied copies and re-derives them from the verified token (api-gateway/.../filter/JwtClaimsFilter.java). Services may additionally validate the JWT directly viastarter-securityfor local/dev or defense-in-depth. /internal/**endpoints are denied at the edge (routed to a local 404 sink) and are reachable only in-cluster (GatewaySecurityConfig).
2. Authentication and token handling (ADR-011)¶
- Keycloak is the identity provider and token issuer. No service mints or refreshes JWTs.
- Refresh-token rotation and reuse detection are realm features (
revokeRefreshToken: true,refreshTokenMaxReuse: 0); realm event logging retains auth events for reuse-detection audit. - identity-service manages users/roles/permissions via the Keycloak Admin API and owns app-specific authorization data plus its own audit trail; it is not a token issuer.
- Note on the brief: MVP brief Section 13 states identity-service issues the JWT; the delivered platform follows ADR-011 (Keycloak issuer). ADR-011 wins and the brief carries a reconciling platform note.
3. Authorization¶
- Role/permission-based via
@PreAuthorizeand the mediatorAuthorizationRule, applied to admin and privileged endpoints. Realm roles are exposed as a flatrolesclaim (telco-rolesclient scope) and read by both the gateway andstarter-security. - Spring Security's
AccessDeniedExceptionand the platformAccessDeniedExceptionboth map to 403 via the platformGlobalExceptionHandler.
4. PII protection¶
4.1 At rest (NFR-06)¶
- Customer TCKN/VKN: AES-256-GCM, random 12-byte IV per record, 128-bit auth tag, Base64 stored in
identity_number_enc(customer-serviceIdentityNumberCryptoConverter). Column stores ciphertext only; proven by an automated native-query DB-inspection test. - Payment card data: not stored. payment-service delegates to a PSP and persists no PAN/CVV/expiry - the correct PCI-DSS posture (nothing to encrypt).
- email / username (identity-service): not in the NFR-06 encryption mandate (TCKN + card only); email
is masked in telemetry via
@Sensitive.
4.2 In telemetry (ADR-021)¶
- Layer A (structural):
@Sensitive+ Jackson masking module masks annotated fields in structured JSON logs. - Layer B (free-text backstop): Logback
%mask/%maskExconverters mask rendered messages and stack traces. Pattern set covers email, IBAN, PAN/card, MSISDN, and TCKN. - Traces/metrics: no PII is ever placed into span attributes or metric labels (verified by source scan); this is the primary control for Tempo/Prometheus.
- Known limitation: the JSON appender shipped to Loki uses
LogstashEncoder, which does not execute Logback pattern words, so Layer B does not run on the JSON body. Structured JSON masking relies on Layer A; keep a review rule that log statements never interpolate raw PII into the message body.
5. Key management and rotation (NFR-06)¶
- The AES key is a Base64 32-byte (AES-256) value supplied via configuration
(
customer.crypto.aes-key). In staging/production it is injected from a Kubernetes Secret / Vault with no default (CUSTOMER_AES_KEY); only the local base profile carries a dev default. - Current model: a single active key encrypts and decrypts (MVP scope).
- Rotation procedure (single active key, MVP):
- Generate a new 32-byte key; store it in the Secret/Vault under a new version.
- Re-encrypt existing rows: read via the old key, write via the new key (offline batch or a maintenance job), within a maintenance window.
- Swap
CUSTOMER_AES_KEYto the new value and roll the pods. - Production recommendation (zero-downtime rotation): introduce a key-id column and an envelope /
multi-key provider so the active key can rotate while old ciphertext remains decryptable by key id
(dual-read, single-write). Extract the AES-GCM converter and key provider into a platform
starter-crypto(already flagged inplatform-capabilities.md) so all services share one rotation path. Back the DEK with a KMS/HSM CMK for envelope encryption.
6. Rate limiting (NFR-18)¶
- Redis-backed fixed-window limiter at the gateway: 100 req/min, keyed by JWT subject (client IP for unauthenticated allowlisted routes), atomic INCR+EXPIRE Lua script; exceed returns 429 in the standard error envelope.
- Fails OPEN on a Redis outage (a Redis failure must not take down the edge) - a deliberate availability-over-strictness tradeoff. Production recommendation: alert on the fail-open path and consider a conservative local fallback limit if Redis is unavailable for an extended period.
7. Error handling (no leakage)¶
- The platform
GlobalExceptionHandlermaps each exception to a status and returns anApiResulterror envelope carryingcode,traceId, and a server-sidelogId. The catch-all returns a genericINTERNAL_ERRORmessage and never the exception message or stack trace; full detail is logged server-side and referenced bylogId. Gateway 401/403 return fixed JSON envelopes. No stack trace is ever exposed to clients.
8. mTLS decision (NFR-05, ADR-011)¶
Decision: deferred for the MVP¶
Internal service-to-service traffic runs over plain HTTP inside the cluster under the gateway-behind-trust model; mTLS is NOT enabled in the MVP. This is explicit in the MVP brief Section 13: "Services do not re-validate the JWT internally; gateway-behind-trust is used. (mTLS is recommended in production; out of scope for the MVP.)"
Rationale¶
- The gateway is the single validated trust boundary; it strips and re-injects identity headers, so spoofing from outside is prevented.
/internal/**is not reachable from the edge.- For the MVP the cluster network is the trust perimeter; adding mTLS (cert issuance, rotation, SPIFFE identities, sidecars/mesh) is significant operational surface that does not change the external threat model for a single-cluster MVP.
Residual risk accepted for the MVP¶
- An attacker with in-cluster network position could call a downstream service directly with forged
X-User-Id/X-User-Rolesheaders, bypassing the gateway. This depends on the cluster network being compromised (NetworkPolicies and namespace isolation are the compensating controls) and is accepted for the MVP.
Production recommendation¶
- Enforce mTLS for all internal service-to-service traffic (service mesh such as Istio/Linkerd, or SPIFFE/SPIRE-issued workload certificates), so identity is cryptographically bound per workload and downstream services can reject any caller that is not the gateway.
- Combine with Kubernetes NetworkPolicies (default-deny, explicit allow) and, for defense-in-depth,
have downstream services validate the JWT directly via
starter-securityin addition to trusting the gateway. - Service-to-service auth in production is mTLS (SPIFFE/PKI), not bearer tokens (ADR-011 Section 3;
enterprise design in
docs/product/TELCO-CRM-ADVANCED.md).
9. Audit logging (NFR-12)¶
- Mandatory in identity, customer, payment, and subscription. Each row carries actor, action, entity, entityId, correlationId, details (no PII), and timestamp, written inside the mediator transaction.
- Current status (Sprint 14 Phase 1 re-sign-off, 2026-07-06): all four mandated services are complete.
identity and subscription were complete from the original review; the two gaps flagged on 2026-07-03
(payment-service had no audit infrastructure at all; customer-service did not audit the address
sub-resource) have both been closed - payment-service now has its own
audit_logtable/entity/ repository/writer wired intoChargePaymentCommandHandlerandRefundPaymentCommandHandler, and customer-service's three address handlers now call the existingAuditLogWriter. Seedocs/tasks/sprint-14-testing-and-hardening/14.2-security-audit-report.md(subtask 14.2.3 and its 2026-07-06 addendum) for the file-level evidence.
10. Production hardening checklist (carried forward)¶
- [ ] mTLS for internal traffic (mesh or SPIFFE/SPIRE) + default-deny NetworkPolicies.
- [x] Close the payment-service audit gap (HIGH) and confirm/close the customer address audit gap. Done 2026-07-06 - see Section 9.
- [ ]
starter-cryptoextraction + key-id/envelope encryption for zero-downtime key rotation, DEK backed by KMS/HSM. - [ ] Real secrets from Vault/K8s Secret; HTTPS everywhere;
sslRequiredenforced; per-env realms. - [ ] Set
CORS_ALLOWED_ORIGINSto the production origin allowlist. - [ ] Alerting on the rate-limiter fail-open path.