diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index aa0d7f2..bb7714a 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -167,6 +167,10 @@ user agents, auth failures, and HTTP errors. : Check the Tetragon runtime-detection DaemonSet and print the command for 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 : `apps/supply-chain-policy` includes audit-mode checks for privileged pods, privilege escalation, hostPath use, resource requests/limits, and mutable image diff --git a/jeannie b/jeannie index 6215535..d5437bb 100755 --- a/jeannie +++ b/jeannie @@ -5829,6 +5829,11 @@ Useful practice trigger: EOF } +security_attack_path() { + require_debian_server "security-attack-path" + "${REPO_ROOT}/scripts/security-attack-path" "${@:2}" +} + case "${1:-}" in up) up @@ -5963,6 +5968,9 @@ case "${1:-}" in security-runtime) security_runtime ;; + security-attack-path) + security_attack_path "$@" + ;; openwrt) openwrt ;; @@ -5974,7 +5982,7 @@ case "${1:-}" in 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 ;; esac diff --git a/scripts/security-attack-path b/scripts/security-attack-path new file mode 100755 index 0000000..ba2aa23 --- /dev/null +++ b/scripts/security-attack-path @@ -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 <