From 7a20f7e156d38fc36e0d366e7f05a292c8cfa345 Mon Sep 17 00:00:00 2001 From: Timo Derstappen Date: Tue, 29 Sep 2026 11:35:54 +0200 Subject: [PATCH] feat(chart): extra CAs for the egress gateway's upstream TLS The egress gateway terminates the actors' TLS and dials the upstream itself with the public roots of its agentgateway image, so an endpoint behind a private CA fails every call with UnknownIssuer, whatever the actor's runtime trusts. The route's backendTLS.root would replace the public roots rather than add to them. atenetEgress.upstreamTrust.caBundle renders a PEM bundle as a ConfigMap, mounts it on the egress agentgateway and sets SSL_CERT_DIR next to SSL_CERT_FILE, the image's public bundle; rustls-native-certs then loads both. With a bundle set, the dynamic HTTPS backend leaves out backendTLS.root, because agentgateway reads the native roots only when root is unset. A checksum annotation rolls the gateway on a change. Empty by default, which leaves the rendered manifests unchanged. (cherry picked from commit 175664c9f7fdeb16b9c941d087cac3a72bf7ba2b) Signed-off-by: Jose Armesto --- charts/substrate/templates/atenet-egress.yaml | 40 ++++++ .../atenet_egress_upstream_trust_test.yaml | 119 ++++++++++++++++++ charts/substrate/values.yaml | 8 ++ 3 files changed, 167 insertions(+) create mode 100644 charts/substrate/tests/atenet_egress_upstream_trust_test.yaml diff --git a/charts/substrate/templates/atenet-egress.yaml b/charts/substrate/templates/atenet-egress.yaml index d62a62207e..c75a29c48a 100644 --- a/charts/substrate/templates/atenet-egress.yaml +++ b/charts/substrate/templates/atenet-egress.yaml @@ -70,8 +70,15 @@ data: - backends: - dynamic: {} policies: + {{- if .Values.atenetEgress.upstreamTrust.caBundle }} + {{- /* No root: the gateway then trusts the native roots, which are + the image's public bundle plus upstreamTrust (SSL_CERT_FILE and + SSL_CERT_DIR on the agentgateway container). */}} + backendTLS: {} + {{- else }} backendTLS: root: /etc/ssl/certs/ca-certificates.crt + {{- end }} policies: substrateEgress: host: {{ include "substrate.fullname" (list "api" .) }}.{{ .Release.Namespace }}.svc:443 @@ -115,6 +122,17 @@ data: tcpRoutes: - backends: - dynamic: {} +{{- with .Values.atenetEgress.upstreamTrust.caBundle }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "substrate.fullname" (list "atenet-egress-upstream-trust" $) }} + namespace: {{ $.Release.Namespace }} +data: + ca.crt: | + {{- . | nindent 4 }} +{{- end }} --- apiVersion: apps/v1 kind: Deployment @@ -132,6 +150,10 @@ spec: metadata: labels: app: atenet-egress + {{- with .Values.atenetEgress.upstreamTrust.caBundle }} + annotations: + checksum/upstream-trust: {{ . | sha256sum }} + {{- end }} spec: serviceAccountName: {{ include "substrate.fullname" (list "atenet-egress" .) }} securityContext: @@ -155,6 +177,14 @@ spec: fieldPath: status.podIP - name: RUST_LOG value: debug + {{- if .Values.atenetEgress.upstreamTrust.caBundle }} + # Both set: the upstream roots are the image's public bundle plus + # upstreamTrust (rustls-native-certs reads the two and nothing else). + - name: SSL_CERT_FILE + value: /etc/ssl/certs/ca-certificates.crt + - name: SSL_CERT_DIR + value: /run/egress-upstream-trust + {{- end }} ports: - name: https containerPort: 8443 @@ -189,7 +219,17 @@ spec: - name: actor-id-ca-certs mountPath: /run/actor-id-ca-certs readOnly: true + {{- if .Values.atenetEgress.upstreamTrust.caBundle }} + - name: upstream-trust + mountPath: /run/egress-upstream-trust + readOnly: true + {{- end }} volumes: + {{- if .Values.atenetEgress.upstreamTrust.caBundle }} + - name: upstream-trust + configMap: + name: {{ include "substrate.fullname" (list "atenet-egress-upstream-trust" .) }} + {{- end }} - name: egress-mitm secret: secretName: egress-mitm-ca-pool diff --git a/charts/substrate/tests/atenet_egress_upstream_trust_test.yaml b/charts/substrate/tests/atenet_egress_upstream_trust_test.yaml new file mode 100644 index 0000000000..fe796ab881 --- /dev/null +++ b/charts/substrate/tests/atenet_egress_upstream_trust_test.yaml @@ -0,0 +1,119 @@ +# 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: atenet egress upstream trust +templates: +- atenet-egress.yaml +tests: +- it: trusts the image's public roots only by default + documentSelector: + path: kind + value: Deployment + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: SSL_CERT_DIR + value: /run/egress-upstream-trust + - notExists: + path: spec.template.metadata.annotations + - lengthEqual: + path: spec.template.spec.volumes + count: 5 + +- it: pins the dynamic HTTPS backend to the image's public bundle by default + documentSelector: + path: metadata.name + value: RELEASE-NAME-atenet-egress-agentgateway-config + asserts: + - matchRegex: + path: data["config.yaml"] + pattern: '- dynamic: \{\}\s+policies:\s+backendTLS:\s+root: /etc/ssl/certs/ca-certificates.crt' + +- it: renders no upstream trust ConfigMap by default + asserts: + - containsDocument: + kind: ConfigMap + apiVersion: v1 + name: RELEASE-NAME-atenet-egress-upstream-trust + not: true + +- it: publishes the bundle as a ConfigMap + set: + atenetEgress.upstreamTrust.caBundle: | + -----BEGIN CERTIFICATE----- + MIIBlab + -----END CERTIFICATE----- + documentSelector: + path: metadata.name + value: RELEASE-NAME-atenet-egress-upstream-trust + asserts: + - equal: + path: data["ca.crt"] + value: | + -----BEGIN CERTIFICATE----- + MIIBlab + -----END CERTIFICATE----- + +- it: adds the bundle to the public roots of the egress agentgateway and rolls on a change + set: + atenetEgress.upstreamTrust.caBundle: | + -----BEGIN CERTIFICATE----- + MIIBlab + -----END CERTIFICATE----- + documentSelector: + path: kind + value: Deployment + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SSL_CERT_FILE + value: /etc/ssl/certs/ca-certificates.crt + - contains: + path: spec.template.spec.containers[0].env + content: + name: SSL_CERT_DIR + value: /run/egress-upstream-trust + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: upstream-trust + mountPath: /run/egress-upstream-trust + readOnly: true + - contains: + path: spec.template.spec.volumes + content: + name: upstream-trust + configMap: + name: RELEASE-NAME-atenet-egress-upstream-trust + - exists: + path: spec.template.metadata.annotations["checksum/upstream-trust"] + +- it: leaves root out of the dynamic HTTPS backend so the gateway loads the native roots + set: + atenetEgress.upstreamTrust.caBundle: | + -----BEGIN CERTIFICATE----- + MIIBlab + -----END CERTIFICATE----- + documentSelector: + path: metadata.name + value: RELEASE-NAME-atenet-egress-agentgateway-config + asserts: + - matchRegex: + path: data["config.yaml"] + pattern: '- dynamic: \{\}\s+policies:\s+backendTLS: \{\}' + - notMatchRegex: + path: data["config.yaml"] + pattern: /etc/ssl/certs/ca-certificates.crt diff --git a/charts/substrate/values.yaml b/charts/substrate/values.yaml index 336fd71cac..023b384ed4 100644 --- a/charts/substrate/values.yaml +++ b/charts/substrate/values.yaml @@ -161,6 +161,14 @@ atenetEgress: limits: cpu: "2" memory: 1Gi + # Extra CAs the egress gateway trusts on the actors' upstream HTTPS, next to + # the public roots of its image: a PEM bundle for model or tool endpoints + # behind a private CA (an in-cluster Gateway, a corporate proxy). Empty + # trusts the public roots only. The egress gateway terminates the actors' TLS + # and dials the upstream itself, so a CA the runtime trusts counts for + # nothing here. A change rolls the gateway: the roots are read at start. + upstreamTrust: + caBundle: "" # Kubernetes Secret provider and AGW HTTP/HTTPS credential injection. # Includes get-only Secret RBAC. HTTPS requires egress-mitm-ca-pool and actor trust.