|
| 1 | +# Securing Internal Traffic with SPIRE (mTLS) |
| 2 | + |
| 3 | +This task shows you how to enable mutual TLS (mTLS) between AgentCube's |
| 4 | +control-plane components using [SPIRE](https://spiffe.io/docs/latest/spire-about/spire-concepts/). |
| 5 | +By the end, every request between the Router and WorkloadManager will be |
| 6 | +cryptographically authenticated using short-lived X.509 certificates that rotate |
| 7 | +automatically. |
| 8 | + |
| 9 | +## Before you begin |
| 10 | + |
| 11 | +1. Follow the [Getting Started](../getting-started.md) guide to install |
| 12 | + AgentCube on your cluster. **Do not** enable SPIRE during the initial |
| 13 | + installation - this tutorial walks through that step explicitly. |
| 14 | + |
| 15 | +2. Make sure you have the following tools installed: |
| 16 | + - [`kubectl`](https://kubernetes.io/docs/tasks/tools/) (v1.25+) |
| 17 | + - [`helm`](https://helm.sh/docs/intro/install/) (v3.12+) |
| 18 | + |
| 19 | +3. Confirm AgentCube is running without SPIRE: |
| 20 | + |
| 21 | + ```bash |
| 22 | + kubectl get pods -n agentcube |
| 23 | + ``` |
| 24 | + |
| 25 | + You should see the Router and WorkloadManager pods in `Running` state, each |
| 26 | + showing `1/1` containers ready (no sidecar yet): |
| 27 | + |
| 28 | +``` |
| 29 | + NAME READY STATUS RESTARTS AGE |
| 30 | + agentcube-router-7fbb7b54c-7khq5 1/1 Running 0 8s |
| 31 | + workloadmanager-6c44454f68-zmfcc 1/1 Running 0 8s |
| 32 | +``` |
| 33 | + |
| 34 | +> **Tip:** |
| 35 | +> If you are running on a local [Kind](https://kind.sigs.k8s.io/) or |
| 36 | +> [Minikube](https://minikube.sigs.k8s.io/) cluster, you will need to pass two |
| 37 | +> extra overrides in the Helm upgrade command shown below. These are already |
| 38 | +> included in the instructions, so just keep them in. |
| 39 | +
|
| 40 | + |
| 41 | +## What gets deployed |
| 42 | + |
| 43 | +When you enable SPIRE, the Helm chart creates the following additional resources |
| 44 | +inside your cluster: |
| 45 | + |
| 46 | +| Resource | Kind | Purpose | |
| 47 | +|---|---|---| |
| 48 | +| `spire-server` | StatefulSet (1 replica) | Central certificate authority. Runs the SPIRE Controller Manager as a sidecar. | |
| 49 | +| `spire-agent` | DaemonSet | Runs on every node. Attests workloads and delivers certificates. | |
| 50 | +| `ClusterSPIFFEID` (×2) | CRD | Declarative identity registration for the Router and WorkloadManager. | |
| 51 | +| `spiffe-helper` sidecar | Container (injected) | Fetches and rotates certificates inside the Router and WorkloadManager pods. | |
| 52 | + |
| 53 | +The Router and WorkloadManager pods will each go from `1/1` to `2/2` containers |
| 54 | +(the main process + the `spiffe-helper` sidecar). |
| 55 | + |
| 56 | +## Step 1 - Install the SPIRE Controller Manager CRDs |
| 57 | + |
| 58 | +The SPIRE Controller Manager watches `ClusterSPIFFEID` custom resources. These |
| 59 | +CRDs must be present in the cluster **before** the Helm upgrade, otherwise the |
| 60 | +chart will fail to create them. |
| 61 | + |
| 62 | +```bash |
| 63 | +kubectl apply -k "https://github.com/spiffe/spire-controller-manager/config/crd?ref=v0.6.4" |
| 64 | +``` |
| 65 | + |
| 66 | +Verify the CRD was installed: |
| 67 | + |
| 68 | +```bash |
| 69 | +kubectl get crd clusterspiffeids.spire.spiffe.io |
| 70 | +``` |
| 71 | + |
| 72 | +Expected output: |
| 73 | + |
| 74 | +``` |
| 75 | +NAME CREATED AT |
| 76 | +clusterspiffeids.spire.spiffe.io 2026-06-01T16:22:32Z |
| 77 | +``` |
| 78 | + |
| 79 | +## Step 2 - Upgrade the Helm release with SPIRE enabled |
| 80 | + |
| 81 | +Run the Helm upgrade with `spire.enabled=true`. Keep `--reuse-values` so your |
| 82 | +existing install-time settings (for example Redis, images, RBAC, or service |
| 83 | +accounts) are preserved while enabling SPIRE. The extra `--set` flags for |
| 84 | +`insecureBootstrap` and `skipKubeletVerification` are needed for local |
| 85 | +development clusters (Kind / Minikube). On a production cluster with proper |
| 86 | +kubelet certificates, you can omit them. |
| 87 | + |
| 88 | +```bash |
| 89 | +helm upgrade agentcube manifests/charts/base \ |
| 90 | + -n agentcube \ |
| 91 | + --reuse-values \ |
| 92 | + --set spire.enabled=true \ |
| 93 | + --set spire.agent.insecureBootstrap=true \ |
| 94 | + --set spire.agent.skipKubeletVerification=true |
| 95 | +``` |
| 96 | + |
| 97 | +This single command deploys the full SPIRE infrastructure **and** injects the |
| 98 | +`spiffe-helper` sidecar into the Router and WorkloadManager pods. |
| 99 | + |
| 100 | +Wait for everything to become ready: |
| 101 | + |
| 102 | +```bash |
| 103 | +kubectl rollout status statefulset/spire-server -n agentcube --timeout=120s |
| 104 | +kubectl rollout status daemonset/spire-agent -n agentcube --timeout=120s |
| 105 | +kubectl rollout status deployment/agentcube-router -n agentcube --timeout=120s |
| 106 | +kubectl rollout status deployment/workloadmanager -n agentcube --timeout=120s |
| 107 | +``` |
| 108 | + |
| 109 | +## Step 3 - Verify SPIRE is healthy |
| 110 | + |
| 111 | +Check that the SPIRE Server is up and has registered agents: |
| 112 | + |
| 113 | +```bash |
| 114 | +kubectl exec -n agentcube statefulset/spire-server -c spire-server -- \ |
| 115 | + /opt/spire/bin/spire-server agent list |
| 116 | +``` |
| 117 | + |
| 118 | +You should see at least one agent entry (one per cluster node): |
| 119 | + |
| 120 | +``` |
| 121 | +Found 1 attested agent(s): |
| 122 | +
|
| 123 | +SPIFFE ID : spiffe://cluster.local/spire/agent/k8s_psat/agentcube-cluster/67790303-3657-42d6-bf4f-c3833ec6dd5e |
| 124 | +Attestation type : k8s_psat |
| 125 | +... |
| 126 | +``` |
| 127 | + |
| 128 | +Next, confirm the identity registrations were picked up from the |
| 129 | +`ClusterSPIFFEID` resources: |
| 130 | + |
| 131 | +```bash |
| 132 | +kubectl exec -n agentcube statefulset/spire-server -c spire-server -- \ |
| 133 | + /opt/spire/bin/spire-server entry show |
| 134 | +``` |
| 135 | + |
| 136 | +You should see entries for both the Router and WorkloadManager, with SPIFFE IDs |
| 137 | +following the format |
| 138 | +`spiffe://cluster.local/ns/agentcube/sa/<service-account>`: |
| 139 | + |
| 140 | +``` |
| 141 | +Entry ID : bfd507ec-10d8-43e5-b984-861a3ff81167 |
| 142 | +SPIFFE ID : spiffe://cluster.local/ns/agentcube/sa/agentcube-router |
| 143 | +Parent ID : spiffe://cluster.local/spire/agent/k8s_psat/agentcube-cluster/67790303-3657-42d6-bf4f-c3833ec6dd5e |
| 144 | +Revision : 0 |
| 145 | +
|
| 146 | +Entry ID : 21e3ba6f-ad13-4076-9e08-90a2d4ff518f |
| 147 | +SPIFFE ID : spiffe://cluster.local/ns/agentcube/sa/workloadmanager |
| 148 | +Parent ID : spiffe://cluster.local/spire/agent/k8s_psat/agentcube-cluster/67790303-3657-42d6-bf4f-c3833ec6dd5e |
| 149 | +Revision : 0 |
| 150 | +``` |
| 151 | + |
| 152 | +## Step 4 - Verify the sidecar and certificates |
| 153 | + |
| 154 | +Confirm that both the Router and WorkloadManager pods now show `2/2` containers |
| 155 | +(the main container + the `spiffe-helper` sidecar): |
| 156 | + |
| 157 | +```bash |
| 158 | +kubectl get pods -n agentcube |
| 159 | +``` |
| 160 | + |
| 161 | +Expected output: |
| 162 | + |
| 163 | +``` |
| 164 | +NAME READY STATUS RESTARTS AGE |
| 165 | +agentcube-router-574d98b76-tr2nr 2/2 Running 5 (2m24s ago) 3m17s |
| 166 | +spire-agent-8r9jx 1/1 Running 3 (2m44s ago) 3m17s |
| 167 | +spire-server-0 2/2 Running 0 3m17s |
| 168 | +workloadmanager-5797888bd4-jm2qj 2/2 Running 3 (118s ago) 3m17s |
| 169 | +``` |
| 170 | + |
| 171 | +Check the Router logs to confirm mTLS is active. You should see a log line |
| 172 | +indicating it is waiting for, and then successfully loading, the certificates: |
| 173 | + |
| 174 | +```bash |
| 175 | +kubectl logs -n agentcube deployment/agentcube-router -c agentcube-router | grep -i mtls |
| 176 | +``` |
| 177 | + |
| 178 | +Expected output: |
| 179 | + |
| 180 | +``` |
| 181 | +I0601 16:25:21.444099 1 main.go:64] Waiting for Router mTLS cert/key/CA files |
| 182 | +I0601 16:25:21.444259 1 wait.go:46] All mTLS cert/key/CA files are present |
| 183 | +I0601 16:25:21.445161 1 session_manager.go:84] Using https:// for WORKLOAD_MANAGER_URL because mTLS is configured |
| 184 | +I0601 16:25:21.445482 1 session_manager.go:93] Router→WorkloadManager mTLS enabled: expecting server SPIFFE ID spiffe://cluster.local/ns/agentcube/sa/workloadmanager |
| 185 | +``` |
| 186 | + |
| 187 | +Do the same for the WorkloadManager: |
| 188 | + |
| 189 | +```bash |
| 190 | +kubectl logs -n agentcube deployment/workloadmanager -c workloadmanager | grep -i mtls |
| 191 | +``` |
| 192 | + |
| 193 | +Expected output: |
| 194 | + |
| 195 | +``` |
| 196 | +I0601 16:25:22.561316 1 main.go:80] Waiting for WorkloadManager mTLS cert/key/CA files |
| 197 | +I0601 16:25:22.561931 1 wait.go:46] All mTLS cert/key/CA files are present |
| 198 | +I0601 16:25:22.678777 1 server.go:218] WorkloadManager mTLS enabled: accepting clients with valid SPIRE-provisioned certificates |
| 199 | +``` |
| 200 | + |
| 201 | +## Step 5 - Test it end-to-end |
| 202 | + |
| 203 | +Deploy a simple agent and invoke it through the Router to confirm the full |
| 204 | +mTLS-secured path works: |
| 205 | + |
| 206 | +```bash |
| 207 | +kubectl apply -f - <<EOF |
| 208 | +apiVersion: runtime.agentcube.volcano.sh/v1alpha1 |
| 209 | +kind: AgentRuntime |
| 210 | +metadata: |
| 211 | + name: mtls-test |
| 212 | + namespace: default |
| 213 | +spec: |
| 214 | + targetPort: |
| 215 | + - pathPrefix: "/" |
| 216 | + port: 8000 |
| 217 | + protocol: "HTTP" |
| 218 | + podTemplate: |
| 219 | + spec: |
| 220 | + containers: |
| 221 | + - name: agent |
| 222 | + image: python:3.11-slim |
| 223 | + command: ["python3", "-m", "http.server", "8000"] |
| 224 | + resources: |
| 225 | + requests: |
| 226 | + cpu: "100m" |
| 227 | + memory: "128Mi" |
| 228 | + limits: |
| 229 | + cpu: "500m" |
| 230 | + memory: "512Mi" |
| 231 | +EOF |
| 232 | +``` |
| 233 | + |
| 234 | +Open a new terminal and port-forward the Router: |
| 235 | + |
| 236 | +```bash |
| 237 | +kubectl port-forward -n agentcube svc/agentcube-router 8080:8080 |
| 238 | +``` |
| 239 | + |
| 240 | +In your original terminal, send a request to the root path of the sandbox: |
| 241 | + |
| 242 | +```bash |
| 243 | +curl -i http://localhost:8080/v1/namespaces/default/agent-runtimes/mtls-test/invocations/ |
| 244 | +``` |
| 245 | + |
| 246 | +If the mTLS handshake between Router and WorkloadManager succeeds, you will see |
| 247 | +a `200 OK` response with a directory listing from the python server (or a `502` |
| 248 | +while the sandbox is still booting - just retry after a few seconds). A |
| 249 | +TLS-related error in the Router logs would indicate a misconfiguration. |
| 250 | + |
| 251 | +## Understanding what changed |
| 252 | + |
| 253 | +Here is how each component is configured behind the scenes. You do **not** need |
| 254 | +to set any of these flags manually - the Helm chart handles it when |
| 255 | +`spire.enabled=true`. |
| 256 | + |
| 257 | +### Router (mTLS client) |
| 258 | + |
| 259 | +The Helm chart passes these flags to the Router binary: |
| 260 | + |
| 261 | +``` |
| 262 | +--mtls-cert=/run/spire/certs/svid.pem |
| 263 | +--mtls-key=/run/spire/certs/svid_key.pem |
| 264 | +--mtls-ca=/run/spire/certs/svid_bundle.pem |
| 265 | +``` |
| 266 | + |
| 267 | +When all three are present, the Router creates a dedicated HTTPS transport for |
| 268 | +its WorkloadManager connection. It verifies that the WorkloadManager's |
| 269 | +certificate contains the expected SPIFFE ID |
| 270 | +(`spiffe://cluster.local/ns/agentcube/sa/workloadmanager`). |
| 271 | + |
| 272 | +### WorkloadManager (mTLS server) |
| 273 | + |
| 274 | +The Helm chart passes these flags to the WorkloadManager binary: |
| 275 | + |
| 276 | +``` |
| 277 | +--tls-cert=/run/spire/certs/svid.pem |
| 278 | +--tls-key=/run/spire/certs/svid_key.pem |
| 279 | +--tls-ca=/run/spire/certs/svid_bundle.pem |
| 280 | +``` |
| 281 | + |
| 282 | +When the CA file is present, the WorkloadManager starts its HTTP server with |
| 283 | +mTLS enabled. It requires every connecting client to present a valid certificate |
| 284 | +signed by the trusted CA. Authorization is handled at the application layer, not |
| 285 | +at the TLS level. |
| 286 | + |
| 287 | +### Certificate rotation |
| 288 | + |
| 289 | +The `spiffe-helper` sidecar continuously fetches fresh SVIDs from the local |
| 290 | +SPIRE Agent and writes them to a shared volume at `/run/spire/certs/`. A |
| 291 | +`CertWatcher` inside each component watches that directory using `fsnotify` and |
| 292 | +hot-reloads the certificates without dropping any active connections. The default |
| 293 | +SVID TTL is **1 hour**. |
| 294 | + |
| 295 | +### What about sandboxes? |
| 296 | + |
| 297 | +mTLS is only used for the control-plane path (Router ↔ WorkloadManager). |
| 298 | +The Router→Sandbox connection continues to use the existing JWT-based |
| 299 | +authentication. This keeps sandbox startup latency low and avoids injecting |
| 300 | +SPIRE dependencies into user-defined runtime containers. |
| 301 | + |
| 302 | +## Cleanup |
| 303 | + |
| 304 | +Remove the test agent: |
| 305 | + |
| 306 | +```bash |
| 307 | +kubectl delete agentruntime mtls-test -n default |
| 308 | +``` |
| 309 | + |
| 310 | +If you want to **disable** SPIRE and go back to plain HTTP between the control |
| 311 | +plane components, run the Helm upgrade again with `spire.enabled=false`: |
| 312 | + |
| 313 | +```bash |
| 314 | +helm upgrade agentcube manifests/charts/base \ |
| 315 | + -n agentcube \ |
| 316 | + --reuse-values \ |
| 317 | + --set spire.enabled=false |
| 318 | +``` |
| 319 | + |
| 320 | +This removes all SPIRE workloads (Server, Agent), sidecars, and ClusterSPIFFEID |
| 321 | +resources from this Helm release. The Router/WorkloadManager pods will restart |
| 322 | +with `1/1` containers. |
| 323 | + |
| 324 | +To also remove the SPIRE Controller Manager CRDs: |
| 325 | + |
| 326 | +```bash |
| 327 | +kubectl delete -k "https://github.com/spiffe/spire-controller-manager/config/crd?ref=v0.6.4" |
| 328 | +``` |
0 commit comments