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]