From 218cc0313e00ce1ce488230cc8260f07cbbecef1 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. --- 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 6a7d7e9ff8..049c9feea2 100644 --- a/charts/substrate/README.md +++ b/charts/substrate/README.md @@ -66,6 +66,7 @@ See `values.yaml` for the full set; the important keys: | `atelet.imageCredentialProviderConfigYAML` | `""` | Credential provider config as text, rendered into a ConfigMap, for nodes where that file cannot be authored or must differ from the kubelet's; mutually exclusive with the host path | | `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://idp..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 ee4501e086..7c540cedd8 100644 --- a/charts/substrate/templates/_helpers.tpl +++ b/charts/substrate/templates/_helpers.tpl @@ -254,3 +254,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 d62a62207e..9214cb3880 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 8bc5bc47f2..1e11b6dc13 100644 --- a/charts/substrate/values.yaml +++ b/charts/substrate/values.yaml @@ -166,6 +166,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, ...).