Fail closed on cross-provider WIF mappings

This commit is contained in:
twentyOne2x 2026-07-15 22:45:24 +02:00
parent 1a3c7fa9f4
commit 0296176355
4 changed files with 151 additions and 2 deletions

View file

@ -87,7 +87,9 @@ they do not route production traffic.
The deployer binding uses the provider-unique The deployer binding uses the provider-unique
`attribute.observatory_workflow` principal set. A pool-wide subject binding `attribute.observatory_workflow` principal set. A pool-wide subject binding
or the general `living-ip-github` provider is diagnostic-only and must not or the general `living-ip-github` provider is diagnostic-only and must not
authenticate the preflight or deploy jobs. authenticate the preflight or deploy jobs. The preflight lists every
provider in `github-actions`, including soft-deleted providers, and blocks
if any non-dedicated provider maps that attribute.
3. Add the dedicated runtime service account as a Cloud SQL IAM 3. Add the dedicated runtime service account as a Cloud SQL IAM
service-account user on `teleo-pgvector-standby`. service-account user on `teleo-pgvector-standby`.
4. From the existing private GCP VM database-admin path, run 4. From the existing private GCP VM database-admin path, run

View file

@ -54,6 +54,7 @@ PREFLIGHT_PERMISSIONS = {
"iam.serviceAccounts.getIamPolicy", "iam.serviceAccounts.getIamPolicy",
"iam.roles.get", "iam.roles.get",
"iam.workloadIdentityPoolProviders.get", "iam.workloadIdentityPoolProviders.get",
"iam.workloadIdentityPoolProviders.list",
"resourcemanager.projects.get", "resourcemanager.projects.get",
"resourcemanager.projects.getIamPolicy", "resourcemanager.projects.getIamPolicy",
"run.operations.get", "run.operations.get",
@ -307,6 +308,35 @@ def wif_provider_is_exact(value: str) -> bool:
) )
def wif_provider_inventory_is_isolated(value: str) -> bool:
providers = json.loads(value)
if not isinstance(providers, list) or not providers:
return False
dedicated: list[dict[str, object]] = []
provider_names: set[str] = set()
for provider in providers:
if not isinstance(provider, dict):
return False
name = provider.get("name")
if not isinstance(name, str) or not name:
return False
provider_id = name.rsplit("/", 1)[-1]
if provider_id in provider_names:
return False
provider_names.add(provider_id)
mapping = provider.get("attributeMapping", {})
if not isinstance(mapping, dict):
return False
if provider_id == DEPLOY_WIF_PROVIDER:
dedicated.append(provider)
elif "attribute.observatory_workflow" in mapping:
return False
return len(dedicated) == 1 and wif_provider_is_exact(json.dumps(dedicated[0]))
def deployer_service_account_policy_is_exact(value: str) -> bool: def deployer_service_account_policy_is_exact(value: str) -> bool:
policy = json.loads(value) policy = json.loads(value)
return role_has_exact_unconditional_members( return role_has_exact_unconditional_members(
@ -482,6 +512,26 @@ def build_checks(image_ref: str) -> list[Check]:
wif_provider_is_exact, wif_provider_is_exact,
"main_and_exact_workflow_only", "main_and_exact_workflow_only",
), ),
command_check(
"wif_pool_provider_attribute_isolation",
[
"gcloud",
"iam",
"workload-identity-pools",
"providers",
"list",
"--project",
PROJECT,
"--location",
"global",
"--workload-identity-pool",
WIF_POOL,
"--show-deleted",
"--format=json",
],
wif_provider_inventory_is_isolated,
"dedicated_provider_is_sole_observatory_workflow_mapper",
),
command_check( command_check(
"preflight_custom_role", "preflight_custom_role",
[ [

View file

@ -55,6 +55,7 @@ PREFLIGHT_PERMISSIONS = (
"iam.serviceAccounts.getIamPolicy", "iam.serviceAccounts.getIamPolicy",
"iam.roles.get", "iam.roles.get",
"iam.workloadIdentityPoolProviders.get", "iam.workloadIdentityPoolProviders.get",
"iam.workloadIdentityPoolProviders.list",
"resourcemanager.projects.get", "resourcemanager.projects.get",
"resourcemanager.projects.getIamPolicy", "resourcemanager.projects.getIamPolicy",
"run.operations.get", "run.operations.get",
@ -448,6 +449,13 @@ def build_plan() -> dict[str, object]:
"fallback_provider_allowed": False, "fallback_provider_allowed": False,
"required_condition": DEPLOY_WIF_ATTRIBUTE_CONDITION, "required_condition": DEPLOY_WIF_ATTRIBUTE_CONDITION,
}, },
"provider_isolation": {
"pool": WIF_POOL,
"dedicated_provider": DEPLOY_WIF_PROVIDER,
"exclusive_attribute": "attribute.observatory_workflow",
"all_providers_must_be_enumerated": True,
"non_dedicated_provider_mapping_allowed": False,
},
"required_apis": list(REQUIRED_APIS), "required_apis": list(REQUIRED_APIS),
"preflight_custom_role": { "preflight_custom_role": {
"name": PREFLIGHT_ROLE, "name": PREFLIGHT_ROLE,
@ -482,7 +490,8 @@ def build_plan() -> dict[str, object]:
"post_apply_checks": [ "post_apply_checks": [
f"gcloud projects describe {PROJECT} --format=value(projectId)", f"gcloud projects describe {PROJECT} --format=value(projectId)",
"python3 ops/check_gcp_infra_readiness.py", "python3 ops/check_gcp_infra_readiness.py",
"Require the adapter preflight to verify the dedicated WIF provider, custom roles, IAM bindings, private Cloud SQL, secret, and immutable image.", "Require the adapter preflight to enumerate every provider in the pool and prove only the dedicated provider maps attribute.observatory_workflow.",
"Require the adapter preflight to verify custom roles, IAM bindings, private Cloud SQL, secret, and immutable image.",
( (
"gh workflow run gcp-observatory-read-adapter.yml --repo " "gh workflow run gcp-observatory-read-adapter.yml --repo "
f"{GITHUB_REPOSITORY} --ref main -f action=preflight_staging" f"{GITHUB_REPOSITORY} --ref main -f action=preflight_staging"

View file

@ -54,6 +54,14 @@ def test_plan_splits_build_deploy_and_runtime_identities() -> None:
"fallback_provider_allowed": False, "fallback_provider_allowed": False,
"required_condition": plan["conditions"]["deployer_wif"], "required_condition": plan["conditions"]["deployer_wif"],
} }
assert plan["provider_isolation"] == {
"pool": "github-actions",
"dedicated_provider": "observatory-read-adapter-main",
"exclusive_attribute": "attribute.observatory_workflow",
"all_providers_must_be_enumerated": True,
"non_dedicated_provider_mapping_allowed": False,
}
assert "iam.workloadIdentityPoolProviders.list" in plan["preflight_custom_role"]["permissions"]
assert "remove-iam-policy-binding" in commands assert "remove-iam-policy-binding" in commands
assert plan["legacy_pool_wide_github_wif_member_removed"] in commands assert plan["legacy_pool_wide_github_wif_member_removed"] in commands
assert "roles/run.serviceAgent" in commands assert "roles/run.serviceAgent" in commands
@ -269,6 +277,10 @@ def test_preflight_end_to_end_snapshot_retains_no_secret(monkeypatch) -> None:
] ]
} }
provider = { provider = {
"name": (
"projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/"
"observatory-read-adapter-main"
),
"state": "ACTIVE", "state": "ACTIVE",
"attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION, "attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION,
"attributeMapping": { "attributeMapping": {
@ -293,6 +305,24 @@ def test_preflight_end_to_end_snapshot_retains_no_secret(monkeypatch) -> None:
stdout = "ENABLED\n" stdout = "ENABLED\n"
elif "workload-identity-pools providers describe" in joined: elif "workload-identity-pools providers describe" in joined:
stdout = json.dumps(provider) stdout = json.dumps(provider)
elif "workload-identity-pools providers list" in joined:
stdout = json.dumps(
[
provider,
{
"name": (
"projects/785938879453/locations/global/workloadIdentityPools/"
"github-actions/providers/living-ip-github"
),
"state": "ACTIVE",
"attributeMapping": {
"google.subject": "assertion.sub",
"attribute.repository": "assertion.repository",
"attribute.ref": "assertion.ref",
},
},
]
)
elif "iam roles describe observatoryStagingPreflight" in joined: elif "iam roles describe observatoryStagingPreflight" in joined:
stdout = json.dumps({"includedPermissions": sorted(preflight.PREFLIGHT_PERMISSIONS)}) stdout = json.dumps({"includedPermissions": sorted(preflight.PREFLIGHT_PERMISSIONS)})
elif "iam roles describe observatoryStagingDeployer" in joined: elif "iam roles describe observatoryStagingDeployer" in joined:
@ -375,6 +405,10 @@ def test_deployer_wif_binding_rejects_pool_wide_or_additional_impersonators() ->
def test_dedicated_provider_requires_provider_unique_workflow_mapping() -> None: def test_dedicated_provider_requires_provider_unique_workflow_mapping() -> None:
provider = { provider = {
"name": (
"projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/"
"observatory-read-adapter-main"
),
"state": "ACTIVE", "state": "ACTIVE",
"attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION, "attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION,
"attributeMapping": { "attributeMapping": {
@ -389,6 +423,60 @@ def test_dedicated_provider_requires_provider_unique_workflow_mapping() -> None:
assert preflight.wif_provider_is_exact(json.dumps(provider)) is True assert preflight.wif_provider_is_exact(json.dumps(provider)) is True
@pytest.mark.parametrize(
"malicious_mapping",
[
"assertion.workflow_ref",
"'living-ip/teleo-infrastructure/.github/workflows/"
"gcp-observatory-read-adapter.yml@refs/heads/main'",
],
)
def test_provider_inventory_rejects_any_second_observatory_mapper(malicious_mapping: str) -> None:
dedicated = {
"name": (
"projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/"
"observatory-read-adapter-main"
),
"state": "ACTIVE",
"attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION,
"attributeMapping": {
"google.subject": "assertion.sub",
"attribute.observatory_workflow": "assertion.workflow_ref",
"attribute.repository": "assertion.repository",
"attribute.ref": "assertion.ref",
"attribute.workflow_ref": "assertion.workflow_ref",
},
}
general = {
"name": (
"projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/"
"living-ip-github"
),
"state": "ACTIVE",
"attributeMapping": {
"google.subject": "assertion.sub",
"attribute.repository": "assertion.repository",
},
}
assert preflight.wif_provider_inventory_is_isolated(json.dumps([dedicated, general])) is True
general["attributeMapping"]["attribute.observatory_workflow"] = malicious_mapping
assert preflight.wif_provider_inventory_is_isolated(json.dumps([dedicated, general])) is False
def test_provider_inventory_fails_closed_on_missing_or_duplicate_dedicated_provider() -> None:
general = {
"name": (
"projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/"
"living-ip-github"
),
"state": "ACTIVE",
"attributeMapping": {"google.subject": "assertion.sub"},
}
assert preflight.wif_provider_inventory_is_isolated(json.dumps([general])) is False
assert preflight.wif_provider_inventory_is_isolated(json.dumps([general, general])) is False
def test_malformed_successful_readback_becomes_blocked_check(monkeypatch) -> None: def test_malformed_successful_readback_becomes_blocked_check(monkeypatch) -> None:
monkeypatch.setattr( monkeypatch.setattr(
preflight, preflight,