|
| 1 | +# SPIFFE Token Exchange Demo |
| 2 | + |
| 3 | +This example validates provider dynamic token exchange using SPIFFE JWT-SVIDs. |
| 4 | +It runs alongside `examples/spiffe-token-grant-demo` but exercises the |
| 5 | +`token_exchange` grant type instead of `client_credentials`. |
| 6 | + |
| 7 | +The demo deploys three in-cluster workloads: |
| 8 | + |
| 9 | +| Workload | Purpose | |
| 10 | +|---|---| |
| 11 | +| `token-exchange-issuer` | Issues a demo user subject token, performs the gateway intermediate token exchange, and performs the supervisor final token exchange | |
| 12 | +| `alpha-exchange` | Requires a final bearer token with audience and scope `alpha` | |
| 13 | +| `beta-exchange` | Requires a final bearer token with audience and scope `beta` | |
| 14 | + |
| 15 | +The OpenShell provider profile in `provider-profile.yaml` declares a stored |
| 16 | +`subject_token` credential and a runtime `access_token` credential with |
| 17 | +`token_grant.grant_type: token_exchange`. |
| 18 | + |
| 19 | +The profile declares exact Kubernetes service hostnames for `alpha-exchange` |
| 20 | +and `beta-exchange`. It intentionally does not set `allowed_ips`, because |
| 21 | +cluster service CIDRs vary across Kubernetes installations. |
| 22 | + |
| 23 | +When a sandbox curls `alpha-exchange` or `beta-exchange`: |
| 24 | + |
| 25 | +1. The supervisor fetches its SPIFFE JWT-SVID. |
| 26 | +2. The supervisor asks the gateway for an intermediate token. |
| 27 | +3. The gateway verifies the supervisor SVID, fetches its own gateway JWT-SVID, |
| 28 | + and exchanges the stored provider `subject_token` at `token-exchange-issuer`. |
| 29 | + The requested intermediate audience is the supervisor SPIFFE ID. |
| 30 | +4. The supervisor exchanges the intermediate token at the same token endpoint |
| 31 | + for the final alpha/beta access token. |
| 32 | +5. The supervisor injects that final token into the outbound HTTP request. |
| 33 | + |
| 34 | +## Prerequisites |
| 35 | + |
| 36 | +- A Kubernetes OpenShell dev cluster. |
| 37 | +- SPIRE enabled for provider token grants and gateway token exchange. |
| 38 | +- Gateway and supervisor access to SPIRE OIDC/JWKS discovery. |
| 39 | +- OpenShell configured with the Kubernetes ServiceAccount supervisor bootstrap |
| 40 | + path. |
| 41 | +- `providers_v2_enabled=true` on the target gateway. |
| 42 | +- Local `curl`, `python3`, `openssl`, `nc`, `kubectl`, and `openshell`. |
| 43 | +- A registered and logged-in CLI gateway. The script uses `GATEWAY_NAME`, then |
| 44 | + `OPENSHELL_GATEWAY`, then the active OpenShell gateway selection. |
| 45 | + |
| 46 | +For the Helm dev environment, deploy with the SPIRE releases and |
| 47 | +`ci/values-spire.yaml` enabled in `deploy/helm/openshell/skaffold.yaml`. |
| 48 | + |
| 49 | +The demo assumes these SPIFFE ID prefixes: |
| 50 | + |
| 51 | +| Identity | Prefix | |
| 52 | +|---|---| |
| 53 | +| Gateway | `spiffe://openshell.local/ns/openshell/sa/` | |
| 54 | +| Supervisor | `spiffe://openshell.local/openshell/sandbox/` | |
| 55 | + |
| 56 | +Override `GATEWAY_TRUST_DOMAIN_PREFIX` or `SUPERVISOR_TRUST_DOMAIN_PREFIX` in |
| 57 | +`k8s/workloads.yaml` if your development cluster uses different SPIFFE IDs. |
| 58 | + |
| 59 | +The demo issuer fetches SPIRE JWKS from the in-cluster OIDC discovery service |
| 60 | +to verify JWT-SVID signatures. The issuer pod runs a `spiffe-helper` sidecar |
| 61 | +that writes the SPIFFE bundle into a shared volume. The Node issuer uses that |
| 62 | +bundle as `SPIRE_JWKS_CA_FILE` when fetching JWKS over HTTPS. |
| 63 | + |
| 64 | +## Kubeconfig And Mise |
| 65 | + |
| 66 | +The repository `mise.toml` sets `KUBECONFIG` to the repo-local `kubeconfig` |
| 67 | +when your shell activates the OpenShell directory. If you are testing against a |
| 68 | +different cluster, run these commands from outside the repository and pass the |
| 69 | +target kubeconfig explicitly. |
| 70 | + |
| 71 | +```bash |
| 72 | +export OPENSHELL_REPO=/path/to/OpenShell |
| 73 | +export DEMO_KUBECONFIG=/path/to/your/kubeconfig |
| 74 | +export OPENSHELL_GATEWAY=local |
| 75 | +``` |
| 76 | + |
| 77 | +## Deploy Workloads |
| 78 | + |
| 79 | +From a directory outside the repository: |
| 80 | + |
| 81 | +```bash |
| 82 | +ACCESS_TOKEN_SECRET="$(openssl rand -hex 32)" |
| 83 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default create secret generic openshell-spiffe-token-exchange-demo \ |
| 84 | + --from-literal=access-token-secret="$ACCESS_TOKEN_SECRET" \ |
| 85 | + --dry-run=client \ |
| 86 | + -o yaml | KUBECONFIG="$DEMO_KUBECONFIG" kubectl apply -f - |
| 87 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl apply -k "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/k8s" |
| 88 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout restart deployment/token-exchange-issuer deployment/alpha-exchange deployment/beta-exchange |
| 89 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout status deployment/token-exchange-issuer --timeout=180s |
| 90 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout status deployment/alpha-exchange --timeout=180s |
| 91 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout status deployment/beta-exchange --timeout=180s |
| 92 | +``` |
| 93 | + |
| 94 | +## Register Provider And Test |
| 95 | + |
| 96 | +Port-forward the local gateway in one terminal: |
| 97 | + |
| 98 | +```bash |
| 99 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl port-forward -n openshell svc/openshell 8097:8080 |
| 100 | +``` |
| 101 | + |
| 102 | +Copy the Helm-generated TLS client bundle into the CLI config used for this |
| 103 | +demo. This uses the same gateway name as `OPENSHELL_GATEWAY`. |
| 104 | + |
| 105 | +```bash |
| 106 | +mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls" |
| 107 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n openshell get secret openshell-client-tls \ |
| 108 | + -o jsonpath='{.data.ca\.crt}' | base64 -d > "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls/ca.crt" |
| 109 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n openshell get secret openshell-client-tls \ |
| 110 | + -o jsonpath='{.data.tls\.crt}' | base64 -d > "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls/tls.crt" |
| 111 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n openshell get secret openshell-client-tls \ |
| 112 | + -o jsonpath='{.data.tls\.key}' | base64 -d > "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls/tls.key" |
| 113 | +``` |
| 114 | + |
| 115 | +Port-forward the token exchange issuer in another terminal and fetch a demo |
| 116 | +subject token: |
| 117 | + |
| 118 | +```bash |
| 119 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl port-forward -n default svc/token-exchange-issuer 18080:80 |
| 120 | +SUBJECT_TOKEN="$( |
| 121 | + curl -fsS http://127.0.0.1:18080/demo-subject-token | |
| 122 | + python3 -c 'import json, sys; print(json.load(sys.stdin)["access_token"])' |
| 123 | +)" |
| 124 | +``` |
| 125 | + |
| 126 | +Then run: |
| 127 | + |
| 128 | +```bash |
| 129 | +export GATEWAY=https://127.0.0.1:8097 |
| 130 | + |
| 131 | +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" settings set \ |
| 132 | + --global --key providers_v2_enabled --value true --yes |
| 133 | + |
| 134 | +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" provider profile import \ |
| 135 | + -f "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/provider-profile.yaml" |
| 136 | + |
| 137 | +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" provider create \ |
| 138 | + --name spiffe-token-exchange-demo \ |
| 139 | + --type spiffe-token-exchange-demo \ |
| 140 | + --credential "subject_token=${SUBJECT_TOKEN}" |
| 141 | + |
| 142 | +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox create \ |
| 143 | + --name spiffe-token-exchange-demo \ |
| 144 | + --provider spiffe-token-exchange-demo \ |
| 145 | + --keep \ |
| 146 | + --no-tty \ |
| 147 | + -- echo "sandbox ready" |
| 148 | + |
| 149 | +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox exec \ |
| 150 | + --name spiffe-token-exchange-demo \ |
| 151 | + --no-tty \ |
| 152 | + -- curl -sS http://alpha-exchange.default.svc.cluster.local/ |
| 153 | + |
| 154 | +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox exec \ |
| 155 | + --name spiffe-token-exchange-demo \ |
| 156 | + --no-tty \ |
| 157 | + -- curl -sS http://beta-exchange.default.svc.cluster.local/ |
| 158 | +``` |
| 159 | + |
| 160 | +Expected output includes the demo user as the token subject and the sandbox |
| 161 | +SPIFFE ID as the authorized party/client: |
| 162 | + |
| 163 | +```text |
| 164 | +alpha called with path /: |
| 165 | + sub: demo-user |
| 166 | + aud: alpha, account |
| 167 | + scope: alpha profile email |
| 168 | + azp: spiffe://openshell.local/openshell/sandbox/<sandbox-id> |
| 169 | + client_id: spiffe://openshell.local/openshell/sandbox/<sandbox-id> |
| 170 | +
|
| 171 | +beta called with path /: |
| 172 | + sub: demo-user |
| 173 | + aud: beta, account |
| 174 | + scope: beta profile email |
| 175 | + azp: spiffe://openshell.local/openshell/sandbox/<sandbox-id> |
| 176 | + client_id: spiffe://openshell.local/openshell/sandbox/<sandbox-id> |
| 177 | +``` |
| 178 | + |
| 179 | +The token issuer logs both token exchange phases: |
| 180 | + |
| 181 | +```bash |
| 182 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default logs deployment/token-exchange-issuer --tail=40 |
| 183 | +``` |
| 184 | + |
| 185 | +Example log lines: |
| 186 | + |
| 187 | +```text |
| 188 | +issued intermediate token for user=demo-user audience=spiffe://openshell.local/openshell/sandbox/<sandbox-id> |
| 189 | +issued final token for user=demo-user audience=alpha client=spiffe://openshell.local/openshell/sandbox/<sandbox-id> |
| 190 | +issued final token for user=demo-user audience=beta client=spiffe://openshell.local/openshell/sandbox/<sandbox-id> |
| 191 | +``` |
| 192 | + |
| 193 | +## Automated Demo |
| 194 | + |
| 195 | +`demo.sh` applies the workloads, fetches a demo subject token, registers the |
| 196 | +provider profile, creates a sandbox, curls alpha/beta, and deletes the sandbox |
| 197 | +with `openshell` on exit. It leaves the Kubernetes demo workloads in place and |
| 198 | +prints diagnostics only when the run fails. |
| 199 | + |
| 200 | +```bash |
| 201 | +cd /tmp |
| 202 | +KUBECONFIG="$DEMO_KUBECONFIG" bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/demo.sh" |
| 203 | +``` |
| 204 | + |
| 205 | +The script reuses your normal OpenShell CLI config so it can load the stored |
| 206 | +OIDC token for `OPENSHELL_GATEWAY`. If you set `ISOLATED_CONFIG=1`, register |
| 207 | +and log in to the gateway in that isolated config before running the demo. |
| 208 | + |
| 209 | +## Cleanup |
| 210 | + |
| 211 | +Delete the sandbox through OpenShell: |
| 212 | + |
| 213 | +```bash |
| 214 | +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox delete spiffe-token-exchange-demo |
| 215 | +``` |
| 216 | + |
| 217 | +Delete the demo workloads with Kubernetes: |
| 218 | + |
| 219 | +```bash |
| 220 | +KUBECONFIG="$DEMO_KUBECONFIG" kubectl delete -k "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/k8s" |
| 221 | +``` |
0 commit comments