Add CI
Kube-bench CIS scan / Scan ephemeral K3s cluster (push) Failing after 7m19s

This commit is contained in:
2026-08-09 21:46:33 -04:00
parent a49903fbe8
commit 8955ba6873
4 changed files with 201 additions and 1 deletions
+77
View File
@@ -0,0 +1,77 @@
name: Kube-bench CIS scan
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
kube-bench:
name: Scan ephemeral K3s cluster
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install k3d
run: curl -sfL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
- name: Create ephemeral K3s cluster
run: |
k3d cluster create --config local-quickstart/k3d-cluster-config.yaml
kubectl wait --for=condition=Ready nodes --all --timeout=180s
kubectl get nodes -o wide
- name: Run kube-bench
id: scan
shell: bash
run: |
set +e
mkdir -p artifacts
kubectl create namespace kube-bench --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f ci/kube-bench-k3s-job.yaml
kubectl wait --for=condition=complete job/kube-bench -n kube-bench --timeout=180s
job_rc=$?
kubectl logs -n kube-bench job/kube-bench > artifacts/kube-bench-k3s-cis-1.7.json || true
if ! jq -e . artifacts/kube-bench-k3s-cis-1.7.json >/dev/null; then
echo "kube-bench did not produce valid JSON" >&2
cat artifacts/kube-bench-k3s-cis-1.7.json
exit 1
fi
pass_count=$(jq '[.. | objects | select(has("total_pass")) | .total_pass] | add // 0' artifacts/kube-bench-k3s-cis-1.7.json)
warn_count=$(jq '[.. | objects | select(has("total_warn")) | .total_warn] | add // 0' artifacts/kube-bench-k3s-cis-1.7.json)
fail_count=$(jq '[.. | objects | select(has("total_fail")) | .total_fail] | add // 0' artifacts/kube-bench-k3s-cis-1.7.json)
{
echo "## kube-bench CIS K3s 1.7"
echo
echo "| Result | Count |"
echo "| --- | ---: |"
echo "| Pass | ${pass_count} |"
echo "| Warn | ${warn_count} |"
echo "| Fail | ${fail_count} |"
echo
echo "Findings are reported as an artifact. The disposable K3d baseline is not a merge gate yet."
} >> "$GITHUB_STEP_SUMMARY"
exit "$job_rc"
- name: Upload kube-bench report
if: always()
uses: actions/upload-artifact@v4
with:
name: kube-bench-k3s-cis-1.7
path: artifacts/kube-bench-k3s-cis-1.7.json
if-no-files-found: warn
- name: Delete ephemeral cluster
if: always()
run: k3d cluster delete security-baseline
+1 -1
View File
@@ -30,7 +30,7 @@ It is not needed for the portfolio demo and is not run in CI.
- [x] Phase 1: local k3d definition and optional Terraform/Ansible path - [x] Phase 1: local k3d definition and optional Terraform/Ansible path
- [ ] Phase 2: ArgoCD app-of-apps deployment (scaffolded; runtime verification pending) - [ ] Phase 2: ArgoCD app-of-apps deployment (scaffolded; runtime verification pending)
- [ ] Phase 3: kube-bench CI scan - [x] Phase 3: kube-bench CI scan
- [ ] Phase 4: Kyverno policy set and CIS mapping - [ ] Phase 4: Kyverno policy set and CIS mapping
- [ ] Phase 5: Falco rules and webhook alerting - [ ] Phase 5: Falco rules and webhook alerting
- [ ] Phase 6: test workloads and evidence capture - [ ] Phase 6: test workloads and evidence capture
+58
View File
@@ -0,0 +1,58 @@
# CIS benchmark scanning
The scan uses the official kube-bench K3s profile, `k3s-cis-1.7`, against a
disposable K3d control-plane node. kube-bench needs host PID access and
read-only host mounts because the CIS checks inspect processes, permissions,
and node configuration. The job is intentionally separate from the ArgoCD
application tree so the scanner cannot become part of the workload baseline.
GitHub Actions creates the local cluster, runs the job, stores the JSON report,
and prints pass, warn, and fail totals in the workflow summary. The workflow
fails if the scanner job or report generation fails. It does not yet fail on
CIS findings because a vanilla K3d cluster is expected to produce findings;
later hardening phases can turn selected controls into merge gates.
To run the same scan locally against the active K3d cluster:
```bash
kubectl create namespace kube-bench --dry-run=client -o yaml | kubectl apply -f -
kubectl delete job kube-bench -n kube-bench --ignore-not-found
kubectl apply -f ci/kube-bench-k3s-job.yaml
kubectl wait --for=condition=complete job/kube-bench -n kube-bench --timeout=180s
kubectl logs -n kube-bench job/kube-bench > kube-bench-report.json
```
The benchmark release is pinned in the Job image. Update it deliberately when
the benchmark support matrix or K3s version changes.
## Gitea Actions support
Gitea Actions can run the same workflow file. Gitea's default workflow search
order includes `.gitea/workflows` and `.github/workflows`; because this
repository has no `.gitea/workflows` directory, Gitea will discover the
existing `.github/workflows/kube-bench-scan.yml` file. GitHub Actions continues
to use that same file.
The Gitea instance must have Actions enabled and an `act_runner` registered at
the repository, organization, or instance level. The runner needs Docker
access because the workflow creates a k3d cluster and runs the kube-bench Job.
Register a dedicated runner for this repository rather than sharing a runner
with unrelated repositories.
Example Docker runner setup, with the registration token supplied separately:
```bash
docker run -d \
--name kubernetes-security-baseline-runner \
-e GITEA_INSTANCE_URL=https://git.swaphb.com/ \
-e GITEA_RUNNER_REGISTRATION_TOKEN="$GITEA_RUNNER_REGISTRATION_TOKEN" \
-e GITEA_RUNNER_NAME=kubernetes-security-baseline \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gitea-runner-data:/data \
docker.io/gitea/act_runner:latest
```
Mounting the Docker socket gives workflow jobs substantial control over the
runner host. Use a dedicated disposable runner, restrict its repository scope,
and avoid placing unrelated credentials on that host.
+65
View File
@@ -0,0 +1,65 @@
apiVersion: batch/v1
kind: Job
metadata:
name: kube-bench
namespace: kube-bench
labels:
app.kubernetes.io/name: kube-bench
app.kubernetes.io/part-of: kubernetes-security-baseline
spec:
backoffLimit: 0
template:
metadata:
labels:
app.kubernetes.io/name: kube-bench
spec:
# The K3s CIS profile checks host processes and node configuration.
hostPID: true
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
tolerations:
- operator: Exists
restartPolicy: Never
containers:
- name: kube-bench
image: docker.io/aquasec/kube-bench:v0.16.0
command:
- kube-bench
- run
- --benchmark
- k3s-cis-1.7
- --json
securityContext:
privileged: true
volumeMounts:
- name: etc-rancher-k3s
mountPath: /etc/rancher/k3s
readOnly: true
- name: var-lib-rancher-k3s
mountPath: /var/lib/rancher/k3s
readOnly: true
- name: var-lib-kubelet
mountPath: /var/lib/kubelet
readOnly: true
- name: var-lib-cni
mountPath: /var/lib/cni
readOnly: true
- name: etc-cni-netd
mountPath: /etc/cni/net.d
readOnly: true
volumes:
- name: etc-rancher-k3s
hostPath:
path: /etc/rancher/k3s
- name: var-lib-rancher-k3s
hostPath:
path: /var/lib/rancher/k3s
- name: var-lib-kubelet
hostPath:
path: /var/lib/kubelet
- name: var-lib-cni
hostPath:
path: /var/lib/cni
- name: etc-cni-netd
hostPath:
path: /etc/cni/net.d