Fail closed on cross-provider WIF mappings
This commit is contained in:
parent
1a3c7fa9f4
commit
0296176355
4 changed files with 151 additions and 2 deletions
|
|
@ -87,7 +87,9 @@ they do not route production traffic.
|
|||
The deployer binding uses the provider-unique
|
||||
`attribute.observatory_workflow` principal set. A pool-wide subject binding
|
||||
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
|
||||
service-account user on `teleo-pgvector-standby`.
|
||||
4. From the existing private GCP VM database-admin path, run
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ PREFLIGHT_PERMISSIONS = {
|
|||
"iam.serviceAccounts.getIamPolicy",
|
||||
"iam.roles.get",
|
||||
"iam.workloadIdentityPoolProviders.get",
|
||||
"iam.workloadIdentityPoolProviders.list",
|
||||
"resourcemanager.projects.get",
|
||||
"resourcemanager.projects.getIamPolicy",
|
||||
"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:
|
||||
policy = json.loads(value)
|
||||
return role_has_exact_unconditional_members(
|
||||
|
|
@ -482,6 +512,26 @@ def build_checks(image_ref: str) -> list[Check]:
|
|||
wif_provider_is_exact,
|
||||
"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(
|
||||
"preflight_custom_role",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ PREFLIGHT_PERMISSIONS = (
|
|||
"iam.serviceAccounts.getIamPolicy",
|
||||
"iam.roles.get",
|
||||
"iam.workloadIdentityPoolProviders.get",
|
||||
"iam.workloadIdentityPoolProviders.list",
|
||||
"resourcemanager.projects.get",
|
||||
"resourcemanager.projects.getIamPolicy",
|
||||
"run.operations.get",
|
||||
|
|
@ -448,6 +449,13 @@ def build_plan() -> dict[str, object]:
|
|||
"fallback_provider_allowed": False,
|
||||
"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),
|
||||
"preflight_custom_role": {
|
||||
"name": PREFLIGHT_ROLE,
|
||||
|
|
@ -482,7 +490,8 @@ def build_plan() -> dict[str, object]:
|
|||
"post_apply_checks": [
|
||||
f"gcloud projects describe {PROJECT} --format=value(projectId)",
|
||||
"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 "
|
||||
f"{GITHUB_REPOSITORY} --ref main -f action=preflight_staging"
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ def test_plan_splits_build_deploy_and_runtime_identities() -> None:
|
|||
"fallback_provider_allowed": False,
|
||||
"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 plan["legacy_pool_wide_github_wif_member_removed"] 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 = {
|
||||
"name": (
|
||||
"projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/"
|
||||
"observatory-read-adapter-main"
|
||||
),
|
||||
"state": "ACTIVE",
|
||||
"attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION,
|
||||
"attributeMapping": {
|
||||
|
|
@ -293,6 +305,24 @@ def test_preflight_end_to_end_snapshot_retains_no_secret(monkeypatch) -> None:
|
|||
stdout = "ENABLED\n"
|
||||
elif "workload-identity-pools providers describe" in joined:
|
||||
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:
|
||||
stdout = json.dumps({"includedPermissions": sorted(preflight.PREFLIGHT_PERMISSIONS)})
|
||||
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:
|
||||
provider = {
|
||||
"name": (
|
||||
"projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/"
|
||||
"observatory-read-adapter-main"
|
||||
),
|
||||
"state": "ACTIVE",
|
||||
"attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION,
|
||||
"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
|
||||
|
||||
|
||||
@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:
|
||||
monkeypatch.setattr(
|
||||
preflight,
|
||||
|
|
|
|||
Loading…
Reference in a new issue