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 - explicit SSH inventory for Debian, RPi, Pimox, and OCI edge
- repo-managed Tailscale ACLs - 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 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

@ -813,6 +813,13 @@ separate identities:
- explicit SSH inventory for Debian, RPi, Pimox, and OCI edge - explicit SSH inventory for Debian, RPi, Pimox, and OCI edge
- repo-managed Tailscale ACLs - 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 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

@ -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 self_heal = true
create_namespace = 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 = { website-production = {
project = "default" project = "default"
path = "apps/website" 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 Tailscale ACL validation, Kubernetes RBAC, automation service accounts, and the
Gitea runner. Gitea runner.
`kubeconfig-readonly`
: Generate a separate local read-only kubeconfig from the repo-managed
`homelab-access/homelab-readonly` ServiceAccount.
`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

@ -5957,6 +5957,11 @@ drill_pihole_restore() {
"${REPO_ROOT}/scripts/restore-drill" pihole "${REPO_ROOT}/scripts/restore-drill" pihole
} }
kubeconfig_readonly() {
require_debian_server "kubeconfig-readonly"
"${REPO_ROOT}/scripts/kubeconfig-readonly"
}
case "${1:-}" in case "${1:-}" in
up) up)
up up
@ -5982,6 +5987,9 @@ case "${1:-}" in
access-audit) access-audit)
access_audit access_audit
;; ;;
kubeconfig-readonly)
kubeconfig_readonly
;;
apps) apps)
require_debian_server "apps" require_debian_server "apps"
jeannie_log_start "apps" jeannie_log_start "apps"
@ -6117,7 +6125,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|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 exit 1
;; ;;
esac 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 "$@"