Add Kubernetes attack path drill

This commit is contained in:
juvdiaz 2026-06-29 15:07:34 -06:00
parent 8964fda2fd
commit 5f8fdfef3d
4 changed files with 144 additions and 1 deletions

View File

@ -167,6 +167,10 @@ user agents, auth failures, and HTTP errors.
: Check the Tetragon runtime-detection DaemonSet and print the command for : Check the Tetragon runtime-detection DaemonSet and print the command for
watching recent events. watching recent events.
`security-attack-path [namespace] [workload]`
: Run a read-only compromise-path drill for a Kubernetes workload. Defaults to
the public website deployment.
Kyverno hardening policies Kyverno hardening policies
: `apps/supply-chain-policy` includes audit-mode checks for privileged pods, : `apps/supply-chain-policy` includes audit-mode checks for privileged pods,
privilege escalation, hostPath use, resource requests/limits, and mutable image privilege escalation, hostPath use, resource requests/limits, and mutable image

10
jeannie
View File

@ -5829,6 +5829,11 @@ Useful practice trigger:
EOF EOF
} }
security_attack_path() {
require_debian_server "security-attack-path"
"${REPO_ROOT}/scripts/security-attack-path" "${@:2}"
}
case "${1:-}" in case "${1:-}" in
up) up)
up up
@ -5963,6 +5968,9 @@ case "${1:-}" in
security-runtime) security-runtime)
security_runtime security_runtime
;; ;;
security-attack-path)
security_attack_path "$@"
;;
openwrt) openwrt)
openwrt openwrt
;; ;;
@ -5974,7 +5982,7 @@ case "${1:-}" in
echo "Log: ${JEANNIE_LOG_FILE}" echo "Log: ${JEANNIE_LOG_FILE}"
;; ;;
*) *)
echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-gitea-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|openwrt|nuke}" echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-gitea-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|security-attack-path|openwrt|nuke}"
exit 1 exit 1
;; ;;
esac esac

121
scripts/security-attack-path Executable file
View File

@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -euo pipefail
KUBECONFIG="${KUBECONFIG:-${HOME}/.kube/config}"
ATTACK_NAMESPACE="${1:-${SECURITY_ATTACK_NAMESPACE:-website-production}}"
ATTACK_WORKLOAD="${2:-${SECURITY_ATTACK_WORKLOAD:-deployment/php-website-deployment}}"
usage() {
cat <<'EOF'
Usage: scripts/security-attack-path [namespace] [workload]
Default:
scripts/security-attack-path website-production deployment/php-website-deployment
This is a read-only compromise-path drill. It inspects what a compromised pod
would inherit: service account, mounted secrets, RBAC bindings, network
policies, and nearby services.
EOF
}
require_kubectl() {
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl is required." >&2
exit 1
fi
if [[ ! -f "${KUBECONFIG}" ]]; then
echo "KUBECONFIG does not exist: ${KUBECONFIG}" >&2
exit 1
fi
}
section() {
printf '\n== %s ==\n' "$1"
}
kubectl_ro() {
kubectl --kubeconfig "${KUBECONFIG}" "$@"
}
jsonpath_or_unknown() {
local resource="$1"
local path="$2"
local value
value="$(kubectl_ro -n "${ATTACK_NAMESPACE}" get "${resource}" -o "jsonpath=${path}" 2>/dev/null || true)"
printf '%s\n' "${value:-unknown}"
}
main() {
local service_account
local automount
local selector
case "${1:-}" in
-h | --help | help)
usage
return 0
;;
esac
require_kubectl
section "Attack Path Drill"
printf 'Namespace: %s\n' "${ATTACK_NAMESPACE}"
printf 'Workload: %s\n' "${ATTACK_WORKLOAD}"
if ! kubectl_ro -n "${ATTACK_NAMESPACE}" get "${ATTACK_WORKLOAD}" >/dev/null 2>&1; then
echo "Target workload not found." >&2
exit 1
fi
section "Identity"
service_account="$(jsonpath_or_unknown "${ATTACK_WORKLOAD}" '{.spec.template.spec.serviceAccountName}')"
automount="$(jsonpath_or_unknown "${ATTACK_WORKLOAD}" '{.spec.template.spec.automountServiceAccountToken}')"
service_account="${service_account:-default}"
printf 'ServiceAccount: %s\n' "${service_account}"
printf 'Automount token: %s\n' "${automount}"
if [[ "${automount}" != "false" ]]; then
printf 'Focus: pod may receive a Kubernetes API token unless blocked by defaults.\n'
fi
section "Mounted Secrets And Config"
kubectl_ro -n "${ATTACK_NAMESPACE}" get "${ATTACK_WORKLOAD}" \
-o jsonpath='{range .spec.template.spec.volumes[*]}{.name}{" secret="}{.secret.secretName}{" configMap="}{.configMap.name}{" pvc="}{.persistentVolumeClaim.claimName}{" hostPath="}{.hostPath.path}{"\n"}{end}' || true
section "RBAC Bindings In Namespace"
kubectl_ro -n "${ATTACK_NAMESPACE}" get rolebinding -o wide 2>/dev/null || true
printf '\nClusterRoleBindings mentioning service account %s/%s:\n' "${ATTACK_NAMESPACE}" "${service_account}"
kubectl_ro get clusterrolebinding -o jsonpath='{range .items[?(@.subjects)]}{.metadata.name}{" "}{range .subjects[*]}{.kind}{"/"}{.namespace}{"/"}{.name}{" "}{end}{"\n"}{end}' 2>/dev/null |
grep -F "ServiceAccount/${ATTACK_NAMESPACE}/${service_account}" || true
section "Network Policies"
kubectl_ro -n "${ATTACK_NAMESPACE}" get networkpolicy -o wide 2>/dev/null || true
if ! kubectl_ro -n "${ATTACK_NAMESPACE}" get networkpolicy >/dev/null 2>&1; then
printf 'Focus: no NetworkPolicy objects found; pod egress is probably unrestricted by namespace policy.\n'
fi
section "Nearby Services"
kubectl_ro -n "${ATTACK_NAMESPACE}" get svc,endpoints -o wide 2>/dev/null || true
section "Selected Pods"
# shellcheck disable=SC2016
selector="$(kubectl_ro -n "${ATTACK_NAMESPACE}" get "${ATTACK_WORKLOAD}" -o go-template='{{range $k,$v := .spec.selector.matchLabels}}{{printf "%s=%s," $k $v}}{{end}}' 2>/dev/null | sed 's/,$//')"
if [[ -n "${selector}" ]]; then
kubectl_ro -n "${ATTACK_NAMESPACE}" get pods -l "${selector}" -o wide
else
kubectl_ro -n "${ATTACK_NAMESPACE}" get pods -o wide
fi
section "Manual Follow-Up"
cat <<EOF
Ask these questions before adding a fix:
- Does the pod need a service account token?
- Are any mounted Secrets unnecessary for runtime?
- Can this namespace use default-deny egress with explicit allows?
- Can the workload run as non-root with allowPrivilegeEscalation=false?
- Should this namespace be isolated from Gitea, registry, Pi-hole, and Kubernetes API?
EOF
}
main "$@"

View File

@ -14,6 +14,7 @@ Use Jeannie from the Debian homelab host:
./jeannie security-web ./jeannie security-web
./jeannie security-logs ./jeannie security-logs
./jeannie security-runtime ./jeannie security-runtime
./jeannie security-attack-path
``` ```
The default `security-scan` runs: The default `security-scan` runs:
@ -38,6 +39,8 @@ The focused commands are:
- `security-logs`: read-only summary of recent Gitea and Traefik security - `security-logs`: read-only summary of recent Gitea and Traefik security
signals. signals.
- `security-runtime`: Tetragon rollout check and event-log helper. - `security-runtime`: Tetragon rollout check and event-log helper.
- `security-attack-path`: read-only compromise-path drill for a Kubernetes
workload.
Reports are written to: Reports are written to:
@ -75,3 +78,10 @@ to learn what container process execution looks like after a pod is compromised.
Kyverno hardening policies live under `apps/supply-chain-policy`. The added pod Kyverno hardening policies live under `apps/supply-chain-policy`. The added pod
hardening rules start in Audit mode so you can review violations before hardening rules start in Audit mode so you can review violations before
promoting any rule to Deny. promoting any rule to Deny.
`security-attack-path` defaults to the public website workload and can target a
different workload:
```sh
./jeannie security-attack-path security-lab deployment/juice-shop
```