Add read-only Kubernetes access model

This commit is contained in:
juvdiaz 2026-06-29 17:11:55 -06:00
parent e10be77cc2
commit 1d553771f0
8 changed files with 230 additions and 1 deletions

View File

@ -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`

View File

@ -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`

View File

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- readonly-rbac.yaml

View File

@ -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

View File

@ -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"

View File

@ -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`.

10
jeannie
View File

@ -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

82
scripts/kubeconfig-readonly Executable file
View File

@ -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}" <<EOF
apiVersion: v1
kind: Config
clusters:
- name: ${cluster_name}
cluster:
server: ${server}
certificate-authority-data: ${ca_data}
users:
- name: ${user_name}
user:
token: ${token}
contexts:
- name: ${context_name}
context:
cluster: ${cluster_name}
user: ${user_name}
namespace: default
current-context: ${context_name}
EOF
chmod 0600 "${READONLY_KUBECONFIG}"
echo "Created read-only kubeconfig: ${READONLY_KUBECONFIG}"
echo "Test with:"
echo " KUBECONFIG=${READONLY_KUBECONFIG} kubectl get pods -A"
}
main "$@"