From 6f03ccd1aad80e0161d0cdc8a92dfc6369413415 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 2 Jul 2026 11:08:28 +0000 Subject: [PATCH] excalidraw: grant emo-browser SA port-forward for drawing uploads Viktor asked to fix emo's permission so his Claude can upload to the Excalidraw service. emo's recent sessions show the documented upload recipe (kubectl port-forward svc/draw + X-Authentik-Username header, from his ~/.claude/CLAUDE.md) failing with: pods/portforward forbidden for system:serviceaccount:chrome-service:emo-browser in namespace excalidraw because his default kubeconfig is the read-only emo-browser SA (its port-forward grant covers only chrome-service) and his old admin kubeconfig at /home/emo/code/config expired and was removed. Add a namespace-scoped Role (pods/portforward create) + RoleBinding for that SA in the excalidraw namespace, mirroring the 2026-06-28 chrome-service grant. Trade-off (any-user drawings via the trusted username header) documented in the file and accepted. Also record the grant in docs/architecture/chrome-service.md. Co-Authored-By: Claude Fable 5 --- docs/architecture/chrome-service.md | 6 ++++ stacks/excalidraw/rbac.tf | 49 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 stacks/excalidraw/rbac.tf diff --git a/docs/architecture/chrome-service.md b/docs/architecture/chrome-service.md index 118c0895..f77518b4 100644 --- a/docs/architecture/chrome-service.md +++ b/docs/architecture/chrome-service.md @@ -329,6 +329,12 @@ Two independent grants make up "browser access" for a user: the provisioner. To revoke: remove from `CHROME_ALLOWED` and delete the SA (rotate a token by deleting its `-browser-token` Secret). +Because the SA is the user's DEFAULT kubectl credential, other per-namespace +port-forward grants hang off the same identity: `stacks/excalidraw/rbac.tf` +grants `emo-browser` `pods/portforward` in `excalidraw` (2026-07-02) so emo's +agent can upload drawings via the port-forward + `X-Authentik-Username` recipe +in his `~/.claude/CLAUDE.md`. Revoking the SA revokes those too. + ## Limits + risks - **Anti-bot vs stealth arms race** — when an upstream beats us (DRM diff --git a/stacks/excalidraw/rbac.tf b/stacks/excalidraw/rbac.tf new file mode 100644 index 00000000..a40898fa --- /dev/null +++ b/stacks/excalidraw/rbac.tf @@ -0,0 +1,49 @@ +# emo's Claude → Excalidraw upload RBAC. +# +# emo's agent uploads drawings with `kubectl -n excalidraw port-forward svc/draw` +# + `PUT /api/drawings/` carrying the X-Authentik-Username header (the +# documented recipe in emo's ~/.claude/CLAUDE.md — the app sits behind Authentik +# forward-auth, so direct curl gets redirected). His hands-off credential is the +# chrome-service/emo-browser ServiceAccount kubeconfig (stacks/chrome-service/rbac.tf); +# its cluster-wide grant (oidc-power-user-readonly) is read-only, so pods/portforward +# must be granted per namespace. This is the excalidraw-namespace grant +# (Viktor's call, 2026-07-02; same pattern as the chrome-service one). +# +# TRADE-OFF (accepted): port-forward into this namespace bypasses the Authentik +# ingress and the drawings API trusts the X-Authentik-Username header, so the SA +# can read/write ANY user's drawings, not only emo's. The namespace runs nothing +# but the drawings app, and the same class of trade-off was already accepted for +# the shared browser (CDP reach into Viktor's sessions). + +resource "kubernetes_role" "portforward" { + metadata { + name = "excalidraw-portforward" + namespace = kubernetes_namespace.excalidraw.metadata[0].name + } + rule { + api_groups = [""] + resources = ["pods/portforward"] + verbs = ["create"] + } +} + +resource "kubernetes_role_binding" "emo_browser_portforward" { + metadata { + name = "emo-browser-portforward" + namespace = kubernetes_namespace.excalidraw.metadata[0].name + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.portforward.metadata[0].name + } + subject { + kind = "ServiceAccount" + # Defined in stacks/chrome-service/rbac.tf — referenced by name across + # stacks, same as that file references the oidc-power-user-readonly + # ClusterRole. get/list on pods+services (needed to resolve svc/draw) comes + # from the SA's cluster-read binding there. + name = "emo-browser" + namespace = "chrome-service" + } +}