From bc17755d20201fd869caac5467bcebfba297ff2a Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Thu, 5 Nov 2026 15:25:59 +0100 Subject: [PATCH] feat(helm): bind the credential provider's Secret reader per namespace The k8s-credential-provider-secret-reader ClusterRole is bound with a ClusterRoleBinding, so the provider can read every Secret in the cluster. credentialProvider.rbac.namespaces binds the ClusterRole by RoleBinding in each listed namespace instead. Empty, the default, keeps the ClusterRoleBinding. The ClusterRole is always rendered so other namespaces can bind it. Signed-off-by: QuentinBisson --- charts/substrate/README.md | 1 + .../templates/k8s-credential-provider.yaml | 23 +++++ ...redential_provider_secret_reader_test.yaml | 86 +++++++++++++++++++ charts/substrate/values.yaml | 6 ++ 4 files changed, 116 insertions(+) create mode 100644 charts/substrate/tests/k8s_credential_provider_secret_reader_test.yaml diff --git a/charts/substrate/README.md b/charts/substrate/README.md index 35d7bc96c8..28fd40ccbc 100644 --- a/charts/substrate/README.md +++ b/charts/substrate/README.md @@ -97,6 +97,7 @@ See `values.yaml` for the full set; the important keys: | `atelet.nodeSelector` | `{}` | Node labels atelet pods must match; must cover the nodes of every WorkerPool | | `atelet.imageCredentialProviderBinDir` | `""` | Host path to the kubelet credential provider binaries; both paths are mounted read-only | | `credentialProvider.enabled` | `true` | Deploy the Kubernetes Secret provider and route `k8s.io` credential URIs to it. With no provider, every credential injection fails closed | +| `credentialProvider.rbac.namespaces` | `[]` | Namespaces the provider reads Secrets in. Empty, its Secret reader ClusterRole (`k8s-credential-provider-secret-reader`, prefixed with the release name unless the release is `substrate`) is bound cluster-wide; set, it is bound by RoleBinding in each listed namespace only, and a namespace added later needs its own RoleBinding to it | | `credentialProvider.namespacePolicies` | `[]` | Default-deny atespace-to-namespace grants; the chart includes get-only Secret RBAC for the provider | | `ateApi.actorJWTIssuer` | `https://..svc` | Issuer placed in actor JWTs; set a stable URL reachable by relying parties | | `objectStorePlugin.extraEnv` | `[]` | Object storage credentials/settings shared by both object-store plugins | diff --git a/charts/substrate/templates/k8s-credential-provider.yaml b/charts/substrate/templates/k8s-credential-provider.yaml index b2cd5290bb..89e28d230e 100644 --- a/charts/substrate/templates/k8s-credential-provider.yaml +++ b/charts/substrate/templates/k8s-credential-provider.yaml @@ -34,6 +34,11 @@ rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get"] +{{- /* Without credentialProvider.rbac.namespaces the reader is bound +cluster-wide. With it, the reader is bound by RoleBinding in each listed +namespace only. */}} +{{- $readerNamespaces := dig "rbac" "namespaces" list .Values.credentialProvider | uniq }} +{{- if not $readerNamespaces }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -47,6 +52,24 @@ subjects: - kind: ServiceAccount name: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }} namespace: {{ .Release.Namespace }} +{{- else }} +{{- range $ns := $readerNamespaces }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "substrate.fullname" (list "k8s-credential-provider-secret-reader" $) }} + namespace: {{ $ns }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "substrate.fullname" (list "k8s-credential-provider-secret-reader" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "substrate.fullname" (list "k8s-credential-provider" $) }} + namespace: {{ $.Release.Namespace }} +{{- end }} +{{- end }} --- apiVersion: apps/v1 kind: Deployment diff --git a/charts/substrate/tests/k8s_credential_provider_secret_reader_test.yaml b/charts/substrate/tests/k8s_credential_provider_secret_reader_test.yaml new file mode 100644 index 0000000000..57c64e8e11 --- /dev/null +++ b/charts/substrate/tests/k8s_credential_provider_secret_reader_test.yaml @@ -0,0 +1,86 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +suite: k8s credential provider Secret reader binding +templates: +- k8s-credential-provider.yaml +release: + name: substrate + namespace: ate-system +tests: +- it: binds the reader cluster-wide by default + asserts: + - containsDocument: + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + name: k8s-credential-provider-secret-reader + any: true + - containsDocument: + kind: RoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + not: true + +- it: binds the reader only in the listed namespaces + set: + credentialProvider.rbac.namespaces: [kagent, agents-acme-x1] + asserts: + - containsDocument: + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + not: true + - containsDocument: + kind: ClusterRole + apiVersion: rbac.authorization.k8s.io/v1 + name: k8s-credential-provider-secret-reader + any: true + - containsDocument: + kind: RoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + name: k8s-credential-provider-secret-reader + namespace: kagent + any: true + - containsDocument: + kind: RoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + name: k8s-credential-provider-secret-reader + namespace: agents-acme-x1 + any: true + +- it: the RoleBindings name the reader and the provider's ServiceAccount + set: + credentialProvider.rbac.namespaces: [kagent] + documentSelector: + path: kind + value: RoleBinding + asserts: + - equal: + path: roleRef + value: {apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: k8s-credential-provider-secret-reader} + - equal: + path: subjects + value: [{kind: ServiceAccount, name: k8s-credential-provider, namespace: ate-system}] + +- it: binds the reader cluster-wide with an empty list + set: + credentialProvider.rbac.namespaces: [] + asserts: + - containsDocument: + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + name: k8s-credential-provider-secret-reader + any: true + - containsDocument: + kind: RoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + not: true diff --git a/charts/substrate/values.yaml b/charts/substrate/values.yaml index 5d0f7a4dce..4e488eeddf 100644 --- a/charts/substrate/values.yaml +++ b/charts/substrate/values.yaml @@ -191,6 +191,12 @@ atenetEgress: credentialProvider: # Deploy the Kubernetes Secret provider and route k8s.io credential URIs to it. enabled: true + rbac: + # Namespaces the provider reads Secrets in. Empty (the default), its + # Secret reader ClusterRole is bound cluster-wide. Set, the ClusterRole is + # bound by RoleBinding in each listed namespace only; a namespace added + # later needs its own RoleBinding to it. + namespaces: [] namespacePolicies: [] # - atespace: team-a # allowedNamespaces: [team-a-secrets]