Skip to content

Claude access

Most of the code and configuration in this project is written by Claude (Anthropic's LLM), run via Claude Code inside a dedicated devcontainer. This page describes what that access actually is: scoped and revocable, not blanket admin.

Environment

Claude runs Claude Code inside a devcontainer defined in thomelab-infra/docker/devcontainer/ and deployed via thomelab-infra/kubernetes/applications/devcontainer/. It is a separate instance from Thomas's own devcontainer (devcontainer-claude vs. devcontainer-user), each reachable at its own subdomain (claude-dev.thomelab.net / thomas-dev.thomelab.net), each gated by Keycloak SSO restricted to a devcontainer-users group.

Kubernetes

Each cluster runs a dedicated claude ServiceAccount in a claude-access namespace, deployed via ArgoCD like everything else. It is read-only, and scoped by an explicit allowlist:

  • Cluster-wide, read-only: namespaces, nodes, PersistentVolumes, StorageClasses, VolumeSnapshots, Velero backup/schedule/restore status, node and pod metrics.
  • Namespace-scoped, read-only, only in explicitly listed namespaces: pods and logs, deployments, statefulsets, daemonsets, replicasets, jobs, cronjobs, services, endpoints, configmaps, events, PVCs, ingresses, network policies, Traefik IngressRoutes/middlewares/TCP-UDP routes, ArgoCD Applications, CNPG clusters.
  • Secrets are excluded entirely — not read, not listed.

Which namespaces are visible is a per-cluster, explicit list (RoleBinding resources in thomelab-infra/kubernetes/cluster-tools/claude-access/overlays/<cluster>/), not a default-allow policy. Adding access to a new namespace is itself a change to thomelab-infra, going through the same issue/branch/merge-request flow as everything else.

The kubeconfig itself is built from OpenBao-backed ExternalSecrets and mounted read-only into the devcontainer — there is no locally-stored admin kubeconfig to fall back on. For public, whose host firewall permits only port 443 by default, the kubeconfig's primary path to the API server goes through a Tailscale sidecar in the devcontainer talking to Tailscale's Kubernetes-operator API-server proxy, not a direct connection; a direct-connection context still exists as an explicit, non-default fallback. The devcontainer's Tailscale identity has its own auth key and state, separate from Thomas's devcontainer.

Git and CI

Access to each repository is a separate GitLab Personal Access Token, one per repo, loaded from env.sh, rather than one token with access to everything. A token scoped to thomelab-site cannot touch thomelab-infra.

Every repository's stated convention is the same: no direct commits to main (enforced by branch protection on main). Every change is a GitLab issue, a branch named after the issue, and a merge request. In practice, this means Claude opens the issue, does the work on a branch, opens the merge request, and assigns it — merging is a separate, human action.

Harbor (the container registry) works the same way by omission: Claude has no credentials for harbor.thomelab.net/thomelab-private, the auth-required project.

Encrypted content

git-crypt, ansible-vault, and sops+age are real boundaries here, not just documented ones: this devcontainer has no GPG private key to unlock git-crypt content (verified — gpg --list-secret-keys is empty), ~/.vaultpass is mounted only into Thomas's own devcontainer, and the age private keys (Thomas's personal one, and the dedicated one seeded for chezmoi) are likewise provisioned only into Thomas's devcontainer, never Claude's. Unlike the OpenBao-backed secrets Claude is handed (kubeconfig, GitLab tokens), nobody has provisioned it with these.

Destructive operations

Force-pushes, git reset --hard, --no-verify, and similar are not run without asking first. On a branch, this is a session-level operating instruction rather than a platform-enforced control — unlike the Kubernetes RBAC and per-repo token scoping above, nothing currently prevents it there at the infrastructure level. main is the exception: branch protection (see above) blocks a force-push regardless of what Claude does.