Files
devops-infra-argo-config-gcp/extra-manifests/toolshed-deployer-rbac.yaml
T
Mukul SharmaandClaude Opus 5 4a5f4d94ab Grant deployer Ingress rights in its own namespace, for custom domains
Overdue: this belongs with toolshed's custom-domains feature, which has
already merged and shipped. Without it, deployer gets "forbidden" the
moment anyone actually adds a custom domain — the exact failure mode
internal/deploy/kubernetes.go's own package doc warns about, since its
resource list and this file are unsynchronised copies in two repositories.

Scoped as a namespaced Role/RoleBinding rather than widening the
ClusterRole above it. A custom domain's Ingress and TLS secret live in the
gateway's own namespace and never in an app's, so this only ever targets
one fixed namespace — unlike the per-app namespaces the ClusterRole
necessarily spans. Secrets are get/list/watch/delete only: cert-manager
writes them, deployer just reads readiness and cleans up on removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wajog7nELA3i8JWTjxYGHF
2026-09-09 12:35:35 +05:30

132 lines
5.5 KiB
YAML

# Cluster write access for toolshed's deployer, and only deployer.
#
# Lives here rather than in toolshed's own Helm chart because these are
# cluster-scoped, and toolshed's Application runs in the `webapp` project,
# whose clusterResourceWhitelist deliberately allows only Namespace. Widening
# that project to permit ClusterRole and ClusterRoleBinding would let *any*
# Application in it — every demo app — create cluster-wide RBAC, which is a
# privilege-escalation surface in the project that holds ordinary apps. The
# narrow fix is to put the two cluster-scoped objects where platform-level
# cluster resources already live.
#
# The ServiceAccount they bind to is namespaced, so it stays in toolshed's
# chart alongside the Deployment that uses it. If that ever moves, this
# binding's subject has to move with it.
#
# Scoped to the resource kinds toolshed creates for an app. Not cluster-admin
# and not a wildcard: this is the one credential in the system whose
# compromise means the cluster, so what it can do should fit on one screen.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: toolshed-deployer
labels:
app.kubernetes.io/part-of: toolshed
rules:
# Namespaces are cluster-scoped, and one is created per app.
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch", "create", "delete"]
# Everything an app needs inside its own namespace.
- apiGroups: [""]
resources: ["services", "resourcequotas"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# An app's configuration, delivered as a Secret so values never appear in
# the pod spec.
#
# Deliberately without list or watch. Kubernetes RBAC cannot scope a
# ClusterRole to a namespace pattern, so this necessarily covers every
# namespace — but without list, deployer cannot enumerate the cluster's
# secrets, only address ones by a name it already knows. That narrows the
# blast radius without removing it: get on a known name still reaches any
# secret in the cluster.
#
# The proper fix, when this has tenants who are not the operator, is a
# RoleBinding created per app namespace instead of one ClusterRole. That
# needs deployer to hold permission to create RoleBindings, which is its
# own escalation path and wants thinking about rather than adding here.
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update", "patch", "delete"]
# Read-only. Pods are listed to report why a rollout failed, never changed,
# and their output is read so an app's own logs can be shown in the
# dashboard without anyone reaching for kubectl.
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# The policy that stops one app reaching another.
- apiGroups: ["networking.k8s.io"]
resources: ["networkpolicies"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Created only for an app with autoscaling enabled (max replicas set
# above min); removed again if it's turned back off. See toolshed's own
# internal/deploy.Client.ensureAutoscaler. Added alongside that feature —
# keep this file and toolshed's deploy/helm/toolshed/templates/rbac.yaml
# in sync, per internal/deploy/kubernetes.go's own package doc warning
# that the two are unsynchronized copies in two repositories.
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: toolshed-deployer
labels:
app.kubernetes.io/part-of: toolshed
subjects:
- kind: ServiceAccount
name: toolshed-deployer
namespace: toolshed
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: toolshed-deployer
---
# A custom domain's Ingress and TLS secret live in the gateway's own
# namespace (toolshed, same as above) — never an app's namespace. Scoped
# with a namespaced Role/RoleBinding rather than widening the ClusterRole
# above: Ingress management here only ever targets this one fixed
# namespace, unlike the per-app namespaces the ClusterRole necessarily
# spans. Added alongside toolshed's custom-domains feature — see
# internal/deploy.Client.EnsureDomainIngress/DomainCertReady/
# RemoveDomainIngress and this file's own header comment about keeping it
# and deploy/helm/toolshed/templates/rbac.yaml in sync.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: toolshed-deployer-ingress
namespace: toolshed
labels:
app.kubernetes.io/part-of: toolshed
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Reads the TLS secret cert-manager's ingress-shim populates for a domain
# Ingress, and deletes it (and the Ingress above) when a domain is
# removed. Never create/update — cert-manager, not deployer, writes this
# secret.
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: toolshed-deployer-ingress
namespace: toolshed
labels:
app.kubernetes.io/part-of: toolshed
subjects:
- kind: ServiceAccount
name: toolshed-deployer
namespace: toolshed
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: toolshed-deployer-ingress