From 57ad4abefc9aa60788df6c6ff9df33642dae26f6 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 00:23:04 +0200 Subject: [PATCH] Fix access inventory evidence and reconcile closure coordination Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a11beb-4eef-7fc1-94a7-597130a01ae9 --- AGENTS.md | 7 +- GOAL.md | 12 +- SCOPE.md | 13 +- history/2026-10-09-review-and-maintenance.md | 57 ++++++++ inventory/access-consumers.md | 49 ++++--- tests/test_access_consumers.py | 70 ++++++++- tools/access_consumers.py | 100 ++++++++++--- ...RGREF-WP-0001-foundation-and-entry-gate.md | 10 ++ ...WP-0002-forge-closure-netkingdom-access.md | 135 ++++++++++-------- 9 files changed, 342 insertions(+), 111 deletions(-) create mode 100644 history/2026-10-09-review-and-maintenance.md diff --git a/AGENTS.md b/AGENTS.md index 4cfc0a0..765a6c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,8 +6,11 @@ Dev Hub (State Hub API): http://127.0.0.1:8000 This project is **deliberately deferred**. Read `GOAL.md` "Why this is deferred" and the `ORGREF-WP-0001` entry gate before proposing execution work. -T01 and T02 are read-only and safe to advance at any time; **T03 onward must -not start until all four entry-gate conditions hold.** +Within ORGREF-WP-0001, T01 and T02 are read-only and safe to advance at any +time; **ORGREF-WP-0001-T03 onward must not start until all four entry-gate +conditions hold.** ORGREF-WP-0002 coordinates access control independently, +under its own accepted policy, owner boundaries and visibility-wave gates. +It does not authorize repository transfers or waive the org-migration gate. If you believe the gate is met, say which of the four conditions changed and what evidence shows it — do not infer readiness from elapsed time. diff --git a/GOAL.md b/GOAL.md index 679dea0..04034c5 100644 --- a/GOAL.md +++ b/GOAL.md @@ -3,7 +3,7 @@ repo: prj-forgejo-org-refactor repo_flavor: project project_status: draft started: "2026-08-11" -reviewed: "2026-09-28" +reviewed: "2026-10-09" --- # Project goal @@ -42,6 +42,16 @@ dependency between the rollback prerequisite and T04. Preparation artifacts: [inventory](inventory/README.md), [taxonomy](taxonomy/README.md), and [preparation record](history/2026-09-28-preparation.md). +## Access-control coordination — 2026-10-09 + +ORGREF-WP-0002 independently coordinates the accepted October 8 policy: private +forge repositories and packages, external deliberate publication, authenticated +consumers and staged visibility changes. T01–T03 have accepted preparation; +identity implementation and closure proofs remain with their owner records. +See [policy](decisions/forge-visibility-policy.md) and the +[October 9 review](history/2026-10-09-review-and-maintenance.md). This work does +not satisfy or waive the four-condition org-transfer entry gate. + ## Invariants 1. **No silent breakage.** Every reference that moves is inventoried before it diff --git a/SCOPE.md b/SCOPE.md index 5cf2f12..801ae2c 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -9,7 +9,7 @@ Temporary coordination repo for splitting the single `coulomb/` Forgejo organization into stewardship organizations without breaking GitOps, image -pulls, or clones. +pulls, or clones, and coordinating private forge access under ORGREF-WP-0002. --- @@ -32,6 +32,8 @@ by ID; it does not duplicate their tasks. package registry - Migration sequencing, rollback plan, and cutover evidence - Promotion of the durable org-assignment convention to a permanent home +- Forge access-consumer inventory, identity-owner coordination and visibility + sequencing under the accepted ORGREF-WP-0002 policy --- @@ -53,9 +55,10 @@ by ID; it does not duplicate their tasks. | Repo | Role | | --- | --- | -| `railiance-forge` | Forgejo runtime owner — executes org/transfer operations | +| `railiance-forge` | Historical forge-owner records; re-confirm ownership before org/transfer operations | +| `key-cape` / `net-kingdom` | OIDC contract and KnownSecurityCase client registration | | `railiance-platform` | ArgoCD Application sources, OpenBao lanes referencing repo URLs | -| `railiance-apps` | Chart image `repository:` paths and chart metadata | +| `railiance-apps` | Current Forgejo Helm/runtime and runner owner; chart image paths and metadata | | `railiance-master` | Durable home candidate for the org-assignment convention | | `the-custodian` | Canon home candidate; repo-classification alignment | | Current server-backed `coulomb/` repo inventory | Subjects of the migration; 112 was the seed count | @@ -97,3 +100,7 @@ project's workplans, never copied. both tasks wait on explicit evidence and acceptance gaps. Execution requires the unchanged `ORGREF-WP-0001` entry gate and resolution of its circular rollback dependency - Evidence and next sequence: [readiness review](history/2026-09-28-readiness-review.md) + +- October 8–9: WP-0002 policy and inventories accepted; identity integration + waits on KEY-WP-0038 / NK-WP-0048 and RAPPS-WP-0016. Private forge access + is independent of the unchanged WP-0001 org-transfer gate. diff --git a/history/2026-10-09-review-and-maintenance.md b/history/2026-10-09-review-and-maintenance.md new file mode 100644 index 0000000..e4293ec --- /dev/null +++ b/history/2026-10-09-review-and-maintenance.md @@ -0,0 +1,57 @@ +# Review and maintenance — 2026-10-09 + +User instruction: check changes and open work, review and implement, then commit +and sync. The checkout began clean and matched origin/main after fetch. + +## Open work and authority + +ORGREF-WP-0001 remains blocked: T01/T02 wait on authenticated completeness and +stewardship acceptance; T03–T05 retain all four entry conditions. No new evidence +establishes readiness and the rollback-gate amendment remains proposed. + +ORGREF-WP-0002 has T01–T03 done and T04–T09 waiting. The October 8 policy settles +private packages, no anonymous forge access, Telegram uploads, the interim +visibility-only guardrails and the agent driver. KEY-WP-0038 has an accepted +claim contract; registration T02 waits on NK-WP-0048-T06, and claims proof T03 +is pending. RAPPS-WP-0016 has break-glass preparation T01 todo; authentication +and account/team proof remain waiting. Their stable IDs are linked in this +project's workplan. Owner implementation stays in those repositories. + +## Implemented + +- Corrected access collector package-index classification: API namespaces do + not establish repository identity. Hostnames remain distinct for legacy-host + disposition, rather than combining old and current forge references. +- Schema 0.2 replaces the unsupported anonymous-pull inference with a count of + pulls without named credentials. Node and service-account authentication + require separate evidence, and named secrets are not proof of working access. +- Made traversal deterministic, excluded the coordinator's generated artifacts, + reused the reference collector's sensitive/binary exclusions, and reported + exclusions and read errors. Symlinks and non-regular files (including FIFOs) + are not followed or read; diagnostics never contain raw exception text. +- Reconciled GOAL/SCOPE and WP-0002 with the accepted policy and actual owner + records. Moved collector authentication proof to T06, before closure; T09 + verifies completeness and handback. Retained interim and normal wave gates. +- Qualified the AGENTS.md gate explicitly as WP-0001's gate, consistent with + WP-0002's existing independence. No condition or rollback exception changed. +- Put task headings before their task blocks so projected titles refer to the + correct stable task IDs. Removed obsolete undecided-policy claims and withdrew + unsafe inline Secret/registry inspection commands while preserving the dated + founder results. + +## Validation and limits + +Ten unit tests pass, covering CLI determinism, separate hosts, package targets, +credential uncertainty, sensitive/binary/symlink/FIFO exclusions, sanitized read +errors and the existing migration inventory checks. Whitespace validation passes. +A scoped CLI replay scanned key-cape and reused the retained October 8 runtime +capture: 10 GitOps consumers, 43 image pulls without named credentials, zero +scan errors. The coordinator was correctly excluded. Temporary output remains +in /tmp; retained accepted evidence was not overwritten. + +A fleet-wide replay was interrupted after it did not finish promptly; it is not +complete evidence. The scoped replay is a collector integration check, not a +fresh fleet census, deployable-coverage receipt or unchanged-fleet G1 pair. +No credential requests, Secret reads, owner assignments, forge visibility +changes, transfers or other runtime changes were made. Remaining work stays +with the documented decisions and owner records. diff --git a/inventory/access-consumers.md b/inventory/access-consumers.md index 58e146f..9791150 100644 --- a/inventory/access-consumers.md +++ b/inventory/access-consumers.md @@ -23,7 +23,7 @@ Secrets and node registry configuration were not inspected. | package index | kaizen-agentic, vergabe-teilnahme | PyPI `simple/` index without credentials | package-read token | | federation | reuse-surface | about 60 capability indexes by raw URL | read identity, or a published index location | | code readers | about 8 files, below | clones, API and raw reads without token handling | owner review; re-point to an authenticated path | -| campaign images | pr-hall-of-helix, fluid-telegram | Telegram fetching post images by raw URL | a deliberate public asset location (ORGREF-WP-0002-T01 point 4) | +| campaign images | pr-hall-of-helix, fluid-telegram | Telegram fetching post images by raw URL | adapter fetches privately and uploads to Telegram (accepted policy; FT-WP-0001-T14) | Lower risk: `repo-gitops` and `repo-image-reference` are the cluster consumers seen from their source repos. `doc-link` means links break for the public, not @@ -71,29 +71,15 @@ URLs and whynot-design's SSH `repository` field. All 160 local clones use SSH with the founder's key (`forgejo-remote`). They keep working as long as that account can read the repositories. -## Founder checks (presence only, no secret values) +## Historical founder checks -Agents do not read Secrets, so run these in your own terminal and paste the output. - -1. ArgoCD repository credentials, by name, type and URL only: - - ```bash - kubectl -n argocd get secrets \ - -l 'argocd.argoproj.io/secret-type in (repository,repo-creds)' \ - -o go-template='{{range .items}}{{.metadata.name}} {{index .metadata.labels "argocd.argoproj.io/secret-type"}} {{if .data.url}}{{.data.url | base64decode}}{{end}}{{"\n"}}{{end}}' - ``` - -2. k3s registry configuration on railiance01, as key names with every value - stripped: - - ```bash - ssh railiance01 'sudo test -f /etc/rancher/k3s/registries.yaml \ - && sudo sed -nE "s/^([[:space:]]*[A-Za-z0-9_.\"-]+:).*/\1/p" /etc/rancher/k3s/registries.yaml \ - || echo absent' - ``` - - `absent`, or no `auth:` under the forge host, means all 43 pulls depend on - anonymous registry access today. +The October 8 results below are preserved as dated owner evidence. The original +inline Secret template and registry-value-stripping commands are withdrawn: +the estate forbids standalone go-template/jsonpath against real Secrets, and +text stripping is not a reliable boundary for registry credentials. Do not +repeat them. Obtain a current sanitized receipt from the platform/node owner; +Secret annotation checks use the output-suppressing platform maintenance helper +specified in the-custodian's agent environment orientation, section 6. ### Results, 2026-10-08 (run by the founder) @@ -114,3 +100,20 @@ Agents do not read Secrets, so run these in your own terminal and paste the outp - Consumers outside these working trees (external sites and links, other people's clones) cannot be found by scanning; T08's anonymous probe after each wave is the backstop. + +## Collector corrections — 2026-10-09 + +The retained October 8 JSON uses schema 0.1. Its `anonymous_image_pulls` field +was an inference from absent pod pull-secret names; only the separate founder +node-auth check supports the anonymous-access conclusion for that snapshot. +Do not reuse this inference for another cluster or capture. + +Schema 0.2 reports `image_pulls_without_named_credentials`, with access marked +unknown until node/service-account credential evidence exists. It preserves +forge hostnames, so legacy references stay separate, and classifies package +API indexes as package consumers with null repository targets (package namespace +is not repository identity). `scan_exclusions` and `scan_error_count` expose +coverage limits; the coordinator is omitted to avoid self-referential output. +Sensitive/encrypted, binary and symlinked paths are not read. The source scan is +deterministic for unchanged inputs, but no claim of full consumer completeness +or deployment state follows from matching outputs. diff --git a/tests/test_access_consumers.py b/tests/test_access_consumers.py index b7fde3d..162fccf 100644 --- a/tests/test_access_consumers.py +++ b/tests/test_access_consumers.py @@ -1,9 +1,14 @@ import sys +import json +import os +import subprocess +import tempfile import unittest from pathlib import Path +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) -from access_consumers import classify, runtime_consumers, target_repo # noqa: E402 +from access_consumers import classify, runtime_consumers, scan_repo, target_repo # noqa: E402 class ClassifyTest(unittest.TestCase): @@ -18,6 +23,8 @@ class ClassifyTest(unittest.TestCase): ("README.md", "/coulomb/x", "see", "doc-link"), ("docs/evidence/run.json", "/coulomb/x", "x", "record"), ("Makefile", "/coulomb/x", "push", "build-tooling"), + ("pyproject.toml", "/api/packages/coulomb/pypi/simple", "url: x", "package-consumer"), + ("client.py", "/api/v1/packages/coulomb", "u = ...", "package-consumer"), ] for f, p, line, want in cases: self.assertEqual(classify(f, p, line), want, f) @@ -28,16 +35,73 @@ class ClassifyTest(unittest.TestCase): self.assertEqual(target_repo("/v2/coulomb/flex-auth/manifests/x"), "coulomb/flex-auth") self.assertEqual(target_repo("/coulomb/activity-core@sha256:ab"), "coulomb/activity-core") self.assertIsNone(target_repo("/user/login")) + self.assertIsNone(target_repo("/api/packages/coulomb/pypi/simple")) + self.assertIsNone(target_repo("/api/v1/packages/coulomb")) + self.assertIsNone(target_repo("/api/v1/orgs/coulomb/repos")) - def test_runtime_anonymous_pull(self): + def test_missing_pull_secret_does_not_prove_anonymous_access(self): rt = {"applications": [], "workloads": [{ "namespace": "ns", "kind": "Deployment", "name": "api", "image_pull_secret_names": [], "images": ["forgejo.coulomb.social/coulomb/x@sha256:00"]}]} (c,) = runtime_consumers(rt) self.assertEqual(c["class"], "cluster-image-pull") - self.assertTrue(c["credential"].startswith("anonymous")) + self.assertTrue(c["credential"].startswith("unknown")) self.assertEqual(c["target"], "coulomb/x") + def test_scan_excludes_sensitive_and_binary_files_without_values(self): + with tempfile.TemporaryDirectory() as d: + repo = Path(d) + (repo / "client.py").write_text('url = "https://user:hidden@forgejo.coulomb.social/coulomb/valid?token=hidden"\n') + (repo / ".env").write_text("forgejo.coulomb.social/coulomb/private-value\n") + (repo / "secret.sops.yaml").write_text("forgejo.coulomb.social/coulomb/encrypted-value\n") + (repo / "binary").write_bytes(b"\0forgejo.coulomb.social/coulomb/binary-value") + (repo / "link.py").symlink_to(repo / "client.py") + os.mkfifo(repo / "pipe") + diagnostics = [] + hits = scan_repo(repo, diagnostics) + self.assertEqual([h["target"] for h in hits], ["coulomb/valid"]) + serialized = json.dumps(hits) + for value in ["hidden", "private-value", "encrypted-value", "binary-value"]: + self.assertNotIn(value, serialized) + self.assertEqual({x["reason"] for x in diagnostics}, + {"sensitive-or-encrypted", "binary-content", "symlink-not-followed", "non-regular-file"}) + + def test_cli_is_repeatable_and_keeps_legacy_host_separate(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + repo = root / "reader" + repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + (repo / "client.py").write_text( + 'a = "https://forgejo.coulomb.social/coulomb/x"\n' + 'b = "https://gitea.coulomb.social/coulomb/x"\n') + runtime = root / "runtime.json" + runtime.write_text(json.dumps({"applications": [], "workloads": []})) + assignments = root / "assignments.csv" + assignments.write_text("repository,proposed_org\nx,netkingdom\n") + output = root / "output.json" + command = [sys.executable, str(Path(__file__).resolve().parents[1] / "tools/access_consumers.py"), + "--root", str(root), "--runtime", str(runtime), + "--assignments", str(assignments), "--output", str(output)] + subprocess.run(command, check=True, capture_output=True) + first = output.read_bytes() + subprocess.run(command, check=True, capture_output=True) + self.assertEqual(first, output.read_bytes()) + report = json.loads(first) + self.assertEqual({c["host"] for c in report["consumers"]}, + {"forgejo.coulomb.social", "gitea.coulomb.social"}) + self.assertEqual(report["scan_error_count"], 0) + + def test_unreadable_file_is_reported_without_raw_error(self): + with tempfile.TemporaryDirectory() as d: + repo = Path(d) + (repo / "client.py").write_text("placeholder") + diagnostics = [] + with patch.object(Path, "read_text", side_effect=OSError("private error detail")): + self.assertEqual(scan_repo(repo, diagnostics), []) + self.assertEqual(diagnostics, [{"repository": repo.name, + "path": "client.py", "reason": "unreadable-file"}]) + if __name__ == "__main__": unittest.main() diff --git a/tools/access_consumers.py b/tools/access_consumers.py index 05becc8..77f8474 100644 --- a/tools/access_consumers.py +++ b/tools/access_consumers.py @@ -17,9 +17,12 @@ import csv import json import os import re +import stat import subprocess from pathlib import Path +from inventory import BINARY, SENSITIVE + FORGE_HOSTS = ("forgejo.coulomb.social", "gitea.coulomb.social") URL = re.compile( r"(?:https?://)?(?Pforgejo\.coulomb\.social|gitea\.coulomb\.social)" @@ -34,6 +37,7 @@ KINDS = [ ("record", lambda f, p, line: f.startswith(("docs/evidence/", "evidence/", "tests/", "test/")) or "/evidence/" in f or "/tests/" in f or f.endswith(("PKG-INFO", ".sum"))), ("raw-fetch", lambda f, p, line: "/raw/" in p or "/media/" in p), + ("package-consumer", lambda f, p, line: p.startswith(("/api/packages/", "/api/v1/packages/"))), ("api-client", lambda f, p, line: p.startswith("/api/")), ("ci-action", lambda f, p, line: ".forgejo/workflows" in f or ".gitea/workflows" in f or ".github/workflows" in f), @@ -65,29 +69,68 @@ def target_repo(path): parts = [p for p in (path or "").split("/") if p] if parts[:2] == ["api", "v1"] and len(parts) >= 5 and parts[2] == "repos": parts = parts[3:] - elif parts and parts[0] in {"api", "v2"}: + elif parts and parts[0] == "api": + # Package paths name an owner/index, not a repository. Other API + # endpoints also cannot establish a repository identity. + return None + elif parts and parts[0] == "v2": parts = parts[1:] if len(parts) >= 2 and parts[0] not in {"user", "explore", "assets"}: return f"{parts[0]}/{parts[1].removesuffix('.git').split('@')[0].split(':')[0]}" return None -def scan_repo(repo): +def scan_repo(repo, diagnostics=None): hits = [] - for dirpath, dirnames, filenames in os.walk(repo): - dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".") - or d in {".forgejo", ".gitea", ".github"}] - for name in filenames: - if name in SKIP_FILES: - continue + diagnostics = diagnostics if diagnostics is not None else [] + def excluded(path, reason): + diagnostics.append({"repository": repo.name, "path": path, "reason": reason}) + def walk_error(error): + excluded(str(Path(error.filename).relative_to(repo)), "unreadable-directory") + for dirpath, dirnames, filenames in os.walk(repo, onerror=walk_error): + kept = [] + for d in sorted(dirnames): + rel = str((Path(dirpath) / d).relative_to(repo)) + if (Path(dirpath) / d).is_symlink(): + excluded(rel, "symlink-not-followed") + elif d in SKIP_DIRS or (d.startswith(".") and d not in {".forgejo", ".gitea", ".github"}): + excluded(rel, "excluded-directory") + else: + kept.append(d) + dirnames[:] = kept + for name in sorted(filenames): full = Path(dirpath) / name + rel = str(full.relative_to(repo)) + if name in SKIP_FILES: + excluded(rel, "generated-work-record") + continue + if SENSITIVE.search(rel): + excluded(rel, "sensitive-or-encrypted") + continue + if full.suffix.lower() in BINARY: + excluded(rel, "binary-extension") + continue try: - if full.is_symlink() or full.stat().st_size > MAX_BYTES: + if full.is_symlink(): + excluded(rel, "symlink-not-followed") + continue + info = full.stat() + if not stat.S_ISREG(info.st_mode): + excluded(rel, "non-regular-file") + continue + if info.st_size > MAX_BYTES: + excluded(rel, "over-1MB") continue text = full.read_text(encoding="utf-8") - except (OSError, UnicodeDecodeError): + except UnicodeDecodeError: + excluded(rel, "non-utf8-content") + continue + except OSError: + excluded(rel, "unreadable-file") + continue + if "\0" in text: + excluded(rel, "binary-content") continue - rel = str(full.relative_to(repo)) for number, line in enumerate(text.splitlines(), 1): for m in URL.finditer(line): hits.append({ @@ -119,7 +162,7 @@ def runtime_consumers(runtime): m = URL.search(u) if m: out.append({"consumer": f"argocd:{app['name']}", "class": "cluster-gitops", - "kind": "gitops-source", "target": target_repo(m.group("path")), + "host": m.group("host"), "kind": "gitops-source", "target": target_repo(m.group("path")), "credential": "unknown (ArgoCD repo credentials not inspected)"}) for w in runtime.get("workloads", []): for image in w.get("images", []): @@ -128,9 +171,10 @@ def runtime_consumers(runtime): secrets = w.get("image_pull_secret_names") or [] out.append({"consumer": f"k3s:{w['namespace']}/{w['kind']}/{w['name']}", "class": "cluster-image-pull", "kind": "image-pull", + "host": m.group("host"), "target": target_repo(m.group("path")), "credential": "pull secret named: " + ",".join(secrets) - if secrets else "anonymous (no imagePullSecrets)"}) + if secrets else "unknown (no imagePullSecrets; node registry auth not inspected)"}) return out @@ -160,17 +204,19 @@ def main(): consumers = runtime_consumers(runtime) remotes = collections.Counter() scanned = [] + diagnostics = [] + coordinator = Path(__file__).resolve().parents[1] for repo in sorted(Path(args.root).iterdir()): - if not (repo / ".git").exists() or repo.name.startswith("rescued-"): + if not (repo / ".git").exists() or repo.name.startswith("rescued-") or repo.resolve() == coordinator: continue scanned.append(repo.name) remotes[remote_form(repo) or "none"] += 1 grouped = collections.defaultdict(list) - for h in scan_repo(repo): - grouped[(h["kind"], h["target"])].append(f"{h['file']}:{h['line']}") - for (kind, target), where in sorted(grouped.items(), key=lambda kv: (kv[0][0], str(kv[0][1]))): + for h in scan_repo(repo, diagnostics): + grouped[(h["host"], h["kind"], h["target"])].append(f"{h['file']}:{h['line']}") + for (host, kind, target), where in sorted(grouped.items(), key=lambda kv: (kv[0][0], kv[0][1], str(kv[0][2]))): consumers.append({"consumer": f"repo:{repo.name}", "class": CLASS_OF_KIND[kind], - "kind": kind, "target": target, "locations": where[:10], + "host": host, "kind": kind, "target": target, "locations": where[:10], "location_count": len(where)}) for c in consumers: @@ -179,19 +225,28 @@ def main(): summary = collections.Counter(c["class"] for c in consumers) report = { - "schema_version": "0.1", + "schema_version": "0.2", "workplan": "ORGREF-WP-0002-T02", "runtime_capture": os.path.basename(args.runtime), "scanned_repositories": len(scanned), "local_remote_forms": dict(remotes), "summary_by_class": dict(sorted(summary.items())), - "anonymous_image_pulls": sum(1 for c in consumers if c["class"] == "cluster-image-pull" - and c["credential"].startswith("anonymous")), + "image_pulls_without_named_credentials": sum(1 for c in consumers if c["class"] == "cluster-image-pull" + and c["credential"].startswith("unknown")), "consumers": consumers, + "scan_exclusions": diagnostics, + "scan_error_count": sum(d["reason"].startswith("unreadable") for d in diagnostics), "limits": [ "Runtime classes come from the runtime capture named above, not a live read.", "ArgoCD repository credentials and pull-secret contents were not inspected; " "a named pull secret is not proof that it authenticates to the forge.", + "Missing imagePullSecrets do not prove anonymous pulls: node-level registry " + "auth and service-account defaults require owner evidence.", + "Sensitive/encrypted, binary, large, symlinked and excluded paths are recorded " + "in scan_exclusions; unreadable paths prevent a completeness claim.", + "The coordinator is excluded to prevent generated inventory self-reference.", + "Hostnames are preserved so legacy-host references can be reviewed separately.", + "Package indexes name a namespace, not a verified repository; their target is null.", "Only literal forge hostnames are found; computed URLs and consumers outside " "these working trees (external sites, Telegram, other people's clones) are not.", "history/ and archived/ directories are skipped; they record, they do not read.", @@ -199,7 +254,8 @@ def main(): } Path(args.output).write_text(json.dumps(report, indent=1, sort_keys=False) + "\n") print(json.dumps({k: report[k] for k in ("scanned_repositories", "local_remote_forms", - "summary_by_class", "anonymous_image_pulls")}, + "summary_by_class", "image_pulls_without_named_credentials", + "scan_error_count")}, indent=1)) diff --git a/workplans/ORGREF-WP-0001-foundation-and-entry-gate.md b/workplans/ORGREF-WP-0001-foundation-and-entry-gate.md index 0ee838c..9d1536f 100644 --- a/workplans/ORGREF-WP-0001-foundation-and-entry-gate.md +++ b/workplans/ORGREF-WP-0001-foundation-and-entry-gate.md @@ -102,6 +102,8 @@ It must not: ## Tasks +### T01 — Build the repeatable reference inventory + ```task id: ORGREF-WP-0001-T01 status: wait @@ -159,6 +161,8 @@ in `railiance-platform/argocd/applications/`; `repository: forgejo.coulomb.social/coulomb/core-hub` in `railiance-apps/charts/core-hub/values.yaml`; `home:`/`sources:` URLs across at least five charts. +### T02 — Agree stewardship criteria and taxonomy + ```task id: ORGREF-WP-0001-T02 status: wait @@ -200,6 +204,8 @@ home, both pending acceptance. Decision whynot as an additional steward, and records evidence for ops-hub/disaster-control. Naming and maintainer acceptance remain pending. +### T03 — Verify Git and package transfer behavior + ```task id: ORGREF-WP-0001-T03 status: wait @@ -222,6 +228,8 @@ documentation — the container-registry path is the dangerous one because its failure is deferred to the next pod reschedule rather than surfacing at rename time. +### T04 — Prove transfer rollback + ```task id: ORGREF-WP-0001-T04 status: wait @@ -240,6 +248,8 @@ Record exact operation, product/version, old/new paths, uncached pulls, GitOps revision/health, retention protections, credentials scope, cleanup and recovery time. Unrelated application rollout rollback is not acceptable transfer proof. +### T05 — Write the staged cutover plan + ```task id: ORGREF-WP-0001-T05 status: wait diff --git a/workplans/ORGREF-WP-0002-forge-closure-netkingdom-access.md b/workplans/ORGREF-WP-0002-forge-closure-netkingdom-access.md index b295b49..65cd4d1 100644 --- a/workplans/ORGREF-WP-0002-forge-closure-netkingdom-access.md +++ b/workplans/ORGREF-WP-0002-forge-closure-netkingdom-access.md @@ -9,7 +9,7 @@ flavor: implementation owner: claude-code topic_slug: railiance created: "2026-10-08" -updated: "2026-10-08" +updated: "2026-10-09" depends_on: [] related_repos: - key-cape @@ -48,9 +48,10 @@ does not apply. It changes **who may read**, not where things live. It reuses ORGREF-WP-0001's preparation: the `inventory/` collectors, the stewardship assignments in `taxonomy/` (as owners for each consumer) and the reference scan. It also gives back to ORGREF-WP-0001: once the forge is closed, -the anonymous collector can no longer see private repositories, so T09 moves it -to an authenticated read identity. That resolves the access half of decision -`5f3680fc` (authenticated metadata). +the anonymous collector can no longer see private repositories. T06 must prove +its authenticated read identity before closure; T09 verifies the handback. +Accepted complete metadata evidence can resolve the access half of decision +`5f3680fc`; having a token alone does not establish completeness. ## Target state @@ -58,14 +59,15 @@ to an authenticated read identity. That resolves the access half of decision use NetKingdom-issued identities or scoped Forgejo tokens with an owner and a rotation path. A local administrator account remains only as sealed break-glass, with its credentials in OpenBao. -- Organisations and repositories are **private by default**. Public visibility - is an explicit, recorded allowlist entry with a reason. +- Organisations, repositories and packages are private, with no anonymous + forge access. Deliberate publication goes to a separate location through the + reviewed per-repository `public/` convention. - Every consumer that read the forge anonymously reads it with its own credential, with least privilege, before its repositories close. -- Campaign images that must stay world-readable (Telegram posts fetch them by - URL) come from a deliberate public asset location, not from a forge repo that - happens to be public. -- Anonymous probes show only the allowlist; the evidence is recorded. +- The fluid-telegram adapter reads private images with its own identity and + uploads them to Telegram (FT-WP-0001-T14). A public asset store is later work. +- Anonymous probes show no forge repositories or packages after the final wave; + the evidence is recorded. ## Boundaries @@ -80,7 +82,9 @@ It must not: - configure key-cape, Forgejo, OpenBao or any consumer itself. Each change lands through the owner repo's own workplan. - flip a production repository's visibility before T07's rehearsal passes and - the founder approves that wave + the founder approves that wave, except the separately approved visibility-only + interim route in `decisions/forge-visibility-policy.md` section 5, whose clone, + sign-in, recovery and one-repository rollback checks remain mandatory - read or print secret values (credential routing: `warden route`) - move, rename or restructure repositories (that is ORGREF-WP-0001) @@ -90,6 +94,8 @@ separate review, not something the flip repairs. ## Tasks +### T01 — Decide the visibility policy + ```task id: ORGREF-WP-0002-T01 status: done @@ -97,8 +103,6 @@ priority: high state_hub_task_id: "6387cb32-dd73-5ee0-b51e-d4aea475bcca" ``` -### T01 — Decide the visibility policy - Record a founder decision covering: 1. **Default:** private for organisations and repositories, plus whether @@ -117,7 +121,7 @@ Record a founder decision covering: now with existing local accounts. If yes: which ones, accepting that their consumers move to local tokens temporarily and are re-homed in T06. -2026-10-08: points 1 and 2 decided, recorded in +2026-10-08: all six policy points and the driver role decided, recorded in `decisions/forge-visibility-policy.md`. The forge is private with no anonymous access, and public codebases go to a separate location. Publication is a per-repo `public/` folder of symlinks only, projected outward by a shared @@ -129,9 +133,14 @@ trust management comes later, not now. Points 5 and 6 decided: interim lockdown of 13 repositories under the founder's guardrail (tighten gradually, no lockout, no asset loss, nothing hard to repair: clones first, one repository, verify and roll back once, then small batches, visibility only). T03 runs in -parallel. Pre-check: executor-sandbox (diverged, unpushed local commits) and -ops-mason (uncommitted changes) wait for their owners. Open: point 3 (packages) -and the owner. +parallel. Packages are private; node registry auth, an ArgoCD credential template +and scoped CI/package identities are the accepted approach. An agent drives the +plan; the founder holds policy and production wave approval. Pre-check: +executor-sandbox (diverged, unpushed local commits) and ops-mason (uncommitted +changes) wait for their owners. All policy points are settled; implementation +proof remains outstanding. + +### T02 — Inventory every anonymous reader ```task id: ORGREF-WP-0002-T02 @@ -140,8 +149,6 @@ priority: high state_hub_task_id: "ace26a22-50e2-58c6-b4ad-85d95c284fce" ``` -### T02 — Inventory every anonymous reader - Read-only. Extend `inventory/` with an access-consumer inventory: every party that reads the forge today without credentials, the repositories or packages it reads, and the owner (from `taxonomy/assignments.csv`) who will give it an @@ -178,6 +185,8 @@ pulls anonymous). One node-level registry auth entry would cover every pull (T06). The stale Gitea credential on the node is a cleanup item. Code-reader confirmation moves to the owners under T06. +### T03 — Review what has already been public + ```task id: ORGREF-WP-0002-T03 status: done @@ -185,8 +194,6 @@ priority: high state_hub_task_id: "9c2ca7be-23f8-58c5-9bfd-e44a26ce4584" ``` -### T03 — Review what has already been public - Before closing, and independent of it: run a secret and sensitive-content scan over the public repositories' history (credentials, tokens, private keys, internal hostnames or IPs, personal data). Findings go to their owners for @@ -207,19 +214,23 @@ rest is benign (identifiers, digests, test fixtures, placeholders). Record and limits: `inventory/history-exposure-2026-10-08.md`. The rotation is tracked with the owners. +### T04 — NetKingdom: Forgejo as an OIDC client (child workplan in key-cape) + ```task id: ORGREF-WP-0002-T04 status: wait -depends_on: [ORGREF-WP-0002-T01] -blocking_reason: "The NetKingdom side follows the visibility and identity decisions; open the key-cape child workplan once T01 is resolved." +depends_on: [ORGREF-WP-0002-T01, KEY-WP-0038-T03] +blocking_reason: "KEY-WP-0038-T03 must deliver claims evidence; registration T02 waits on NK-WP-0048-T06 (KnownSecurityCase), not an ad-hoc helper." priority: high state_hub_task_id: "112617e1-a7c3-59c1-bf25-235d58ec118b" ``` -### T04 — NetKingdom: Forgejo as an OIDC client (child workplan in key-cape) - -Drafted 2026-10-08 as key-cape `KEY-WP-0038` (ready, founder-approved). -Open a key-cape workplan, linked here by ID, to: +Owner record: key-cape `KEY-WP-0038` +(`098e3e2c-a138-560e-b2d3-3a15eb49d8a9`). Reviewed October 9: T01 claim +contract accepted; T02 registration waits on net-kingdom `NK-WP-0048-T06` +(`d9b26bcf-a7c6-59a2-92a0-f913d76a203c`); T03 claims proof is pending. +The owner uses the accepted KnownSecurityCase route, with ops-mason and +ops-warden, rather than a case-specific script. The owner record covers: - register Forgejo as a confidential OIDC client (redirect URI, scopes, PKCE) with its client secret in OpenBao, through the railiance-platform custody path @@ -233,20 +244,22 @@ Open a key-cape workplan, linked here by ID, to: Acceptance: a test login against a staging client returns the expected claims for one human, one agent and one service identity. +### T05 — Forgejo: NetKingdom login and team mapping (child workplan in railiance-apps) + ```task id: ORGREF-WP-0002-T05 status: wait -depends_on: [ORGREF-WP-0002-T04] -blocking_reason: "Forgejo can only add the authentication source once the NetKingdom client and claims exist." +depends_on: [ORGREF-WP-0002-T04, RAPPS-WP-0016-T03] +blocking_reason: "RAPPS-WP-0016-T03 must prove account linking and team grants after KEY-WP-0038 delivers the client and claims." priority: high state_hub_task_id: "6c635b25-a4ae-5259-9317-de3fe38f6234" ``` -### T05 — Forgejo: NetKingdom login and team mapping (child workplan in railiance-apps) - railiance-apps owns the Forgejo Helm release (`docs/forgejo-on-railiance01.md`). -Drafted 2026-10-08 as railiance-apps `RAPPS-WP-0016` (ready, founder-approved). -Open a railiance-apps workplan, linked here by ID, to: +Owner record: railiance-apps `RAPPS-WP-0016` +(`f6b1d6f6-0abb-5774-87b9-bf3cfb640a3b`). Reviewed October 9: break-glass +preparation T01 is todo; authentication, linking and grants remain waiting. +The owner record covers: - add the NetKingdom OIDC authentication source and claim-to-team mapping; disable open registration; link existing local accounts (founder, agent @@ -260,6 +273,8 @@ Open a railiance-apps workplan, linked here by ID, to: Acceptance: the founder and one agent sign in through NetKingdom and see what their team grants. Break-glass login is verified once and recorded. +### T06 — Give every consumer its own read identity + ```task id: ORGREF-WP-0002-T06 status: wait @@ -269,23 +284,25 @@ priority: high state_hub_task_id: "1285f94d-3b57-502b-8aeb-96fab9578287" ``` -### T06 — Give every consumer its own read identity - For each consumer class in T02, its owner provisions a scoped credential and proves it works **against a private canary repository and package** created by this project, while anonymous access to that canary fails: -- cluster: deploy tokens for ArgoCD/Flux repo credentials, `imagePullSecrets` - for registry pulls (railiance-apps, railiance-platform) +- cluster: the accepted ArgoCD repository credential template and node-level + read-only registry login (railiance-apps, railiance-platform and node owner); + prove fresh pulls, including service-account and node credential behavior - runners: tokens or OIDC for checkout and reusable actions (railiance-apps, which runs `forgejo-runner`) - agents and workstations: NetKingdom identities or scoped tokens routed through ops-warden; nothing pasted into repositories -- State Hub, repo-manager and the ORGREF collector: authenticated read identity -- fluid-telegram and pr-hall-of-helix: images moved to the T01 public asset - location, with the `compose visual` raw-URL bases updated +- State Hub, repo-manager and the ORGREF collector: authenticated read identity, + with private repository/package enumeration proven before T08 +- fluid-telegram and pr-hall-of-helix: private image fetch and Telegram upload + through FT-WP-0001-T14; prove posts without anonymous raw-image reads Each owner records its proof in its own workplan. This plan tracks the list. +### T07 — Rehearse the flip and the rollback + ```task id: ORGREF-WP-0002-T07 status: wait @@ -295,14 +312,14 @@ priority: high state_hub_task_id: "0dcf25e2-cda1-5f87-b7fe-5b7e27635565" ``` -### T07 — Rehearse the flip and the rollback - On throwaway repositories and packages: switch public → private → public, and verify consumer reads, anonymous denial, web links, Actions runs and image pulls at each step. Record the exact commands or API calls, the time each step takes, and what a rollback restores (visibility) and what it does not (anything already fetched). This rehearsal is the gate for T08. +### T08 — Close the forge in waves + ```task id: ORGREF-WP-0002-T08 status: wait @@ -313,8 +330,6 @@ priority: high state_hub_task_id: "42724077-9837-5aef-ad3b-6551f2103aa2" ``` -### T08 — Close the forge in waves - Change production visibility in founder-approved waves. Each wave lists its repositories and packages and the consumers that read them. Proposed order: @@ -327,6 +342,8 @@ After each wave: consumer health checks, anonymous probe, and a recorded rollback point. A failing consumer rolls its repositories back to public and stops the wave. The fix lands with the consumer's owner. +### T09 — Verify, hand back and record + ```task id: ORGREF-WP-0002-T09 status: wait @@ -336,23 +353,27 @@ priority: medium state_hub_task_id: "82f53328-02f5-5953-830a-00019d7dee92" ``` -### T09 — Verify, hand back and record - -- Anonymous probe of the server: only the T01 allowlist is visible. Record the - evidence under `inventory/`. -- Move ORGREF-WP-0001's collector to the authenticated identity from T06 and - note the effect on decision `5f3680fc`. +- Anonymous probe of the server: no repositories or packages are visible. + Record the evidence under `inventory/`. +- Verify ORGREF-WP-0001's collector still enumerates private metadata with the + identity proven in T06; reconcile completeness and note the effect on decision + `5f3680fc`. Do not postpone collector migration until after closure. - Add a row to the-custodian `docs/security-close-down-checklist.md` recording that the forge is closed by default, plus anything relaxed along the way that must be re-armed. - Record any consumer still on a temporary credential as live owner work, not as prose in this file. -## Open questions for review +## Review — 2026-10-09 -- Owner: is the founder the right owner, or should an agent own the driver with - the founder holding T01/T08? -- Should T03 (history review) block T08, or run in parallel and only gate - rotations? -- Is an interim lockdown (T01 point 5) worth the temporary-token churn, given how - long T04–T07 will take? +T01–T03 are done; T04–T09 remain waiting on owner implementation and proof. +The October 8 policy settles driver ownership, package treatment, interim +lockdown and parallel exposure review. No new production wave is authorized by +this review. The interim route must satisfy its own recorded guardrails; the +normal closure route retains T06/T07 and founder wave approval. + +Collector schema 0.2 fixes package-index classification, separates legacy hosts, +records scan exclusions/errors and treats missing pull secrets as unknown access. +October 8 founder checks remain historical proof for that runtime snapshot; +new runtime records require current owner confirmation. This review does not +reopen T02 or replace its accepted evidence.