diff --git a/README.md b/README.md index 31ae231..6af19fa 100644 --- a/README.md +++ b/README.md @@ -813,6 +813,13 @@ separate identities: - explicit SSH inventory for Debian, RPi, Pimox, and OCI edge - repo-managed Tailscale ACLs +The read-only Kubernetes identity is managed in `apps/access-control`. After +that app syncs, generate a separate read-only kubeconfig on the Debian host: + +```bash +./jeannie kubeconfig-readonly +``` + The workflow only blocks automatic deploy for external Gitea service changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, `install_gitea_backup_timer`, `backup_gitea`, or `drill_gitea_restore` diff --git a/README.md.tmpl b/README.md.tmpl index 33e7fc1..0b93809 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -813,6 +813,13 @@ separate identities: - explicit SSH inventory for Debian, RPi, Pimox, and OCI edge - repo-managed Tailscale ACLs +The read-only Kubernetes identity is managed in `apps/access-control`. After +that app syncs, generate a separate read-only kubeconfig on the Debian host: + +```bash +./{{ main_script }} kubeconfig-readonly +``` + The workflow only blocks automatic deploy for external Gitea service changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, `install_gitea_backup_timer`, `backup_gitea`, or `drill_gitea_restore` diff --git a/apps/access-control/kustomization.yaml b/apps/access-control/kustomization.yaml new file mode 100644 index 0000000..cfa07c3 --- /dev/null +++ b/apps/access-control/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - readonly-rbac.yaml diff --git a/apps/access-control/readonly-rbac.yaml b/apps/access-control/readonly-rbac.yaml new file mode 100644 index 0000000..32f1045 --- /dev/null +++ b/apps/access-control/readonly-rbac.yaml @@ -0,0 +1,108 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: homelab-access +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: homelab-readonly + namespace: homelab-access +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: homelab-readonly +rules: + - apiGroups: + - "" + resources: + - componentstatuses + - configmaps + - endpoints + - events + - namespaces + - nodes + - persistentvolumeclaims + - persistentvolumes + - pods + - pods/log + - replicationcontrollers + - resourcequotas + - services + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - daemonsets + - deployments + - replicasets + - statefulsets + verbs: + - get + - list + - watch + - apiGroups: + - batch + resources: + - cronjobs + - jobs + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + - networkpolicies + verbs: + - get + - list + - watch + - apiGroups: + - monitoring.coreos.com + resources: + - prometheusrules + - servicemonitors + - podmonitors + verbs: + - get + - list + - watch + - apiGroups: + - argoproj.io + resources: + - applications + - appprojects + verbs: + - get + - list + - watch + - apiGroups: + - kyverno.io + - policies.kyverno.io + resources: + - clusterpolicies + - policies + - imagevalidatingpolicies + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: homelab-readonly +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: homelab-readonly +subjects: + - kind: ServiceAccount + name: homelab-readonly + namespace: homelab-access diff --git a/bootstrap/apps/variables.tf b/bootstrap/apps/variables.tf index ac52cad..d91e3f5 100644 --- a/bootstrap/apps/variables.tf +++ b/bootstrap/apps/variables.tf @@ -39,6 +39,15 @@ variable "applications" { self_heal = true create_namespace = true } + access-control = { + project = "default" + path = "apps/access-control" + namespace = "homelab-access" + target_revision = "main" + prune = true + self_heal = true + create_namespace = true + } website-production = { project = "default" path = "apps/website" diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index dcaae4d..5c4c1b6 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -72,6 +72,10 @@ formatting, tailnet policy validation, and optional static security checks. Tailscale ACL validation, Kubernetes RBAC, automation service accounts, and the Gitea runner. +`kubeconfig-readonly` +: Generate a separate local read-only kubeconfig from the repo-managed +`homelab-access/homelab-readonly` ServiceAccount. + `nuke` : Destroy Kubernetes state and Pimox worker VMs. Requires `LAB_CONFIRM_NUKE=homelab`. diff --git a/jeannie b/jeannie index d8a57b2..7fd56a6 100755 --- a/jeannie +++ b/jeannie @@ -5957,6 +5957,11 @@ drill_pihole_restore() { "${REPO_ROOT}/scripts/restore-drill" pihole } +kubeconfig_readonly() { + require_debian_server "kubeconfig-readonly" + "${REPO_ROOT}/scripts/kubeconfig-readonly" +} + case "${1:-}" in up) up @@ -5982,6 +5987,9 @@ case "${1:-}" in access-audit) access_audit ;; + kubeconfig-readonly) + kubeconfig_readonly + ;; apps) require_debian_server "apps" jeannie_log_start "apps" @@ -6117,7 +6125,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|validate|access-audit|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-restore|drill-gitea-restore|drill-pihole-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|kubeconfig-readonly|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-restore|drill-gitea-restore|drill-pihole-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/kubeconfig-readonly b/scripts/kubeconfig-readonly new file mode 100755 index 0000000..5b151c5 --- /dev/null +++ b/scripts/kubeconfig-readonly @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +ADMIN_KUBECONFIG="${KUBECONFIG:-${KUBECONFIG_PATH:-${HOME}/.kube/config}}" +READONLY_NAMESPACE="${READONLY_NAMESPACE:-homelab-access}" +READONLY_SERVICE_ACCOUNT="${READONLY_SERVICE_ACCOUNT:-homelab-readonly}" +READONLY_KUBECONFIG="${READONLY_KUBECONFIG:-${HOME}/.kube/homelab-readonly.conf}" +READONLY_TOKEN_DURATION="${READONLY_TOKEN_DURATION:-24h}" + +usage() { + cat <<'EOF' +Usage: scripts/kubeconfig-readonly + +Generate a separate read-only kubeconfig from the repo-managed +homelab-access/homelab-readonly ServiceAccount. +EOF +} + +require_kubectl() { + if ! command -v kubectl >/dev/null 2>&1; then + echo "kubectl is required." >&2 + exit 1 + fi + if [[ ! -s "${ADMIN_KUBECONFIG}" ]]; then + echo "admin kubeconfig missing: ${ADMIN_KUBECONFIG}" >&2 + exit 1 + fi +} + +main() { + local server + local cluster_name + local ca_data + local token + local user_name="homelab-readonly" + local context_name="homelab-readonly" + + case "${1:-}" in + -h | --help | help) + usage + return 0 + ;; + esac + + require_kubectl + + kubectl --kubeconfig "${ADMIN_KUBECONFIG}" -n "${READONLY_NAMESPACE}" get serviceaccount "${READONLY_SERVICE_ACCOUNT}" >/dev/null + token="$(kubectl --kubeconfig "${ADMIN_KUBECONFIG}" -n "${READONLY_NAMESPACE}" create token "${READONLY_SERVICE_ACCOUNT}" --duration="${READONLY_TOKEN_DURATION}")" + server="$(kubectl --kubeconfig "${ADMIN_KUBECONFIG}" config view --minify -o jsonpath='{.clusters[0].cluster.server}')" + cluster_name="$(kubectl --kubeconfig "${ADMIN_KUBECONFIG}" config view --minify -o jsonpath='{.clusters[0].name}')" + ca_data="$(kubectl --kubeconfig "${ADMIN_KUBECONFIG}" config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')" + + mkdir -p "$(dirname "${READONLY_KUBECONFIG}")" + umask 077 + cat >"${READONLY_KUBECONFIG}" <