Add defensive security scan commands

This commit is contained in:
juvdiaz 2026-06-29 14:25:47 -06:00
parent 45efd87960
commit 4f1c973cb5
5 changed files with 320 additions and 1 deletions

View File

@ -127,6 +127,24 @@ structure.
: Check local AI helper prerequisites, knowledge index, and Ollama availability : Check local AI helper prerequisites, knowledge index, and Ollama availability
when the backstage helper is enabled. when the backstage helper is enabled.
### Defensive Security
`security-scan`
: Run the default defensive scan set: Trivy repo/IaC scans plus OWASP ZAP
baseline passive scans for targets in `security/targets.txt`.
`security-zap`
: Run only OWASP ZAP baseline passive scans against public targets.
`security-k8s`
: Run kube-bench CIS checks against the Debian kubeadm host.
`security-trivy`
: Run only Trivy filesystem and IaC/config scans.
`security-nuclei`
: Run low-rate nuclei HTTP exposure scans against public targets.
### Maintenance ### Maintenance
`state-backup` `state-backup`
@ -184,6 +202,20 @@ checks already show short output by default.
: DNS name used by `status` and `doctor-preapply` for the Pi-hole resolver : DNS name used by `status` and `doctor-preapply` for the Pi-hole resolver
smoke test. Defaults to `cloudflare.com`. smoke test. Defaults to `cloudflare.com`.
`SECURITY_REPORT_DIR`
: Output directory for defensive security reports. Defaults to
`$HOMELAB_STATE_DIR/security-reports`.
`SECURITY_TARGETS_FILE`
: Public target allowlist for ZAP and nuclei scans. Defaults to
`security/targets.txt`.
`SECURITY_ZAP_TARGET`
: Optional single URL override for `security-zap`.
`SECURITY_NUCLEI_TARGET`
: Optional single URL override for `security-nuclei`.
`LAB_AUTO_APPROVE=false` `LAB_AUTO_APPROVE=false`
: Disable automatic OpenTofu approval for apply paths that support confirmation. : Disable automatic OpenTofu approval for apply paths that support confirmation.
@ -249,6 +281,12 @@ script name.
`$HOMELAB_STATE_DIR/tofu-state-backups` `$HOMELAB_STATE_DIR/tofu-state-backups`
: Local OpenTofu and generated state backups. : Local OpenTofu and generated state backups.
`$HOMELAB_STATE_DIR/security-reports`
: Defensive scan reports.
`security/targets.txt`
: Public target allowlist for defensive web scans.
## EXAMPLES ## EXAMPLES
Start a three-worker homelab: Start a three-worker homelab:
@ -269,6 +307,18 @@ Run focused edge diagnostics:
./jeannie doctor-edge ./jeannie doctor-edge
``` ```
Run the default defensive scan set:
```sh
./jeannie security-scan
```
Run a ZAP baseline scan against one URL:
```sh
SECURITY_ZAP_TARGET=https://lab2025.duckdns.org/ ./jeannie security-zap
```
Run the blocking pre-apply doctor directly: Run the blocking pre-apply doctor directly:
```sh ```sh

42
jeannie
View File

@ -5760,6 +5760,31 @@ PY
echo "AI check passed." echo "AI check passed."
} }
security_scan() {
require_debian_server "security-scan"
"${REPO_ROOT}/scripts/security-scan" all
}
security_zap() {
require_debian_server "security-zap"
"${REPO_ROOT}/scripts/security-scan" zap
}
security_k8s() {
require_debian_server "security-k8s"
"${REPO_ROOT}/scripts/security-scan" kube-bench
}
security_trivy() {
require_debian_server "security-trivy"
"${REPO_ROOT}/scripts/security-scan" trivy
}
security_nuclei() {
require_debian_server "security-nuclei"
"${REPO_ROOT}/scripts/security-scan" nuclei
}
case "${1:-}" in case "${1:-}" in
up) up)
up up
@ -5861,6 +5886,21 @@ case "${1:-}" in
ai-check) ai-check)
ai_check ai_check
;; ;;
security-scan)
security_scan
;;
security-zap)
security_zap
;;
security-k8s)
security_k8s
;;
security-trivy)
security_trivy
;;
security-nuclei)
security_nuclei
;;
openwrt) openwrt)
openwrt openwrt
;; ;;
@ -5872,7 +5912,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|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-zap|security-k8s|security-trivy|security-nuclei|openwrt|nuke}"
exit 1 exit 1
;; ;;
esac esac

174
scripts/security-scan Executable file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOMELAB_STATE_DIR="${HOMELAB_STATE_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/homelab}"
SECURITY_REPORT_DIR="${SECURITY_REPORT_DIR:-${HOMELAB_STATE_DIR}/security-reports}"
SECURITY_TARGETS_FILE="${SECURITY_TARGETS_FILE:-${REPO_ROOT}/security/targets.txt}"
TRIVY_IMAGE="${TRIVY_IMAGE:-aquasec/trivy:latest}"
ZAP_IMAGE="${ZAP_IMAGE:-ghcr.io/zaproxy/zaproxy:stable}"
KUBE_BENCH_IMAGE="${KUBE_BENCH_IMAGE:-aquasec/kube-bench:latest}"
NUCLEI_IMAGE="${NUCLEI_IMAGE:-projectdiscovery/nuclei:latest}"
usage() {
cat <<'EOF'
Usage: scripts/security-scan {all|trivy|zap|kube-bench|nuclei}
Environment:
SECURITY_REPORT_DIR Report output directory.
SECURITY_TARGETS_FILE Target allowlist for web scans.
SECURITY_ZAP_TARGET Single URL override for ZAP.
SECURITY_NUCLEI_TARGET Single URL override for nuclei.
EOF
}
timestamp() {
date -u +%Y%m%dT%H%M%SZ
}
ensure_report_dir() {
mkdir -p "${SECURITY_REPORT_DIR}"
}
require_docker() {
if ! command -v docker >/dev/null 2>&1; then
echo "docker is required for containerized security scans." >&2
exit 1
fi
}
run_trivy() {
local stamp
stamp="$(timestamp)"
ensure_report_dir
echo "Running Trivy filesystem and IaC/config scans..."
if command -v trivy >/dev/null 2>&1; then
trivy fs --scanners vuln,secret,misconfig --format table --output "${SECURITY_REPORT_DIR}/trivy-fs-${stamp}.txt" "${REPO_ROOT}"
trivy config --format table --output "${SECURITY_REPORT_DIR}/trivy-config-${stamp}.txt" "${REPO_ROOT}"
else
require_docker
docker run --rm \
-v "${REPO_ROOT}:/repo:ro" \
-v "${SECURITY_REPORT_DIR}:/reports" \
"${TRIVY_IMAGE}" fs --scanners vuln,secret,misconfig --format table --output "/reports/trivy-fs-${stamp}.txt" /repo
docker run --rm \
-v "${REPO_ROOT}:/repo:ro" \
-v "${SECURITY_REPORT_DIR}:/reports" \
"${TRIVY_IMAGE}" config --format table --output "/reports/trivy-config-${stamp}.txt" /repo
fi
echo "Trivy reports written to ${SECURITY_REPORT_DIR}"
}
scan_targets() {
local target_override="$1"
if [[ -n "${target_override}" ]]; then
printf '%s\n' "${target_override}"
return 0
fi
if [[ ! -s "${SECURITY_TARGETS_FILE}" ]]; then
echo "Missing target file: ${SECURITY_TARGETS_FILE}" >&2
exit 1
fi
sed -e 's/#.*//' -e '/^[[:space:]]*$/d' "${SECURITY_TARGETS_FILE}"
}
safe_target_name() {
printf '%s' "$1" | sed -E 's#^https?://##; s#[^A-Za-z0-9._-]+#_#g; s#_+$##'
}
run_zap() {
local stamp
local target
local safe_name
stamp="$(timestamp)"
ensure_report_dir
require_docker
echo "Running OWASP ZAP baseline passive scans..."
while IFS= read -r target; do
[[ -n "${target}" ]] || continue
safe_name="$(safe_target_name "${target}")"
echo "--> ZAP baseline ${target}"
docker run --rm \
-v "${SECURITY_REPORT_DIR}:/zap/wrk:rw" \
"${ZAP_IMAGE}" zap-baseline.py \
-t "${target}" \
-I \
-r "zap-${safe_name}-${stamp}.html" \
-J "zap-${safe_name}-${stamp}.json"
done < <(scan_targets "${SECURITY_ZAP_TARGET:-}")
echo "ZAP reports written to ${SECURITY_REPORT_DIR}"
}
run_nuclei() {
local stamp
local target
local safe_name
stamp="$(timestamp)"
ensure_report_dir
require_docker
echo "Running nuclei low-noise HTTP exposure scans..."
while IFS= read -r target; do
[[ -n "${target}" ]] || continue
safe_name="$(safe_target_name "${target}")"
echo "--> nuclei ${target}"
docker run --rm \
-v "${SECURITY_REPORT_DIR}:/reports:rw" \
"${NUCLEI_IMAGE}" \
-u "${target}" \
-severity low,medium,high,critical \
-rate-limit "${SECURITY_NUCLEI_RATE_LIMIT:-5}" \
-retries 1 \
-timeout 5 \
-o "/reports/nuclei-${safe_name}-${stamp}.txt"
done < <(scan_targets "${SECURITY_NUCLEI_TARGET:-}")
echo "nuclei reports written to ${SECURITY_REPORT_DIR}"
}
run_kube_bench() {
local stamp
stamp="$(timestamp)"
ensure_report_dir
require_docker
echo "Running kube-bench CIS checks against this kubeadm host..."
docker run --rm --pid=host \
-v /etc:/etc:ro \
-v /var:/var:ro \
-v /usr/bin:/usr/local/mount-from-host/bin:ro \
"${KUBE_BENCH_IMAGE}" run --targets master,node | tee "${SECURITY_REPORT_DIR}/kube-bench-${stamp}.txt"
}
case "${1:-all}" in
all)
run_trivy
run_zap
;;
trivy)
run_trivy
;;
zap)
run_zap
;;
nuclei)
run_nuclei
;;
kube-bench)
run_kube_bench
;;
-h | --help | help)
usage
;;
*)
usage >&2
exit 1
;;
esac

37
security/README.md Normal file
View File

@ -0,0 +1,37 @@
# Defensive Security Scans
This directory contains defensive scan configuration for the homelab and public
website. These checks are for assets owned by this lab only.
Use Jeannie from the Debian homelab host:
```bash
./jeannie security-scan
./jeannie security-zap
./jeannie security-k8s
```
The default `security-scan` runs:
- Trivy filesystem and IaC/config scans against this repo.
- OWASP ZAP baseline passive scans against `security/targets.txt`.
The focused commands are:
- `security-zap`: OWASP ZAP baseline scan only.
- `security-k8s`: kube-bench CIS checks against the kubeadm host.
- `security-trivy`: Trivy repo and IaC/config scans only.
- `security-nuclei`: low-rate nuclei HTTP exposure scans only.
Reports are written to:
```text
${HOMELAB_STATE_DIR}/security-reports
```
The ZAP scan uses baseline/passive mode. Do not run active attack scans against
public endpoints unless you intentionally schedule a maintenance window and
understand the traffic it will generate.
`security/targets.txt` is the public target allowlist. Keep it limited to
services you own.

18
security/targets.txt Normal file
View File

@ -0,0 +1,18 @@
# Public targets for defensive baseline scans.
# Keep this list scoped to services you own.
https://lab2025.duckdns.org/
https://lab2025.duckdns.org/git/
https://demos.lab2025.duckdns.org/
https://heimdall.lab2025.duckdns.org/
https://argocd.lab2025.duckdns.org/
https://grafana.lab2025.duckdns.org/
https://prometheus.lab2025.duckdns.org/
https://alertmanager.lab2025.duckdns.org/
https://n8n.lab2025.duckdns.org/
https://prowlarr.lab2025.duckdns.org/
https://sonarr.lab2025.duckdns.org/
https://radarr.lab2025.duckdns.org/
https://qbittorrent.lab2025.duckdns.org/
https://kapowarr.lab2025.duckdns.org/
https://suwayomi.lab2025.duckdns.org/
https://maintainerr.lab2025.duckdns.org/