Skip to content

feat(policy): deny security groups by name in the example policy - #23

Merged
plusky merged 1 commit into
mainfrom
feat/policy-security-group-default
Jul 28, 2026
Merged

feat(policy): deny security groups by name in the example policy#23
plusky merged 1 commit into
mainfrom
feat/policy-security-group-default

Conversation

@plusky

@plusky plusky commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What

Adds a dedicated security-groups deny rule to examples/policy.toml, matching group names by glob (*security*, *embargo*, *internal*), placed directly below the broad group-restricted rule, plus a test pinning the shipped file's behaviour. Header scenario and the broad rule's narrowing advice updated to match.

Why

The example hid a security-internal bug only through rules incidental to "security": group-restricted (matches ANY group) and undisclosed-marker (an "embargo" substring in the title). The file's own comment invites operators to narrow the broad rule to their instance's group names — and an operator who followed that advice, or dropped the rule to make partner-confidential bugs visible, ended up with no rule naming the security groups at all. A security-group bug whose title carries no marker would then be served in full.

The new rule is redundant while group-restricted keeps its full breadth (first match wins, so it is simply never reached) and becomes load-bearing the moment that rule is narrowed or removed. The three stems cover the common naming families for sensitive groups — the security team's own groups, embargo-state groups, and internality-named groups — and the comment tells operators that group vocabularies are per-instance and to check theirs. Over-matching only hides more (safe direction); a missed name is what the broad rule exists to catch.

Adversarial review against DESIGN.md invariants

  • The rule is action = "deny" with no capabilities: it can only remove access, never grant it, so it cannot weaken any existing guarantee.
  • It consults groups, which is unknowable for a not-yet-created bug, so the file's documented refuse-all-creation property now survives removal of the broad rule too (I4 fail-closed, unknown-metadata tri-state).
  • No engine change: any-glob-any group matching, first-match-wins, and Unknown⇒deny already behave as required; this is defence in depth at the shipped-default layer only.

Considered but not changed: security products at any age

Under a narrowed rule 1, a world-readable bug in a security-named product is visible unless a fresh-security-* rule holds it back — and those rules match keywords and whiteboard tags, not products, so an unlabelled one is visible at any age (this was already true before this change). I deliberately did not add a product-based always-deny: the example's stated scenario is that security-labelled bugs become visible after their freshness window, and a bug that is world-readable on the instance, unmarked, unkeyworded, and past the window is disclosed by the instance itself — hiding it forever would contradict the file's documented 5-day disclosure model without adding confidentiality. The dangerous case (an incident that must stay hidden) is characterised precisely by its group restriction, which is what the new rule pins by name. Operators with a stricter stance can add a products = ["*security*"] deny themselves.

Verification

  • New test shipped_example_policy_denies_security_named_groups_on_their_own: a bug in a security-named group with no other deniable signal (no marker, no keywords, empty whiteboard, years old) is denied by the shipped file; after excising the whole group-restricted rule from the policy text it is still denied, and Access::Denied { rule } proves security-groups itself decides; a restricted non-security bug becomes visible under the narrowed policy, which is what narrowing is for.
  • Mutation check: with the security-groups rule additionally deleted from the policy text, that test fails ("removing the broad rule must not expose security-group bugs").
  • cargo test --workspace --locked (195 passed), cargo clippy --workspace --all-targets --locked -- -D warnings, cargo fmt --check, typos — all clean.

The example hides a security-internal bug through two rules that are
both incidental to "security": group-restricted (any group at all) and
undisclosed-marker (an "embargo" substring in the title). Its own
comment invites the operator to narrow the broad rule to specific
group names — but an operator who did so, or who dropped it to make
partner-confidential bugs visible, was left with no rule naming the
security groups, and a security-group bug with an unmarked title would
be served in full.

Add a dedicated "security-groups" deny rule matching group names by
glob (*security*, *embargo*, *internal*) directly below the broad
rule: redundant while that rule keeps its full breadth, load-bearing
the moment it is narrowed or removed. Update the header scenario and
the narrowing advice accordingly. The rule can only deny more, never
grant, and because it consults `groups` — unknowable for a
not-yet-created bug — the file's refuse-all-creation property holds
even without the broad rule (I4 fail-closed).

The new test pins the shipped file: a bug in a security-named group
carrying no other deniable signal is denied; it stays denied by
"security-groups" itself once the group-restricted rule is excised
from the policy text; and a restricted non-security bug does become
visible then, which is exactly what narrowing is for. Mutation
checked: deleting the security-groups rule from the text makes the
test fail.
@plusky
plusky force-pushed the feat/policy-security-group-default branch from daf66d5 to 8df47a5 Compare July 28, 2026 08:57
@plusky
plusky merged commit 651f0bb into main Jul 28, 2026
10 checks passed
@plusky
plusky deleted the feat/policy-security-group-default branch July 28, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant