|
1 | 1 | --- |
2 | 2 | name: create-rpc-api-key |
3 | | -description: Add a new public RPC API key consumer to an Aztec RPC environment (mainnet or testnet) — mint the GCP Secret Manager secret with the helper script, wire it into Kong via Terraform, apply, verify it is live, and open the PR. Use when asked to add/create an RPC API key, onboard a new RPC client/consumer, or "give <someone> a mainnet/testnet rpc key". |
| 3 | +description: Manage public RPC API key consumers for an Aztec RPC environment (mainnet or testnet) — add a new consumer end-to-end (mint the GCP Secret Manager secret, wire it into Kong via Terraform, apply, verify, open the PR), list existing consumers and their real owners, and check or change a consumer's per-minute rate limit. Use when asked to add/create an RPC API key, onboard a new RPC client/consumer, "give <someone> a mainnet/testnet rpc key", or to list/inspect/raise/lower an RPC consumer's rate limit. |
4 | 4 | --- |
5 | 5 |
|
6 | | -# Create a public RPC API key |
| 6 | +# Manage public RPC API keys |
7 | 7 |
|
8 | | -Adds one API key for the public RPC gateway. A key is a value stored in GCP Secret Manager; Terraform turns it into a Kong consumer that can call every keyed route in the environment. Externally the consumer is just `clientN`; the real recipient is recorded only as a private annotation on the secret. |
| 8 | +This runbook covers the full lifecycle of a public RPC gateway consumer: **creating** a key (Steps 1–6), **listing** who has keys, and **checking / changing** a consumer's rate limit. |
| 9 | + |
| 10 | +A key is a value stored in GCP Secret Manager; Terraform turns it into a Kong consumer that can call every keyed route in the environment. Externally the consumer is just `clientN`; the real recipient is recorded only as a private `client_name` annotation on the secret. |
9 | 11 |
|
10 | 12 | The two RPC environments live in `spartan/terraform/deploy-rpc/environments/`: |
11 | 13 |
|
12 | 14 | | Environment | dir | secret prefix | namespace / release prefix | |
13 | 15 | |---|---|---|---| |
14 | 16 | | mainnet | `environments/mainnet` | `mainnet-rpc-consumer` | `mainnet-rpc` / `mainnet` | |
15 | | -| testnet | `environments/testnet` | (check that env's `CONSUMERS` entries) | `testnet-rpc` / `testnet` | |
| 17 | +| testnet | `environments/testnet` | `testnet-rpc-consumer` | `testnet-rpc` / `testnet` | |
| 18 | + |
| 19 | +The examples below use **mainnet**. For testnet, swap the dir, prefix, namespace, and context. Always read the target env's existing consumer entries and copy their exact prefix rather than assuming. |
| 20 | + |
| 21 | +## Two consumer-config shapes (mainnet vs testnet differ) |
16 | 22 |
|
17 | | -The examples below use **mainnet**. For testnet, swap the dir, prefix, namespace, and context. Always read the target env's existing `CONSUMERS` entries and copy their exact prefix rather than assuming. |
| 23 | +The `CONSUMERS` map passed to the environment module is written differently in each env — read the target env's `main.tf` before editing: |
| 24 | + |
| 25 | +- **mainnet** — an **explicit map**, one `clientN` block per consumer, each with its own `rate_limit_minute`. This is where you set an individual mainnet consumer's limit. |
| 26 | +- **testnet** — **generated** from a `consumer_secret_names` list via a `for` comprehension that hardcodes `rate_limit_minute = 0` for every entry. So every testnet key is unlimited by construction, and there is no per-consumer limit knob unless you break a consumer out of the loop into its own explicit entry. |
| 27 | + |
| 28 | +**Rate-limit tiers.** `rate_limit_minute = 0` means **unlimited** (no rate-limit plugin is generated; see the `consumers_with_rate_limit` filter in `spartan/terraform/modules/rpc-gateway/main.tf`). A positive value generates a `rate-limiting` KongPlugin with `policy = "local"` and `limit_by = "consumer"` — **`local` means the limit is enforced per Kong pod**, so the effective ceiling is `rate_limit_minute × (number of Kong gateway pods)`. When `ALLOW_ANONYMOUS = true` (testnet), keyless callers fall back to an anonymous consumer capped at `ANONYMOUS_RATE_LIMIT_MINUTE` (default 300) **per IP, per pod**; mainnet sets `ALLOW_ANONYMOUS = false` so a key is mandatory there. |
18 | 29 |
|
19 | 30 | ## Before you start — access and tooling |
20 | 31 |
|
@@ -63,6 +74,17 @@ Edit `spartan/terraform/deploy-rpc/environments/<env>/main.tf` and append a `cli |
63 | 74 |
|
64 | 75 | `rate_limit_minute = 0` means unlimited (no rate-limit plugin is generated). Use a positive value only if a cap is intended. |
65 | 76 |
|
| 77 | +**testnet variant.** testnet does not use an explicit map — it builds `CONSUMERS` from a `consumer_secret_names` list. Add the new secret name to that list instead: |
| 78 | + |
| 79 | +```hcl |
| 80 | + consumer_secret_names = [ |
| 81 | + "testnet-rpc-consumer-client1", |
| 82 | + "testnet-rpc-consumer-client2", # new |
| 83 | + ] |
| 84 | +``` |
| 85 | + |
| 86 | +Every entry inherits `rate_limit_minute = 0` (unlimited) from the comprehension. To give one testnet consumer a finite cap, it cannot stay in the uniform loop — break it out into its own explicit `CONSUMERS` entry (or merge an override) with the desired `rate_limit_minute`. |
| 87 | + |
66 | 88 | ## Step 3 — plan and verify (the guardrail) |
67 | 89 |
|
68 | 90 | ```bash |
@@ -138,3 +160,69 @@ It is a bearer credential — share it over a secure channel, not plain chat/ema |
138 | 160 | - Header form: `x-aztec-api-key: <KEY>` against `https://canonical.mainnet.rpc.aztec-labs.com/` |
139 | 161 |
|
140 | 162 | The consumer is defined at the environment level, so the key authenticates on **every** keyed route in that environment (e.g. mainnet `v4`, `canonical`, and `v5` hosts) — one key, all routes. |
| 163 | + |
| 164 | +## Listing consumers and their owners |
| 165 | + |
| 166 | +The `clientN` → real-recipient mapping lives **only** in the `client_name` annotation on each GCP secret — it is never in the committed Terraform or in the cluster. To find who owns which key, list the secrets with their annotation: |
| 167 | + |
| 168 | +```bash |
| 169 | +# one env |
| 170 | +gcloud secrets list --filter="name:mainnet-rpc-consumer" \ |
| 171 | + --format="table(name.basename(), createTime, annotations.client_name)" |
| 172 | + |
| 173 | +# every RPC consumer across all envs (mainnet, testnet, staging, testnet-prover, eth-sepolia, …) |
| 174 | +gcloud secrets list --filter="name~rpc-consumer" \ |
| 175 | + --format="value(name.basename(), annotations.client_name)" | sort |
| 176 | +``` |
| 177 | + |
| 178 | +To find a specific person's key, grep the annotation column — the owner may not have a key at all (in which case, on testnet, they are just using the anonymous tier and "bumping" them means creating a key): |
| 179 | + |
| 180 | +```bash |
| 181 | +gcloud secrets list --filter="name~rpc-consumer" --format="value(name.basename(), annotations.client_name)" | grep -i <name> |
| 182 | +``` |
| 183 | + |
| 184 | +Cross-check against what is actually live in the cluster (the committed `main.tf` can lag behind live — always reconcile against `origin/merge-train/spartan`, not a possibly-stale checkout): |
| 185 | + |
| 186 | +```bash |
| 187 | +CTX=gke_testnet-440309_us-west1-a_aztec-gke-public |
| 188 | +kubectl --context "$CTX" -n <ns>-rpc get kongconsumer |
| 189 | +``` |
| 190 | + |
| 191 | +## Checking a consumer's rate limit |
| 192 | + |
| 193 | +The declared limit is the consumer's `rate_limit_minute` in the env `main.tf` (`0` = unlimited). To read the **effective live** limit instead of the declared one: |
| 194 | + |
| 195 | +```bash |
| 196 | +CTX=gke_testnet-440309_us-west1-a_aztec-gke-public |
| 197 | +NS=testnet-rpc |
| 198 | + |
| 199 | +# does the consumer carry a rate-limit plugin annotation? (absent ⇒ unlimited) |
| 200 | +kubectl --context "$CTX" -n "$NS" get kongconsumer <release-prefix>-<key> -o jsonpath='{.metadata.annotations}'; echo |
| 201 | + |
| 202 | +# the per-consumer rate-limit plugin config, if any |
| 203 | +kubectl --context "$CTX" -n "$NS" get kongplugin <release-prefix>-<key>-rpc-rate-limit -o jsonpath='{.config}'; echo |
| 204 | + |
| 205 | +# the anonymous (keyless) limit, per route — testnet only |
| 206 | +kubectl --context "$CTX" -n "$NS" get kongplugin <release-prefix>-<route>-anonymous-rpc-rate-limit -o jsonpath='{.config}'; echo |
| 207 | + |
| 208 | +# Kong pod count — the local policy multiplies the limit by this |
| 209 | +kubectl --context "$CTX" -n "$NS" get pods | grep kong-gateway |
| 210 | +``` |
| 211 | + |
| 212 | +Remember `policy = "local"`: the real ceiling is `minute × (Kong gateway pod count)`. With a single gateway pod, the number in the plugin is the number the client sees. |
| 213 | + |
| 214 | +## Changing a consumer's rate limit |
| 215 | + |
| 216 | +Editing `rate_limit_minute` for an existing consumer, then `plan`/`apply` exactly as in Steps 3–4. **The Step 3 guardrail still applies**: the plan must touch only that one consumer's rate-limit plugin and its `KongConsumer` annotation — nothing else. Expected plan deltas by transition: |
| 217 | + |
| 218 | +| Change | Expected plan | |
| 219 | +|---|---| |
| 220 | +| `0` → `N` (add a cap) | `1 to add, 1 to change` — creates the rate-limit `KongPlugin`, annotates the consumer to use it | |
| 221 | +| `N` → `M` (both > 0) | `1 to change` — just the plugin's `minute` | |
| 222 | +| `N` → `0` (remove the cap) | `1 to change, 1 to destroy` — drops the plugin, removes the consumer annotation | |
| 223 | + |
| 224 | +Notes: |
| 225 | + |
| 226 | +- **testnet caveat**: a testnet consumer generated by the `consumer_secret_names` loop is uniformly `rate_limit_minute = 0`. You cannot raise *one* testnet consumer above/below the others without pulling it out of the loop into an explicit `CONSUMERS` entry (see the testnet variant under Step 2). If the intent is simply "more than the 300/min anonymous tier", giving them a key already makes them unlimited — no per-consumer plugin needed. |
| 227 | +- **Raising the anonymous tier** (testnet, keyless users) is a different lever: set `ANONYMOUS_RATE_LIMIT_MINUTE` on the testnet env module (default 300). This affects **every** keyless caller, not one person. |
| 228 | +- Commit message convention for a limit change is a normal `chore:`/`fix:` describing the change (not `chore: add new client`), on base `merge-train/spartan`. Do not name the private owner in the PR. |
0 commit comments