From 461f9d5578d1d370a503eb4389c5da240907fdba Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Mon, 19 Oct 2026 09:49:20 +0200 Subject: [PATCH] k8s/agent-access: a namespace-limited service account and kubeconfig for a coding agent Read-only by default, optional deploy role; no Secrets, exec, port-forward or RBAC. Tokens from kubectl create token, time-limited. Not applied. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki --- SPECS.md | 5 +++ k8s/README.md | 6 +++ k8s/agent-access/README.md | 59 ++++++++++++++++++++++++++++ k8s/agent-access/kustomization.yaml | 8 ++++ k8s/agent-access/make-kubeconfig.sh | 49 +++++++++++++++++++++++ k8s/agent-access/role-deploy.yaml | 59 ++++++++++++++++++++++++++++ k8s/agent-access/role-read.yaml | 49 +++++++++++++++++++++++ k8s/agent-access/serviceaccount.yaml | 11 ++++++ 8 files changed, 246 insertions(+) create mode 100644 k8s/agent-access/README.md create mode 100644 k8s/agent-access/kustomization.yaml create mode 100755 k8s/agent-access/make-kubeconfig.sh create mode 100644 k8s/agent-access/role-deploy.yaml create mode 100644 k8s/agent-access/role-read.yaml create mode 100644 k8s/agent-access/serviceaccount.yaml diff --git a/SPECS.md b/SPECS.md index 96a0bea..1424d39 100644 --- a/SPECS.md +++ b/SPECS.md @@ -118,6 +118,11 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). +- 2026-10-19 — `k8s/agent-access/`: a service account `mg4-agent` for a coding agent, limited to + the `mg4` namespace. Read-only role by default; an optional deploy role. Neither may touch + Secrets, exec, port-forward or RBAC; the deploy role cannot delete PVCs. `make-kubeconfig.sh` + writes a kubeconfig with a `kubectl create token` token that expires (default 24 h). Validated + with kubeconform and shellcheck, the script against a stub kubectl; not applied. - 2026-10-19 — App 2.19.0: EVHardware updated from malys/EVHardware `b595ccb` (2026-09-17, the base Fatih copied) to `e4e1158` (2026-10-18, after 1.18.0), 42 commits, merged three ways so Fatih's changes stay (listed in `LICENSE.md`). Upstream brings among others: trip, charge and eco diff --git a/k8s/README.md b/k8s/README.md index cefdaa7..3b86e5a 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -54,6 +54,12 @@ the forge that can only **read** the `nils/mg4` repository, put it there, apply restart the server. Without it, updates are off (the car's updater gets a 404). `FORGE_URL` and `FORGE_REPO` are set in `server.yaml`. +## Agent access + +[`agent-access/`](agent-access/README.md) creates a service account limited to the `mg4` +namespace (read-only by default, no Secrets, no exec) and a script that writes a kubeconfig with a +time-limited token for it. Applied separately, with `kubectl apply -k k8s/agent-access/`. + ## Notes - InfluxDB's setup mode runs only on an empty volume. Changing `admin-token` in the Secret later diff --git a/k8s/agent-access/README.md b/k8s/agent-access/README.md new file mode 100644 index 0000000..84cd0aa --- /dev/null +++ b/k8s/agent-access/README.md @@ -0,0 +1,59 @@ +# Agent access to the mg4 namespace + +A kubeconfig for a coding agent that reaches **only the `mg4` namespace**, with a token that +expires on its own. You create and apply everything here yourself; the agent never does. + +| File | What | +|---|---| +| `serviceaccount.yaml` | the `mg4-agent` service account in `mg4`, never mounted into a pod | +| `role-read.yaml` | read-only: workloads, pods and their logs, services, ConfigMaps, PVCs, events, network policies, IngressRoutes | +| `role-deploy.yaml` | **optional**: apply this repository's manifests and restart workloads | +| `make-kubeconfig.sh` | writes the kubeconfig with a time-limited token | + +Neither role allows **Secrets** (any verb), `pods/exec`, `pods/attach`, `pods/portforward`, RBAC +objects, the Namespace itself, or anything in another namespace or at cluster scope. The deploy +role also cannot delete PersistentVolumeClaims, so InfluxDB's data volume stays safe. + +## 1. Create the service account and its role + +With your own kubeconfig, after `k8s/` itself is applied (the namespace must exist): + +```sh +kubectl apply -k k8s/agent-access/ +``` + +That is read-only. To let the agent also apply changes, uncomment `role-deploy.yaml` in +`kustomization.yaml` first. Be aware of what that means: whoever may change a Deployment may run +any image in the namespace, and such a pod could mount mg4's Secrets and print them. Read-only is +the limit that holds even against a misbehaving agent; the deploy role is a convenience. + +## 2. Check what it may do + +```sh +kubectl auth can-i --list -n mg4 --as=system:serviceaccount:mg4:mg4-agent +kubectl auth can-i get secrets -n mg4 --as=system:serviceaccount:mg4:mg4-agent # no +kubectl auth can-i create pods/exec -n mg4 --as=system:serviceaccount:mg4:mg4-agent # no +kubectl auth can-i list pods -n kube-system --as=system:serviceaccount:mg4:mg4-agent # no +``` + +## 3. Make the kubeconfig + +```sh +k8s/agent-access/make-kubeconfig.sh 24h +``` + +It takes the API server address and CA from your current context, asks the cluster for a token +for `mg4-agent` (`kubectl create token`, valid for the duration you give; the API server may cap +it), and writes `~/.config/nilsmg4/kubeconfig-mg4-agent` with mode 600. The token is never +printed. The file is outside the repository, so it cannot be committed. + +Then tell the agent it may use it, e.g. *"you may use ~/.config/nilsmg4/kubeconfig-mg4-agent"*. +It runs `kubectl` with `KUBECONFIG` pointing at that file, so your own kubeconfig is never used. + +## Ending access + +- **Wait:** the token expires after its duration. Run the script again for a new one. +- **Now:** `kubectl delete serviceaccount mg4-agent -n mg4` invalidates every token made for + it at once. Remove the rest with `kubectl delete -k k8s/agent-access/`. +- **Less:** take `role-deploy.yaml` out of the kustomization and + `kubectl delete rolebinding,role mg4-agent-deploy -n mg4`. diff --git a/k8s/agent-access/kustomization.yaml b/k8s/agent-access/kustomization.yaml new file mode 100644 index 0000000..167c608 --- /dev/null +++ b/k8s/agent-access/kustomization.yaml @@ -0,0 +1,8 @@ +# Agent access to the mg4 namespace, applied separately from k8s/: `kubectl apply -k k8s/agent-access/`. +# Read-only by default; add role-deploy.yaml below to let the agent apply changes too. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - serviceaccount.yaml + - role-read.yaml + # - role-deploy.yaml diff --git a/k8s/agent-access/make-kubeconfig.sh b/k8s/agent-access/make-kubeconfig.sh new file mode 100755 index 0000000..5f8060e --- /dev/null +++ b/k8s/agent-access/make-kubeconfig.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Writes a kubeconfig for the mg4-agent service account, valid for a limited time. +# +# k8s/agent-access/make-kubeconfig.sh [duration] [output] +# +# Run it yourself, with your own (admin) kubeconfig active. duration defaults to 24h; the API +# server may cap it (--service-account-max-token-expiration). The token is written to the file +# only, never printed. +set -euo pipefail + +duration="${1:-24h}" +out="${2:-$HOME/.config/nilsmg4/kubeconfig-mg4-agent}" + +cluster="$(kubectl config view --minify -o jsonpath='{.clusters[0].name}')" +server="$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')" +ca="$(kubectl config view --minify --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')" +if [ -z "$ca" ]; then + ca_file="$(kubectl config view --minify --raw -o jsonpath='{.clusters[0].cluster.certificate-authority}')" + [ -n "$ca_file" ] || { echo "no cluster CA in the current kubeconfig" >&2; exit 1; } + ca="$(base64 -w0 < "$ca_file")" +fi + +token="$(kubectl create token mg4-agent --namespace mg4 --duration "$duration")" + +mkdir -p "$(dirname "$out")" +umask 077 +cat > "$out" <