A read rule over one branch of secret names silently matched a neighbouring branch — what about prefix matching caused that?
answer
- a shape, not a list
- over-matching never fails loudly
- characters against name segments
- anchor on the separator
- test the names it must refuse
basics
~20 sThe rule matched characters rather than whole name segments, so a pattern written for teams/search also covered teams/search-internal and teams/searchtest. Anchoring the pattern on the separator, and stating how deep it reaches, is what turns a prefix into a boundary.
solid answer
~40 sA rule over a branch of names is a pattern, and the pattern's matching rules decide where the branch ends. If matching is over characters, `teams/search` is a prefix of `teams/search-internal/prod/root-db` too, so a neighbouring team's values fall inside a rule nobody meant to widen — and nothing fails, because over-matching only ever grants. The second half of the same problem is depth: a pattern whose wildcard consumes one segment stops at `teams/search/prod/index-writer-db` and refuses `teams/search/prod/eu/index-writer-db`, so a team that nests one level deeper starts getting refusals that look like an outage. Anchor the pattern on the separator, say explicitly how far down it reaches, and test it against the names it must refuse, not only the ones it must allow.
code
yaml · 18 linesrule:
grantedTo: search-indexer
rights: [read]
nameMatch: "teams/search*" # character prefix, not anchored on the separator
namesMatched:
- teams/search/prod/index-writer-db # intended
- teams/search-internal/prod/root-db # not intended: another team
- teams/searchtest/staging/api-key # not intended: sandbox branch
anchoredRule:
grantedTo: search-indexer
rights: [read]
nameMatch: "teams/search/**" # continues only at a separator, any depth below
namesMatchedAfter:
- teams/search/prod/index-writer-db
- teams/search/prod/eu/index-writer-dbgo deeper
Know that a branch rule is a pattern matched against the name in each request, not a list of the values it covers today.
Explain both directions of failure: a character prefix reaching a sibling branch, and a wildcard that covers one level refusing a value nested deeper.
Demonstrate the operational habit — a refusal list including a similarly named neighbour, and a rule review after any restructure of the name space.
Set the convention that makes the boundary cheap: where the boundary segment sits, which wildcard form teams use, and what is deny-by-default around it.
## What a rule over a branch really is A rule granting rights over *a branch of names* does not hold a list of values. It holds a **pattern**, and the store evaluates that pattern against the name in each request. Everything surprising about branch-scoped rules follows from that one fact: the scope is a shape, not a membership list, and the shape is only as precise as the matching rules behind it. ## The over-match: characters instead of segments The classic failure is a pattern anchored on a name rather than on a separator. Take a rule written for the search team: - pattern `teams/search*` - intended: `teams/search/prod/index-writer-db` - also matched: `teams/search-internal/prod/root-db` — a different team - also matched: `teams/searchtest/staging/api-key` — a sandbox nobody reviewed Nothing about this fails loudly. Over-matching only ever **grants**, so there is no refusal to investigate, no error in a log, no failed deployment. It surfaces when someone reviews the rule, or when the neighbouring branch turns out to have been readable all along after an incident. The fix is to anchor on the separator — `teams/search/` — so the pattern can only continue at a segment boundary. Some stores match by segment natively and the question does not arise; some match by characters; some let the rule author choose. That difference is precisely the thing to check in the store you actually run, and the reason the answer to *does my rule cover only my branch* is **test it**, not **read it**. ## The under-match: depth The mirror image bites in production instead of in review. A pattern whose wildcard consumes **one** segment covers `teams/search/prod/index-writer-db` and refuses `teams/search/prod/eu/index-writer-db`. The day a team introduces a region segment, or splits a value into a sub-branch, reads start being refused — and the refusal looks like the store being broken rather than like a rule being precise. | pattern | matches | does not match | |---|---|---| | `teams/search*` | `teams/search/prod/index-writer-db`, `teams/search-internal/prod/root-db` | `platform/search/prod/index-writer-db` | | `teams/search/*` (one segment) | `teams/search/prod` | `teams/search/prod/eu/index-writer-db` | | `teams/search/**` (all below) | everything under the branch, at any depth | `teams/search-internal/...` | The temptation at that moment is to widen the pattern until the refusals stop, which is how a rule scoped to one environment quietly becomes a rule over a team, then over the tree. Widening under pressure is how most over-broad grants are actually born. ## Names that do not exist yet Because the scope is a shape, **anything created later under that shape is inside the grant from the moment it exists**. That is the feature — a team adds a value and its own service can read it without a rule change — and it is the hazard, because a name created under a branch by anyone who can write there lands inside every rule that matches it. When you review a rule, you are reviewing what it will match, not what it matches today. ## Where designs genuinely differ Do not state one store's evaluation as a fact about stores. Real differences you should ask about rather than assume: - Whether a **deny** rule exists at all, and whether it beats an allow that also matches. - Whether several matching rules are **unioned**, or the **most specific** one is taken alone. - Whether matching is over characters or segments, and whether one wildcard form means *one level* and another means *all levels below*. - Whether a rule on a branch implies the right to list it, or listing must be granted separately. ## Making the boundary hold 1. Anchor every pattern on the separator so a sibling whose name starts with the same characters cannot fall in. 2. State the depth deliberately — one level or everything below — rather than inheriting whichever the default is. 3. Write down the names the rule must **refuse**, including one neighbour with a similar name, and check them the way you check the names it must allow. 4. Re-check the rule whenever the name space is restructured; a restructure moves values across boundaries without touching a single rule. 5. Where the store has deny rules, put one at the boundary between teams so an over-broad pattern elsewhere cannot reach across it.
- Why does an over-broad branch rule usually go unnoticed for months?Because over-matching only grants. There is no refusal, no failed request and no broken deployment to investigate — the extra access is exercised only if something goes looking for it. Under-matching is the opposite: it fails immediately and loudly, which is why depth mistakes get fixed and width mistakes do not.
- A restructure moves a team's values one level deeper. What do you check before it ships?Every rule whose pattern mentions any segment of the old names, in both directions: which reads will start being refused because the pattern no longer reaches, and which values now sit inside a pattern that previously missed them. The rules do not change during a restructure, so the boundary moves silently.
- How do you test a branch rule properly?With a list of names it must allow and a list it must refuse, evaluated against the rule as written. The refusal list is the one that catches real defects, and it should always include a sibling branch whose name shares a prefix with the intended one.
saying these in an interview costs you the question
- Assumes a prefix always stops at a separator
- Tests a rule only against names it should allow
- Widens the pattern until refusals stop
- Thinks a rule covers only names existing when it was written
- States one store's match and precedence rules as universal
- Treats a restructure as safe because no rule text changed