chrome-service: grant emo shared browser access (noVNC + homelab browser CLI)
All checks were successful
ci/woodpecker/push/default Pipeline was successful

Viktor asked to give emo access to the cluster's headed Chrome so he can fill
in forms and get past anti-bot / captcha pages. emo was deliberately locked
out of chrome-service (noVNC Authentik allowlist was Viktor-only + his
power-user RBAC has no pods/portforward). Viktor's explicit decision: SHARE
his existing browser rather than stand up an isolated per-user instance,
accepting that emo can therefore reach Viktor's warmed logged-in sessions
(CDP has no per-context auth, so the single shared persistent profile is
reachable by anyone who can drive the browser). emo's CLI use is hands-off
(his agent can run it unattended).

- authentik: add emo (emil.barzin / emil.barzin@gmail.com) to CHROME_ALLOWED
  so the admin-services-restriction policy admits him to chrome.viktorbarzin.me
  (noVNC). Reverses the prior Viktor-only lock; comment updated to record why.
- chrome-service/rbac.tf (new): emo-browser ServiceAccount + long-lived token
  (dashboard-sa.tf pattern), a chrome-service-portforward Role granting
  pods/portforward, and a cluster read-only binding (oidc-power-user-readonly)
  so the SA can resolve the Service and emo's normal read access doesn't regress.
- t3-provision-users.sh: install_browser_kubeconfig installs a dual-context
  kubeconfig for any user with a <user>-browser SA — SA token as the default
  context (non-interactive, works headless), personal OIDC retained as the
  oidc@homelab named context. emo's OIDC-only kubeconfig can't authenticate the
  headless agent session that homelab browser needs.
- docs/architecture/chrome-service.md: document the shared-browser multi-user
  access model, the session-exposure trade-off, and how to grant/revoke a user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-28 15:20:07 +00:00
parent 50077b43d4
commit 2e50c1235c
4 changed files with 214 additions and 8 deletions

View file

@ -0,0 +1,95 @@
# emo's hands-off "homelab browser" credential + chrome-service port-forward RBAC.
#
# Access decision (2026-06-28, Viktor's explicit call): emo SHARES Viktor's single
# chrome-service browser rather than getting an isolated instance. The noVNC half of
# that grant is the Authentik allowlist in
# stacks/authentik/admin-services-restriction.tf (CHROME_ALLOWED); THIS file is the
# CLI half it lets emo's `homelab browser` reach the headed Chrome over CDP.
#
# `homelab browser` shells out to `kubectl port-forward -n chrome-service svc/chrome-service`
# (cli/browser.go). emo's normal kubeconfig is interactive-OIDC-only (kubelogin) and
# can't authenticate a headless agent session, and his power-user tier has no
# pods/portforward. So we mint a dedicated ServiceAccount with a long-lived token
# (the dashboard-sa.tf pattern) that the devvm provisioner installs as emo's DEFAULT
# kubeconfig context (scripts/t3-provision-users.sh install_browser_kubeconfig); his
# personal OIDC login stays available as the `oidc@homelab` named context.
#
# TRADE-OFF (accepted): CDP access == full control of the shared browser, including
# the persistent profile (browser.contexts[0]) where Viktor's warmed logins live.
# CDP has no per-context auth, so this SA can reach Viktor's sessions. That is inherent
# to sharing one browser (the isolated per-user instance was declined).
# See docs/architecture/chrome-service.md "Multi-user access".
resource "kubernetes_service_account" "emo_browser" {
metadata {
name = "emo-browser"
namespace = kubernetes_namespace.chrome_service.metadata[0].name
}
}
# Long-lived (non-expiring) token for the SA the devvm provisioner reads this and
# writes it into emo's kubeconfig. Same pattern as stacks/rbac/.../dashboard-sa.tf.
resource "kubernetes_secret" "emo_browser_token" {
metadata {
name = "emo-browser-token"
namespace = kubernetes_namespace.chrome_service.metadata[0].name
annotations = {
"kubernetes.io/service-account.name" = kubernetes_service_account.emo_browser.metadata[0].name
}
}
type = "kubernetes.io/service-account-token"
wait_for_service_account_token = true
}
# The ONLY verb emo's SA lacks for `kubectl port-forward svc/chrome-service`: the
# port-forward subresource. (get/list of pods + services + endpoints comes from the
# cluster-read binding below.) Namespace-scoped to chrome-service.
resource "kubernetes_role" "browser_portforward" {
metadata {
name = "chrome-service-portforward"
namespace = kubernetes_namespace.chrome_service.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.chrome_service.metadata[0].name
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "Role"
name = kubernetes_role.browser_portforward.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.emo_browser.metadata[0].name
namespace = kubernetes_namespace.chrome_service.metadata[0].name
}
}
# Cluster-wide read-only (NO secrets), mirroring emo's power-user OIDC access, bound
# to the SA. Needed because the SA becomes emo's DEFAULT kubectl context, so without
# this his everyday `kubectl get ...` would regress AND port-forward itself needs
# get/list on services + pods + endpoints (all covered by oidc-power-user-readonly).
# That ClusterRole is defined in stacks/rbac (modules/rbac/main.tf); referenced by name.
resource "kubernetes_cluster_role_binding" "emo_browser_readonly" {
metadata {
name = "emo-browser-readonly"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = "oidc-power-user-readonly"
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.emo_browser.metadata[0].name
namespace = kubernetes_namespace.chrome_service.metadata[0].name
}
}