policy/v2: add deny test captures that pass while access remains

SaaS passes a tests deny when one node of the dst alias, or one member
of a group source, is blocked, though another still gets in. Headscale
fails the dst case; it passes the group case as SaaS does.
This commit is contained in:
Kristoffer Dalby committed 2026-10-30 14:55:37 +00:00
1 parent 5f1c166140
commit f3944e3d98
3 files changed
+348

No files matched your search

@@ -44,6 +44,8 @@ var knownPolicyTesterDivergences = map[string]string{ //nolint:gosec // strings
"policytest-src-unknown-group/breakdown": testUnknownAlias,
"policytest-src-unknown-user-email/breakdown": testUnknownAlias,
"policytest-dst-multinode-partial-deny/verdict": "TEST_DST_EVERY_NODE: SaaS denies when one node of the dst alias is not reached; headscale only when none is",
"policytest-src-split-across-rules/verdict": "TEST_SRC_ONE_RULE: SaaS needs one rule to cover a group source; headscale lets each member pass through its own rule",
}
@@ -0,0 +1,172 @@
// policytest-dst-multinode-partial-deny
//
// tests dst multinode partial deny
//
// Steps: 1
// Captured at: 2026-10-17T15:33:40Z
// tool version: tscap/e3511f1
// schema version: 1
{
"schema_version": 1,
"test_id": "policytest-dst-multinode-partial-deny",
"description": "tests dst multinode partial deny",
"category": "policytest",
"captured_at": "2026-10-17T15:33:40.992948752Z",
"tool_version": "tscap/e3511f1",
"tailnet": "odin@example.com",
"input": {
"api_response_code": 200,
"tailnet": {
"dns": {"magic_dns": false, "nameservers": [], "search_paths": [], "split_dns": {}},
"settings": {}
},
"scenario_hujson": " \n \n \n{\n\t\"id\": \"policytest-dst-multinode-partial-deny\",\n\t\"category\": \"policytest\",\n\t\"description\": \"tests dst multinode partial deny\",\n\t\"topology\": \"../../_topologies/grant.hujson\",\n\t\"tailnet\": {\n\t\t\"dns\": {\"magic_dns\": false, \"nameservers\": [], \"search_paths\": [], \"split_dns\": {}},\n\t\t\"settings\": {}\n\t},\n\t\"options\": {\"capture\": \"none\"},\n\t\"policy\": {\n\t\t\"tagOwners\": {\"tag:exit\": [\"odin@example.com\"]},\n\t\t\"acls\": [{\n\t\t\t\"action\": \"accept\",\n\t\t\t\"src\": [\"thor@example.org\"],\n\t\t\t\"dst\": [\"100.64.0.4:22\"]\n\t\t}],\n\t\t\"tests\": [{\"src\": \"thor@example.org\", \"deny\": [\"tag:exit:22\"]}]\n\t}\n}\n",
"scenario_path": "scenarios/grant-matrix/grant/policytest-dst-multinode-partial-deny.hujson",
"full_policy": {
"acls": [{
"action": "accept",
"dst": ["100.64.0.4:22"],
"src": ["thor@example.org"]
}],
"tagOwners": {"tag:exit": ["odin@example.com"]},
"tests": [{"deny": ["tag:exit:22"], "src": "thor@example.org"}]
}
},
"topology": {"users": [
{"id": 1, "name": "odin", "email": "odin@example.com"},
{"id": 2, "name": "thor", "email": "thor@example.org"},
{"id": 3, "name": "freya", "email": "freya@example.com"}
], "nodes": {"beedrill": {
"hostname": "beedrill",
"tags": ["tag:server"],
"ipv4": "100.64.0.16",
"ipv6": "fd7a:115c:a1e0::10",
"routable_ips": [],
"approved_routes": []
}, "blastoise": {
"hostname": "blastoise",
"tags": ["tag:exit", "tag:router"],
"ipv4": "100.64.0.9",
"ipv6": "fd7a:115c:a1e0::9",
"routable_ips": ["10.33.0.0/16", "0.0.0.0/0", "::/0"],
"approved_routes": []
}, "bulbasaur": {
"hostname": "bulbasaur",
"tags": [],
"ipv4": "100.64.0.19",
"ipv6": "fd7a:115c:a1e0::13",
"user": "odin",
"routable_ips": [],
"approved_routes": []
}, "charmander": {
"hostname": "charmander",
"tags": ["tag:exit"],
"ipv4": "100.64.0.4",
"ipv6": "fd7a:115c:a1e0::4",
"routable_ips": ["0.0.0.0/0", "::/0"],
"approved_routes": []
}, "fearow": {
"hostname": "fearow",
"tags": ["tag:fearow"],
"ipv4": "100.64.0.12",
"ipv6": "fd7a:115c:a1e0::c",
"routable_ips": ["10.55.0.0/16"],
"approved_routes": []
}, "ivysaur": {
"hostname": "ivysaur",
"tags": [],
"ipv4": "100.64.0.17",
"ipv6": "fd7a:115c:a1e0::11",
"user": "thor",
"routable_ips": [],
"approved_routes": []
}, "kakuna": {
"hostname": "kakuna",
"tags": ["tag:prod"],
"ipv4": "100.64.0.15",
"ipv6": "fd7a:115c:a1e0::f",
"routable_ips": [],
"approved_routes": []
}, "pidgeotto": {
"hostname": "pidgeotto",
"tags": ["tag:pidgeotto"],
"ipv4": "100.64.0.3",
"ipv6": "fd7a:115c:a1e0::3",
"routable_ips": ["0.0.0.0/0", "::/0"],
"approved_routes": []
}, "pidgey": {
"hostname": "pidgey",
"tags": ["tag:pidgey"],
"ipv4": "100.64.0.2",
"ipv6": "fd7a:115c:a1e0::2",
"routable_ips": ["0.0.0.0/0", "::/0"],
"approved_routes": []
}, "raticate": {
"hostname": "raticate",
"tags": ["tag:group-b"],
"ipv4": "100.64.0.6",
"ipv6": "fd7a:115c:a1e0::6",
"routable_ips": [],
"approved_routes": []
}, "rattata": {
"hostname": "rattata",
"tags": ["tag:group-a"],
"ipv4": "100.64.0.5",
"ipv6": "fd7a:115c:a1e0::5",
"routable_ips": [],
"approved_routes": []
}, "spearow": {
"hostname": "spearow",
"tags": ["tag:spearow"],
"ipv4": "100.64.0.11",
"ipv6": "fd7a:115c:a1e0::b",
"routable_ips": ["10.44.0.0/16"],
"approved_routes": []
}, "squirtle": {
"hostname": "squirtle",
"tags": ["tag:router"],
"ipv4": "100.64.0.13",
"ipv6": "fd7a:115c:a1e0::d",
"routable_ips": ["10.33.0.0/16"],
"approved_routes": []
}, "venusaur": {
"hostname": "venusaur",
"tags": [],
"ipv4": "100.64.0.18",
"ipv6": "fd7a:115c:a1e0::12",
"user": "freya",
"routable_ips": [],
"approved_routes": []
}, "weedle": {
"hostname": "weedle",
"tags": ["tag:client"],
"ipv4": "100.64.0.14",
"ipv6": "fd7a:115c:a1e0::e",
"routable_ips": [],
"approved_routes": []
}}},
"captures": {},
"steps": [{
"name": "validate",
"at": "2026-10-17T15:33:44.419667586Z",
"result": "observed",
"actions": [{
"kind": "api",
"principal": "admin",
"method": "POST",
"path": "/tailnet/-/acl/validate",
"header": {"Content-Type": "application/hujson"},
"body": {
"acls": [{
"action": "accept",
"dst": ["100.64.0.4:22"],
"src": ["thor@example.org"]
}],
"tagOwners": {"tag:exit": ["odin@example.com"]},
"tests": [{"deny": ["tag:exit:22"], "src": "thor@example.org"}]
},
"status": 200,
"response": {}
}]
}]
}
@@ -0,0 +1,174 @@
// policytest-src-group-partial-deny
//
// tests src group partial deny
//
// Steps: 1
// Captured at: 2026-10-17T15:35:03Z
// tool version: tscap/e3511f1
// schema version: 1
{
"schema_version": 1,
"test_id": "policytest-src-group-partial-deny",
"description": "tests src group partial deny",
"category": "policytest",
"captured_at": "2026-10-17T15:35:03.867563686Z",
"tool_version": "tscap/e3511f1",
"tailnet": "odin@example.com",
"input": {
"api_response_code": 200,
"tailnet": {
"dns": {"magic_dns": false, "nameservers": [], "search_paths": [], "split_dns": {}},
"settings": {}
},
"scenario_hujson": " \n \n \n{\n\t\"id\": \"policytest-src-group-partial-deny\",\n\t\"category\": \"policytest\",\n\t\"description\": \"tests src group partial deny\",\n\t\"topology\": \"../../_topologies/grant.hujson\",\n\t\"tailnet\": {\n\t\t\"dns\": {\"magic_dns\": false, \"nameservers\": [], \"search_paths\": [], \"split_dns\": {}},\n\t\t\"settings\": {}\n\t},\n\t\"options\": {\"capture\": \"none\"},\n\t\"policy\": {\n\t\t\"groups\": {\"group:g\": [\"thor@example.org\", \"odin@example.com\"]},\n\t\t\"tagOwners\": {\"tag:server\": [\"odin@example.com\"]},\n\t\t\"acls\": [{\n\t\t\t\"action\": \"accept\",\n\t\t\t\"src\": [\"thor@example.org\"],\n\t\t\t\"dst\": [\"tag:server:22\"]\n\t\t}],\n\t\t\"tests\": [{\"src\": \"group:g\", \"deny\": [\"tag:server:22\"]}]\n\t}\n}\n",
"scenario_path": "scenarios/grant-matrix/grant/policytest-src-group-partial-deny.hujson",
"full_policy": {
"acls": [{
"action": "accept",
"dst": ["tag:server:22"],
"src": ["thor@example.org"]
}],
"groups": {"group:g": ["thor@example.org", "odin@example.com"]},
"tagOwners": {"tag:server": ["odin@example.com"]},
"tests": [{"deny": ["tag:server:22"], "src": "group:g"}]
}
},
"topology": {"users": [
{"id": 1, "name": "odin", "email": "odin@example.com"},
{"id": 2, "name": "thor", "email": "thor@example.org"},
{"id": 3, "name": "freya", "email": "freya@example.com"}
], "nodes": {"beedrill": {
"hostname": "beedrill",
"tags": ["tag:server"],
"ipv4": "100.64.0.16",
"ipv6": "fd7a:115c:a1e0::10",
"routable_ips": [],
"approved_routes": []
}, "blastoise": {
"hostname": "blastoise",
"tags": ["tag:exit", "tag:router"],
"ipv4": "100.64.0.9",
"ipv6": "fd7a:115c:a1e0::9",
"routable_ips": ["10.33.0.0/16", "0.0.0.0/0", "::/0"],
"approved_routes": []
}, "bulbasaur": {
"hostname": "bulbasaur",
"tags": [],
"ipv4": "100.64.0.19",
"ipv6": "fd7a:115c:a1e0::13",
"user": "odin",
"routable_ips": [],
"approved_routes": []
}, "charmander": {
"hostname": "charmander",
"tags": ["tag:exit"],
"ipv4": "100.64.0.4",
"ipv6": "fd7a:115c:a1e0::4",
"routable_ips": ["0.0.0.0/0", "::/0"],
"approved_routes": []
}, "fearow": {
"hostname": "fearow",
"tags": ["tag:fearow"],
"ipv4": "100.64.0.12",
"ipv6": "fd7a:115c:a1e0::c",
"routable_ips": ["10.55.0.0/16"],
"approved_routes": []
}, "ivysaur": {
"hostname": "ivysaur",
"tags": [],
"ipv4": "100.64.0.17",
"ipv6": "fd7a:115c:a1e0::11",
"user": "thor",
"routable_ips": [],
"approved_routes": []
}, "kakuna": {
"hostname": "kakuna",
"tags": ["tag:prod"],
"ipv4": "100.64.0.15",
"ipv6": "fd7a:115c:a1e0::f",
"routable_ips": [],
"approved_routes": []
}, "pidgeotto": {
"hostname": "pidgeotto",
"tags": ["tag:pidgeotto"],
"ipv4": "100.64.0.3",
"ipv6": "fd7a:115c:a1e0::3",
"routable_ips": ["0.0.0.0/0", "::/0"],
"approved_routes": []
}, "pidgey": {
"hostname": "pidgey",
"tags": ["tag:pidgey"],
"ipv4": "100.64.0.2",
"ipv6": "fd7a:115c:a1e0::2",
"routable_ips": ["0.0.0.0/0", "::/0"],
"approved_routes": []
}, "raticate": {
"hostname": "raticate",
"tags": ["tag:group-b"],
"ipv4": "100.64.0.6",
"ipv6": "fd7a:115c:a1e0::6",
"routable_ips": [],
"approved_routes": []
}, "rattata": {
"hostname": "rattata",
"tags": ["tag:group-a"],
"ipv4": "100.64.0.5",
"ipv6": "fd7a:115c:a1e0::5",
"routable_ips": [],
"approved_routes": []
}, "spearow": {
"hostname": "spearow",
"tags": ["tag:spearow"],
"ipv4": "100.64.0.11",
"ipv6": "fd7a:115c:a1e0::b",
"routable_ips": ["10.44.0.0/16"],
"approved_routes": []
}, "squirtle": {
"hostname": "squirtle",
"tags": ["tag:router"],
"ipv4": "100.64.0.13",
"ipv6": "fd7a:115c:a1e0::d",
"routable_ips": ["10.33.0.0/16"],
"approved_routes": []
}, "venusaur": {
"hostname": "venusaur",
"tags": [],
"ipv4": "100.64.0.18",
"ipv6": "fd7a:115c:a1e0::12",
"user": "freya",
"routable_ips": [],
"approved_routes": []
}, "weedle": {
"hostname": "weedle",
"tags": ["tag:client"],
"ipv4": "100.64.0.14",
"ipv6": "fd7a:115c:a1e0::e",
"routable_ips": [],
"approved_routes": []
}}},
"captures": {},
"steps": [{
"name": "validate",
"at": "2026-10-17T15:35:09.906337724Z",
"result": "observed",
"actions": [{
"kind": "api",
"principal": "admin",
"method": "POST",
"path": "/tailnet/-/acl/validate",
"header": {"Content-Type": "application/hujson"},
"body": {
"acls": [{
"action": "accept",
"dst": ["tag:server:22"],
"src": ["thor@example.org"]
}],
"groups": {"group:g": ["thor@example.org", "odin@example.com"]},
"tagOwners": {"tag:server": ["odin@example.com"]},
"tests": [{"deny": ["tag:server:22"], "src": "group:g"}]
},
"status": 200,
"response": {}
}]
}]
}