Set rate limit quotas without locking yourself out of removing them - #88
Merged
Merged
Conversation
Rate limit quotas were the last item on the after-v1.0 list reachable
without a cloud account. scripts/bootstrap-quotas.sh configures them and
tests/quotas exercises the result against a real cluster.
The feature is two `vault write` calls. The script exists because two of
the three obvious ways to make those calls look like they worked and did
not, and because one of them takes away your ability to undo it.
sys/quotas/config REPLACES, IT DOES NOT MERGE
Vault ships seven exempt paths, sys/health, sys/seal-status and
sys/unseal among them. Writing a single unrelated field to that endpoint
empties the list. Nothing warns you, and the next global quota then
applies to your load balancer's health checks and to unseal. So anything
that writes that endpoint has to write all of it, defaults included,
every time.
THE LIST IS A LIST
rate_limit_exempt_paths takes an array, and the CLI's k=v form does not
produce one:
rate_limit_exempt_paths="sys/health,sys/seal-status"
-> ["sys/health,sys/seal-status"]
One element, containing a comma, matching no path that exists. It writes
successfully and protects nothing. The script sends JSON on stdin and
then reads the list back and counts it, because the broken form and the
working form differ only in the length of an array nobody looks at.
A QUOTA RATE LIMITS THE ENDPOINT THAT REMOVES IT
sys/quotas/* is not exempt by default, so a global quota set too low
answers 429 to the DELETE that would remove it. Standbys do not help;
they forward to the leader, which is where the limiter is. The script
always exempts sys/quotas/config and sys/quotas/rate-limit, which turns
that outage into an inconvenience.
The way out, if it is already too late: send nothing for one full
interval, then spend the first request of the new window on the delete.
It returns 204. Any request before it — including the read you would
naturally run first to see what is happening — consumes the budget, and
you wait again. The suite creates that lockout deliberately and then
demonstrates the recovery.
429 DOES NOT IDENTIFY A QUOTA
A healthy Vault standby answers 429 on sys/health, which is why
terraform/aws matches 200,429 on the target group. A tripped quota also
answers 429, so a node refusing traffic stays in the pool, healthy by
every signal the infrastructure collects. This is the quorum-less node
answering 200 to standbyok=true, arriving from the other direction.
Response headers are on by default because that is what separates them:
the quota's 429 carries retry-after and x-ratelimit-*, and the standby's
carries neither. --no-headers exists and says what it costs.
Four of the suite's assertions are about Vault rather than the script,
for that reason: the script is shaped around those behaviours, and if any
stops being true the suite should say so rather than the script quietly
guarding nothing.
21 assertions. Three mutations, all watched to fail; the table is in the
suite's README. Q1 is the one worth reading — the mutation is not a
mistake, it is the form the CLI leads you to, and it writes successfully.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Markdown lint failed on the quotas change. MD013 applies to fenced code here — .markdownlint.yaml exempts tables and nothing else — and four `vault read`/`vault write` examples ran to 83-89 columns. Wrapped with backslash continuations, which is what the reader would type anyway. My own pre-push check missed them because it skipped fenced blocks, an exemption the config does not grant. Corrected, and run across every markdown file in the repository rather than the ones I remembered touching. The nine remaining long lines it reports elsewhere are pre-existing and pass CI, because MD013 does not flag a line with no whitespace past the limit — an unbreakable path or URL is allowed. Those are left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The last item on the after-v1.0 list reachable without a cloud account.
scripts/bootstrap-quotas.shconfigures quotas;tests/quotasexercises the result against a real cluster — 21 assertions.The feature is two
vault writecalls. The script exists because two of the three obvious ways to make those calls look like they worked and did not, and because one of them takes away your ability to undo it.sys/quotas/configreplaces, it does not mergeVault ships seven exempt paths —
sys/health,sys/seal-statusandsys/unsealamong them. Writing a single unrelated field empties the list:Nothing warns you. The next global quota then applies to your load balancer's health checks and to
sys/unseal, and you find out at the next restart.The list is a list
rate_limit_exempt_pathstakes an array, andk=vdoes not produce one:One element containing a comma, matching no path that exists. It writes successfully and protects nothing. The script sends JSON, then reads the list back and counts it — the broken form and the working one differ only in the length of an array nobody looks at.
A quota rate limits the endpoint that removes it
sys/quotas/*isn't exempt by default, so a quota set too low answers 429 to the DELETE that would remove it. Standbys don't help — they forward to the leader, which is where the limiter is.The script always exempts
sys/quotas/configandsys/quotas/rate-limit, turning that outage into an inconvenience. And if it's already too late: send nothing for one full interval, then spend the first request of the new window on the delete. It returns 204. Any request before it — including the read you'd naturally run first to see what's happening — consumes the budget and you wait again.The suite creates that lockout deliberately, then demonstrates the recovery.
429 does not identify a quota
A healthy standby answers 429 on
sys/health— which is whyterraform/awsmatches200,429on the target group. A tripped quota also answers 429, so a node refusing traffic stays in the pool, healthy by every signal the infrastructure collects. That's the quorum-less node answering 200 tostandbyok=true, arriving from the other direction.Response headers are on by default because that's what separates them:
retry-after,x-ratelimit-*Testing
Four assertions are about Vault rather than the script, because the script is shaped around those behaviours — if any stops being true, the suite should say so rather than the script quietly guarding nothing.
Three mutations, all watched to fail:
sys/quotasleft unexemptedQ1 is the one worth reading: the mutation isn't a mistake, it's the form the CLI leads you to, and it writes successfully.
Auto-classified slow by the #84 derivation, no Makefile edit. 28 suites, 38 jobs, all wired.
Not covered
Lease count, role- and namespace-scoped quotas are Enterprise. How the limit behaves across three nodes isn't established — what was measured is that a standby returns 429 for a request whose budget the leader had already spent.
🤖 Generated with Claude Code