Spring Boot Deployment Security Guide: From Monolith to Cloud‑Native Zero‑Trust Production
This comprehensive guide walks through why simple Spring Security is insufficient for production, outlines eight core security principles, presents a threat model, and provides step‑by‑step recommendations—including authentication, token design, configuration management, input validation, logging, rate limiting, container hardening, Kubernetes policies, service‑mesh mTLS, supply‑chain scanning, and audit—to transform a Spring Boot application into a zero‑trust, production‑grade service.
Why Spring Boot deployment security goes beyond adding Spring Security
Adding JWT, BCrypt or endpoint permissions is only a part of the picture. Production‑grade security requires a complete trust chain covering code provenance, configuration secrecy, image hygiene, minimal‑privilege deployment, request filtering, service‑to‑service protection, data leakage prevention and rapid incident response.
Eight core principles for a secure deployment
Least‑privilege – run containers as non‑root, isolate DB accounts, restrict admin interfaces.
Default‑deny – explicit allow rules for network, gateway and WAF.
Zero‑trust – enforce mTLS, signed tokens, MFA for operations.
Defense‑in‑depth – protect at boundary, gateway, application, runtime and data layers.
Auditability – record who accessed what, when and from where.
Rollbackability – configuration gray‑release, versioned policies, dynamic switches.
High‑concurrency stability – layered rate limiting, thread‑pool isolation, circuit breaking.
Supply‑chain trust – signed images, SBOM, vulnerability scanning.
Threat model and common high‑risk issues
The typical attack surface includes public entry, gateway, APIs, management endpoints, config stores, images, DB/Redis/MQ, logging and CI/CD. Concrete consequences are actuator exposure, root containers, long‑lived symmetric keys, hard‑coded passwords and insufficient logging.
Production‑grade architecture
A layered diagram (Internet → CDN/WAF → Ingress → Gateway → Spring Boot filter chain → business logic → DB/Redis → observability) shows where security controls belong.
Authentication & Authorization design
Instead of hand‑rolled JWT providers, use standard OAuth2/OIDC with Spring Security 6 Resource Server, asymmetric RSA/ECDSA keys, short‑lived access tokens (5‑15 min), refresh tokens (7‑30 days) and token revocation via Redis blacklists.
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(session -> session.disable())
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.xssProtection(Customizer.withDefaults())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/actuator/prometheus").permitAll()
.requestMatchers("/actuator/**").hasRole("OPS")
.requestMatchers(HttpMethod.POST, "/api/auth/logout").authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/orders/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
return http.build();
}
// jwtAuthenticationConverter implementation omitted for brevity
}Token recommendations
Access Token TTL: 5‑15 minutes
Refresh Token TTL: 7‑30 days (stateful on server)
Signature algorithm: RS256 or ES256
Mandatory claims: iss, sub, aud, exp, iat, jti
Risk‑control claims: device_id, tenant_id, client_id, scope
Revocation: Redis blacklist or version‑based mechanism
Key rotation: support at least dual‑key coexistence
Configuration & secret management
Avoid hard‑coding passwords in application-prod.yml. Use Vault, KMS or cloud Secrets Manager and inject secrets at runtime via environment variables or mounted secrets, never logging them.
spring:
datasource:
url: jdbc:mysql://prod-db:3306/order
username: ${DB_USER}
password: ${DB_PASSWORD}Input validation & output sanitization
All request DTOs should use Bean Validation, enforce pagination limits and reject unsafe content types. Sensitive fields (phone, ID, bank card) must be masked before logging or returning.
public record QueryOrderRequest(
@NotBlank @Pattern(regexp = "^[A-Z0-9_-]{8,32}$") String orderNo,
@Min(1) @Max(100) int pageSize) {}Logging governance
Separate audit logs from business logs, mask PII and include traceId, userId, tenantId, IP, endpoint, resourceId, outcome, latency and risk tags.
{
"timestamp":"2026-04-10T11:20:31.210+08:00",
"eventType":"ORDER_QUERY",
"traceId":"2f2d754c7f3341f8",
"userId":"100245",
"tenantId":"mall-cn",
"resourceId":"89001231",
"clientIp":"10.20.18.2",
"outcome":"SUCCESS",
"latencyMs":32
}Rate limiting, isolation and idempotency
Application‑level limits are insufficient; combine CDN/WAF, gateway Redis token bucket and local fallback.
package com.example.common.ratelimit;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
@Component
public class LocalRateLimiter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
public boolean tryConsume(String key, long capacity, long refillTokens, Duration period) {
Bucket bucket = buckets.computeIfAbsent(key, k -> Bucket.builder()
.addLimit(Bandwidth.classic(capacity, Refill.intervally(refillTokens, period)))
.build());
return bucket.tryConsume(1);
}
} package com.example.common.ratelimit;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class LoginRateLimitFilter extends OncePerRequestFilter {
private final LocalRateLimiter rateLimiter;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !"/api/auth/login".equals(request.getRequestURI());
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String key = request.getRemoteAddr();
boolean allowed = rateLimiter.tryConsume(key, 20, 20, Duration.ofMinutes(1));
if (!allowed) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("{\"code\":\"RATE_LIMITED\",\"message\":\"请求过于频繁\"}");
return;
}
filterChain.doFilter(request, response);
}
}Thread‑pool & connection‑pool segregation
Separate pools for authentication, order processing, file export and external calls to avoid resource contention during spikes.
Container security
A multi‑stage Dockerfile builds a minimal JRE image, creates a non‑root user, sets a read‑only root filesystem, disables privileged mode and adds health checks.
FROM maven:3.9.0-eclipse-temurin-21 AS builder
WORKDIR /workspace
COPY pom.xml .
COPY src ./src
RUN mvn -B -DskipTests clean package
FROM eclipse-temurin:21-jre-jammy
RUN groupadd --system spring && useradd --system --gid spring --create-home spring
WORKDIR /app
COPY --from=builder /workspace/target/*.jar app.jar
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=30 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/./urandom -Dfile.encoding=UTF-8"
RUN chown -R spring:spring /app
USER spring:spring
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD wget -qO- http://127.0.0.1:8080/actuator/health/readiness || exit 1
ENTRYPOINT ["java","-jar","/app/app.jar"]Kubernetes deployment hardening
Deployment adds a securityContext (runAsNonRoot, dropAll capabilities, readOnlyRootFilesystem), disables automatic ServiceAccount token mounting, and defines a NetworkPolicy that only allows traffic from the ingress namespace and restricts egress to the database and Redis.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: prod-order
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
serviceAccountName: order-service
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
containers:
- name: order-service
image: registry.example.com/order-service:1.4.2
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
envFrom:
- secretRef:
name: order-service-secret
resources:
requests:
cpu: "500m"
memory: "768Mi"
limits:
cpu: "2"
memory: "2Gi"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 40
periodSeconds: 15
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {} apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: order-service-policy
namespace: prod-order
spec:
podSelector:
matchLabels:
app: order-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-system
- podSelector:
matchLabels:
app: gateway
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
name: prod-data
ports:
- protocol: TCP
port: 3306
- protocol: TCP
port: 6379Service‑mesh mTLS example (Istio)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: prod-order
spec:
mtls:
mode: STRICT
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: order-service-allow-gateway
namespace: prod-order
spec:
selector:
matchLabels:
app: order-service
rules:
- from:
- source:
principals: ["cluster.local/ns/ingress-system/sa/gateway"]Actuator hardening
Expose Actuator on a separate internal port, limit exposure to health and Prometheus endpoints, and protect the rest with ROLE_OPS.
management:
server:
port: 18080
endpoints:
web:
exposure:
include: health,prometheus
base-path: /actuator
endpoint:
health:
probes:
enabled: true
show-details: never .authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/actuator/prometheus").permitAll()
.requestMatchers("/actuator/**").hasRole("OPS"))Database, Redis and MQ hardening
Separate DB accounts per service, grant least‑privilege, enforce TLS, enable audit logs.
Redis: no public exposure, enable ACLs, apply cache‑penetration and avalanche protection.
MQ: distinct producer/consumer accounts, per‑service topics, message signing, dead‑letter queues.
Supply‑chain security
Lock dependency versions, run OWASP Dependency‑Check, generate SBOM, scan images with Trivy/Grype, sign images with Cosign, and enforce admission control.
<build>
<plugins>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>10.0.4</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<formats>
<format>HTML</format>
<format>JSON</format>
</formats>
</configuration>
</plugin>
</plugins>
</build> name: secure-build
on:
push:
branches: ["main"]
jobs:
build-and-scan:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: maven
- name: Build
run: mvn -B clean verify
- name: Dependency Check
run: mvn -B org.owasp:dependency-check-maven:check
- name: Build Image
run: docker build -t registry.example.com/order-service:${{ github.sha }} .
- name: Trivy Scan
uses: aquasecurity/[email protected]
with:
image-ref: registry.example.com/order-service:${{ github.sha }}
format: sarif
output: trivy-results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarifAudit & monitoring checklist
Structured audit logs must contain traceId, requestId, userId, tenantId, clientIp, endpoint, resourceId, outcome, latency and risk tags. Monitor login failures, token validation failures, 403/429/5xx ratios, per‑IP request peaks, slow‑request ratios, circuit‑breaker trips, blacklist hits and illegal actuator accesses. Define alert rules for brute‑force login, privilege escalation, resource exhaustion, management‑plane access and supply‑chain vulnerabilities.
Pre‑deployment checklist
Application config: minimal actuator exposure, disabled debug/Swagger, proper request limits.
Identity & permissions: OAuth2/OIDC, mandatory JWT claims, resource‑level RBAC, no default accounts.
Secrets: never in code, use Vault/KMS, support rotation, no secret leakage in logs.
Container & K8s: non‑root, drop capabilities, readOnlyRootFilesystem, disable SA token auto‑mount, enforce NetworkPolicy.
Traffic governance: WAF, gateway rate limiting, separate core vs non‑core pools.
Supply‑chain: dependency & image scanning, SBOM, image signing, minimal CI credentials.
Audit & monitoring: structured logs, traceability, security alerts, regular drills.
Following this end‑to‑end process transforms a simple Spring Boot monolith into a hardened, cloud‑native, zero‑trust production system.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
