Container Security Compliance: Integrating Docker Bench Security with Java Applications


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

  1. Compliance Requirements: Meet CIS benchmarks and regulatory standards
  2. Vulnerability Prevention: Catch security misconfigurations early
  3. Consistent Security: Ensure all Java containers follow the same security standards
  4. CI/CD Integration: Automated security validation in pipelines
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper