Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/helm-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
env:
VERSION: helm-e2e
E2E_ATENET_DATAPLANE: agentgateway
E2E_CREDENTIAL_PROVIDER: "1"
E2E_EGRESS_MITM: "1"
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
Expand Down Expand Up @@ -61,8 +63,8 @@ jobs:
# the chart composes {registry}/{repository}/{component}, so the local
# registry serves each image where the default repository expects it --
# the same path-preserving rule a production mirror follows.
for component in ateapi atecontroller atelet podcertcontroller atenet; do
KO_DOCKER_REPO="localhost:5001/kagent-dev/substrate/${component}" \
for component in ateapi atecontroller atelet podcertcontroller atenet credential-provider/kubernetes-secrets; do
KO_DOCKER_REPO="localhost:5001/kagent-dev/substrate/${component##*/}" \
./hack/run-tool.sh ko build --bare --tags helm-e2e \
--platform linux/amd64 "./cmd/${component}"
done
Expand All @@ -72,6 +74,7 @@ jobs:
helm upgrade --install substrate charts/substrate \
--namespace ate-system \
--create-namespace \
-f internal/e2e/suites/credentials/values.yaml \
--set image.registry=localhost:5001 \
--set image.tag=helm-e2e \
--set 'atelet.extraArgs[0]=--localhost-registry-replacement=kind-registry:5000' \
Expand All @@ -84,6 +87,7 @@ jobs:
hack/install-ate-kind.sh --create-actor-id-ca-pool-secret
hack/install-ate-kind.sh --create-actor-id-ca-certs-secret
hack/install-ate-kind.sh --create-api-authentication-config
hack/install-ate-kind.sh --create-egress-mitm-ca-pool-secret
- name: Wait for Helm install
run: |
helm upgrade substrate charts/substrate \
Expand All @@ -104,7 +108,7 @@ jobs:
- name: Deploy gVisor counter demo
run: hack/install-ate-kind.sh --deploy-demo-counter
- name: Deploy egress demo
run: hack/install-ate-kind.sh --deploy-demo-egress
run: hack/install-ate-kind.sh --deploy-demo-egress-mitm
- name: Run E2E tests (gVisor)
run: hack/run-e2e-kind.sh -v -args --no-color
- name: Run E2E tests (micro-VM)
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ CONTROL_PLANE_IMAGES := ./cmd/ateapi \
./cmd/atecontroller \
./cmd/atelet \
./cmd/atenet \
./cmd/credential-provider/kubernetes-secrets \
./cmd/podcertcontroller
WORKER_IMAGES := ./cmd/ateom-gvisor \
./cmd/ateom-microvm
Expand Down
6 changes: 6 additions & 0 deletions charts/substrate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ By default, component images are pulled from `ghcr.io/kagent-dev/substrate`
using the chart `appVersion` as the tag. Override `image.registry` and
`image.tag` to install from a different image repository or tag.

The chart installs the Kubernetes credential provider and enables HTTPS egress
interception. Create the `egress-mitm-ca-pool` Secret and configure actor trust
as described in the [credential provider setup](../../docs/kubernetes-credential-provider.md).
Namespace grants default to an empty list, denying credential access.

## Render manifests without applying

```bash
Expand All @@ -42,6 +47,7 @@ See `values.yaml` for the full set; the important keys:
| `rustfs.enabled` | `true` | Deploy an in-cluster S3-compatible RustFS bucket for snapshots |
| `atelet.storageBackend` | `s3` | Default snapshot backend, wired to RustFS when `rustfs.enabled=true` |
| `atelet.gcpAuthForImagePulls` | `false` | Enable only when using GCP registry auth |
| `credentialProvider.namespacePolicies` | `[]` | Default-deny atespace-to-namespace grants; the chart includes get-only Secret RBAC for the provider |
| `ateApi.extraArgs` | `[]` | Additional command-line arguments appended to the ateapi defaults |
| `otel.endpoint` | `""` | Set to an OTLP endpoint to export traces, metrics and the router access log |
| `otel.traces.enabled` | `true` | Set to `false` to export no traces from the router; the Go components do not honor this yet |
Expand Down
51 changes: 46 additions & 5 deletions charts/substrate/templates/atenet-egress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,33 @@ data:
- mode: internal
protocol: AUTO
listeners:
- protocol: TLS
hostname: "*"
tcpRoutes:
- protocol: HTTPS
tls:
mode: dynamicCa
cert: /run/egress-mitm/tls.crt
key: /run/egress-mitm/tls.key
routes:
- backends:
- dynamic:
target: source.connectHeaders["host"]
- dynamic: {}
policies:
backendTLS: {}
policies:
substrateEgress:
host: {{ include "substrate.fullname" (list "api" .) }}.{{ .Release.Namespace }}.svc:443
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
credentialProviders:
- uriAuthority: kubernetes.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
- protocol: HTTP
routes:
- backends:
Expand All @@ -75,6 +96,15 @@ data:
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
credentialProviders:
- uriAuthority: kubernetes.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
- protocol: TCP
tcpRoutes:
- backends:
Expand Down Expand Up @@ -133,6 +163,9 @@ spec:
port: readiness
periodSeconds: 1
volumeMounts:
- name: egress-mitm
mountPath: /run/egress-mitm
readOnly: true
- name: config
mountPath: /etc/agentgateway
readOnly: true
Expand Down Expand Up @@ -194,6 +227,14 @@ spec:
- name: drain-signal
mountPath: /var/run/atenet
volumes:
- name: egress-mitm
secret:
secretName: egress-mitm-ca-pool
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
- name: config
configMap:
name: {{ include "substrate.fullname" (list "atenet-egress-agentgateway-config" .) }}
Expand Down
167 changes: 167 additions & 0 deletions charts/substrate/templates/k8s-credential-provider.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
{{/*
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.
*/}}

# The credential provider: a gRPC service that resolves ate-secret:// URIs
# of the kubernetes.io class to Kubernetes Secret values. It is the ONLY
# component in the egress credential-injection path with Kubernetes access; the
# egress gateway and the injector never read Secrets.
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
namespace: {{ .Release.Namespace }}
---
# The provider checks the actor's atespace-to-namespace grant before reading.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "substrate.fullname" (list "k8s-credential-provider-secret-reader" .) }}
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "substrate.fullname" (list "k8s-credential-provider-secret-reader" .) }}
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 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
template:
metadata:
annotations:
checksum/namespace-policy: {{ toJson .Values.credentialProvider.namespacePolicies | sha256sum }}
labels:
app: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
spec:
serviceAccountName: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
{{- with include "substrate.imagePullSecrets" . }}{{- . | nindent 6 }}{{- end }}
securityContext:
runAsUser: 65532
runAsGroup: 65532
runAsNonRoot: true
containers:
- name: k8s-credential-provider
image: {{ include "substrate.componentImage" (list "kubernetes-secrets" .) }}
imagePullPolicy: {{ include "substrate.imagePullPolicy" . }}
args:
- "--listen-address=:50051"
- "--metrics-address=:9090"
# Use this Service's DNS certificate; only the egress injector may call.
- "--server-cred-bundle=/run/servicedns.podcert.ate.dev/credential-bundle.pem"
- "--client-ca-file=/run/podidentity.podcert.ate.dev/trust-bundle.pem"
# Enforce the atespace→namespace authorization policy (default-deny).
- "--namespace-policy-file=/etc/k8s-credential-provider/namespace-policy.yaml"
- "--injector-spiffe-id=spiffe://cluster.local/ns/{{ .Release.Namespace }}/sa/{{ include "substrate.fullname" (list "atenet-egress" .) }}"
- "--log-level=info"
ports:
- name: grpc
containerPort: 50051
- name: metrics
containerPort: 9090
readinessProbe:
httpGet:
path: /readyz
port: metrics
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: namespace-policy
mountPath: /etc/k8s-credential-provider
readOnly: true
- name: servicedns
mountPath: /run/servicedns.podcert.ate.dev
readOnly: true
- name: podidentity
mountPath: /run/podidentity.podcert.ate.dev
readOnly: true
volumes:
- name: namespace-policy
configMap:
name: {{ include "substrate.fullname" (list "k8s-credential-provider-namespace-policy" .) }}
- name: servicedns
projected:
sources:
- podCertificate:
signerName: servicedns.podcert.ate.dev/identity
keyType: ECDSAP256
credentialBundlePath: credential-bundle.pem
- clusterTrustBundle:
signerName: servicedns.podcert.ate.dev/identity
labelSelector:
matchLabels:
podcert.ate.dev/canarying: live
path: trust-bundle.pem
- name: podidentity
projected:
sources:
- podCertificate:
signerName: podidentity.podcert.ate.dev/identity
keyType: ECDSAP256
credentialBundlePath: credential-bundle.pem
- clusterTrustBundle:
signerName: podidentity.podcert.ate.dev/identity
labelSelector:
matchLabels:
podcert.ate.dev/canarying: live
path: trust-bundle.pem
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
namespace: {{ .Release.Namespace }}
spec:
type: ClusterIP
selector:
app: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }}
ports:
- name: grpc
port: 50051
targetPort: grpc
protocol: TCP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "substrate.fullname" (list "k8s-credential-provider-namespace-policy" .) }}
namespace: {{ .Release.Namespace }}
data:
namespace-policy.yaml: |
policies: {{ toJson .Values.credentialProvider.namespacePolicies }}
9 changes: 8 additions & 1 deletion charts/substrate/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ atelet:
ateApi:
extraArgs: []

# Kubernetes Secret provider and AGW HTTP/HTTPS credential injection.
# Includes get-only Secret RBAC. HTTPS requires egress-mitm-ca-pool and actor trust.
credentialProvider:
namespacePolicies: []
# - atespace: team-a
# allowedNamespaces: [team-a-secrets]

# Name of a ConfigMap in the release namespace that supplies per-environment
# overrides for ate-api-server (ATE_API_POSTGRES_CONNECTION_STRING, ...).
# Mounted via envFrom with optional=true. Created by the chart from these values.
Expand Down Expand Up @@ -117,5 +124,5 @@ images:
postgres: postgres:18-alpine@sha256:9a8afca54e7861fd90fab5fdf4c42477a6b1cb7d293595148e674e0a3181de15
rustfs: rustfs/rustfs:1.0.0-beta.3@sha256:378642b05b7dcb4849fb77ebe6aca4ced1c3f66e7e504247df95a5c9018d3358
awsCli: amazon/aws-cli:2.17.0@sha256:643507c10ada7964ca6157b3d799f030b90577643da9955d319a77399ed80d73
agentgateway: ghcr.io/agentgateway/agentgateway:v0.0.0-alpha.9f9744cf
agentgateway: ghcr.io/agentgateway/agentgateway:v0.0.0-alpha.8dba3989@sha256:fdde26d4b0ea11d3e740dc19905dfe9b26e88f40fa8b9985f94ed1d8420e389e
busybox: busybox:1.36
1 change: 1 addition & 0 deletions cmd/ate-setup/internal/images/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var Components = []string{
"cmd/atecontroller",
"cmd/atelet",
"cmd/atenet",
"cmd/credential-provider/kubernetes-secrets",
"cmd/ateom-gvisor",
"cmd/ateom-microvm",
"cmd/podcertcontroller",
Expand Down
Loading
Loading