"""Coulomb-only identity bootstrap; stdin auth is temporary, API pairs stay private. No personal Console session is a routine credential. Default preview has no effects. Native S3/escrow boundary checks remain mandatory after this IAM stage. """ from __future__ import annotations import argparse import fcntl import json import os import stat import sys import tempfile from datetime import UTC, datetime, timedelta from pathlib import Path from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import HTTPRedirectHandler, Request, build_opener PUBLISHER = "681d00bf-b965-4323-b657-97bfde423511" PROJECT = "coulomb-social-recovery" DECISION = "27bbc1ac-e275-42be-ba34-6cf16eae4e78" PROGRAMMATIC_DIRECTION = "6139e02b-3fb7-4eff-be8a-efe127c45ce5" ROLES = { "bootstrap": {"name": "coulomb-social-recovery-bootstrap", "permissions": [ "ObjectStorageFullAccess", "SecretManagerFullAccess"]}, "reader": {"name": "coulomb-social-recovery-reader", "permissions": [ "ObjectStorageReadOnly"]}, "custodian": {"name": "coulomb-social-recovery-custodian", "permissions": [ "ObjectStorageReadOnly", "ObjectStorageObjectsWrite", "SecretManagerReadOnly", "SecretManagerSecretAccess"]}, "publisher": {"name": "coulomb-social-backup-primary", "permissions": [ "ObjectStorageObjectsRead", "ObjectStorageObjectsWrite", "ObjectStorageObjectsDelete", "ObjectStorageBucketsRead"]}, } class Refused(RuntimeError): pass class ProviderError(Refused): def __init__(self, status): super().__init__("provider_request_refused") self.status = status def inside_git(path): return any((parent / ".git").exists() for parent in [path.resolve(), *path.resolve().parents]) def private_directory(path): info = path.lstat() if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077: raise Refused("private_owner_directory_required") if inside_git(path): raise Refused("credential_directory_must_be_outside_git") def fsync_directory(path): fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY) try: os.fsync(fd) finally: os.close(fd) def private_json(path): info = path.lstat() if not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077: raise Refused("private_owner_file_required") return json.loads(path.read_text()) def checkpoint(path, value): fd, name = tempfile.mkstemp(prefix=".checkpoint-", dir=path.parent) try: with os.fdopen(fd, "w") as stream: json.dump(value, stream, sort_keys=True, indent=2) stream.write("\n") stream.flush() os.fsync(stream.fileno()) os.replace(name, path) fsync_directory(path.parent) finally: if os.path.exists(name): os.unlink(name) class Transport: def __init__(self, auth): if set(auth) != {"session_token", "user_agent"} or any( not isinstance(v, str) or not v or "\n" in v or "\r" in v for v in auth.values() ): raise Refused("temporary_stdin_auth_required") self.headers = {"X-Session-Token": auth["session_token"], "User-Agent": auth["user_agent"], "Content-Type": "application/json"} class NoRedirect(HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): raise Refused("provider_redirect_refused") self.opener = build_opener(NoRedirect()) def __call__(self, method, path, data=None): if not path.startswith(("/iam/v1alpha1/", "/account/v3/")) or ".." in path: raise Refused("provider_api_scope_refused") request = Request("https://api.scaleway.com" + path, method=method, headers=self.headers, data=None if data is None else json.dumps(data).encode()) try: with self.opener.open(request, timeout=20) as response: raw = response.read(8 * 1024**2) return json.loads(raw) if raw else {} except HTTPError as error: raise ProviderError(error.code) from None class Bootstrap: def __init__(self, api, directory, now=None): self.api, self.directory = api, directory private_directory(directory) self.now = now or datetime.now(UTC) self.path = directory / "identity-state.json" self.state = private_json(self.path) if self.path.exists() else { "schema": "platform.coulomb-recovery-identities.v1", "decision": DECISION, "access_decision": PROGRAMMATIC_DIRECTION, "apps": {}, "policies": {}, "keys": {}, } if self.state.get("decision") != DECISION or self.state.get("access_decision") != ( PROGRAMMATIC_DIRECTION ) or self.state.get("schema") != "platform.coulomb-recovery-identities.v1": raise Refused("journal_scope_refused") if self.state.get("pending_key_issue"): raise Refused("uncertain_key_issue_needs_reconciliation") def save(self): checkpoint(self.path, self.state) def inventory(self, path, key, **filters): rows = [] for page in range(1, 101): value = self.api("GET", path + "?" + urlencode({**filters, "page_size": 100, "page": page})) batch = value[key] if not isinstance(batch, list): raise Refused("invalid_provider_inventory") rows.extend(batch) count = int(value["total_count"]) if len(rows) == count: return rows if not batch or len(rows) > count: break raise Refused("complete_inventory_required") def project(self, organization): if self.state.get("project"): project = self.api("GET", "/account/v3/projects/" + self.state["project"]["id"]) else: found = [v for v in self.inventory("/account/v3/projects", "projects", organization_id=organization) if v["name"] == PROJECT] if found: raise Refused("untracked_existing_project_requires_review") project = self.api("POST", "/account/v3/projects", { "name": PROJECT, "organization_id": organization, "description": "Coulomb-only recovery custody; decision " + DECISION, }) self.state["project"] = project self.save() if project["name"] != PROJECT or project["organization_id"] != organization: raise Refused("project_binding_refused") return project def application(self, role, organization, applications): spec = ROLES[role] if role == "publisher": app = self.api("GET", "/iam/v1alpha1/applications/" + PUBLISHER) elif role in self.state["apps"]: app = self.api("GET", "/iam/v1alpha1/applications/" + self.state["apps"][role]["id"]) else: if any(v["name"] == spec["name"] for v in applications): raise Refused("untracked_existing_application_requires_review") app = self.api("POST", "/iam/v1alpha1/applications", { "name": spec["name"], "organization_id": organization, "description": "Coulomb-only " + role + "; " + DECISION, }) self.state["apps"][role] = app self.save() if app["name"] != spec["name"] or app["organization_id"] != organization: raise Refused("application_binding_refused") return app def policy(self, role, app, project, organization): name = "coulomb-recovery-project-" + role if role in self.state["policies"]: policy = self.api("GET", "/iam/v1alpha1/policies/" + self.state["policies"][role]["id"]) else: policies = self.inventory("/iam/v1alpha1/policies", "policies", organization_id=organization) if any(p["name"] == name for p in policies): raise Refused("untracked_existing_policy_requires_review") policy = self.api("POST", "/iam/v1alpha1/policies", { "name": name, "description": "Coulomb recovery project only; " + DECISION, "organization_id": organization, "application_id": app["id"], "rules": [{"project_ids": [project["id"]], "permission_set_names": ROLES[role]["permissions"]}], }) self.state["policies"][role] = policy self.save() if (policy["application_id"] != app["id"] or policy["name"] != name or policy["organization_id"] != organization): raise Refused("policy_binding_refused") rules = self.api("GET", "/iam/v1alpha1/rules?" + urlencode({"policy_id": policy["id"]})) if len(rules["rules"]) != 1: raise Refused("single_project_rule_required") rule = rules["rules"][0] if (rule.get("organization_id") or rule.get("project_ids") != [project["id"]] or set(rule.get("permission_set_names", [])) != set(ROLES[role]["permissions"]) or rule.get("condition") or rule.get("deny")): raise Refused("effective_project_rule_refused") def key(self, role, app, project): path = self.directory / (role + "-pair.json") # A paused attended window can outlast the bounded construction key. # Renew only this role, after exact own-key revocation/readback. Never # replace reader/custodian/publisher credentials as a bootstrap retry. prior = self.state["keys"].get(role, {}) if role == "bootstrap" and prior: expired = datetime.fromisoformat(prior["expires_at"].replace("Z", "+00:00")) <= self.now if expired: if path.exists(): old = private_json(path) actual = self.api("GET", "/iam/v1alpha1/api-keys/" + old["AWS_ACCESS_KEY_ID"]) native_expiry = datetime.fromisoformat(actual["expires_at"].replace("Z", "+00:00")) if native_expiry > self.now: raise Refused("native_bootstrap_expiry_not_reached") self.retire_bootstrap() self.state.setdefault("retired_bootstrap_keys", []).append(dict(prior)) self.state["keys"].pop(role) self.save() if role in self.state["keys"]: pair = private_json(path) if set(pair) != {"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"}: raise Refused("exact_pair_fields_required") current = self.api("GET", "/iam/v1alpha1/api-keys/" + pair["AWS_ACCESS_KEY_ID"]) if (current["application_id"] != app["id"] or current["default_project_id"] != project["id"]): raise Refused("existing_key_binding_refused") return if path.exists() or path.is_symlink(): raise Refused("untracked_pair_requires_review") self.state["pending_key_issue"] = {"role": role, "application_id": app["id"]} self.save() payload = {"application_id": app["id"], "default_project_id": project["id"], "description": "Coulomb recovery " + role + "; " + DECISION} if role == "bootstrap": payload["expires_at"] = (self.now + timedelta(hours=2)).isoformat() try: issued = self.api("POST", "/iam/v1alpha1/api-keys", payload) except ProviderError as error: if error.status in {400, 401, 403, 404, 422, 429}: self.state.pop("pending_key_issue") self.save() raise try: if issued.get("application_id") != app["id"]: raise Refused("issued_key_binding_refused") if role == "bootstrap": expires = datetime.fromisoformat( (issued.get("expires_at") or "").replace("Z", "+00:00")) if not self.now < expires <= self.now + timedelta(hours=2): raise Refused("bounded_bootstrap_expiry_required") pair = {"AWS_ACCESS_KEY_ID": issued["access_key"], "AWS_SECRET_ACCESS_KEY": issued["secret_key"]} if any(not isinstance(v, str) or not v for v in pair.values()): raise Refused("complete_pair_required") fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as stream: json.dump(pair, stream) stream.write("\n") stream.flush() os.fsync(stream.fileno()) fsync_directory(path.parent) except (Refused, OSError, KeyError, ValueError, TypeError): if issued.get("access_key") and issued.get("application_id") == app["id"]: try: self.api("DELETE", "/iam/v1alpha1/api-keys/" + issued["access_key"]) self.state.pop("pending_key_issue") self.save() except (Refused, OSError, KeyError, ValueError, TypeError): raise Refused("pair_persistence_and_revocation_failed_review_required") from None raise Refused("pair_persistence_failed_review_required") from None self.state["keys"][role] = {"application_id": app["id"], "file": path.name, "expires_at": issued.get("expires_at")} self.state.pop("pending_key_issue") self.save() def run(self): publisher = self.api("GET", "/iam/v1alpha1/applications/" + PUBLISHER) if publisher["id"] != PUBLISHER or publisher["name"] != ROLES["publisher"]["name"]: raise Refused("publisher_binding_refused") organization = publisher["organization_id"] permissions = self.inventory("/iam/v1alpha1/permission-sets", "permission_sets", organization_id=organization) names = {v["name"] for v in permissions} if any(set(role["permissions"]) - names for role in ROLES.values()): raise Refused("required_permission_sets_unavailable") applications = self.inventory("/iam/v1alpha1/applications", "applications", organization_id=organization) project = self.project(organization) for role in ROLES: app = self.application(role, organization, applications) self.policy(role, app, project, organization) if role != "publisher": self.key(role, app, project) self.state["status"] = "identity_bootstrap_complete_native_boundaries_pending" self.save() return {"status": self.state["status"], "new_project": PROJECT, "managed_recovery_applications": 3, "project_policies_verified": 4, "private_pairs": 3, "publisher_pair_replaced": False, "organization_wide_grants": False, "bootstrap_key_max_hours": 2, "native_s3_escrow_boundaries_verified": False, "credential_values_printed": False} def retire_bootstrap(self): if self.state.get("status") != "identity_bootstrap_complete_native_boundaries_pending": raise Refused("completed_identity_stage_required") app = self.state["apps"]["bootstrap"] actual = self.api("GET", "/iam/v1alpha1/applications/" + app["id"]) if actual["name"] != ROLES["bootstrap"]["name"]: raise Refused("bootstrap_application_binding_refused") path = self.directory / "bootstrap-pair.json" if self.state["keys"]["bootstrap"].get("revoked_verified") and not path.exists(): return {"status": "bootstrap_api_key_retired", "credential_values_printed": False} pair = private_json(path) key_path = "/iam/v1alpha1/api-keys/" + pair["AWS_ACCESS_KEY_ID"] try: current = self.api("GET", key_path) except ProviderError as error: if error.status != 404: raise else: if (current["application_id"] != app["id"] or current["default_project_id"] != ( self.state["project"]["id"] )): raise Refused("retirement_key_binding_refused") self.api("DELETE", key_path) try: self.api("GET", key_path) except ProviderError as error: if error.status != 404: raise else: raise Refused("bootstrap_revocation_unconfirmed") self.state["keys"]["bootstrap"]["revoked_verified"] = True self.save() path.unlink() # Only this just-revoked temporary pair, never retained archives/pairs. fsync_directory(path.parent) return {"status": "bootstrap_api_key_retired", "own_temporary_pair_removed": True, "credential_values_printed": False} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--execute", action="store_true") parser.add_argument("--directory", type=Path) parser.add_argument("--retire-bootstrap", action="store_true") args = parser.parse_args() if not args.execute: print(json.dumps({"status": "preview", "project": PROJECT, "applications": [v["name"] for k, v in ROLES.items() if k != "publisher"], "provider_contacted": False, "credential_values_printed": False})) return 0 if not args.directory or not args.directory.is_absolute(): raise Refused("private_absolute_directory_required") private_directory(args.directory) fd = os.open(args.directory / "identity-bootstrap.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) with os.fdopen(fd, "w") as lock: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) raw = sys.stdin.read(65537) if len(raw) > 65536: raise Refused("bounded_temporary_auth_required") auth = json.loads(raw) bootstrap = Bootstrap(Transport(auth), args.directory) print(json.dumps(bootstrap.retire_bootstrap() if args.retire_bootstrap else bootstrap.run())) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (Refused, OSError, KeyError, ValueError, TypeError) as error: print(json.dumps({"status": "failed", "failure_type": type(error).__name__, "http_status": getattr(error, "status", None), "refusal": str(error) if isinstance(error, Refused) and all( c.islower() or c == "_" for c in str(error)) else None, "credential_values_printed": False})) raise SystemExit(1) from None