Skip to content

Commit be6f3ef

Browse files
khareyash05claudeofficialasishkumar
authored
docs(k8s-proxy): document Console + browser widget for PAT exchange (#853)
* docs(k8s-proxy): document Console + browser widget for PAT exchange The Authentication section now lists three ways to swap a PAT for the shared token: - Option A: the Keploy Console "Get Shared Token" dialog on the cluster detail page (with screenshots). - Option B: an embedded SharedTokenExchanger widget so readers can fire the exchange against their own proxy without leaving the page. - Option C: the original curl path, kept for CI scripts. This makes the bootstrap flow feel like a documented product surface rather than a curl-only one, and lets readers verify the call before trusting it in CI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Yash Khare <khareyash05@gmail.com> * chore: apply prettier formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Yash Khare <khareyash05@gmail.com> * chore: fix vale lint errors Add sharedToken/deploymentType/ingressUrl/cleartext to vale accept list and remove spaces around em-dashes to satisfy Google.EmDash rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Yash Khare <khareyash05@gmail.com> * chore: rephrase deploymentType bullet to avoid Google.EmDash false positive Signed-off-by: Yash Khare <khareyash05@gmail.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Yash Khare <khareyash05@gmail.com> * chore: remove unused content from k8s-proxy-api docs Signed-off-by: Yash Khare <khareyash05@gmail.com> * chore: remove unused SharedTokenExchanger component The widget is no longer referenced from any doc after the inline "Try it right here" section was removed from k8s-proxy-api.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Yash Khare <khareyash05@gmail.com> * docs(k8s-proxy): correct PAT exchange docs Signed-off-by: Asish Kumar <officialasishkumar@gmail.com> * docs(k8s-proxy): document /shared-token/info and rotation behavior Builds on f9af5de (which already corrected the misleading "stable for the lifetime of the Helm release" claim) by: - Adding the new GET /shared-token/info introspection endpoint to the endpoint reference table and the unauthenticated-routes list. - Surfacing tokenId + issuedAt in the example /get-shared-token response so callers can see what to cache. - Adding a "Shared token lifetime and rotation" section that lists the actual rotation triggers and a "Detecting a rotated token" section with a CI-side cache-and-revalidate pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Yash Khare <khareyash05@gmail.com> * chore: update docs Signed-off-by: Yash Khare <khareyash05@gmail.com> * chore: fix vale Google.Quotes lint error Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Yash Khare <khareyash05@gmail.com> * docs: clarify k8s proxy PAT scope requirements Signed-off-by: Asish Kumar <officialasishkumar@gmail.com> --------- Signed-off-by: Yash Khare <khareyash05@gmail.com> Signed-off-by: Asish Kumar <officialasishkumar@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Asish Kumar <officialasishkumar@gmail.com> Co-authored-by: Asish Kumar <87874775+officialasishkumar@users.noreply.github.com>
1 parent ece6117 commit be6f3ef

5 files changed

Lines changed: 111 additions & 55 deletions

File tree

87.6 KB
Loading
85.3 KB
Loading
144 KB
Loading

vale_styles/config/vocabularies/Base/accept.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,7 @@ PostStart
189189
[Dd]ev
190190
[Cc]Rs?
191191
[Ss]ubresource[s]?
192+
sharedToken
193+
deploymentType
194+
ingressUrl
195+
[Cc]leartext

versioned_docs/version-4.0.0/running-keploy/k8s-proxy-api.md

Lines changed: 107 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -98,112 +98,155 @@ Combined with capture-time static deduplication (benefit 2), this keeps the curr
9898

9999
## Authentication
100100

101-
:::info In development
102-
The authentication flow is currently in development.
103-
:::
101+
Authenticating to the proxy is a **two-step exchange**:
104102

105-
Every protected proxy endpoint requires the cluster **shared token**. Send it as a Bearer token:
103+
1. Create a **Personal Access Token (PAT)** in the Keploy Console.
104+
2. `POST /get-shared-token` with that PAT to receive the cluster's **shared token**.
105+
106+
Every other protected route on the proxy is then gated on the shared token sent as a Bearer header:
106107

107108
```text
108109
Authorization: Bearer <K8S_PROXY_SHARED_TOKEN>
109110
```
110111

111-
Only `GET /healthz` and the admission webhook `POST /mutate` are unauthenticated. Every other route rejects missing or malformed headers with `401 Unauthorized`.
112+
Only `GET /healthz`, the admission webhook `POST /mutate`, the bootstrap `POST /get-shared-token`, and the introspection `GET /shared-token/info` are unauthenticated. Every other route rejects missing or malformed headers with `401 Unauthorized`.
112113

113114
```bash
114115
# Verify the proxy is up (no auth required)
115116
curl -sf https://$PROXY/healthz
116117
# {"status":"ok"}
117118
```
118119

119-
### How the token is provisioned
120+
### Why a two-step exchange?
120121

121-
The shared token is generated **at Helm install time** and stored as a Kubernetes Secret named `<release>-shared-token` in the proxy's namespace. The chart's pre-render step uses Helm's `randAlphaNum 48` to produce the value on the very first install and a `lookup` + `helm.sh/resource-policy: keep` annotation to preserve it across upgrades, so the token is **stable for the lifetime of the release**—Pod restarts and chart upgrades do not rotate it.
122+
The PAT identifies a specific user; the shared token authorizes calls against a specific cluster. Splitting the two lets callers (CI scripts, AI agents, internal tooling) store one user-rotatable PAT and exchange it at run time without `kubectl` access to the proxy namespace or an interactive user login. The shared token is still a cluster-wide secret and can also be surfaced through existing heartbeat-backed Console/API flows, so handle it as sensitive wherever it appears.
122123

123-
The k8s-proxy Deployment and the per-node DaemonSet both mount the Secret as the `KEPLOY_SHARED_TOKEN` env var via `secretKeyRef`. On startup the proxy reports the value to the Keploy API server in its first heartbeat (`POST /cluster/status`) so the Console can display it under the cluster's app entries.
124+
### 1. Issue a PAT
124125

125-
For local/dev runs without a Secret, if `KEPLOY_SHARED_TOKEN` is unset the proxy falls back to generating a random 32-byte value via `crypto/rand` (hex-encoded). This fallback is fresh on every restart and is **not** the path used in any Helm-managed deployment.
126+
In the Keploy Console, open **Settings → API Keys** and click **Create token**. PATs are 47-character strings prefixed with `kep_`.
126127

127-
### Retrieve the token
128+
- The PAT must belong to the same tenant (`cid`) as the cluster the proxy is registered to and include `write` or `admin` scope. The proxy will reject cross-tenant PATs or PATs with insufficient scope with `403 Forbidden`.
129+
- Treat the PAT like a password—it is the long-lived credential. Store it in your CI provider's secret store, not in the repo.
130+
- A user can have multiple PATs. Revoke or rotate them from the same Console screen; revoked PATs stop working immediately.
128131

129-
Two equally valid paths.
132+
### 2. Exchange the PAT for the shared token
130133

131-
**(a) Read it directly from the Secret** if you have `kubectl` access to the proxy namespace:
134+
There are two ways to do the exchange. Pick whichever fits your workflow—they both hit the same `POST /get-shared-token` endpoint.
132135

133-
```bash
134-
kubectl -n keploy get secret <release>-shared-token -o jsonpath='{.data.token}' | base64 -d
135-
```
136+
#### Option A: From the Keploy Console
137+
138+
Open the cluster's detail page in the Console. Each cluster card on the top row shows the live ingress URL; the **Get Shared Token** button sits inside that card.
139+
140+
![Get Shared Token button on the Ingress URL card](/img/k8s-proxy-shared-token-button.png)
141+
142+
Clicking it opens a dialog pre-filled with the cluster's ingress URL. Paste your PAT and submit.
143+
144+
![Exchange PAT for shared token dialog](/img/k8s-proxy-shared-token-dialog.png)
145+
146+
On success, the dialog displays the `sharedToken`, `ingressUrl`, and `deploymentType` returned by the proxy. Use the **Copy sharedToken** button to grab the token for your CI script or terminal.
147+
148+
![Successful exchange showing sharedToken, ingressUrl, deploymentType](/img/k8s-proxy-shared-token-success.png)
136149

137-
**(b) Fetch it from the Keploy API server**, which mirrors what the proxy reported in its last heartbeat. Log in once to obtain a user JWT, then look up the proxy app for the Deployment you want to drive:
150+
The PAT is held in browser memory for the lifetime of the dialog only—it's never persisted to local storage and never sent to the Keploy API server from the Console (the proxy itself does that validation server-side).
151+
152+
#### Option B: From a shell
138153

139154
```bash
140-
API_SERVER="https://api.keploy.io"
141-
NS="default"
142-
DEPLOY="orders-api"
143-
CLUSTER="prod-use1"
155+
PROXY="https://your-proxy-ingress"
156+
PAT="kep_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
144157

145-
# 1. Authenticate as a Keploy user (admin, user, or cicd role)
146-
JWT=$(curl -s -X POST "$API_SERVER/login" \
147-
-H "Content-Type: application/json" \
148-
-d '{"email":"you@example.com","password":"..."}' | jq -r '.token')
158+
RESP=$(curl -sS -X POST "$PROXY/get-shared-token" \
159+
-H "Authorization: Bearer $PAT")
149160

150-
# 2. Look up the proxy app for this Deployment and read its sharedToken
151-
K8S_PROXY_SHARED_TOKEN=$(curl -s -H "Authorization: Bearer $JWT" \
152-
"$API_SERVER/cluster/getApp?namespace=$NS&deployment=$DEPLOY&clusterName=$CLUSTER" \
153-
| jq -r '.sharedToken')
161+
K8S_PROXY_SHARED_TOKEN=$(echo "$RESP" | jq -r '.sharedToken')
162+
INGRESS_URL=$(echo "$RESP" | jq -r '.ingressUrl')
154163

155164
AUTH="Authorization: Bearer $K8S_PROXY_SHARED_TOKEN"
156165
```
157166

158-
`GET /cluster/getApps` returns the same `sharedToken` field for every proxy-managed app in your organization in a single response, which is convenient when you want to script across many Deployments at once.
167+
A successful exchange returns:
159168

160-
> The proxy shared token is cluster-wide, not per-user. The API server still uses normal user JWT/cookie authentication on its own routes (including `/cluster/getApp`). The token is sticky across Pod restarts and chart upgrades, so callers can cache it for the lifetime of the Helm release.
169+
```json
170+
{
171+
"ingressUrl": "https://your-proxy-ingress",
172+
"sharedToken": "3e14be232bce3e3cf6f6d58f284b6eb88db3280c54d93a7951e5000c6bbe3e9a",
173+
"deploymentType": "saas",
174+
"tokenId": "408aaecaba458939",
175+
"issuedAt": 1777883713
176+
}
177+
```
161178

162-
---
179+
- `sharedToken`—use this on every subsequent call as `Authorization: Bearer <sharedToken>`.
180+
- `ingressUrl`—echoes back the address the proxy was installed with, so a script can derive every other URL from one bootstrap call.
181+
- `deploymentType`—either `"saas"` for the hosted control plane or `"self-hosted"` for self-hosted installs.
182+
- `tokenId`—a short non-secret identifier that changes whenever the proxy regenerates `sharedToken`. Cache it alongside the token; use `GET /shared-token/info` to check whether your cached value is still current.
183+
- `issuedAt`—unix timestamp when the proxy minted this `sharedToken`.
163184

164-
## Response format
185+
### Shared token lifetime and rotation
165186

166-
Handlers return JSON with `application/json` on success. Validation failures usually return `{"error": "..."}` with a 4xx status; shared-token auth failures return `{"success": false, "message": "Unauthorized: ..."}`. A handful of endpoints stream newline-delimited JSON instead - they are called out explicitly below.
187+
The shared token is generated **fresh in process memory at every Pod startup** with `crypto/rand` (32 bytes, hex-encoded). It is _not_ stored in a Kubernetes Secret, _not_ persisted to disk, and _not_ stable across restarts. Concretely, the token rotates whenever:
167188

168-
```js
169-
// Successful record start (200)
170-
{ "record": "started", "id": "default-orders-api" }
189+
- the proxy Pod restarts (CrashLoop, eviction, node drain, OOM),
190+
- `helm upgrade` rolls the proxy Deployment,
191+
- you call `POST /proxy/update` and the new image becomes ready.
171192

172-
// Validation error (400)
173-
{ "error": "namespace and deployment are required" }
193+
Treat the shared token as **scoped to one running process**, not "the lifetime of the install." A CI script that exchanges the PAT once and caches the result for hours will start getting `401 Invalid token` the moment the proxy is rolled.
174194

175-
// Auth error (401)
176-
{ "success": false, "message": "Unauthorized: Missing authorization header" }
195+
#### Detecting a rotated token
177196

178-
// Namespace-scoped proxy rejecting a cross-namespace call (403)
179-
{ "error": "this proxy is scoped to namespace \"payments\"" }
197+
`GET /shared-token/info` returns only the current `tokenId` and `issuedAt`—never the token itself, so it is safe to call without the shared token.
198+
199+
```bash
200+
curl -sS "$PROXY/shared-token/info"
201+
# {"tokenId":"408aaecaba458939","issuedAt":1777883713}
202+
```
203+
204+
Recommended pattern in CI: cache `(tokenId, sharedToken)` together when you exchange, and before every long-running operation (or as a quick guard at the start of every step) hit `/shared-token/info` to compare. If `tokenId` differs from your cached value, re-exchange the PAT.
205+
206+
```bash
207+
CACHED_TOKEN_ID="$(jq -r '.tokenId' < ~/.cache/keploy-proxy.json)"
208+
CURRENT_TOKEN_ID="$(curl -sS "$PROXY/shared-token/info" | jq -r '.tokenId')"
209+
if [ "$CACHED_TOKEN_ID" != "$CURRENT_TOKEN_ID" ]; then
210+
# proxy was restarted, re-exchange the PAT
211+
RESP=$(curl -sS -X POST "$PROXY/get-shared-token" -H "Authorization: Bearer $PAT")
212+
echo "$RESP" > ~/.cache/keploy-proxy.json
213+
fi
180214
```
181215

182-
### Error status codes
216+
If you instead see a sudden `401 Invalid token` on a previously working `sharedToken`, that's the same signal: the proxy was rolled. Re-exchange the PAT and retry once.
217+
218+
### Exchange failure modes
219+
220+
| Status | When |
221+
| ------ | --------------------------------------------------------------------------------------------------------------------------- |
222+
| `401` | Missing/empty `Authorization` header, or the PAT is invalid, revoked, or expired. |
223+
| `403` | The PAT is valid but belongs to a different tenant than this proxy's cluster, or it does not include `write`/`admin` scope. |
224+
| `502` | The proxy could not reach the API server to validate the PAT (transient—retry). |
225+
| `503` | The proxy is still booting and has not authenticated to the API server yet (retry). |
183226

184-
| HTTP | When it happens |
185-
| ---- | ----------------------------------------------------------------------------------------------- |
186-
| 400 | Missing or malformed request body, missing required fields |
187-
| 401 | Missing or invalid `Authorization: Bearer` header |
188-
| 403 | Request touches a namespace outside `watchNamespace`, or image repo mismatch on `/proxy/update` |
189-
| 404 | Recording/replay session ID not found, or deployment/config does not exist |
190-
| 405 | Wrong HTTP method for the route |
191-
| 500 | Kubernetes API error, storage backend unavailable, or unexpected server error |
192-
| 503 | Kubernetes client or self-discovery not initialised (proxy is still starting or missing RBAC) |
227+
Under the hood, `POST /get-shared-token` calls `POST /cluster/pat/validate` on the API server (using the proxy's own cluster JWT) to verify the PAT, then returns the cached shared token only on success. The PAT is never echoed back, never stored on the proxy, and never logged in cleartext.
228+
229+
> The shared token is cluster-wide, not per-user. The PAT-exchange path authenticates who is allowed to bootstrap it, but the returned shared token should still be treated as a sensitive cluster credential.
193230
194231
---
195232

196233
## Quick start: Trigger and watch a live recording
197234

198-
The golden path: pick a Deployment, start a recording, stream its status, and stop it when you have the traffic you need.
235+
The golden path: authenticate, pick a Deployment, start a recording, stream its status, and stop it when you have the traffic you need.
199236

200-
### 1. Set up variables
237+
### 1. Set up variables and authenticate
201238

202239
```bash
203240
PROXY="https://k8s-proxy.example.com" # ingressUrl from Helm install
204-
AUTH="Authorization: Bearer $K8S_PROXY_SHARED_TOKEN"
241+
PAT="kep_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
205242
NS="default"
206243
DEPLOY="orders-api"
244+
245+
# One-time PAT → shared-token exchange (see Authentication above)
246+
K8S_PROXY_SHARED_TOKEN=$(curl -sS -X POST "$PROXY/get-shared-token" \
247+
-H "Authorization: Bearer $PAT" | jq -r '.sharedToken')
248+
249+
AUTH="Authorization: Bearer $K8S_PROXY_SHARED_TOKEN"
207250
```
208251

209252
### 2. Discover target Deployments
@@ -331,6 +374,15 @@ All paths are relative to the proxy base URL. Unless noted, every route requires
331374
| `GET` | `/healthz` | No | Liveness probe. Returns `{"status":"ok"}`. |
332375
| `POST` | `/mutate` | No | Kubernetes MutatingAdmissionWebhook endpoint. Do not call directly. |
333376

377+
### Bootstrap
378+
379+
| Method | Path | Auth | Description |
380+
| ------ | -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
381+
| `POST` | `/get-shared-token` | `Authorization: Bearer <PAT>` | Exchange a Personal Access Token for the proxy's shared token. See [Exchange the PAT for the shared token](#2-exchange-the-pat-for-the-shared-token) for details. |
382+
| `GET` | `/shared-token/info` | None | Return only the current `tokenId` and `issuedAt`; never the token itself. Use this to detect when the proxy has rotated the shared token (Pod restart, `helm upgrade`, self-update). |
383+
384+
`POST /get-shared-token` gates on a PAT instead of the shared token because the caller does not yet have one. `GET /shared-token/info` is unauthenticated because the response leaks no credential material. Every other route below requires `Authorization: Bearer <K8S_PROXY_SHARED_TOKEN>`.
385+
334386
### Deployments
335387

336388
| Method | Path | Description |

0 commit comments

Comments
 (0)