diff --git a/kubernetes/apps/hermes/SOUL.md b/kubernetes/apps/hermes/SOUL.md index c89ee407..579e07c6 100644 --- a/kubernetes/apps/hermes/SOUL.md +++ b/kubernetes/apps/hermes/SOUL.md @@ -5,11 +5,13 @@ When you use a pronoun for someone whose pronouns haven't been stated, use they/ # What you can touch - `kubectl`: read-only cluster access (the `view` ClusterRole, no Secrets). You cannot change the cluster. +- `talosctl`: read-only (`os:reader`) on the Talos nodes; `TALOSCONFIG` is set, the endpoint is `talos.default`. Pass nodes with `-n ` taken from `kubectl get nodes -o wide` at the time (e.g. `talosctl -n get members`). +- `bao`: OpenBao at `BAO_ADDR`, read-only on your own `homelab/apps/hermes/*`. Log in per command, never store the token: `export VAULT_TOKEN=$(bao write -field=token auth/kubernetes/login role=hermes jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token)` then e.g. `bao kv list -mount=homelab apps/hermes`. Never print secret values. - Monitoring read APIs: Prometheus `http://kube-prometheus-stack-prometheus.monitoring.svc:9090`, Alertmanager `http://kube-prometheus-stack-alertmanager.monitoring.svc:9093`, Loki `http://loki.monitoring.svc:3100`. - The homelab wiki through the `homelab-wiki` skill (read, and deposit facts). - The web (HTTPS) and LAN hostnames under `renner.uno` through Traefik. -- Gitea (`https://gitea.renner.uno`) as `hermes-bot`: `git` over HTTPS authenticates and sets the commit identity on its own. For the API, send `Authorization: token ` (read it inside the command, never print it). `tea` is installed but not logged in; use the REST API (`/api/v1`). You write only to repos where `hermes-bot` is a collaborator. +- Gitea (`https://gitea.renner.uno`) as `hermes-bot`: `git` over HTTPS authenticates and sets the commit identity on its own. For the API, send `Authorization: token ` (read it inside the command, never print it). `tea` is logged in as `hermes-bot` (login `hermes-bot`, the default). You write only to repos where `hermes-bot` is a collaborator. All state of record lives in git: infrastructure in the `homelab` repo (Kubernetes/Flux, Ansible, OpenTofu, ESPHome), Home Assistant config in the `homeassistant` repo. Changes land as a feature branch plus a PR for the owner's review (see below). diff --git a/kubernetes/apps/hermes/hermes.yaml b/kubernetes/apps/hermes/hermes.yaml index d3cb7e94..8bcfd24d 100644 --- a/kubernetes/apps/hermes/hermes.yaml +++ b/kubernetes/apps/hermes/hermes.yaml @@ -95,6 +95,36 @@ spec: volumeMounts: - name: data mountPath: /opt/data + # tea login hermes-bot, written from the mounted token into an + # emptyDir at the agent's ~/.config/tea. + - name: tea-login + image: gitea.renner.uno/konairius/hermes:build.1792397861 # {"$imagepolicy": "flux-system:hermes"} + command: + - sh + - -ec + - | + umask 077 + { + echo 'logins:' + echo ' - name: hermes-bot' + echo ' url: https://gitea.renner.uno' + echo " token: $(cat /var/run/secrets/gitea/api-token)" + echo ' default: true' + echo ' user: hermes-bot' + } > /tea/config.yml + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + volumeMounts: + - name: tea-config + mountPath: /tea + - name: gitea-token + mountPath: /var/run/secrets/gitea + readOnly: true containers: - name: hermes image: gitea.renner.uno/konairius/hermes:build.1792397861 # {"$imagepolicy": "flux-system:hermes"} @@ -114,6 +144,14 @@ spec: value: "true" - name: API_SERVER_HOST value: 0.0.0.0 + # bao: log in with the pod's ServiceAccount token, role hermes + # (kubernetes/infrastructure/openbao/policies/hermes.hcl). + - name: BAO_ADDR + value: http://openbao.openbao.svc.cluster.local:8200 + # talosctl: os:reader talosconfig from the talos.dev ServiceAccount + # hermes-talos (talos-serviceaccount.yaml). + - name: TALOSCONFIG + value: /var/run/secrets/talos.dev/config # git as hermes-bot: the credential helper reads the mounted token. - name: GIT_CONFIG_COUNT value: "3" @@ -179,6 +217,11 @@ spec: - name: gitea-token mountPath: /var/run/secrets/gitea readOnly: true + - name: tea-config + mountPath: /opt/data/home/.config/tea + - name: talos-config + mountPath: /var/run/secrets/talos.dev + readOnly: true volumes: - name: data persistentVolumeClaim: @@ -201,6 +244,12 @@ spec: - name: gitea-token secret: secretName: hermes-gitea + - name: tea-config + emptyDir: {} + - name: talos-config + secret: + secretName: hermes-talos + optional: true --- apiVersion: v1 kind: Service diff --git a/kubernetes/apps/hermes/kustomization.yaml b/kubernetes/apps/hermes/kustomization.yaml index e53bcdf3..4d848d9e 100644 --- a/kubernetes/apps/hermes/kustomization.yaml +++ b/kubernetes/apps/hermes/kustomization.yaml @@ -5,6 +5,7 @@ resources: - api-key.yaml - wiki-externalsecret.yaml - gitea-externalsecret.yaml + - talos-serviceaccount.yaml - hermes.yaml - ingress.yaml configMapGenerator: diff --git a/kubernetes/apps/hermes/talos-serviceaccount.yaml b/kubernetes/apps/hermes/talos-serviceaccount.yaml new file mode 100644 index 00000000..89245bd9 --- /dev/null +++ b/kubernetes/apps/hermes/talos-serviceaccount.yaml @@ -0,0 +1,12 @@ +# Talos issues an os:reader talosconfig into Secret hermes-talos (key +# `config`, endpoint talos.default:50000). Needs +# machine.features.kubernetesTalosAPIAccess on the control planes +# (kubernetes/talos/patches/cluster.yaml). +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: hermes-talos + namespace: hermes +spec: + roles: + - os:reader diff --git a/kubernetes/infrastructure/network-policies/hermes-ns.yaml b/kubernetes/infrastructure/network-policies/hermes-ns.yaml index 24c47c87..638624c2 100644 --- a/kubernetes/infrastructure/network-policies/hermes-ns.yaml +++ b/kubernetes/infrastructure/network-policies/hermes-ns.yaml @@ -122,6 +122,24 @@ spec: - ports: - port: "8080" protocol: TCP + # OpenBao Kubernetes-auth login and apps/hermes reads; ingress side is + # openbao-ns.yaml. + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": openbao + app.kubernetes.io/name: openbao + toPorts: + - ports: + - port: "8200" + protocol: TCP + # Talos apid (talosctl via Service default/talos, endpoints are the node IPs). + - toEntities: + - host + - remote-node + toPorts: + - ports: + - port: "50000" + protocol: TCP --- # Traefik serves the dashboard (9119) and the OpenAI-compatible API (8642). apiVersion: cilium.io/v2 diff --git a/kubernetes/infrastructure/network-policies/openbao-ns.yaml b/kubernetes/infrastructure/network-policies/openbao-ns.yaml index 42e1d066..bb286179 100644 --- a/kubernetes/infrastructure/network-policies/openbao-ns.yaml +++ b/kubernetes/infrastructure/network-policies/openbao-ns.yaml @@ -41,6 +41,15 @@ spec: - ports: - port: "8200" protocol: TCP + # Hermes Agent logs in via auth/kubernetes/role/hermes and reads apps/hermes. + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": hermes + app.kubernetes.io/name: hermes + toPorts: + - ports: + - port: "8200" + protocol: TCP # (Secret-rotation CronJobs run in THIS namespace since 2026-07-18, so their # OpenBao access is covered by the intra-namespace ingress rule above — no # per-app rule needed.) diff --git a/kubernetes/infrastructure/openbao/policies/hermes.hcl b/kubernetes/infrastructure/openbao/policies/hermes.hcl new file mode 100644 index 00000000..1676a647 --- /dev/null +++ b/kubernetes/infrastructure/openbao/policies/hermes.hcl @@ -0,0 +1,26 @@ +# OpenBao policy: hermes +# +# Hermes Agent (Deployment hermes, namespace hermes) logs in with its pod +# ServiceAccount token and reads its own app secrets. +# +# NOT applied by Flux. Apply ONCE with an admin token: +# +# bao policy write hermes kubernetes/infrastructure/openbao/policies/hermes.hcl +# bao write auth/kubernetes/role/hermes \ +# bound_service_account_names=hermes \ +# bound_service_account_namespaces=hermes \ +# policies=hermes ttl=1h +# +# Do NOT set token_no_default_policy=true (kv-v2 preflight needs the default policy). + +path "homelab/data/apps/hermes/*" { + capabilities = ["read"] +} + +path "homelab/metadata/apps/hermes/*" { + capabilities = ["read", "list"] +} + +path "homelab/metadata/apps/hermes" { + capabilities = ["list"] +} diff --git a/kubernetes/talos/patches/cluster.yaml b/kubernetes/talos/patches/cluster.yaml index e0e5d41e..c9d891b0 100644 --- a/kubernetes/talos/patches/cluster.yaml +++ b/kubernetes/talos/patches/cluster.yaml @@ -38,6 +38,14 @@ machine: hostDNS: enabled: true forwardKubeDNSToHost: true + # talos.dev ServiceAccounts: os:reader talosconfigs for Hermes Agent + # (apps/hermes/talos-serviceaccount.yaml). Control planes only. + kubernetesTalosAPIAccess: + enabled: true + allowedRoles: + - os:reader + allowedKubernetesNamespaces: + - hermes nodeLabels: node.kubernetes.io/exclude-from-external-load-balancers: "" kubelet: