Check GCP communication posture (#40)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
This commit is contained in:
parent
d89ee7565a
commit
d878a61d8d
2 changed files with 87 additions and 0 deletions
|
|
@ -70,6 +70,10 @@ The check is read-only and prints no secret values. It verifies:
|
||||||
- Cloud Build config contract.
|
- Cloud Build config contract.
|
||||||
- Dedicated Cloud Build service account and roles.
|
- Dedicated Cloud Build service account and roles.
|
||||||
- GitHub Actions WIF Artifact Registry publishing contract.
|
- GitHub Actions WIF Artifact Registry publishing contract.
|
||||||
|
- Network ingress posture:
|
||||||
|
- no enabled broad SSH/RDP ingress;
|
||||||
|
- Teleo SSH rules are scoped to `/32` source ranges and target tags.
|
||||||
|
- Runtime service-account posture for the prod/staging VMs.
|
||||||
- Compute disk snapshot policy attachment.
|
- Compute disk snapshot policy attachment.
|
||||||
- Backup bucket versioning and uniform access.
|
- Backup bucket versioning and uniform access.
|
||||||
- Whether Cloud SQL/Postgres exists.
|
- Whether Cloud SQL/Postgres exists.
|
||||||
|
|
@ -94,5 +98,6 @@ Current GCP communication hardening is limited to the surfaces already read back
|
||||||
- default SSH/RDP firewall rules are disabled;
|
- default SSH/RDP firewall rules are disabled;
|
||||||
- Teleo SSH access is scoped by instance tags and current source IP rules;
|
- Teleo SSH access is scoped by instance tags and current source IP rules;
|
||||||
- the custom Teleo subnet has Private Google Access enabled.
|
- the custom Teleo subnet has Private Google Access enabled.
|
||||||
|
- prod/staging VMs run as dedicated Teleo service accounts, not the default Compute Engine service account.
|
||||||
|
|
||||||
Before production cutover, add a dedicated runtime communication contract for service-to-service paths, including exact source tags/service accounts, allowed ports, health checks, and rollback.
|
Before production cutover, add a dedicated runtime communication contract for service-to-service paths, including exact source tags/service accounts, allowed ports, health checks, and rollback.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,14 @@ BACKUP_BUCKETS = ("teleo-501523-prod-backups", "teleo-501523-leoclean-backups")
|
||||||
DISKS = ("teleo-prod-1", "teleo-staging-1")
|
DISKS = ("teleo-prod-1", "teleo-staging-1")
|
||||||
SNAPSHOT_POLICY = "teleo-daily-7d-snapshots"
|
SNAPSHOT_POLICY = "teleo-daily-7d-snapshots"
|
||||||
KB_HOST = "teleo@77.42.65.182"
|
KB_HOST = "teleo@77.42.65.182"
|
||||||
|
TELEO_VMS = {
|
||||||
|
"teleo-prod-1": "sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com",
|
||||||
|
"teleo-staging-1": "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com",
|
||||||
|
}
|
||||||
|
TELEO_SSH_RULES = {
|
||||||
|
"teleo-prod-allow-ssh-current-ip": "teleo-prod-ssh",
|
||||||
|
"teleo-staging-allow-ssh-current-ip": "teleo-staging-ssh",
|
||||||
|
}
|
||||||
CLOUDBUILD_SERVICE_ACCOUNT = f"sa-teleo-cloudbuild@{PROJECT}.iam.gserviceaccount.com"
|
CLOUDBUILD_SERVICE_ACCOUNT = f"sa-teleo-cloudbuild@{PROJECT}.iam.gserviceaccount.com"
|
||||||
CLOUDBUILD_REQUIRED_ROLES = (
|
CLOUDBUILD_REQUIRED_ROLES = (
|
||||||
"roles/artifactregistry.writer",
|
"roles/artifactregistry.writer",
|
||||||
|
|
@ -241,6 +249,78 @@ def check_github_actions_artifact_ci() -> Check:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rule_allows_port(rule: dict, port: str) -> bool:
|
||||||
|
for allowed in rule.get("allowed", []):
|
||||||
|
if allowed.get("IPProtocol") in ("tcp", "all"):
|
||||||
|
ports = allowed.get("ports")
|
||||||
|
if not ports or port in ports:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_network_ingress() -> Check:
|
||||||
|
rules = run_json(["gcloud", "compute", "firewall-rules", "list", "--project", PROJECT, "--format=json"])
|
||||||
|
enabled_ingress = [
|
||||||
|
rule
|
||||||
|
for rule in rules
|
||||||
|
if rule.get("direction", "INGRESS") == "INGRESS" and not rule.get("disabled", False)
|
||||||
|
]
|
||||||
|
|
||||||
|
broad_sensitive = []
|
||||||
|
for rule in enabled_ingress:
|
||||||
|
sources = set(rule.get("sourceRanges", []))
|
||||||
|
broad = bool({"0.0.0.0/0", "::/0"} & sources)
|
||||||
|
if broad and (_rule_allows_port(rule, "22") or _rule_allows_port(rule, "3389")):
|
||||||
|
broad_sensitive.append(rule.get("name", "unknown"))
|
||||||
|
if broad_sensitive:
|
||||||
|
return Check("network_ingress", "fail", f"broad_ssh_or_rdp_rules={broad_sensitive}")
|
||||||
|
|
||||||
|
by_name = {rule.get("name"): rule for rule in rules}
|
||||||
|
weak_ssh_rules = []
|
||||||
|
for rule_name, target_tag in TELEO_SSH_RULES.items():
|
||||||
|
rule = by_name.get(rule_name)
|
||||||
|
if not rule or rule.get("disabled", False):
|
||||||
|
weak_ssh_rules.append(f"{rule_name}:missing_or_disabled")
|
||||||
|
continue
|
||||||
|
sources = rule.get("sourceRanges", [])
|
||||||
|
target_tags = rule.get("targetTags", [])
|
||||||
|
if not sources or any(not source.endswith("/32") for source in sources):
|
||||||
|
weak_ssh_rules.append(f"{rule_name}:sources={sources}")
|
||||||
|
if target_tag not in target_tags:
|
||||||
|
weak_ssh_rules.append(f"{rule_name}:targetTags={target_tags}")
|
||||||
|
if not _rule_allows_port(rule, "22"):
|
||||||
|
weak_ssh_rules.append(f"{rule_name}:does_not_allow_tcp_22")
|
||||||
|
if weak_ssh_rules:
|
||||||
|
return Check("network_ingress", "fail", " ".join(weak_ssh_rules))
|
||||||
|
|
||||||
|
return Check(
|
||||||
|
"network_ingress",
|
||||||
|
"pass",
|
||||||
|
"no enabled broad SSH/RDP ingress; Teleo SSH rules are /32-scoped and target-tagged",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_compute_runtime_service_accounts() -> Check:
|
||||||
|
instances = run_json(["gcloud", "compute", "instances", "list", "--project", PROJECT, "--format=json"])
|
||||||
|
by_name = {instance.get("name"): instance for instance in instances}
|
||||||
|
problems = []
|
||||||
|
details = []
|
||||||
|
for name, expected_email in TELEO_VMS.items():
|
||||||
|
instance = by_name.get(name)
|
||||||
|
if not instance:
|
||||||
|
problems.append(f"{name}:missing")
|
||||||
|
continue
|
||||||
|
emails = [account.get("email") for account in instance.get("serviceAccounts", [])]
|
||||||
|
details.append(f"{name}:{','.join(email for email in emails if email)}")
|
||||||
|
if expected_email not in emails:
|
||||||
|
problems.append(f"{name}:expected={expected_email}:actual={emails}")
|
||||||
|
if any(email and email.endswith("-compute@developer.gserviceaccount.com") for email in emails):
|
||||||
|
problems.append(f"{name}:uses_default_compute_service_account")
|
||||||
|
if problems:
|
||||||
|
return Check("compute_runtime_service_accounts", "fail", " ".join(problems))
|
||||||
|
return Check("compute_runtime_service_accounts", "pass", " ".join(details))
|
||||||
|
|
||||||
|
|
||||||
def check_snapshot_policy() -> Check:
|
def check_snapshot_policy() -> Check:
|
||||||
policy = run_json(
|
policy = run_json(
|
||||||
[
|
[
|
||||||
|
|
@ -344,6 +424,8 @@ def main() -> int:
|
||||||
safe_check("cloud_build_contract", check_cloud_build_contract),
|
safe_check("cloud_build_contract", check_cloud_build_contract),
|
||||||
safe_check("cloud_build_service_account", check_cloud_build_service_account),
|
safe_check("cloud_build_service_account", check_cloud_build_service_account),
|
||||||
safe_check("github_actions_artifact_ci", check_github_actions_artifact_ci),
|
safe_check("github_actions_artifact_ci", check_github_actions_artifact_ci),
|
||||||
|
safe_check("network_ingress", check_network_ingress),
|
||||||
|
safe_check("compute_runtime_service_accounts", check_compute_runtime_service_accounts),
|
||||||
safe_check("compute_disk_snapshots", check_snapshot_policy),
|
safe_check("compute_disk_snapshots", check_snapshot_policy),
|
||||||
safe_check("backup_buckets", check_backup_buckets),
|
safe_check("backup_buckets", check_backup_buckets),
|
||||||
safe_check("cloud_sql_pgvector", check_cloud_sql),
|
safe_check("cloud_sql_pgvector", check_cloud_sql),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue