fix(controller): stop a data-plane outage from revoking issued credentials - #41
Merged
Merged
Conversation
duckhawk
force-pushed
the
fix/transient-outage-revokes-credentials
branch
from
July 29, 2026 12:52
7a35526 to
6674114
Compare
duckhawk
force-pushed
the
fix/transient-outage-revokes-credentials
branch
from
July 29, 2026 13:31
6674114 to
c508f18
Compare
duckhawk
force-pushed
the
fix/transient-outage-revokes-credentials
branch
from
July 30, 2026 02:00
c508f18 to
9a8c585
Compare
duckhawk
force-pushed
the
test/e2e-registry-mode-pin
branch
from
July 30, 2026 16:52
038bbec to
8fbafae
Compare
duckhawk
force-pushed
the
fix/transient-outage-revokes-credentials
branch
from
July 30, 2026 16:52
930b949 to
de56db1
Compare
…tials
The e2e run on the current main fails system-durability ("keeps every replica on
its own volume and node across a full data-plane restart"): after the restart the
BucketAccess reports a different access key than before —
a restart must not look like a recreate: the backend keys must still be there
Expected <string>: GK1e67171c989402cec63e48a3
to equal <string>: GK89d24858ab7875a1873da8eb
and the controller log shows why. While the Garage pods were down it was calling
DeleteKey for every issued access, over and over:
Reconciler error err="delete access key: Post \".../v2/DeleteKey?id=GK89d2…\":
dial tcp …:3903: connect: connection refused"
Not the delete path: the accesses were alive and Ready again afterwards. It is the
gate in reconcileNormal, which lumped `bucket is not Ready` together with `claim is
no longer bound` and `namespace is no longer authorized` and revoked on all three.
A restarting store flips its Bucket out of Ready, so every access on it lost its
backend key and its credentials Secret; once the store came back, EnsureAccess
found the recorded key gone and minted a fresh one. The caller's credentials were
silently rotated because of an outage the controller only had to wait out.
Revocation is now driven by intent, not by health. A bucket that exists but is not
Ready reports AccessGranted=False/InProgress and requeues, leaving the grant and
the Secret untouched. The claim losing its binding, the bucket or store being gone,
and the namespace losing its authorization all still revoke immediately — the
deny-by-default enforcement is unchanged.
Tested: an unready bucket must not touch the backend, the status or the Secret (the
test fails on the old gate exactly the way the cluster did); a withdrawn policy must
still revoke with DeniedByPolicy; an unbound claim must still revoke.
Two fixes to the test side come along, both found on the same run:
The e2e pod dump could only say `phase=Pending ready=false`, so the run that lost
its three-hour budget to two Pending Garage pods — the System store's third replica
and a brand-new Lightweight store's first pod — never printed the scheduler's own
answer, the node the pod did or did not land on, or the cluster's node list. The
dump now carries the node and, for anything unplaced, the PodScheduled reason and
message; a node dump lists role, Ready (with reason when it is not), unschedulable
and taints. The control-plane count in particular drives the System profile's
placement, so a resize that half-succeeded is now visible directly.
And a Garage unit test dereferenced a Secret it had only checked with Errorf, so a
missing node identity panicked instead of reporting what went wrong — staticcheck
flags it, which surfaced as a red Go linter on one runner and a green one on
another for the same tree. Split the nil case out as Fatalf.
Signed-off-by: v.oleynikov <vasily.oleynikov@flant.com>
duckhawk
force-pushed
the
fix/transient-outage-revokes-credentials
branch
from
July 30, 2026 16:57
de56db1 to
dc3a633
Compare
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.
Symptom
The e2e run on the current
mainfailssystem-durability→ keeps every replica on its own volume and node across a full data-plane restart. Everything about the volumes is fine — same PVC → same PV → same node, pods back on their pinned nodes, the stored object still readable — and then the last assertion catches it:The access key issued to the caller is not the one it had before the restart.
Cause
The controller log, taken while the Garage pods were down, says it plainly — it was deleting the issued keys, retry after retry, for both accesses on the store:
Not the delete path: both
BucketAccessobjects were alive and back to Ready afterwards. It is the gate inreconcileNormal, which treated three very different situations as one:A restarting store flips its
Bucketout ofReady. That is a health signal, but the gate read it as "the claim is no longer usable" and revoked: backend key deleted, credentials Secret deleted,status.accessKeyIDcleared. Once the store came back,EnsureAccessfound no recorded key and minted a fresh one — so a caller's credentials were silently rotated because of an outage the controller only had to wait out. The bigger the store, the longer the window: every access on it is affected for as long as the data plane is down.Fix
Revocation is driven by intent, not by health:
Ready(restart, backend unreachable, still coming up)AccessGranted=False/InProgress, key and Secret untouched, requeueBucketClaimPolicyThe deny-by-default enforcement is untouched: losing authorization still revokes on the spot, and it is still re-checked on every reconcile of a granted access.
Tests
bucket_access_revoke_test.go, three unit tests over a recording driver:status.accessKeyID/status.secretRefand the credentials Secret, and must reportFalse/InProgress— this one fails on the old gate exactly the way the cluster did (the backend key was revoked 1 time(s),status.accessKeyID = "");DeniedByPolicy;Boundstill revokes.The
system-durabilitye2e spec is the end-to-end check — it is what caught this.Base
Found while running the e2e suite for the Commander
registry_modefix and unrelated to it, so it was originally stacked on #40 to get both into one e2e run. #40 is merged now, and this is a single commit rebased straight ontomain— no dependency left, nothing to merge first.Also in here
Two fixes to the test side, both found on the same run and squashed into the same commit:
phase=Pending ready=false, so the run that lost its three-hour budget to two Pending Garage pods never printed the scheduler's own answer. It now carries the node and, for anything unplaced, thePodScheduledreason and message, plus a node dump with role, Ready, unschedulable and taints;Secretit had only checked withErrorf, so a missing node identity panicked instead of reporting what went wrong —staticcheckflags it, which is why the Go linter was red on one runner and green on another for the same tree. Split out asFatalf.