Skip to content

Commit 41879c6

Browse files
docs(valkey): procedure to settle the RESP2 pin against a real Valkey [KAN-260]
KAN-209 shipped a scoped justification for `protocol: 2` (#3448) but could not close, because the justification is reasoning rather than a wire capture: we know ioredis 6 sends `HELLO 3 AUTH default <token>` under RESP3, and we know Google documents Memorystore IAM auth as password-only with no username — but we have never established that Memorystore actually REJECTS the RESP3 shape. Writes the procedure to find out. The key point is that the decisive test does not need a staging environment or an app deploy: it is a `valkey-cli` HELLO against a throwaway instance from a VM inside the VPC, ~15 minutes. Full app-level verification is optional and only worth doing if the handshake passes. Covers instance discovery (the deployed name differs from the planning doc's), which prod fields must be cloned and why, the probe VM, the three tests with a result-to-action table, teardown, and how to close KAN-209 in either direction — including that "pin verified necessary" is a real outcome, not a non-result. gcloud auth had expired when this was written, so instance names and the memorystore-vs-redis API surface are flagged unverified with a discovery step rather than guessed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvEu7ANRWdhTQVsLXhHgWj
1 parent ba8e039 commit 41879c6

1 file changed

Lines changed: 331 additions & 0 deletions

File tree

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# KAN-260 — settle the RESP2 pin against a real Valkey
2+
3+
> **Status:** not started · **Blocks:** KAN-209 (can't close on evidence without this)
4+
> **Written:** 2026-08-28, at the end of the Sprint 9 close-out session.
5+
> **Related:** KAN-209, KAN-182 (no staging env), PR #3448 (merged — scoped the pin's justification)
6+
7+
---
8+
9+
## What this is
10+
11+
`server/valkey.ts` pins the Valkey wire protocol to RESP2:
12+
13+
```ts
14+
protocol: 2,
15+
```
16+
17+
That pin is **conservative, not proven necessary**. This document is the procedure
18+
to find out which, so KAN-209 can close on evidence instead of on a guess.
19+
20+
**Do not remove the pin without running this.** And do not run the first attempt
21+
against production — see §0.
22+
23+
---
24+
25+
## The actual question (one sentence)
26+
27+
> Does Memorystore, with IAM auth enabled, accept ioredis 6's RESP3 handshake
28+
> `HELLO 3 AUTH default <iam-access-token>` — which sends a **username** — given
29+
> that Google documents IAM auth as password-only with **no** username?
30+
31+
### What is already established
32+
33+
- ioredis 6 defaults to `protocol: 3`.
34+
- Reply shapes are **not** the risk. ioredis' `replyMapping: "legacy"` default keeps
35+
replies identical to RESP2, and we only consume two: `rate-limit-redis`'s EVAL
36+
array and AUTH's simple string.
37+
- Under RESP3 ioredis authenticates via `HELLO`, and with a password-only
38+
credential it injects the username `default`
39+
(`ioredis/built/redis/event_handler.js`).
40+
- `refreshTokenInPlace()` in `server/valkey.ts` deliberately sends password-only
41+
`await client.call('AUTH', token)` — the form Google documents for Memorystore
42+
IAM auth ("the authentication uses the access token directly"; the CLI example
43+
is `valkey-cli -h HOST -p PORT -a ACCESS_TOKEN`, no `--user`).
44+
- A rejected AUTH is **not** a protocol-negotiation error, so ioredis' automatic
45+
RESP2 fallback (which only catches `NOPROTO` / unknown-command) would not
46+
rescue it. The client would simply fail to connect, and `getValkeyClient()`
47+
would silently degrade **every replica** to in-memory rate limiting — on the
48+
limiter metering paid Gemini and Imagen calls.
49+
50+
### What is NOT established
51+
52+
That Memorystore actually **rejects** the RESP3 shape. Google documents no
53+
required username; it documents sending none. Those are different claims, and
54+
only the test below tells them apart.
55+
56+
---
57+
58+
## §0 — Read this before you start
59+
60+
**The decisive test is ~15 minutes and needs no app deploy and no staging
61+
environment.** It is a `valkey-cli` handshake against a throwaway instance.
62+
Do that first (§1–§4). Only do the full app-level verification (§6) if you want
63+
belt-and-braces before shipping.
64+
65+
**Why a throwaway and not production:** `HELLO` mutates no data, so the test is
66+
read-safe. But a failed IAM handshake against prod is still an auth event on the
67+
instance the production limiter depends on, and if you fat-finger a `CONFIG` or
68+
`FLUSHALL` while poking around you have taken out the rate limiter. Spend the
69+
~$0.30 on a scratch instance.
70+
71+
**Memorystore is private-IP only.** You cannot reach it from your laptop. You
72+
need a VM inside the same VPC (§3). This is the same constraint that makes
73+
Cloud SQL Studio necessary for the database.
74+
75+
---
76+
77+
## §1 — Discover the real production config
78+
79+
`gcloud` auth had expired when this file was written, so the exact instance name
80+
and API surface below are **unverified**. Establish them first — everything else
81+
depends on it.
82+
83+
```bash
84+
gcloud auth login # run this yourself; it needs a browser
85+
86+
PROJECT=comdottasteslikegood
87+
REGION=us-central1
88+
89+
# Which API is this instance on? Try both — one will return nothing.
90+
gcloud memorystore instances list --project="$PROJECT" --location="$REGION"
91+
gcloud redis instances list --project="$PROJECT" --region="$REGION"
92+
```
93+
94+
Docs in this repo (`docs/MCP_GCP_MONITORING.md`) name the deployed instance
95+
**`veganchef-valkeymem`**, and explicitly note it differs from the
96+
`vegangenius-valkey` that appears in the planning doc. Trust the list output over
97+
both.
98+
99+
Dump the full config — this is what you will clone:
100+
101+
```bash
102+
INSTANCE=<name from the list above>
103+
104+
# Whichever API answered:
105+
gcloud memorystore instances describe "$INSTANCE" \
106+
--project="$PROJECT" --location="$REGION" --format=yaml > /tmp/valkey-prod.yaml
107+
# ...or:
108+
gcloud redis instances describe "$INSTANCE" \
109+
--project="$PROJECT" --region="$REGION" --format=yaml > /tmp/valkey-prod.yaml
110+
111+
cat /tmp/valkey-prod.yaml
112+
```
113+
114+
**Fields that must match on the clone**, because they are the ones under test:
115+
116+
| Field | Why it matters |
117+
| -------------------------------------------- | ----------------------------------------------------------- |
118+
| auth / authorization mode (IAM) | The whole question is about the IAM handshake |
119+
| transit encryption / TLS mode | TLS changes the `valkey-cli` invocation and the CA handling |
120+
| engine + version (Valkey vs Redis, x.y) | RESP3 support is version-dependent |
121+
| authorized network / VPC | Determines which VPC your test VM must live in |
122+
| connect mode (private service access vs PSC) | Determines whether a plain VM in the VPC can reach it |
123+
124+
Node type / shard count / size do **not** matter — use the smallest available.
125+
126+
---
127+
128+
## §2 — Create the throwaway instance
129+
130+
Fill the flags from `/tmp/valkey-prod.yaml`. Exact flag names differ between the
131+
`memorystore` and `redis` surfaces and between engine versions, so check:
132+
133+
```bash
134+
gcloud memorystore instances create --help # or: gcloud redis instances create --help
135+
```
136+
137+
Skeleton (Memorystore for Valkey):
138+
139+
```bash
140+
SCRATCH=valkey-resp3-scratch
141+
142+
gcloud memorystore instances create "$SCRATCH" \
143+
--project="$PROJECT" \
144+
--location="$REGION" \
145+
--node-type=<smallest available> \
146+
--shard-count=1 \
147+
--replica-count=0 \
148+
--engine-version=<match prod> \
149+
--authorization-mode=<match prod: IAM> \
150+
--transit-encryption-mode=<match prod> \
151+
--network=<authorized network from prod>
152+
```
153+
154+
Then capture the endpoint:
155+
156+
```bash
157+
gcloud memorystore instances describe "$SCRATCH" \
158+
--project="$PROJECT" --location="$REGION" \
159+
--format='value(discoveryEndpoints[0].address,discoveryEndpoints[0].port)'
160+
```
161+
162+
> ⚠️ **Set a teardown reminder right now.** An idle Memorystore instance bills
163+
> continuously. §5 is not optional.
164+
165+
---
166+
167+
## §3 — A VM inside the VPC to test from
168+
169+
```bash
170+
NETWORK=<authorized network from prod>
171+
SUBNET=<a subnet of that network in $REGION>
172+
173+
gcloud compute instances create valkey-probe \
174+
--project="$PROJECT" \
175+
--zone="${REGION}-a" \
176+
--machine-type=e2-micro \
177+
--network="$NETWORK" \
178+
--subnet="$SUBNET" \
179+
--no-address \
180+
--scopes=https://www.googleapis.com/auth/cloud-platform \
181+
--image-family=debian-12 --image-project=debian-cloud
182+
```
183+
184+
`--no-address` (no public IP) means you SSH via IAP:
185+
186+
```bash
187+
gcloud compute ssh valkey-probe --project="$PROJECT" --zone="${REGION}-a" --tunnel-through-iap
188+
```
189+
190+
If IAP is not enabled, either enable it or drop `--no-address` for the life of
191+
the probe. `--scopes=cloud-platform` is what lets the VM mint its own IAM access
192+
token in §4.
193+
194+
On the VM:
195+
196+
```bash
197+
sudo apt-get update && sudo apt-get install -y redis-tools
198+
# redis-cli speaks the Valkey protocol; if you want the real thing, build
199+
# valkey-cli from the valkey-io/valkey release tarball instead.
200+
```
201+
202+
---
203+
204+
## §4 — THE TEST (this is the whole point)
205+
206+
On the probe VM:
207+
208+
```bash
209+
HOST=<scratch instance address>
210+
PORT=<scratch instance port>
211+
212+
# The IAM access token IS the password for Memorystore IAM auth.
213+
TOKEN=$(gcloud auth print-access-token)
214+
```
215+
216+
If prod uses TLS, download the server CA into `ca.pem` first (the describe output
217+
tells you where it comes from) and add `--tls --cacert ca.pem` to every command
218+
below.
219+
220+
### Test A — the form we ship today (RESP2, password-only). Expect success.
221+
222+
```bash
223+
redis-cli -h "$HOST" -p "$PORT" -a "$TOKEN" --no-auth-warning PING
224+
# expect: PONG
225+
```
226+
227+
If this fails, stop — your instance config or token is wrong, not the protocol.
228+
229+
### Test B — the form ioredis 6 would send under RESP3. **This is the answer.**
230+
231+
```bash
232+
redis-cli -h "$HOST" -p "$PORT" --no-auth-warning
233+
# then, at the prompt:
234+
HELLO 3 AUTH default <paste $TOKEN>
235+
```
236+
237+
| Result | Meaning | Action |
238+
| --------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------- |
239+
| Returns a map (`server`, `version`, `proto: 3`, …) | Memorystore accepts a `default` username with an IAM token under RESP3 | **The pin is unnecessary.** Go to §6, then remove it. |
240+
| `WRONGPASS` / `invalid username-password pair` / any auth error | The username breaks IAM auth, exactly as feared | **The pin is load-bearing.** Keep it, and record this as the proof. |
241+
| `NOPROTO` / unknown command `HELLO` | The engine predates RESP3 entirely | Pin stays; note the engine version. |
242+
243+
### Test C — token refresh under RESP3 (only if B succeeded)
244+
245+
`refreshTokenInPlace()` re-authenticates a live connection with password-only
246+
`AUTH <token>`. Confirm that still works on a connection that negotiated RESP3:
247+
248+
```bash
249+
# in the same redis-cli session that ran HELLO 3 successfully:
250+
AUTH <a freshly minted $TOKEN>
251+
# expect: OK
252+
PING
253+
# expect: PONG
254+
```
255+
256+
If B passes but C fails, the pin still comes out **only** if
257+
`refreshTokenInPlace()` is changed to match — that is a code change, not just a
258+
flag flip. Record it.
259+
260+
**Copy the raw terminal output of B and C into KAN-209.** The whole reason this
261+
ticket is open is that the current justification is reasoning, not a capture.
262+
263+
---
264+
265+
## §5 — TEAR DOWN (do not skip)
266+
267+
```bash
268+
gcloud compute instances delete valkey-probe \
269+
--project="$PROJECT" --zone="${REGION}-a" --quiet
270+
271+
gcloud memorystore instances delete "$SCRATCH" \
272+
--project="$PROJECT" --location="$REGION" --quiet
273+
# ...or the `gcloud redis instances delete` equivalent.
274+
```
275+
276+
Verify both are gone:
277+
278+
```bash
279+
gcloud memorystore instances list --project="$PROJECT" --location="$REGION"
280+
gcloud compute instances list --project="$PROJECT" --filter='name:valkey-probe'
281+
```
282+
283+
---
284+
285+
## §6 — Optional app-level verification (only if §4B passed)
286+
287+
Only worth doing if you intend to actually remove the pin.
288+
289+
1. In `server/valkey.ts`, change `protocol: 2``protocol: 3` (or delete the
290+
line, since 3 is the ioredis 6 default).
291+
2. Point a staging Express at the scratch instance:
292+
`VALKEY_HOST`, `VALKEY_PORT`, `VALKEY_AUTH_MODE`, and `VALKEY_CA_CERT` if TLS.
293+
(`VALKEY_TLS_INSECURE=true` exists but is dev-only — do not use it here; it
294+
would hide precisely the TLS/CA problems you are testing for.)
295+
3. Confirm in the logs that you get **neither**
296+
`[Valkey] VALKEY_HOST not set — using in-memory rate limiting` **nor** a
297+
connection failure. Silent degradation to in-memory is the failure mode that
298+
makes this dangerous, and it does not announce itself as an error.
299+
4. Drive enough requests to trip the limiter and confirm the count is shared
300+
across replicas (that is what proves Valkey is actually backing it, not the
301+
in-memory fallback).
302+
303+
---
304+
305+
## §7 — Closing out
306+
307+
**If the pin is unnecessary (§4B returned a map):**
308+
309+
- Remove `protocol: 2` and the now-obsolete half of the comment block.
310+
- Keep the part documenting that IAM auth is password-only — that is still true
311+
and still explains `refreshTokenInPlace()`.
312+
- Paste the §4 capture into KAN-209 and close it.
313+
314+
**If the pin is load-bearing (§4B errored):**
315+
316+
- Leave the code exactly as it is.
317+
- Replace the comment's hedged "this is a deviation from documented guidance,
318+
NOT evidence that Memorystore rejects the RESP3 shape" with the actual error
319+
string and the date it was captured.
320+
- Close KAN-209 as "verified necessary" — that is a real outcome, not a
321+
non-result.
322+
323+
Either way KAN-260 closes when the capture is on the ticket.
324+
325+
---
326+
327+
## Cost note
328+
329+
An `e2-micro` plus the smallest Memorystore node for under an hour is roughly
330+
pocket change. The thing that costs money is **forgetting §5**. Set the reminder
331+
when you create the instance, not after.

0 commit comments

Comments
 (0)