Skip to content
Open
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
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ verify: test
.PHONY: clean
clean:
rm -rf $(BINDIR)

# Render the substrate Helm chart into manifests/ate-install/ (mTLS mode,
# the historical default install). Run this whenever charts/substrate/ changes.
.PHONY: helm-template
helm-template:
@./hack/render-manifests.sh

# Verify that manifests/ate-install/ matches the chart output. Used in CI.
.PHONY: verify-helm-template
verify-helm-template:
@./hack/render-manifests.sh --check
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ To quickly set up the complete environment:

2. Run the following steps:
```shell
# create cluster and local registry
# create cluster and local registry (enables podcert feature gates for mTLS)
hack/create-kind-cluster.sh

# install ate, valkey, rustfs
Expand All @@ -121,6 +121,25 @@ kubectl port-forward -n ate-system svc/atenet-router 8000:80
curl -X POST -H "Host: my-counter-1.actors.resources.substrate.ate.dev" -i http://localhost:8000/
```

#### JWT mode (no feature gates)

For clusters where you can't enable the `ClusterTrustBundle` /
`PodCertificateRequest` feature gates (most managed Kubernetes), use the
JWT install path. Authentication is via projected ServiceAccount tokens
verified against the cluster's OIDC issuer; server certs come from a
self-signed pair bootstrapped by the install script.

```shell
# create cluster WITHOUT podcert feature gates
KIND_ENABLE_PODCERT=false hack/create-kind-cluster.sh

# install ate via Helm in JWT mode (auto-bootstraps Secret/ConfigMap)
hack/install-ate-kind-jwt.sh

# the demo + kubectl-ate + port-forward steps from the mTLS Quickstart
# above work identically from here.
```

### GKE Quickstart (Development)

1. Create and configure your environment file:
Expand Down
13 changes: 13 additions & 0 deletions charts/substrate/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v2
name: substrate
description: Agent Substrate — actor runtime, control plane, and data-plane router.
type: application
version: 0.1.0
appVersion: "0.1.0"
home: https://github.com/agent-substrate/substrate
sources:
- https://github.com/agent-substrate/substrate
keywords:
- agent
- actor
- substrate
86 changes: 86 additions & 0 deletions charts/substrate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# substrate

Helm chart for installing Agent Substrate.

## Install modes

| Mode | Default? | Cluster requirements | Trade-off |
|------|----------|----------------------|-----------|
| `mtls` | yes | feature gates `ClusterTrustBundle`, `ClusterTrustBundleProjection`, `PodCertificateRequest` + `certificates.k8s.io/v1beta1` API | Full in-cluster mTLS via the bundled `podcertcontroller`. |
| `jwt` | | none beyond stock K8s | Server certs come from a user-provided Secret; clients authenticate via projected ServiceAccount tokens. Valkey runs plaintext intra-cluster. |

```bash
# mTLS mode (default)
helm upgrade --install substrate ./charts/substrate

# JWT mode (no off-by-default feature gates)
helm upgrade --install substrate ./charts/substrate \
--set auth.mode=jwt \
--set auth.jwt.issuer=https://kubernetes.default.svc.cluster.local
```

## JWT-mode prerequisites

You provide two resources out-of-band:

1. `Secret/ateapi-tls` (type `kubernetes.io/tls`) in the release namespace.
This is the server cert for `ateapi` and the Envoy data-plane listener.
2. `ConfigMap/ateapi-ca` with key `ca.crt` in the release namespace.
This is the CA bundle clients use to verify the server.

Bootstrap snippet using `openssl`:

```bash
NS=ate-system
kubectl create ns "$NS" --dry-run=client -o yaml | kubectl apply -f -

# 1. Self-signed CA.
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-subj "/CN=ateapi-ca" \
-keyout ca.key -out ca.crt

# 2. Server key + CSR + signed cert.
openssl req -newkey rsa:2048 -nodes \
-subj "/CN=api.ate-system.svc" \
-keyout server.key -out server.csr
cat > server.ext <<EOF
subjectAltName = DNS:api.ate-system.svc,DNS:api.ate-system.svc.cluster.local,DNS:atenet-router.ate-system.svc
EOF
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 365 -extfile server.ext

# 3. Apply as Secret + ConfigMap.
kubectl -n "$NS" create secret tls ateapi-tls \
--cert=server.crt --key=server.key \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n "$NS" create configmap ateapi-ca \
--from-file=ca.crt=ca.crt \
--dry-run=client -o yaml | kubectl apply -f -
```

## Render manifests without applying

```bash
helm template substrate ./charts/substrate # mtls
helm template substrate ./charts/substrate --set auth.mode=jwt \
--set auth.jwt.issuer=https://kubernetes.default.svc.cluster.local
```

`manifests/ate-install/` in the repo is the rendered mTLS output and is
regenerated by `make helm-template`.

## Values

See `values.yaml` for the full set; the important keys:

| Key | Default | Notes |
|-----|---------|-------|
| `auth.mode` | `mtls` | `mtls` or `jwt` |
| `auth.jwt.issuer` | `""` | required when `auth.mode=jwt` |
| `auth.jwt.audience` | `api.ate-system.svc` | SA token audience |
| `auth.jwt.serverCertSecret` | `ateapi-tls` | Secret name |
| `auth.jwt.caBundleConfigMap` | `ateapi-ca` | ConfigMap name |
| `valkey.enabled` | `true` | Set false if you bring your own Redis/Valkey |
| `valkey.replicas` | `6` | StatefulSet size |
| `redis.clusterAddress` | `""` (in-cluster) | Override to use external Redis |
| `redis.useIAMAuth` | `false` | Google IAM auth |
20 changes: 20 additions & 0 deletions charts/substrate/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
substrate {{ .Chart.AppVersion }} installed in mode: {{ .Values.auth.mode }}

{{ if eq .Values.auth.mode "mtls" -}}
NOTE: mtls mode REQUIRES the following Kubernetes feature gates to be enabled:
- ClusterTrustBundle
- ClusterTrustBundleProjection
- PodCertificateRequest
plus the v1beta1 certificates API. On vanilla clusters (kind, EKS, etc.) you
must enable these explicitly. To install without them, pick auth.mode=jwt.
{{- else }}
JWT mode is active.

Required out-of-band resources (you must create these before pods become healthy):
1. Secret {{ .Values.auth.jwt.serverCertSecret }} (kubernetes.io/tls) in {{ .Release.Namespace }}
— server certificate for ateapi + atenet Envoy listener.
2. ConfigMap {{ .Values.auth.jwt.caBundleConfigMap }} in {{ .Release.Namespace }} with key "ca.crt"
— CA bundle clients use to verify the ateapi server.

See charts/substrate/README.md for an openssl bootstrap snippet.
{{- end }}
64 changes: 64 additions & 0 deletions charts/substrate/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{{/*
Qualified resource name for a chart component.

Usage:
{{ include "substrate.fullname" (list "ate-api-server" .) }}

When the release name equals the chart name (the canonical render in
hack/render-manifests.sh — `helm template substrate charts/substrate`), this
returns the bare component name, so the generated manifests/ate-install/
files keep their historical names ("ate-api-server", "ate-controller", ...).

Otherwise resources are prefixed with the release name in the standard Helm
style ("foo-ate-api-server", ...) so multiple releases coexist without
colliding.
*/}}
{{- define "substrate.fullname" -}}
{{- $name := index . 0 -}}
{{- $ctx := index . 1 -}}
{{- if eq $ctx.Release.Name $ctx.Chart.Name -}}
{{- $name -}}
{{- else -}}
{{- printf "%s-%s" $ctx.Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}

{{/*
Build an image reference for a substrate component binary.

Usage:
{{ include "substrate.componentImage" (list "ateapi" .) }}

Produces {image.registry}/{name}:{tag} where tag is resolved as:
1. image.tag value, if set and not the sentinel "<none>"
2. .Chart.AppVersion, if image.tag is empty
3. no tag (no colon) when image.tag is the sentinel "<none>"

The "<none>" sentinel is used by hack/render-manifests.sh so that ko:// refs
are emitted without a tag, letting `ko resolve` supply the digest at build time.
*/}}
{{- define "substrate.componentImage" -}}
{{- $name := index . 0 -}}
{{- $ctx := index . 1 -}}
{{- $registry := $ctx.Values.image.registry -}}
{{- $tag := $ctx.Values.image.tag | default $ctx.Chart.AppVersion -}}
{{- if ne $tag "<none>" -}}
{{- printf "%s/%s:%s" $registry $name $tag -}}
{{- else -}}
{{- printf "%s/%s" $registry $name -}}
{{- end -}}
{{- end -}}

{{/*
Validate auth.mode at template time.
*/}}
{{- define "substrate.validateAuthMode" -}}
{{- if not (or (eq .Values.auth.mode "mtls") (eq .Values.auth.mode "jwt")) -}}
{{- fail (printf "auth.mode must be 'mtls' or 'jwt', got %q" .Values.auth.mode) -}}
{{- end -}}
{{- if eq .Values.auth.mode "jwt" -}}
{{- if not .Values.auth.jwt.issuer -}}
{{- fail "auth.jwt.issuer is required when auth.mode=jwt" -}}
{{- end -}}
{{- end -}}
{{- end -}}
Loading