Skip to content

acl/v2: add the v2 ACL format and validator - #360

Open
shubh-stripe wants to merge 1 commit into
shubh/aclv2-1-globfrom
shubh/aclv2-2-policy
Open

shubh-stripe wants to merge 1 commit into
shubh/aclv2-1-globfrom
shubh/aclv2-2-policy

Conversation

@shubh-stripe

@shubh-stripe shubh-stripe commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #359, which moves the glob matcher to acl/v2. This PR adds the v2 ACL format and its validator. It has no engine yet, and nothing decides with it. The engine comes in the next PR.

Shape

version: v2
services:
  - name: web
    project: web
    action: enforce
    allow:
      - domains: [api.partner.example, "*.cdn.example.org"]
  - name: ci
    project: ci-infra
    action: open
    allow:
      - domains: [pypi.org]
        override_global_policy: true
    deny: [tracking.example.com]
default:
  project: security
  action: enforce
global:
  deny: [pypi.org, github.com, "regis*.npmjs.org"]

ci can reach pypi.org because its allow entry sets override_global_policy: true. The exception covers only the domains of that entry, so ci still can't reach github.com. If the field is missing, the file fails to load.

What changes

A v2 ACL keeps one entry per service, as v1 does, and makes these rules explicit:

  • A service has allow entries. Each entry holds its domains and options, so a MITM config sits on the entry it applies to. v1 needed a separate mitm_domains list that repeated each domain.
  • Service and global deny lists are plain strings. A deny glob always matches across public suffixes, the same as v1's global deny list, so a deny list blocks every host that its text describes.
  • An allow domain is a string, or a mapping that sets cross_public_suffix for that domain only. v1 used one flag for the whole file.
  • A service allow entry wins over global.deny only if it sets override_global_policy: true. Validation rejects a service allow entry that overlaps global.deny without the field.
  • Options.Compat keeps the v1 behavior for these entries: the ACL loads, and validation reports each entry to Options.Warn. This gives existing ACLs a migration path. The CLI flag comes in a later PR.

Parsing is strict. Unknown keys, a second YAML document and a version other than v2 are errors. This includes unknown keys inside a domain mapping, which yaml.Node.Decode does not check by itself. A policy also marshals back to YAML that Parse accepts.

LoadFiles joins several files into one policy, so you can split an ACL by service.

The overlap check compares globs by the rules that match hosts, not by glob text. Spelling a domain differently in the two lists does not hide an overlap. For two label wildcards, the check compares every label, so sts.* against iam.* and api* against admin* are not flagged.

Test plan

Table tests cover parsing (including unknown keys, multiple documents and a mapping in a deny list), the marshal round trip, validation in strict and compat modes, LoadFiles, and the overlap check (case, a trailing dot, the wildcard forms, differing fixed labels, sibling labels, cross_public_suffix, and deny globs that cross public suffixes).

I also checked locally that a translated v1 ACL gives the same decision as v1 for about 85,000 requests. I checked two ways: with override_global_policy set on overlapping entries in strict mode, and with no fields set in compat mode.

🤖 Generated with Claude Code

A v2 ACL keeps v1's shape of one entry per service, and adds structure where v1 hides behavior. A service has allow entries, each holding domains and its options, so MITM sits on the allow entry it applies to. A domain is a string or a mapping that sets cross_public_suffix for that domain only. Service and global deny lists are plain strings, and a deny glob always matches across public suffixes, as v1's global deny list does, so a deny list is as broad as it reads.

A service allow entry can win over the global deny list only when it sets override_global_policy: true, so every exception is visible on the entry that makes it. By default, a service allow entry that overlaps the global deny list without the field fails validation. Options.Compat keeps the v1 behavior for these entries and reports each one as a warning, so existing ACLs have a migration path.

Parsing is strict: unknown keys, a second YAML document, and a version other than v2 are errors, including inside a domain mapping, which yaml.Node.Decode does not check on its own. A policy marshals back to YAML that Parse accepts. The overlap check compares globs by concrete host rules, not by glob text, so it can't be bypassed by spelling a domain differently in the two lists. For two confined label wildcards it compares every label, so sts.* against iam.*, or api* against admin*, is not flagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Committed-By-Agent: claude
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