From 165cae9707f074b3635a7d17228b6d4e14f43a7a Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Sun, 18 Oct 2026 14:53:04 +0200 Subject: [PATCH] feat(helm): further credential providers for the egress gateway The egress gateway already selects a credential provider by URI authority, but the chart rendered the bundled k8s.io provider only. The value credentialProvider.additionalProviders adds providers beside it on both the HTTPS and the HTTP listener, each dialed with the gateway's pod identity and verified against the servicedns.podcert.ate.dev trust bundle. An entry needs a lowercase DNS name as uriAuthority and ..svc: as host, cannot replace k8s.io and cannot repeat an authority. The default render is unchanged. Signed-off-by: QuentinBisson --- charts/substrate/README.md | 1 + charts/substrate/templates/_helpers.tpl | 41 ++++++++ charts/substrate/templates/atenet-egress.yaml | 18 +--- ...enet_egress_credential_providers_test.yaml | 96 +++++++++++++++++++ charts/substrate/values.yaml | 12 +++ 5 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 charts/substrate/tests/atenet_egress_credential_providers_test.yaml diff --git a/charts/substrate/README.md b/charts/substrate/README.md index f82e429528..f94036167f 100644 --- a/charts/substrate/README.md +++ b/charts/substrate/README.md @@ -96,6 +96,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.namespacePolicies` | `[]` | Default-deny atespace-to-namespace grants; the chart includes get-only Secret RBAC for the provider | +| `credentialProvider.additionalProviders` | `[]` | Further credential providers for the egress gateway, each `uriAuthority` (a lowercase DNS name) and `host` (`..svc:`); the gateway verifies the provider's `servicedns.podcert.ate.dev` serving certificate and presents its own pod identity, which the provider must require | | `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 | | `objectStorePlugin.control.resources` / `objectStorePlugin.node.resources` | see `values.yaml` | Resources for the API and node object-store sidecars | diff --git a/charts/substrate/templates/_helpers.tpl b/charts/substrate/templates/_helpers.tpl index 0b15b6bf0f..0cb9532bf5 100644 --- a/charts/substrate/templates/_helpers.tpl +++ b/charts/substrate/templates/_helpers.tpl @@ -259,3 +259,44 @@ definition so the fallback cannot drift between pods. {{ toYaml . }} {{- end }} {{- end }} + +{{/* +The egress gateway's credential providers: the bundled k8s.io one and +credentialProvider.additionalProviders, each dialed with the gateway's pod +identity and verified against the servicedns.podcert.ate.dev trust bundle. +*/}} +{{- define "substrate.egressCredentialProviders" -}} +{{- $providers := list (dict "uriAuthority" "k8s.io" "host" (printf "%s.%s.svc:50051" (include "substrate.fullname" (list "k8s-credential-provider" .)) .Release.Namespace)) -}} +{{- range .Values.credentialProvider.additionalProviders -}} +{{- if or (not .uriAuthority) (not .host) -}} +{{- fail "credentialProvider.additionalProviders entries need uriAuthority and host" -}} +{{- end -}} +{{- $authority := toString .uriAuthority -}} +{{- $host := toString .host -}} +{{- if not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$" $authority) -}} +{{- fail (printf "credentialProvider.additionalProviders: uriAuthority %q must be a lowercase DNS name" $authority) -}} +{{- end -}} +{{- if eq $authority "k8s.io" -}} +{{- fail "credentialProvider.additionalProviders cannot replace the bundled k8s.io provider" -}} +{{- end -}} +{{- if not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?\\.svc:[0-9]{1,5}$" $host) -}} +{{- fail (printf "credentialProvider.additionalProviders: host %q must be ..svc:" $host) -}} +{{- end -}} +{{- $providers = append $providers (dict "uriAuthority" $authority "host" $host) -}} +{{- end -}} +{{- $seen := dict -}} +{{- range $providers }} +{{- if hasKey $seen .uriAuthority -}} +{{- fail (printf "credentialProvider.additionalProviders names %s twice" .uriAuthority) -}} +{{- end -}} +{{- $_ := set $seen .uriAuthority true }} +- uriAuthority: {{ .uriAuthority }} + target: + host: {{ .host }} + policies: + backendTLS: + cert: /run/podidentity.podcert.ate.dev/credential-bundle.pem + key: /run/podidentity.podcert.ate.dev/credential-bundle.pem + root: /run/servicedns.podcert.ate.dev/trust-bundle.pem +{{- end }} +{{- end -}} diff --git a/charts/substrate/templates/atenet-egress.yaml b/charts/substrate/templates/atenet-egress.yaml index 52c1c6bc60..8945b7c2ac 100644 --- a/charts/substrate/templates/atenet-egress.yaml +++ b/charts/substrate/templates/atenet-egress.yaml @@ -81,14 +81,7 @@ data: key: /run/podidentity.podcert.ate.dev/credential-bundle.pem root: /run/servicedns.podcert.ate.dev/trust-bundle.pem credentialProviders: - - uriAuthority: k8s.io - target: - host: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}.{{ .Release.Namespace }}.svc:50051 - policies: - backendTLS: - cert: /run/podidentity.podcert.ate.dev/credential-bundle.pem - key: /run/podidentity.podcert.ate.dev/credential-bundle.pem - root: /run/servicedns.podcert.ate.dev/trust-bundle.pem + {{- include "substrate.egressCredentialProviders" . | trim | nindent 14 }} - protocol: HTTP routes: - backends: @@ -103,14 +96,7 @@ data: key: /run/podidentity.podcert.ate.dev/credential-bundle.pem root: /run/servicedns.podcert.ate.dev/trust-bundle.pem credentialProviders: - - uriAuthority: k8s.io - target: - host: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}.{{ .Release.Namespace }}.svc:50051 - policies: - backendTLS: - cert: /run/podidentity.podcert.ate.dev/credential-bundle.pem - key: /run/podidentity.podcert.ate.dev/credential-bundle.pem - root: /run/servicedns.podcert.ate.dev/trust-bundle.pem + {{- include "substrate.egressCredentialProviders" . | trim | nindent 14 }} - protocol: TLS tcpRoutes: - backends: diff --git a/charts/substrate/tests/atenet_egress_credential_providers_test.yaml b/charts/substrate/tests/atenet_egress_credential_providers_test.yaml new file mode 100644 index 0000000000..71a5ddc7f1 --- /dev/null +++ b/charts/substrate/tests/atenet_egress_credential_providers_test.yaml @@ -0,0 +1,96 @@ +# Copyright 2026 The Agent Substrate Authors +# +# 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: atenet egress credential providers +templates: +- atenet-egress.yaml +tests: +- it: resolves only the bundled k8s.io provider by default + documentSelector: + path: kind + value: ConfigMap + asserts: + - matchRegex: + path: data["config.yaml"] + pattern: '(?s)credentialProviders:\n\s+- uriAuthority: k8s\.io\n\s+target:\n\s+host: RELEASE-NAME-k8s-credential-provider\.NAMESPACE\.svc:50051' + - notMatchRegex: + path: data["config.yaml"] + pattern: '(?s)(?:uriAuthority:.*){3}' + +- it: adds a provider to both the HTTPS and the HTTP egress listener + set: + credentialProvider.additionalProviders: + - uriAuthority: kagent.dev + host: kagent-credentials.kagent.svc:50051 + documentSelector: + path: kind + value: ConfigMap + asserts: + - matchRegex: + path: data["config.yaml"] + pattern: '(?s)protocol: HTTPS.*- uriAuthority: kagent\.dev\n\s+target:\n\s+host: kagent-credentials\.kagent\.svc:50051\n\s+policies:\n\s+backendTLS:\n\s+cert: /run/podidentity\.podcert\.ate\.dev/credential-bundle\.pem\n\s+key: /run/podidentity\.podcert\.ate\.dev/credential-bundle\.pem\n\s+root: /run/servicedns\.podcert\.ate\.dev/trust-bundle\.pem.*protocol: HTTP\n.*- uriAuthority: kagent\.dev' + +- it: refuses an entry without a host + set: + credentialProvider.additionalProviders: + - uriAuthority: kagent.dev + asserts: + - failedTemplate: + errorMessage: credentialProvider.additionalProviders entries need uriAuthority and host + +- it: refuses to replace the bundled provider + set: + credentialProvider.additionalProviders: + - uriAuthority: k8s.io + host: other.example.svc:50051 + asserts: + - failedTemplate: + errorMessage: credentialProvider.additionalProviders cannot replace the bundled k8s.io provider + +- it: refuses the same authority twice + set: + credentialProvider.additionalProviders: + - uriAuthority: kagent.dev + host: a.kagent.svc:50051 + - uriAuthority: kagent.dev + host: b.kagent.svc:50051 + asserts: + - failedTemplate: + errorMessage: credentialProvider.additionalProviders names kagent.dev twice + +- it: refuses an entry without a uriAuthority + set: + credentialProvider.additionalProviders: + - host: kagent-credentials.kagent.svc:50051 + asserts: + - failedTemplate: + errorMessage: credentialProvider.additionalProviders entries need uriAuthority and host + +- it: refuses a uriAuthority that is not a lowercase DNS name + set: + credentialProvider.additionalProviders: + - uriAuthority: "Kagent.dev\n target: {}" + host: kagent-credentials.kagent.svc:50051 + asserts: + - failedTemplate: + errorMessage: "credentialProvider.additionalProviders: uriAuthority \"Kagent.dev\\n target: {}\" must be a lowercase DNS name" + +- it: refuses a host the servicedns certificate cannot name + set: + credentialProvider.additionalProviders: + - uriAuthority: kagent.dev + host: kagent-credentials.kagent.svc.cluster.local:50051 + asserts: + - failedTemplate: + errorMessage: "credentialProvider.additionalProviders: host \"kagent-credentials.kagent.svc.cluster.local:50051\" must be ..svc:" diff --git a/charts/substrate/values.yaml b/charts/substrate/values.yaml index bdcb57452d..e0f63e811c 100644 --- a/charts/substrate/values.yaml +++ b/charts/substrate/values.yaml @@ -191,6 +191,18 @@ credentialProvider: limits: cpu: 500m memory: 256Mi + # Further providers the egress gateway resolves ate-secret:// URIs through, + # by URI authority (a lowercase DNS name), beside the bundled k8s.io + # one. host is ..svc:: the gateway verifies the + # provider's servicedns.podcert.ate.dev serving certificate, which names its + # Service in that form, and the Service must select the provider's pod. The + # gateway presents its pod identity certificate + # (spiffe://cluster.local/ns//sa/-atenet-egress); + # the provider must require that client certificate, chained to the + # podidentity.podcert.ate.dev trust bundle, and refuse any other caller. + additionalProviders: [] + # - uriAuthority: kagent.dev + # host: kagent-credentials.kagent.svc:50051 # Name of a ConfigMap in the release namespace that supplies per-environment # overrides for ate-api-server (ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING, ...).