The cache keeps no ACL file, and the watch keeps the cache

The lab's diagnostics said it in one line: AUTH called without any
password configured for the default user. With an aclfile configured,
redis takes the default user from the file and quietly ignores
requirepass — so the empty seed this module shipped left the store
without any password at all, politely refusing the credential the mesh
had sealed for it.

And the file could never have worked here anyway: it was host-declared
content, which the host reconciles, so every re-apply would have wiped
what ACL SAVE wrote — a fight between two reconcilers with the tenants
as the ball.

So no file. requirepass alone does what it says, and durability moves
to the watch, which now checks the store and not only its inputs: a
restarted store comes back empty and is re-granted within a tick,
because reconciling is against reality, not against a diff of
instructions. A run that failed leaves last empty, so the next tick
retries instead of believing the inputs were handled.
This commit is contained in:
2026-09-02 01:23:38 +02:00
parent 1b63e21c0f
commit 1c4e10e0d8
2 changed files with 57 additions and 17 deletions
+56 -8
View File
@@ -86,9 +86,15 @@ func watch(ctx context.Context) error {
switch {
case err != nil:
fmt.Fprintf(os.Stderr, "cannot read what was granted: %v\n", err)
case state != last:
case state != last || !granted(ctx):
// Re-run when the inputs changed — or when the store no longer holds what was
// granted. The store keeps its users in memory, so a restart forgets every tenant;
// nothing rewrites the grants when that happens, and a watch that only re-read its
// inputs would leave the store empty until the next unrelated change. Reconciling is
// against reality, not against a diff of instructions.
if err := run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
last = ""
} else {
last = state
}
@@ -207,16 +213,58 @@ func run(ctx context.Context) error {
fmt.Printf("revoked %s\n", user)
}
// Persisted, or the next restart forgets every grant. The server runs with an aclfile for
// exactly this; a server without one refuses the save, and saying so beats a cache that
// silently loses its tenants on restart.
if _, err := r.do("ACL", "SAVE"); err != nil {
return fmt.Errorf("the grants were applied and could not be persisted — a restart would "+
"forget them: %w", err)
}
// Not persisted in the store, on purpose. An ACL file was tried and lost twice over: with
// one configured, redis takes the default user from the file and quietly ignores
// `requirepass`, so an empty seed left the store without any password at all — and the file
// is host-declared content, which the host reconciles, so every re-apply would have wiped
// what ACL SAVE wrote. Durability lives in the watch instead: a restarted store comes back
// empty and is re-granted within a tick, because the watch checks the store and not only
// its inputs.
return nil
}
// granted says whether the store still holds every user the mesh has granted — cheaply, so the
// watch can ask every tick.
func granted(ctx context.Context) bool {
raw, err := os.ReadFile(filepath.Join(grantsDir(), "mesh.json"))
if err != nil {
// Nothing granted (or nothing readable): nothing to be missing.
return true
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
return true
}
wanted := map[string]bool{}
for _, c := range m.Given {
if c.Node != "" && strings.HasPrefix(c.As, mark) {
wanted[c.As] = true
}
}
if len(wanted) == 0 {
return true
}
r, err := connect(ctx)
if err != nil {
return false
}
defer r.Close()
users, err := r.strings("ACL", "USERS")
if err != nil {
return false
}
holds := map[string]bool{}
for _, u := range users {
holds[u] = true
}
for u := range wanted {
if !holds[u] {
return false
}
}
return true
}
// resp is the five commands this needs, spoken directly.
type resp struct {
conn net.Conn