diff --git a/charts/substrate/README.md b/charts/substrate/README.md index 4f04e61153..b9916aa5a9 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://..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 d8a497a4ef..9f04293775 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 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 9dcd7f6473..a71114e7a8 100644 --- a/charts/substrate/values.yaml +++ b/charts/substrate/values.yaml @@ -169,6 +169,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, ...).