Kube-Bench is a tool that checks whether Kubernetes is deployed securely by running the checks documented in the CIS Kubernetes Benchmark. This Java implementation provides a native way to perform CIS compliance checks.
Overview and Concepts
CIS Kubernetes Benchmark:
- CIS Controls: Center for Internet Security benchmarks
- Kubernetes Security: Master node, worker node, and etcd checks
- Compliance Levels: Level 1 (basic) and Level 2 (advanced)
- Automated Checks: Programmatic validation of security configurations
Key Components:
- Check Definitions: YAML-based test definitions
- Execution Engine: Runs security checks
- Reporting: Comprehensive compliance reports
- Remediation: Automatic fix suggestions
Dependencies and Setup
Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<kubernetes-client.version>6.7.2</kubernetes-client.version>
<snakeyaml.version>2.0</snakeyaml.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Kubernetes Client -->
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>${kubernetes-client.version}</version>
</dependency>
<!-- YAML Processing -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
Application Configuration
# application.yml kube-bench: cis-version: "1.6" benchmark-version: "1.0" target: "master" # master, node, etcd, policies output-format: "json" # json, yaml, text, junit fail-threshold: "FAIL" # FAIL, WARN, INFO include-tests: "" # Comma-separated test IDs to include exclude-tests: "" # Comma-separated test IDs to exclude checks: config-dir: "/etc/kube-bench/cfg" results-dir: "/var/log/kube-bench" remediation: auto-fix: false backup-files: true logging: level: com.example.kubebench: DEBUG
Configuration Properties
@Configuration
@ConfigurationProperties(prefix = "kube-bench")
@Data
public class KubeBenchProperties {
private String cisVersion = "1.6";
private String benchmarkVersion = "1.0";
private String target = "master";
private String outputFormat = "json";
private String failThreshold = "FAIL";
private String includeTests = "";
private String excludeTests = "";
private CheckProperties checks = new CheckProperties();
private RemediationProperties remediation = new RemediationProperties();
@Data
public static class CheckProperties {
private String configDir = "/etc/kube-bench/cfg";
private String resultsDir = "/var/log/kube-bench";
}
@Data
public static class RemediationProperties {
private boolean autoFix = false;
private boolean backupFiles = true;
}
}
Core Models
1. CIS Check Definitions
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CisCheck {
private String id;
private String text;
private String type;
private List<String> groups;
private List<Check> checks;
private Remediation remediation;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Check {
private String id;
private String text;
private String audit;
private String auditConfig;
private String type;
private List<String> tests;
private List<String> set;
private List<String> commands;
private String expectedResult;
private String actualResult;
private String remediation;
private List<String> exceptions;
private Map<String, String> labels;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Remediation {
private String manual;
private String automated;
private List<String> commands;
private String impact;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BenchmarkDefinition {
private String version;
private String cisVersion;
private Map<String, List<CisCheck>> controls;
private Map<String, Object> metadata;
}
2. Check Results and Reports
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CheckResult {
private String checkId;
private String checkText;
private CheckStatus status;
private String actualResult;
private String expectedResult;
private String remediation;
private String severity;
private Instant timestamp;
private String node;
private String component;
private List<String> evidence;
private Map<String, Object> metadata;
public enum CheckStatus {
PASS, FAIL, WARN, INFO, SKIP, ERROR
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ComplianceReport {
private String reportId;
private String cisVersion;
private String benchmarkVersion;
private String target;
private Instant generatedAt;
private String clusterName;
private Summary summary;
private List<CheckResult> results;
private List<RemediationAction> remediations;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Summary {
private int total;
private int pass;
private int fail;
private int warn;
private int skip;
private int error;
private double complianceScore;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RemediationAction {
private String checkId;
private String description;
private List<String> commands;
private String impact;
private boolean automated;
private RemediationStatus status;
public enum RemediationStatus {
PENDING, APPLIED, FAILED, SKIPPED
}
}
3. Kubernetes Resource Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class KubeResource {
private String name;
private String namespace;
private String kind;
private Map<String, String> labels;
private Map<String, String> annotations;
private Map<String, Object> spec;
private Map<String, Object> status;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PodSecurityContext {
private Boolean runAsNonRoot;
private Long runAsUser;
private Long runAsGroup;
private Long fsGroup;
private List<String> sysctls;
private SeLinuxOptions seLinuxOptions;
private SeccompProfile seccompProfile;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SeLinuxOptions {
private String level;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SeccompProfile {
private String type;
private String localhostProfile;
}
}
Core Services
1. CIS Benchmark Loader
@Service
@Slf4j
public class CisBenchmarkLoader {
private final KubeBenchProperties properties;
private final ObjectMapper yamlMapper;
public CisBenchmarkLoader(KubeBenchProperties properties) {
this.properties = properties;
this.yamlMapper = new ObjectMapper(new YAMLFactory());
this.yamlMapper.findAndRegisterModules();
}
public BenchmarkDefinition loadBenchmark(String target) {
log.info("Loading CIS benchmark for target: {}", target);
try {
String configFile = String.format("%s/%s.yaml",
properties.getChecks().getConfigDir(), target);
File file = new File(configFile);
if (!file.exists()) {
log.warn("Benchmark config file not found: {}", configFile);
return loadDefaultBenchmark(target);
}
BenchmarkDefinition benchmark = yamlMapper.readValue(file, BenchmarkDefinition.class);
log.info("Loaded benchmark with {} controls",
benchmark.getControls() != null ? benchmark.getControls().size() : 0);
return benchmark;
} catch (Exception e) {
log.error("Failed to load benchmark for target: {}", target, e);
return loadDefaultBenchmark(target);
}
}
public List<CisCheck> loadChecksForTarget(String target) {
BenchmarkDefinition benchmark = loadBenchmark(target);
if (benchmark == null || benchmark.getControls() == null) {
return Collections.emptyList();
}
return benchmark.getControls().values().stream()
.flatMap(List::stream)
.collect(Collectors.toList());
}
private BenchmarkDefinition loadDefaultBenchmark(String target) {
log.info("Loading default benchmark for target: {}", target);
// Create default benchmark structure
BenchmarkDefinition benchmark = new BenchmarkDefinition();
benchmark.setVersion("1.0");
benchmark.setCisVersion(properties.getCisVersion());
benchmark.setControls(new HashMap<>());
benchmark.setMetadata(Map.of(
"description", "Default CIS Kubernetes Benchmark",
"target", target,
"generated", Instant.now().toString()
));
return benchmark;
}
public void saveBenchmark(BenchmarkDefinition benchmark, String target) {
try {
String configFile = String.format("%s/%s.yaml",
properties.getChecks().getConfigDir(), target);
yamlMapper.writeValue(new File(configFile), benchmark);
log.info("Saved benchmark to: {}", configFile);
} catch (Exception e) {
log.error("Failed to save benchmark for target: {}", target, e);
throw new RuntimeException("Failed to save benchmark", e);
}
}
public List<String> getAvailableTargets() {
File configDir = new File(properties.getChecks().getConfigDir());
if (!configDir.exists()) {
return List.of("master", "node", "etcd", "policies");
}
return Arrays.stream(configDir.listFiles((dir, name) -> name.endsWith(".yaml")))
.map(file -> file.getName().replace(".yaml", ""))
.collect(Collectors.toList());
}
}
2. Kubernetes Security Checker
@Service
@Slf4j
public class KubernetesSecurityChecker {
private final KubernetesClient kubernetesClient;
private final CisBenchmarkLoader benchmarkLoader;
private final CommandExecutor commandExecutor;
public KubernetesSecurityChecker(KubernetesClient kubernetesClient,
CisBenchmarkLoader benchmarkLoader,
CommandExecutor commandExecutor) {
this.kubernetesClient = kubernetesClient;
this.benchmarkLoader = benchmarkLoader;
this.commandExecutor = commandExecutor;
}
public ComplianceReport runComplianceCheck(String target, Set<String> includeTests,
Set<String> excludeTests) {
log.info("Running compliance check for target: {}", target);
List<CisCheck> checks = benchmarkLoader.loadChecksForTarget(target);
List<CheckResult> results = new ArrayList<>();
for (CisCheck cisCheck : checks) {
if (shouldSkipCheck(cisCheck, includeTests, excludeTests)) {
results.add(createSkippedResult(cisCheck));
continue;
}
for (CisCheck.Check check : cisCheck.getChecks()) {
CheckResult result = executeSingleCheck(cisCheck, check, target);
results.add(result);
}
}
return buildComplianceReport(target, results);
}
public CheckResult executeSingleCheck(CisCheck cisCheck, CisCheck.Check check, String target) {
log.debug("Executing check: {} - {}", cisCheck.getId(), check.getId());
try {
String actualResult = executeAuditCommand(check, target);
CheckResult.CheckStatus status = evaluateCheckResult(check, actualResult);
return CheckResult.builder()
.checkId(check.getId())
.checkText(check.getText())
.status(status)
.actualResult(actualResult)
.expectedResult(check.getExpectedResult())
.remediation(getRemediation(cisCheck, check))
.severity(determineSeverity(cisCheck))
.timestamp(Instant.now())
.node(getCurrentNode())
.component(target)
.evidence(collectEvidence(check, actualResult))
.metadata(buildCheckMetadata(cisCheck, check))
.build();
} catch (Exception e) {
log.error("Failed to execute check {}: {}", check.getId(), e.getMessage());
return CheckResult.builder()
.checkId(check.getId())
.checkText(check.getText())
.status(CheckResult.CheckStatus.ERROR)
.actualResult("Check execution failed: " + e.getMessage())
.timestamp(Instant.now())
.component(target)
.build();
}
}
private String executeAuditCommand(CisCheck.Check check, String target) {
if (check.getAudit() != null) {
return commandExecutor.executeCommand(check.getAudit());
} else if (check.getAuditConfig() != null) {
return auditKubernetesConfig(check.getAuditConfig());
} else if (check.getCommands() != null && !check.getCommands().isEmpty()) {
return executeMultipleCommands(check.getCommands());
}
return "No audit command specified";
}
private String auditKubernetesConfig(String configPath) {
try {
// Parse config path format: "pods[?(@.metadata.namespace=='kube-system')]"
if (configPath.startsWith("pods")) {
return auditPodsConfig(configPath);
} else if (configPath.startsWith("nodes")) {
return auditNodesConfig(configPath);
} else if (configPath.startsWith("deployments")) {
return auditDeploymentsConfig(configPath);
}
return "Unsupported config audit: " + configPath;
} catch (Exception e) {
return "Config audit failed: " + e.getMessage();
}
}
private String auditPodsConfig(String configPath) {
try {
List<Pod> pods = kubernetesClient.pods().list().getItems();
List<String> violations = new ArrayList<>();
for (Pod pod : pods) {
// Check pod security context
if (configPath.contains("securityContext")) {
PodSecurityContextSpec securityContext = extractSecurityContext(pod);
if (!isPodSecurityContextCompliant(securityContext)) {
violations.add(pod.getMetadata().getName());
}
}
// Check container security
if (configPath.contains("containers")) {
List<Container> containers = pod.getSpec().getContainers();
for (Container container : containers) {
if (!isContainerSecurityCompliant(container)) {
violations.add(pod.getMetadata().getName() + ":" + container.getName());
}
}
}
}
return violations.isEmpty() ? "PASS" : "FAIL: " + String.join(", ", violations);
} catch (Exception e) {
return "Pod audit failed: " + e.getMessage();
}
}
private PodSecurityContextSpec extractSecurityContext(Pod pod) {
// Extract and convert security context from Pod
io.fabric8.kubernetes.api.model.PodSecurityContext k8sContext =
pod.getSpec().getSecurityContext();
if (k8sContext == null) {
return PodSecurityContextSpec.builder().build();
}
return PodSecurityContextSpec.builder()
.runAsNonRoot(k8sContext.getRunAsNonRoot())
.runAsUser(k8sContext.getRunAsUser())
.runAsGroup(k8sContext.getRunAsGroup())
.fsGroup(k8sContext.getFsGroup())
.build();
}
private boolean isPodSecurityContextCompliant(PodSecurityContextSpec context) {
// CIS Check: 5.2.6 Ensure that the --allow-privileged argument is set to false
// Check if pod is running as non-root
if (context.getRunAsNonRoot() == null || !context.getRunAsNonRoot()) {
return false;
}
// Check if runAsUser is not 0 (root)
if (context.getRunAsUser() != null && context.getRunAsUser() == 0) {
return false;
}
return true;
}
private boolean isContainerSecurityCompliant(Container container) {
// Check container security context
SecurityContext securityContext = container.getSecurityContext();
if (securityContext == null) {
return false;
}
// Check privileged mode
if (Boolean.TRUE.equals(securityContext.getPrivileged())) {
return false;
}
// Check read-only root filesystem
if (securityContext.getReadOnlyRootFilesystem() == null ||
!securityContext.getReadOnlyRootFilesystem()) {
return false;
}
return true;
}
private String auditNodesConfig(String configPath) {
try {
List<Node> nodes = kubernetesClient.nodes().list().getItems();
List<String> violations = new ArrayList<>();
for (Node node : nodes) {
// Check node labels and taints
if (configPath.contains("labels")) {
Map<String, String> labels = node.getMetadata().getLabels();
if (!areNodeLabelsCompliant(labels)) {
violations.add(node.getMetadata().getName());
}
}
// Check node conditions
if (configPath.contains("conditions")) {
List<NodeCondition> conditions = node.getStatus().getConditions();
if (!areNodeConditionsHealthy(conditions)) {
violations.add(node.getMetadata().getName());
}
}
}
return violations.isEmpty() ? "PASS" : "FAIL: " + String.join(", ", violations);
} catch (Exception e) {
return "Node audit failed: " + e.getMessage();
}
}
private boolean areNodeLabelsCompliant(Map<String, String> labels) {
// CIS Check: 4.2.1 Ensure that the --anonymous-auth argument is set to false
// Check for required security labels
return labels != null &&
labels.containsKey("kubernetes.io/role") &&
!"master".equals(labels.get("node-role.kubernetes.io/master"));
}
private boolean areNodeConditionsHealthy(List<NodeCondition> conditions) {
if (conditions == null) return false;
for (NodeCondition condition : conditions) {
if ("Ready".equals(condition.getType()) &&
!"True".equals(condition.getStatus())) {
return false;
}
}
return true;
}
private CheckResult.CheckStatus evaluateCheckResult(CisCheck.Check check, String actualResult) {
if (actualResult == null) {
return CheckResult.CheckStatus.ERROR;
}
if (check.getTests() != null) {
for (String test : check.getTests()) {
if (test.startsWith("fail ") && actualResult.matches(test.substring(5))) {
return CheckResult.CheckStatus.FAIL;
} else if (test.startsWith("pass ") && actualResult.matches(test.substring(5))) {
return CheckResult.CheckStatus.PASS;
}
}
}
// Default evaluation based on expected result
if (check.getExpectedResult() != null) {
return check.getExpectedResult().equals(actualResult) ?
CheckResult.CheckStatus.PASS : CheckResult.CheckStatus.FAIL;
}
return CheckResult.CheckStatus.WARN;
}
private boolean shouldSkipCheck(CisCheck cisCheck, Set<String> includeTests,
Set<String> excludeTests) {
if (!includeTests.isEmpty() && !includeTests.contains(cisCheck.getId())) {
return true;
}
if (!excludeTests.isEmpty() && excludeTests.contains(cisCheck.getId())) {
return true;
}
return false;
}
private CheckResult createSkippedResult(CisCheck cisCheck) {
return CheckResult.builder()
.checkId(cisCheck.getId())
.checkText(cisCheck.getText())
.status(CheckResult.CheckStatus.SKIP)
.timestamp(Instant.now())
.build();
}
private String getRemediation(CisCheck cisCheck, CisCheck.Check check) {
if (check.getRemediation() != null) {
return check.getRemediation();
} else if (cisCheck.getRemediation() != null &&
cisCheck.getRemediation().getManual() != null) {
return cisCheck.getRemediation().getManual();
}
return "No remediation provided";
}
private String determineSeverity(CisCheck cisCheck) {
// Determine severity based on CIS level and check type
if (cisCheck.getGroups() != null && cisCheck.getGroups().contains("level2")) {
return "HIGH";
}
return "MEDIUM";
}
private String getCurrentNode() {
try {
return System.getenv("NODE_NAME");
} catch (Exception e) {
return "unknown";
}
}
private List<String> collectEvidence(CisCheck.Check check, String actualResult) {
List<String> evidence = new ArrayList<>();
evidence.add("Command: " + check.getAudit());
evidence.add("Result: " + actualResult);
return evidence;
}
private Map<String, Object> buildCheckMetadata(CisCheck cisCheck, CisCheck.Check check) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("cisId", cisCheck.getId());
metadata.put("checkType", check.getType());
metadata.put("groups", cisCheck.getGroups());
return metadata;
}
private ComplianceReport buildComplianceReport(String target, List<CheckResult> results) {
ComplianceReport.Summary summary = calculateSummary(results);
return ComplianceReport.builder()
.reportId(UUID.randomUUID().toString())
.cisVersion(properties.getCisVersion())
.benchmarkVersion(properties.getBenchmarkVersion())
.target(target)
.generatedAt(Instant.now())
.clusterName(getClusterName())
.summary(summary)
.results(results)
.remediations(generateRemediations(results))
.build();
}
private ComplianceReport.Summary calculateSummary(List<CheckResult> results) {
int total = results.size();
int pass = (int) results.stream().filter(r -> r.getStatus() == CheckResult.CheckStatus.PASS).count();
int fail = (int) results.stream().filter(r -> r.getStatus() == CheckResult.CheckStatus.FAIL).count();
int warn = (int) results.stream().filter(r -> r.getStatus() == CheckResult.CheckStatus.WARN).count();
int skip = (int) results.stream().filter(r -> r.getStatus() == CheckResult.CheckStatus.SKIP).count();
int error = (int) results.stream().filter(r -> r.getStatus() == CheckResult.CheckStatus.ERROR).count();
double complianceScore = total > 0 ? (double) pass / total * 100 : 0;
return ComplianceReport.Summary.builder()
.total(total)
.pass(pass)
.fail(fail)
.warn(warn)
.skip(skip)
.error(error)
.complianceScore(Math.round(complianceScore * 100.0) / 100.0)
.build();
}
private String getClusterName() {
try {
return kubernetesClient.getMasterUrl().getHost();
} catch (Exception e) {
return "unknown-cluster";
}
}
private List<RemediationAction> generateRemediations(List<CheckResult> results) {
return results.stream()
.filter(result -> result.getStatus() == CheckResult.CheckStatus.FAIL)
.map(this::createRemediationAction)
.collect(Collectors.toList());
}
private RemediationAction createRemediationAction(CheckResult result) {
return RemediationAction.builder()
.checkId(result.getCheckId())
.description(result.getRemediation())
.commands(extractRemediationCommands(result.getRemediation()))
.impact("MEDIUM")
.automated(canAutomateRemediation(result))
.status(RemediationAction.RemediationStatus.PENDING)
.build();
}
private List<String> extractRemediationCommands(String remediation) {
// Extract commands from remediation text
List<String> commands = new ArrayList<>();
if (remediation != null && remediation.contains("kubectl")) {
// Simple extraction - in real implementation, use proper parsing
String[] lines = remediation.split("\n");
for (String line : lines) {
if (line.trim().startsWith("kubectl")) {
commands.add(line.trim());
}
}
}
return commands;
}
private boolean canAutomateRemediation(CheckResult result) {
// Determine if remediation can be automated
return result.getRemediation() != null &&
result.getRemediation().contains("kubectl") &&
!result.getRemediation().contains("manual");
}
private String executeMultipleCommands(List<String> commands) {
StringBuilder result = new StringBuilder();
for (String command : commands) {
String commandResult = commandExecutor.executeCommand(command);
result.append("Command: ").append(command)
.append("\nResult: ").append(commandResult)
.append("\n\n");
}
return result.toString();
}
}
3. Command Executor
@Service
@Slf4j
public class CommandExecutor {
public String executeCommand(String command) {
log.debug("Executing command: {}", command);
try {
Process process = Runtime.getRuntime().exec(command);
process.waitFor(30, TimeUnit.SECONDS);
String output = readStream(process.getInputStream());
String error = readStream(process.getErrorStream());
if (process.exitValue() != 0) {
log.warn("Command failed: {} - Error: {}", command, error);
return "ERROR: " + error;
}
return output.trim();
} catch (Exception e) {
log.error("Command execution failed: {}", command, e);
return "EXCEPTION: " + e.getMessage();
}
}
public String executeCommandWithTimeout(String command, long timeoutSeconds) {
log.debug("Executing command with timeout: {} ({}s)", command, timeoutSeconds);
try {
Process process = Runtime.getRuntime().exec(command);
boolean completed = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
if (!completed) {
process.destroyForcibly();
return "TIMEOUT: Command exceeded timeout of " + timeoutSeconds + " seconds";
}
String output = readStream(process.getInputStream());
String error = readStream(process.getErrorStream());
if (process.exitValue() != 0) {
return "ERROR: " + error;
}
return output.trim();
} catch (Exception e) {
return "EXCEPTION: " + e.getMessage();
}
}
private String readStream(InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
public boolean testCommandAvailability(String command) {
try {
String result = executeCommand("which " + command);
return result != null && !result.contains("not found") && !result.startsWith("ERROR");
} catch (Exception e) {
return false;
}
}
}
4. Report Generator
@Service
@Slf4j
public class ReportGenerator {
private final ObjectMapper jsonMapper;
private final ObjectMapper yamlMapper;
public ReportGenerator() {
this.jsonMapper = new ObjectMapper();
this.jsonMapper.enable(SerializationFeature.INDENT_OUTPUT);
this.yamlMapper = new ObjectMapper(new YAMLFactory());
this.yamlMapper.findAndRegisterModules();
}
public String generateJsonReport(ComplianceReport report) {
try {
return jsonMapper.writeValueAsString(report);
} catch (Exception e) {
log.error("Failed to generate JSON report", e);
return "{\"error\": \"Failed to generate report: " + e.getMessage() + "\"}";
}
}
public String generateYamlReport(ComplianceReport report) {
try {
return yamlMapper.writeValueAsString(report);
} catch (Exception e) {
log.error("Failed to generate YAML report", e);
return "error: Failed to generate report: " + e.getMessage();
}
}
public String generateTextReport(ComplianceReport report) {
StringBuilder sb = new StringBuilder();
// Header
sb.append("=== CIS Kubernetes Benchmark Compliance Report ===\n");
sb.append("Report ID: ").append(report.getReportId()).append("\n");
sb.append("Cluster: ").append(report.getClusterName()).append("\n");
sb.append("Target: ").append(report.getTarget()).append("\n");
sb.append("Generated: ").append(report.getGeneratedAt()).append("\n");
sb.append("CIS Version: ").append(report.getCisVersion()).append("\n\n");
// Summary
ComplianceReport.Summary summary = report.getSummary();
sb.append("=== Summary ===\n");
sb.append(String.format("Total Checks: %d\n", summary.getTotal()));
sb.append(String.format("Passed: %d\n", summary.getPass()));
sb.append(String.format("Failed: %d\n", summary.getFail()));
sb.append(String.format("Warnings: %d\n", summary.getWarn()));
sb.append(String.format("Skipped: %d\n", summary.getSkip()));
sb.append(String.format("Errors: %d\n", summary.getError()));
sb.append(String.format("Compliance Score: %.2f%%\n\n", summary.getComplianceScore()));
// Failed Checks
List<CheckResult> failedChecks = report.getResults().stream()
.filter(r -> r.getStatus() == CheckResult.CheckStatus.FAIL)
.collect(Collectors.toList());
if (!failedChecks.isEmpty()) {
sb.append("=== Failed Checks ===\n");
for (CheckResult check : failedChecks) {
sb.append(String.format("[%s] %s\n", check.getCheckId(), check.getCheckText()));
sb.append(" Actual Result: ").append(check.getActualResult()).append("\n");
sb.append(" Expected: ").append(check.getExpectedResult()).append("\n");
sb.append(" Remediation: ").append(check.getRemediation()).append("\n\n");
}
}
return sb.toString();
}
public String generateJunitReport(ComplianceReport report) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
sb.append("<testsuite name=\"CIS-Kubernetes-Benchmark\" tests=\"")
.append(report.getSummary().getTotal())
.append("\" failures=\"")
.append(report.getSummary().getFail())
.append("\" errors=\"")
.append(report.getSummary().getError())
.append("\" skipped=\"")
.append(report.getSummary().getSkip())
.append("\">\n");
for (CheckResult check : report.getResults()) {
sb.append(" <testcase name=\"").append(escapeXml(check.getCheckId())).append("\" ");
sb.append("classname=\"").append(escapeXml(report.getTarget())).append("\">\n");
if (check.getStatus() == CheckResult.CheckStatus.FAIL) {
sb.append(" <failure message=\"").append(escapeXml(check.getActualResult())).append("\">");
sb.append(escapeXml(check.getRemediation())).append("</failure>\n");
} else if (check.getStatus() == CheckResult.CheckStatus.SKIP) {
sb.append(" <skipped/>\n");
} else if (check.getStatus() == CheckResult.CheckStatus.ERROR) {
sb.append(" <error message=\"").append(escapeXml(check.getActualResult())).append("\"/>\n");
}
sb.append(" <system-out>").append(escapeXml(check.getCheckText())).append("</system-out>\n");
sb.append(" </testcase>\n");
}
sb.append("</testsuite>");
return sb.toString();
}
private String escapeXml(String text) {
if (text == null) return "";
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
public void saveReport(ComplianceReport report, String format, String filePath) {
try {
String content;
switch (format.toUpperCase()) {
case "JSON":
content = generateJsonReport(report);
break;
case "YAML":
content = generateYamlReport(report);
break;
case "JUNIT":
content = generateJunitReport(report);
break;
case "TEXT":
default:
content = generateTextReport(report);
break;
}
Files.write(Paths.get(filePath), content.getBytes(StandardCharsets.UTF_8));
log.info("Report saved to: {}", filePath);
} catch (Exception e) {
log.error("Failed to save report to: {}", filePath, e);
throw new RuntimeException("Failed to save report", e);
}
}
}
REST Controllers
1. Compliance Check Controller
@RestController
@RequestMapping("/api/kube-bench")
@Validated
@Slf4j
public class KubeBenchController {
private final KubernetesSecurityChecker securityChecker;
private final ReportGenerator reportGenerator;
private final KubeBenchProperties properties;
public KubeBenchController(KubernetesSecurityChecker securityChecker,
ReportGenerator reportGenerator,
KubeBenchProperties properties) {
this.securityChecker = securityChecker;
this.reportGenerator = reportGenerator;
this.properties = properties;
}
@PostMapping("/run")
public ResponseEntity<ComplianceReport> runComplianceCheck(
@RequestParam(defaultValue = "master") String target,
@RequestParam(required = false) String includeTests,
@RequestParam(required = false) String excludeTests,
@RequestParam(defaultValue = "json") String format) {
log.info("Running compliance check for target: {}", target);
try {
Set<String> includeSet = parseTestList(includeTests);
Set<String> excludeSet = parseTestList(excludeTests);
ComplianceReport report = securityChecker.runComplianceCheck(target, includeSet, excludeSet);
// Save report to file
if (properties.getChecks().getResultsDir() != null) {
String fileName = String.format("kube-bench-%s-%s.%s",
target, Instant.now().getEpochSecond(), format.toLowerCase());
String filePath = properties.getChecks().getResultsDir() + "/" + fileName;
reportGenerator.saveReport(report, format, filePath);
}
return ResponseEntity.ok(report);
} catch (Exception e) {
log.error("Compliance check failed for target: {}", target, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/targets")
public ResponseEntity<List<String>> getAvailableTargets() {
// This would typically come from the benchmark loader
List<String> targets = List.of("master", "node", "etcd", "policies", "managedservices");
return ResponseEntity.ok(targets);
}
@GetMapping("/report/{reportId}")
public ResponseEntity<String> getReport(@PathVariable String reportId,
@RequestParam(defaultValue = "json") String format) {
// This would retrieve a previously generated report
// Implementation depends on storage strategy
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build();
}
@GetMapping("/health")
public ResponseEntity<HealthStatus> healthCheck() {
try {
// Test Kubernetes connectivity
securityChecker.runComplianceCheck("master", Set.of(), Set.of());
return ResponseEntity.ok(HealthStatus.builder()
.status("UP")
.timestamp(Instant.now())
.details(Map.of("kubernetes", "connected", "checks", "available"))
.build());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(HealthStatus.builder()
.status("DOWN")
.timestamp(Instant.now())
.details(Map.of("error", e.getMessage()))
.build());
}
}
private Set<String> parseTestList(String testList) {
if (testList == null || testList.trim().isEmpty()) {
return Set.of();
}
return Arrays.stream(testList.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class HealthStatus {
private String status;
private Instant timestamp;
private Map<String, Object> details;
}
2. Remediation Controller
@RestController
@RequestMapping("/api/kube-bench/remediation")
@Slf4j
public class RemediationController {
private final KubernetesSecurityChecker securityChecker;
private final CommandExecutor commandExecutor;
public RemediationController(KubernetesSecurityChecker securityChecker,
CommandExecutor commandExecutor) {
this.securityChecker = securityChecker;
this.commandExecutor = commandExecutor;
}
@PostMapping("/apply")
public ResponseEntity<RemediationResult> applyRemediation(
@RequestParam String checkId,
@RequestParam(defaultValue = "false") boolean dryRun) {
log.info("Applying remediation for check: {} (dryRun: {})", checkId, dryRun);
try {
// Get the failed check result
ComplianceReport report = securityChecker.runComplianceCheck("master", Set.of(checkId), Set.of());
Optional<CheckResult> failedCheck = report.getResults().stream()
.filter(r -> r.getStatus() == CheckResult.CheckStatus.FAIL && checkId.equals(r.getCheckId()))
.findFirst();
if (failedCheck.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(RemediationResult.notFound(checkId));
}
// Apply remediation
RemediationResult result = applyRemediationForCheck(failedCheck.get(), dryRun);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("Remediation failed for check: {}", checkId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(RemediationResult.error(checkId, e.getMessage()));
}
}
@PostMapping("/batch-apply")
public ResponseEntity<BatchRemediationResult> applyBatchRemediation(
@RequestParam String target,
@RequestParam(defaultValue = "false") boolean dryRun) {
log.info("Applying batch remediation for target: {} (dryRun: {})", target, dryRun);
try {
ComplianceReport report = securityChecker.runComplianceCheck(target, Set.of(), Set.of());
List<RemediationResult> results = new ArrayList<>();
for (CheckResult failedCheck : report.getResults().stream()
.filter(r -> r.getStatus() == CheckResult.CheckStatus.FAIL)
.collect(Collectors.toList())) {
RemediationResult result = applyRemediationForCheck(failedCheck, dryRun);
results.add(result);
}
BatchRemediationResult batchResult = BatchRemediationResult.builder()
.target(target)
.totalApplied(results.size())
.successful((int) results.stream().filter(r -> r.isSuccess()).count())
.failed((int) results.stream().filter(r -> !r.isSuccess()).count())
.results(results)
.timestamp(Instant.now())
.build();
return ResponseEntity.ok(batchResult);
} catch (Exception e) {
log.error("Batch remediation failed for target: {}", target, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private RemediationResult applyRemediationForCheck(CheckResult check, boolean dryRun) {
List<String> commands = extractRemediationCommands(check.getRemediation());
if (commands.isEmpty()) {
return RemediationResult.builder()
.checkId(check.getCheckId())
.success(false)
.message("No automated remediation commands available")
.timestamp(Instant.now())
.build();
}
List<String> executedCommands = new ArrayList<>();
List<String> commandResults = new ArrayList<>();
for (String command : commands) {
if (dryRun) {
executedCommands.add("[DRY-RUN] " + command);
commandResults.add("Command would be executed");
} else {
String result = commandExecutor.executeCommand(command);
executedCommands.add(command);
commandResults.add(result);
}
}
boolean success = commandResults.stream()
.noneMatch(result -> result.startsWith("ERROR") || result.startsWith("EXCEPTION"));
return RemediationResult.builder()
.checkId(check.getCheckId())
.success(success)
.message(success ? "Remediation applied successfully" : "Remediation failed")
.executedCommands(executedCommands)
.commandResults(commandResults)
.timestamp(Instant.now())
.dryRun(dryRun)
.build();
}
private List<String> extractRemediationCommands(String remediation) {
List<String> commands = new ArrayList<>();
if (remediation == null) return commands;
// Simple command extraction - improve with better parsing
String[] lines = remediation.split("\n");
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.startsWith("kubectl ") || trimmed.startsWith("sudo ")) {
commands.add(trimmed);
}
}
return commands;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class RemediationResult {
private String checkId;
private boolean success;
private String message;
private List<String> executedCommands;
private List<String> commandResults;
private Instant timestamp;
private boolean dryRun;
public static RemediationResult notFound(String checkId) {
return RemediationResult.builder()
.checkId(checkId)
.success(false)
.message("Check not found or not failed")
.timestamp(Instant.now())
.build();
}
public static RemediationResult error(String checkId, String error) {
return RemediationResult.builder()
.checkId(checkId)
.success(false)
.message("Remediation error: " + error)
.timestamp(Instant.now())
.build();
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class BatchRemediationResult {
private String target;
private int totalApplied;
private int successful;
private int failed;
private List<RemediationResult> results;
private Instant timestamp;
}
Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/kube-bench/health").permitAll()
.requestMatchers("/api/kube-bench/run").hasRole("SCAN")
.requestMatchers("/api/kube-bench/remediation/**").hasRole("REMEDIATE")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
Testing
1. Unit Tests
@ExtendWith(MockitoExtension.class)
class KubernetesSecurityCheckerTest {
@Mock
private KubernetesClient kubernetesClient;
@Mock
private CisBenchmarkLoader benchmarkLoader;
@Mock
private CommandExecutor commandExecutor;
@InjectMocks
private KubernetesSecurityChecker securityChecker;
@Test
void testRunComplianceCheck() {
// Setup
CisCheck cisCheck = createTestCisCheck();
when(benchmarkLoader.loadChecksForTarget("master"))
.thenReturn(List.of(cisCheck));
when(commandExecutor.executeCommand(anyString()))
.thenReturn("PASS");
// Execute
ComplianceReport report = securityChecker.runComplianceCheck("master", Set.of(), Set.of());
// Verify
assertNotNull(report);
assertEquals("master", report.getTarget());
assertTrue(report.getSummary().getTotal() > 0);
}
@Test
void testEvaluateCheckResult() {
CisCheck.Check check = CisCheck.Check.builder()
.id("1.1.1")
.text("Ensure foo is configured")
.tests(List.of("pass .*PASS.*", "fail .*FAIL.*"))
.build();
// Test pass
CheckResult result = securityChecker.executeSingleCheck(
createTestCisCheck(), check, "master");
assertEquals(CheckResult.CheckStatus.PASS, result.getStatus());
}
private CisCheck createTestCisCheck() {
return CisCheck.builder()
.id("1.1")
.text("Master Node Security Configuration")
.groups(List.of("level1"))
.checks(List.of(
CisCheck.Check.builder()
.id("1.1.1")
.text("Ensure foo is configured")
.audit("kubectl get pods -n kube-system")
.tests(List.of("pass .*PASS.*"))
.build()
))
.build();
}
}
@SpringBootTest
class KubeBenchIntegrationTest {
@Autowired
private KubeBenchController controller;
@Test
void testHealthEndpoint() {
ResponseEntity<HealthStatus> response = controller.healthCheck();
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
}
2. Test Configuration
@TestConfiguration
public class TestKubeBenchConfig {
@Bean
@Primary
public KubeBenchProperties testKubeBenchProperties() {
KubeBenchProperties properties = new KubeBenchProperties();
properties.setCisVersion("1.6");
properties.setTarget("master");
properties.setOutputFormat("json");
properties.setChecks(new KubeBenchProperties.CheckProperties());
properties.getChecks().setConfigDir("src/test/resources/cfg");
properties.getChecks().setResultsDir("target/test-results");
return properties;
}
}
Best Practices
- Security:
- Run with minimal required permissions
- Secure API endpoints with authentication
- Validate all inputs and commands
- Sanitize command outputs
- Performance:
- Use connection pooling for Kubernetes API
- Implement caching for benchmark definitions
- Run checks in parallel where possible
- Set appropriate timeouts
- Reliability:
- Implement comprehensive error handling
- Add retry mechanisms for transient failures
- Validate Kubernetes connectivity
- Test command availability before execution
- Maintainability:
- Use structured logging
- Implement proper monitoring and metrics
- Keep CIS benchmark definitions updated
- Provide clear documentation
// Example of secure command execution
@Component
public class SecureCommandExecutor {
private final Set<String> allowedCommands = Set.of(
"kubectl", "cat", "grep", "awk", "sed"
);
public String executeSecureCommand(String command) {
if (!isCommandAllowed(command)) {
return "ERROR: Command not allowed";
}
// Execute command with security constraints
return executeWithConstraints(command);
}
private boolean isCommandAllowed(String command) {
return allowedCommands.stream().anyMatch(command::startsWith);
}
}
Conclusion
This Java implementation of Kube-Bench for CIS provides:
- Comprehensive CIS compliance checking for Kubernetes clusters
- Flexible reporting in multiple formats (JSON, YAML, JUnit, text)
- Automated remediation for failed checks
- RESTful API for integration with CI/CD pipelines
- Enterprise-ready with security and monitoring features
The solution can be easily extended with custom checks, integrated with security tools, and scaled for large Kubernetes environments while maintaining the security and reliability required for production use.