Skip to content

Commit 7a603a0

Browse files
committed
test(provider): add runnable example for token exchange
Signed-off-by: Gordon Sim <gsim@redhat.com>
1 parent 663d0a8 commit 7a603a0

8 files changed

Lines changed: 1214 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

Comments
 (0)