Add identity and access audit

This commit is contained in:
juvdiaz 2026-06-29 16:58:45 -06:00
parent 0777bed10c
commit d2c959bebd
5 changed files with 176 additions and 1 deletions

View File

@ -776,6 +776,26 @@ Run the same validation locally before promoting:
./jeannie validate ./jeannie validate
``` ```
## Identity and access audit
Run a read-only access inventory from the Debian server with:
```bash
./jeannie access-audit
```
The audit reports configured SSH targets and key file modes, Git/Gitea remotes,
Tailscale ACL policy validation, current Kubernetes authorization, broad
cluster-admin bindings, automation service accounts, and Gitea runner status.
It does not create users, tokens, kubeconfigs, or keys. Use it to move toward
separate identities:
- admin kubeconfig only on the Debian control host
- read-only kubeconfig for dashboards and audits
- separate service accounts for automation
- explicit SSH inventory for Debian, RPi, Pimox, and OCI edge
- repo-managed Tailscale ACLs
The workflow only blocks automatic deploy for external Gitea service The workflow only blocks automatic deploy for external Gitea service
changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`,
`install_gitea_backup_timer`, `backup_gitea`, or `drill_gitea_restore` `install_gitea_backup_timer`, `backup_gitea`, or `drill_gitea_restore`

View File

@ -776,6 +776,26 @@ Run the same validation locally before promoting:
./{{ main_script }} validate ./{{ main_script }} validate
``` ```
## Identity and access audit
Run a read-only access inventory from the Debian server with:
```bash
./{{ main_script }} access-audit
```
The audit reports configured SSH targets and key file modes, Git/Gitea remotes,
Tailscale ACL policy validation, current Kubernetes authorization, broad
cluster-admin bindings, automation service accounts, and Gitea runner status.
It does not create users, tokens, kubeconfigs, or keys. Use it to move toward
separate identities:
- admin kubeconfig only on the Debian control host
- read-only kubeconfig for dashboards and audits
- separate service accounts for automation
- explicit SSH inventory for Debian, RPi, Pimox, and OCI edge
- repo-managed Tailscale ACLs
The workflow only blocks automatic deploy for external Gitea service The workflow only blocks automatic deploy for external Gitea service
changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`,
`install_gitea_backup_timer`, `backup_gitea`, or `drill_gitea_restore` `install_gitea_backup_timer`, `backup_gitea`, or `drill_gitea_restore`

View File

@ -57,6 +57,11 @@ deployment readiness, pod restarts, node pressure, disk pressure, Traefik
inventory, generated docs, Bash syntax, shellcheck, YAML parsing, OpenTofu inventory, generated docs, Bash syntax, shellcheck, YAML parsing, OpenTofu
formatting, tailnet policy validation, and optional static security checks. formatting, tailnet policy validation, and optional static security checks.
`access-audit`
: Run a read-only access inventory across SSH targets, Git/Gitea remotes,
Tailscale ACL validation, Kubernetes RBAC, automation service accounts, and the
Gitea runner.
`nuke` `nuke`
: Destroy Kubernetes state and Pimox worker VMs. Requires : Destroy Kubernetes state and Pimox worker VMs. Requires
`LAB_CONFIRM_NUKE=homelab`. `LAB_CONFIRM_NUKE=homelab`.

10
jeannie
View File

@ -5943,6 +5943,11 @@ validate_homelab() {
"${REPO_ROOT}/scripts/validate-homelab" "${REPO_ROOT}/scripts/validate-homelab"
} }
access_audit() {
require_debian_server "access-audit"
"${REPO_ROOT}/scripts/access-audit"
}
case "${1:-}" in case "${1:-}" in
up) up)
up up
@ -5965,6 +5970,9 @@ case "${1:-}" in
validate) validate)
validate_homelab validate_homelab
;; ;;
access-audit)
access_audit
;;
apps) apps)
require_debian_server "apps" require_debian_server "apps"
jeannie_log_start "apps" jeannie_log_start "apps"
@ -6094,7 +6102,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|validate|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}" echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|validate|access-audit|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

122
scripts/access-audit Executable file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
KUBECONFIG="${KUBECONFIG:-${HOME}/.kube/config}"
section() {
printf '\n== %s ==\n' "$1"
}
warn_file_mode() {
local path="$1"
local mode
if [[ ! -e "${path}" ]]; then
printf '%-34s missing - %s\n' "key file" "${path}"
return 0
fi
mode="$(stat -c '%a' "${path}" 2>/dev/null || stat -f '%Lp' "${path}" 2>/dev/null || true)"
printf '%-34s %s mode=%s\n' "key file" "${path}" "${mode:-unknown}"
case "${mode}" in
600 | 400)
;;
*)
printf ' warn: private SSH keys should normally be 0600 or 0400\n'
;;
esac
}
ssh_inventory() {
section "SSH Inventory"
printf '%-18s %s@%s key=%s\n' "Debian" "${USER:-jv}" "${LAB_DEBIAN_HOST:-${LAB_GITEA_HOST:-192.168.100.73}}" "${LAB_DEBIAN_SSH_KEY_PATH:-${HOME}/.ssh/id_ed25519}"
printf '%-18s %s@%s key=%s\n' "RPi4" "${LAB_RPI_USER:-${LAB_RASPBERRY_USER:-jv}}" "${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}" "${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-${HOME}/.ssh/id_ed25519}}"
printf '%-18s %s@%s key=%s\n' "Pimox" "${LAB_PIMOX_USER:-${TF_VAR_pimox_user:-jv}}" "${LAB_PIMOX_HOST:-${TF_VAR_pimox_host:-192.168.100.80}}" "${LAB_PIMOX_SSH_KEY_PATH:-${TF_VAR_pimox_ssh_key_path:-${HOME}/.ssh/id_ed25519}}"
printf '%-18s %s@%s\n' "OCI edge" "${LAB_EDGE_USER:-ubuntu}" "${LAB_EDGE_HOST:-132.145.170.74}"
warn_file_mode "${LAB_DEBIAN_SSH_KEY_PATH:-${HOME}/.ssh/id_ed25519}"
}
git_access() {
section "Git Remotes"
git -C "${REPO_ROOT}" remote -v || true
printf '\nCurrent branch:\n'
git -C "${REPO_ROOT}" branch -vv || true
printf '\nGitea deploy key expected path: %s\n' "${LAB_GITEA_REPO_SSH_KEY_PATH:-${HOME}/.ssh/id_ed25519.pub}"
printf 'Gitea deploy key title: %s\n' "${LAB_GITEA_REPO_DEPLOY_KEY_TITLE:-debian-host-my-homelab-configs}"
printf 'Gitea deploy key read-only: %s\n' "${LAB_GITEA_REPO_DEPLOY_KEY_READ_ONLY:-false}"
if [[ "${LAB_GITEA_REPO_DEPLOY_KEY_READ_ONLY:-false}" != "true" ]]; then
printf ' warn: deploy key is expected to be write-capable for Actions/deploy mirroring\n'
fi
}
tailnet_access() {
section "Tailscale"
if command -v tailscale >/dev/null 2>&1; then
tailscale ip -4 || true
tailscale status || true
else
printf 'tailscale command not installed\n'
fi
printf '\nRepo-managed ACL validation:\n'
"${REPO_ROOT}/scripts/validate-tailnet-policy" || true
}
kubernetes_access() {
section "Kubernetes Access"
if ! command -v kubectl >/dev/null 2>&1; then
printf 'kubectl not installed\n'
return 0
fi
if [[ ! -s "${KUBECONFIG}" ]]; then
printf 'kubeconfig missing: %s\n' "${KUBECONFIG}"
return 0
fi
printf 'KUBECONFIG: %s\n' "${KUBECONFIG}"
kubectl --kubeconfig "${KUBECONFIG}" config current-context 2>/dev/null || true
printf '\nCan current user perform broad actions?\n'
for verb in get list create update delete; do
printf '%-10s ' "${verb}"
kubectl --kubeconfig "${KUBECONFIG}" auth can-i "${verb}" pods --all-namespaces 2>/dev/null || true
done
printf '\nClusterRoleBindings to cluster-admin:\n'
kubectl --kubeconfig "${KUBECONFIG}" get clusterrolebinding -o jsonpath='{range .items[?(@.roleRef.name=="cluster-admin")]}{.metadata.name}{" -> "}{range .subjects[*]}{.kind}{"/"}{.namespace}{"/"}{.name}{" "}{end}{"\n"}{end}' 2>/dev/null || true
printf '\nAutomation service accounts:\n'
kubectl --kubeconfig "${KUBECONFIG}" get serviceaccounts -A 2>/dev/null |
awk 'NR == 1 || /argocd|gitea|runner|kyverno|tetragon|prometheus|jeannie|homelab/'
cat <<'EOF'
Read-only kubeconfig target:
- Create a dedicated read-only ClusterRole/Binding for dashboards and audits.
- Keep admin kubeconfig only on the Debian control host.
- Use separate service accounts for automation instead of reusing admin.
EOF
}
runner_access() {
section "Automation Runners"
if command -v systemctl >/dev/null 2>&1; then
systemctl is-active homelab-gitea-runner.service 2>/dev/null || true
systemctl status homelab-gitea-runner.service --no-pager -l 2>/dev/null | sed -n '1,12p' || true
else
printf 'systemctl unavailable\n'
fi
}
main() {
ssh_inventory
git_access
tailnet_access
kubernetes_access
runner_access
}
main "$@"