Article
As Java applications increasingly run in Docker containers, ensuring container security compliance becomes critical. Docker Bench Security is an open-source script that checks for dozens of common best practices for deploying Docker containers in production. For Java development teams, integrating Docker Bench Security into the CI/CD pipeline ensures that containerized applications meet security standards before reaching production.
What is Docker Bench Security?
Docker Bench Security is an implementation of the CIS (Center for Internet Security) Docker Benchmark. It automatically validates Docker configurations against security best practices across these areas:
- Host Configuration - Docker daemon and host security
- Docker Daemon Configuration - Runtime security settings
- Container Images and Build Files - Image security practices
- Container Runtime - Running container security
- Docker Security Operations - Operational security practices
- Docker Swarm Configuration - Orchestration security (if applicable)
Why Java Applications Need Docker Bench Security
- Compliance Requirements: Meet CIS benchmarks and regulatory standards
- Vulnerability Prevention: Catch security misconfigurations early
- Consistent Security: Ensure all Java containers follow the same security standards
- CI/CD Integration: Automated security validation in pipelines
- Production Readiness: Verify containers are production-hardened
Docker Bench Security Workflow for Java
Java Source Code → Dockerfile → Docker Image → Docker Bench Security → Security Report ↓ CI/CD Pipeline → Fail Build if Critical Issues
Integrating Docker Bench Security with Java Applications
1. Dockerfile Security Hardening for Java:
# Secure Java Application Dockerfile
FROM eclipse-temurin:17-jre-jammy as builder
# Install security updates
RUN apt-get update && \
apt-get upgrade -y && \
rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r javaapp && \
useradd -r -g javaapp -d /app -s /sbin/nologin -c "Java Application User" javaapp
# Install Docker Bench Security dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
jq \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy application
COPY target/my-java-app.jar app.jar
COPY entrypoint.sh /usr/local/bin/
# Security hardening
RUN chown -R javaapp:javaapp /app && \
chmod -R 750 /app && \
chmod 755 /usr/local/bin/entrypoint.sh && \
chmod 644 /app/app.jar
# Remove setuid/setgid binaries
RUN find / -perm /6000 -type f -exec chmod a-s {} \; || true
# Switch to non-root user
USER javaapp
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# Secure entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# Default command
CMD ["java", \
"-Djava.security.egd=file:/dev/./urandom", \
"-Djava.awt.headless=true", \
"-Dfile.encoding=UTF-8", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:+UnlockExperimentalVMOptions", \
"-XX:+UseCGroupMemoryLimitForHeap", \
"-Dspring.config.location=file:/app/config/", \
"-jar", "/app/app.jar"]
2. Secure Entrypoint Script:
#!/bin/bash
# entrypoint.sh
set -e
# Security: Fail on undefined variables
set -u
# Apply security settings
export JAVA_TOOL_OPTIONS="-Djava.security.manager -Djava.security.policy==/app/security.policy"
# Run as non-root user verification
if [ "$(id -u)" = "0" ]; then
echo "ERROR: Container should not run as root" >&2
exit 1
fi
# Check for required environment variables
if [ -z "${SPRING_PROFILES_ACTIVE:-}" ]; then
echo "WARNING: SPRING_PROFILES_ACTIVE not set, using default"
export SPRING_PROFILES_ACTIVE=default
fi
exec java $JAVA_OPTS -jar /app/app.jar "$@"
Docker Bench Security Implementation
1. Running Docker Bench Security Locally:
# Clone Docker Bench Security git clone https://github.com/docker/docker-bench-security.git cd docker-bench-security # Run against your Java application container docker run -it --net host --pid host --userns host --cap-add audit_control \ -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \ -v /var/lib:/var/lib \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /usr/lib/systemd:/usr/lib/systemd \ -v /etc:/etc \ --label docker_bench_security \ docker/docker-bench-security
2. Docker Compose for Testing:
# docker-compose.bench.yml version: '3.8' services: java-app: build: context: . dockerfile: Dockerfile image: my-java-app:latest container_name: java-application restart: unless-stopped security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - SETGID - SETUID read_only: true tmpfs: - /tmp:rw,noexec,nosuid,size=64m volumes: - ./config:/app/config:ro - ./logs:/app/logs:rw environment: - SPRING_PROFILES_ACTIVE=production - JAVA_OPTS=-Xmx512m -Xms256m labels: - "com.example.maintainer=Java Team" - "com.example.security-tier=high" docker-bench-security: image: docker/docker-bench-security container_name: docker-bench hostname: docker-bench privileged: true volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /etc:/etc:ro - ./bench-results:/results command: ["-c", "container_images"] depends_on: - java-app
CI/CD Integration
1. GitHub Actions Workflow:
# .github/workflows/docker-bench-security.yml
name: Docker Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build Java application
run: |
mvn clean package -DskipTests
- name: Build Docker image
run: |
docker build -t my-java-app:${{ github.sha }} .
- name: Run Docker Bench Security
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/bench-results:/results \
docker/docker-bench-security \
-c container_images \
-l json > bench-results/security-report.json
- name: Analyze Security Results
run: |
pip install jq
python scripts/analyze-bench-results.py bench-results/security-report.json
- name: Fail on Critical Issues
run: |
CRITICAL_ISSUES=$(jq '.tests[].results[] | select(.result == "FAIL" and .level == "WARN") | length' bench-results/security-report.json)
if [ "$CRITICAL_ISSUES" -gt 0 ]; then
echo "Critical security issues found: $CRITICAL_ISSUES"
exit 1
fi
2. Jenkins Pipeline Integration:
// Jenkinsfile
pipeline {
agent any
environment {
DOCKER_IMAGE = "my-java-app:${env.BUILD_ID}"
}
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Build Docker Image') {
steps {
sh "docker build -t ${DOCKER_IMAGE} ."
}
}
stage('Docker Security Scan') {
steps {
script {
// Run Docker Bench Security
sh """
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ${WORKSPACE}/security-reports:/results \
docker/docker-bench-security \
-c container_images \
-l json > security-reports/bench-security.json
"""
// Parse results
def securityReport = readJSON file: 'security-reports/bench-security.json'
def criticalFailures = securityReport.tests.findAll { test ->
test.results.any { result ->
result.result == "FAIL" && result.level == "WARN"
}
}
if (criticalFailures.size() > 0) {
unstable "Docker security scan found ${criticalFailures.size()} critical issues"
}
}
}
}
stage('Push to Registry') {
when {
expression { currentBuild.result != 'FAILURE' }
}
steps {
sh "docker push ${DOCKER_IMAGE}"
}
}
}
post {
always {
// Archive security reports
archiveArtifacts artifacts: 'security-reports/**/*.json', fingerprint: true
// Cleanup
sh 'docker system prune -f'
}
}
}
Java-Specific Security Configuration
1. Application Security Properties:
# application-security.yml
spring:
security:
user:
name: ${APP_USER:javaapp}
password: ${APP_PASSWORD:!change_me!}
management:
endpoint:
health:
show-details: when_authorized
probes:
enabled: true
env:
enabled: false
beans:
enabled: false
conditions:
enabled: false
configprops:
enabled: false
endpoints:
web:
exposure:
include: health,info,metrics
base-path: /internal
info:
env:
enabled: false
server:
port: 8080
servlet:
context-path: /api
# Security headers
servlet:
session:
cookie:
secure: true
http-only: true
same-site: strict
# Custom security properties
app:
security:
cors:
allowed-origins: ${ALLOWED_ORIGINS:https://myapp.com}
csrf:
enabled: true
headers:
hsts: max-age=31536000
2. Security Configuration Class:
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(ApplicationSecurityProperties.class)
public class SecurityConfig {
private final ApplicationSecurityProperties securityProperties;
public SecurityConfig(ApplicationSecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.ignoringRequestMatchers("/internal/health")
)
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'")
)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
.frameOptions(frame -> frame
.deny()
)
.xssProtection(xss -> xss
.block(true)
)
)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/internal/health", "/internal/info").permitAll()
.requestMatchers("/internal/**").hasRole("MONITORING")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionFixation().migrateSession()
.maximumSessions(1)
);
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(
"/css/**", "/js/**", "/webjars/**", "/error"
);
}
}
Automated Security Testing
1. Security Test Class:
@SpringBootTest
@AutoConfigureTestDatabase
@TestPropertySource(properties = {
"spring.security.user.name=test",
"spring.security.user.password=test",
"management.endpoints.web.exposure.include=health,info"
})
public class DockerSecurityTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testHealthEndpointAccessible() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("test", "test")
.getForEntity("/internal/health", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void testSensitiveEndpointsProtected() {
ResponseEntity<String> response = restTemplate
.getForEntity("/internal/env", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void testSecurityHeadersPresent() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("test", "test")
.getForEntity("/internal/health", String.class);
HttpHeaders headers = response.getHeaders();
assertThat(headers.get("X-Content-Type-Options")).contains("nosniff");
assertThat(headers.get("X-Frame-Options")).contains("DENY");
assertThat(headers.get("X-XSS-Protection")).contains("1; mode=block");
assertThat(headers.get("Strict-Transport-Security")).isNotNull();
}
}
2. Docker Security Test Script:
#!/bin/bash # scripts/docker-security-test.sh set -e echo "Running Docker security tests for Java application..." # Check if running as root if [ "$(id -u)" -eq 0 ]; then echo "ERROR: Should not run as root" exit 1 fi # Check for environment variables if [ -z "$SPRING_PROFILES_ACTIVE" ]; then echo "WARNING: SPRING_PROFILES_ACTIVE not set" fi # Test container security echo "Testing container security..." # Check for security options if docker inspect java-app | jq -e '.[].HostConfig.SecurityOpt | contains(["no-new-privileges"])' > /dev/null; then echo "✓ no-new-privileges enabled" else echo "✗ no-new-privileges not enabled" exit 1 fi # Check for read-only filesystem if docker inspect java-app | jq -e '.[].HostConfig.ReadonlyRootfs' > /dev/null; then echo "✓ Read-only root filesystem enabled" else echo "✗ Read-only root filesystem not enabled" fi # Check user CONTAINER_USER=$(docker inspect java-app | jq -r '.[].Config.User') if [ "$CONTAINER_USER" != "0" ] && [ -n "$CONTAINER_USER" ]; then echo "✓ Running as non-root user: $CONTAINER_USER" else echo "✗ Running as root or user not set" exit 1 fi echo "All security tests passed!"
Security Monitoring and Reporting
1. Security Report Generator:
@Component
public class DockerSecurityReporter {
private static final Logger logger = LoggerFactory.getLogger(DockerSecurityReporter.class);
public void generateSecurityReport(Path benchResultsPath) throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode report = mapper.readTree(benchResultsPath.toFile());
SecurityReport securityReport = new SecurityReport();
for (JsonNode test : report.get("tests")) {
String testDesc = test.get("test_desc").asText();
List<TestResult> results = parseTestResults(test.get("results"));
securityReport.addTestResult(testDesc, results);
// Log critical failures
results.stream()
.filter(r -> r.getLevel().equals("WARN") && r.getResult().equals("FAIL"))
.forEach(r -> logger.warn("CRITICAL: {} - {}", testDesc, r.getDesc()));
}
generateHtmlReport(securityReport);
sendSecurityAlert(securityReport);
}
private List<TestResult> parseTestResults(JsonNode resultsNode) {
List<TestResult> results = new ArrayList<>();
for (JsonNode result : resultsNode) {
results.add(new TestResult(
result.get("result").asText(),
result.get("desc").asText(),
result.get("level").asText()
));
}
return results;
}
private void generateHtmlReport(SecurityReport report) {
// Generate HTML security report
// Implementation depends on reporting requirements
}
private void sendSecurityAlert(SecurityReport report) {
long criticalFailures = report.getCriticalFailuresCount();
if (criticalFailures > 0) {
// Send alert to security team
logger.error("Security scan found {} critical issues", criticalFailures);
}
}
}
@Data
class SecurityReport {
private Map<String, List<TestResult>> testResults = new HashMap<>();
private Instant reportTime = Instant.now();
public void addTestResult(String testDesc, List<TestResult> results) {
testResults.put(testDesc, results);
}
public long getCriticalFailuresCount() {
return testResults.values().stream()
.flatMap(List::stream)
.filter(r -> r.getLevel().equals("WARN") && r.getResult().equals("FAIL"))
.count();
}
}
@Data
@AllArgsConstructor
class TestResult {
private String result;
private String desc;
private String level;
}
Best Practices for Java Docker Security
1. Regular Security Updates:
# Security update script FROM eclipse-temurin:17-jre-jammy # Install security updates RUN apt-get update && \ apt-get upgrade -y --only-upgrade security && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Use specific version for reproducibility FROM eclipse-temurin:17.0.9_9-jre-jammy
2. Multi-stage Build for Security:
# Multi-stage build for security FROM eclipse-temurin:17-jdk-jammy as builder # Build application COPY . /app WORKDIR /app RUN ./mvnw clean package -DskipTests FROM eclipse-temurin:17-jre-jammy as runtime # Install security tools RUN apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/* # Copy application from builder stage COPY --from=builder /app/target/my-app.jar /app/app.jar # Security hardening RUN adduser --system --home /app --shell /sbin/nologin javaapp && \ chown -R javaapp /app && \ chmod -R 750 /app USER javaapp CMD ["java", "-jar", "/app/app.jar"]
Conclusion
Integrating Docker Bench Security with Java applications provides a comprehensive approach to container security that spans from development to production. By automating security checks in CI/CD pipelines, enforcing security best practices in Dockerfiles, and monitoring running containers, Java development teams can ensure their containerized applications meet enterprise security standards.
The combination of Docker Bench Security with Java-specific security configurations creates a robust security posture that addresses both container-level and application-level security concerns. As container security becomes increasingly important in cloud-native environments, this integrated approach ensures Java applications remain secure throughout their lifecycle.