diff --git a/docs/evidence/README.md b/docs/evidence/README.md index 8733ea2..6a47427 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -1,6 +1,21 @@ -# Evidence index +# Evidence -Each core claim maps to a captured artifact. Every one of these states is reproducible on demand — the code regenerates them. +Every core claim maps to a captured artifact, and every state shown is +reproducible on demand — the code regenerates it. + +This page has two layers: an **at-a-glance table** for each section (scan the +whole picture on one screen), followed by the **full walkthrough** where each +claim is paired with its screenshot inline (scroll top to bottom for the detail). +Jump to the walkthrough via any row, or just keep scrolling. + +Two bodies of evidence: **Acts 1–3** (the GCP org baseline) and **Part 2** (the +self-hosted AI-agent gateway audit). + +--- + +## At a glance + +### Acts 1–3 — GCP organization baseline | # | Claim | Artifact | Status | |---|---|---|---| @@ -8,31 +23,187 @@ Each core claim maps to a captured artifact. Every one of these states is reprod | 2 | Keyless pipeline: plan-on-PR via WIF, zero SA keys in existence | `act2-keyless-pipeline-pr.png` | ✅ | | 3 | Apply-on-merge: a merge becoming real infrastructure, keyless | `act2-apply-on-merge-builds.png` | ✅ | | 4 | The platform refuses violations: SA key creation denied by org policy | `act1-org-policy-denies-sa-key.png` | ✅ | -| 5 | Deny-by-default in the wild: the default compute SA, stripped of auto-grants by org policy, cannot read Cloud Build's staging bucket until granted objectViewer on exactly that bucket | `act1-default-sa-born-with-nothing.png` | ✅ | -| 6 | The vending machine: a hardened project created by a pull request — 14 resources, and the workload gate visibly closed (`lb_ip: not yet deployed`) | `act3-project-vended-via-pr-{1,2,3}.png` | ✅ | +| 5 | Deny-by-default: the default compute SA, stripped of auto-grants, cannot read Cloud Build's staging bucket until granted objectViewer on exactly that bucket | `act1-default-sa-born-with-nothing.png` | ✅ | +| 6 | The vending machine: a hardened project created by a pull request — 14 resources, workload gate visibly closed (`lb_ip: not yet deployed`) | `act3-project-vended-via-pr-{1,2,3}.png` | ✅ | | 7 | The paid zero-trust layer, reviewed in full before a dollar was spent | `act3-workload-plan-{1,2,3}.png` | ✅ | | 8 | Zero trust, front door: anonymous request → identity challenge | `act3-iap-identity-challenge.png` | ✅ | | 9 | Zero trust, allow path: IAP-asserted identity rendered by the app | `act3-iap-asserted-identity.png` | ✅ | | 10 | Zero trust, deny path: an authenticated Google account, absent from the access list, receives 403 | `act3-iap-403-unauthorized.png` | ✅ | | 11 | Reproducibility: the paid layer destroyed through the same pipeline | `act3-teardown-plan.png` | ✅ | -Rows 8–10 are the zero-trust proof set: anonymous → challenged, authorized → named, authenticated-but-unauthorized → denied. A control you've only seen permit is a control you haven't tested. - -Trap 11 to the runbook's field notes: deletion_protection is enforced from state, not config — flipping it and destroying in one change deadlocks; apply the flag first, or evict via terraform state rm + delete out-of-band. Set it false from birth on resources with planned lifespans. +Rows 8–10 are the zero-trust proof set: anonymous → challenged, authorized → +named, authenticated-but-unauthorized → denied. *A control you've only seen +permit is a control you haven't tested.* -## Part 2 — MCP / AI-agent connector audit (`securing-mcp-setup.md`) +### Part 2 — self-hosted AI-agent gateway audit -Host hardening and containment evidence for the self-hosted gateway audit. Rows p2-08–10 are captured on a non-production staging host; rows p2-01–07 are from the production gateway. +Host hardening and containment evidence for the runbook +[`docs/runbooks/securing-mcp-setup.md`](../runbooks/securing-mcp-setup.md). +Rows p2-01–07 are from the **production** gateway; p2-08–10 are from a +**non-production staging** host, labeled as such. | # | Claim | Artifact | Status | |---|---|---|---| -| p2-01/02 | SSH password authentication disabled: effective `sshd` config `yes` → `no` | `p2-01-ssh-passwordauth-before.png`, `p2-02-ssh-passwordauth-after.png` | ✅ | -| p2-03/04 | Control-UI insecure auth disabled: `allowInsecureAuth` `true` → `false`, gateway active after change | `p2-03-insecureauth-before.png`, `p2-04-insecureauth-after.png` | ✅ | -| p2-05 | Exec authorization baseline: `security=full, ask=off`, empty allowlist — ungated shell/file-write | `p2-05-exec-approvals-before.png` | ✅ | +| p2-01/02 | SSH password authentication disabled: effective `sshd` config `yes` → `no` | `p2-01…before.png`, `p2-02…after.png` | ✅ | +| p2-03/04 | Control-UI insecure auth disabled: `allowInsecureAuth` `true` → `false`, gateway active after | `p2-03…before.png`, `p2-04…after.png` | ✅ | +| p2-05 | Exec baseline (High): `security=full, ask=off`, empty allowlist — ungated shell/file-write | `p2-05-exec-approvals-before.png` | ✅ | | p2-06 | Exec-approval gate configured: `ask=always` | `p2-06-exec-approvals-after.png` | ✅ | -| p2-07 | Gate enforced (fail-closed): a shell command over the channel returns an approval prompt, does not execute | `p2-07-exec-gate-approval-required-telegram.png` | ✅ | -| p2-08 | Container isolation: read-only rootfs, `cap_drop=ALL`, non-root uid 1000, `no-new-privileges`, not privileged | `p2-08-container-isolation-hardening.png` | ✅ (staging) | +| p2-07 | Gate enforced (fail-closed): a channel command returns an approval prompt, does not execute | `p2-07-exec-gate-approval-required-telegram.png` | ✅ | +| p2-08 | Container isolation: read-only rootfs, `cap_drop=ALL`, non-root uid 1000, `no-new-privileges` | `p2-08-container-isolation-hardening.png` | ✅ (staging) | | p2-09 | Exec-approval gate operates in-container: `security=full, ask=always, askFallback=deny` | `p2-09-exec-gate-in-container.png` | ✅ (staging) | | p2-10 | Image credential-free: zero credential-string matches in image layers | `p2-10-image-credential-free.png` | ✅ (staging) | -The exec-gate set (p2-05 → p2-07) is the B5 remediation: ungated → configured → enforced, verified behaviorally rather than by config inspection. The container set (p2-08 → p2-10) is the containment layer, validated on staging ahead of a documented production promotion. +The exec-gate set (p2-05 → p2-07) is the B5 remediation: ungated → configured → +enforced, verified behaviorally rather than by config inspection. The container +set (p2-08 → p2-10) is the containment layer, validated on staging ahead of a +documented production promotion. + +> **Field note (Trap 11):** `deletion_protection` is enforced from *state*, not +> config — flipping it and destroying in one change deadlocks. Apply the flag +> first, or evict via `terraform state rm` + delete out-of-band. Set it `false` +> from birth on resources with planned lifespans. + +--- + +# Walkthrough + +Each claim, paired with the screenshot that proves it. + +## Acts 1–3 — the GCP organization baseline + +### 1 · Org converged +Seven guardrails, folder hierarchy, and Workload Identity Federation — all +code-managed, plan showing no drift. + +![Converged Terraform plan: no changes, infrastructure matches configuration](act1-converged-plan.png) + +### 2 · Keyless pipeline (plan-on-PR) +A pull request renders a Terraform plan, authenticated to GCP via WIF — zero +service-account keys in existence. + +![Plan-on-PR: GitHub Actions rendering a Terraform plan via Workload Identity Federation](act2-keyless-pipeline-pr.png) + +### 3 · Apply-on-merge +A merge becoming real infrastructure — still keyless, end to end. + +![Apply-on-merge: a merged PR building infrastructure through the keyless pipeline](act2-apply-on-merge-builds.png) + +### 4 · The platform refuses violations +A service-account key creation request, denied by org policy at the platform +level — not by review, by the platform itself. + +![Org policy denying a service-account key creation request](act1-org-policy-denies-sa-key.png) + +### 5 · Deny-by-default in the wild +The default compute service account, stripped of automatic grants by org policy, +cannot read Cloud Build's staging bucket until explicitly granted `objectViewer` +on exactly that bucket. + +![Default compute SA born with no privilege, unable to read a bucket until granted](act1-default-sa-born-with-nothing.png) + +### 6 · The vending machine +A hardened project created by a pull request — 14 resources, with the workload +gate visibly closed (`lb_ip: not yet deployed`). + +![A hardened project vended by pull request, part 1](act3-project-vended-via-pr-1.png) + +![A hardened project vended by pull request, part 2](act3-project-vended-via-pr-2.png) + +![A hardened project vended by pull request, part 3](act3-project-vended-via-pr-3.png) + +### 7 · The paid layer, reviewed before a dollar was spent +The zero-trust workload plan, read in full prior to apply. + +![Zero-trust workload Terraform plan, part 1](act3-workload-plan-1.png) + +![Zero-trust workload Terraform plan, part 2](act3-workload-plan-2.png) + +![Zero-trust workload Terraform plan, part 3](act3-workload-plan-3.png) + +### 8–10 · Zero trust, proven three ways +Anonymous → challenged, authorized → named, authenticated-but-unauthorized → +denied. + +**Front door — anonymous request meets an identity challenge:** + +![IAP identity challenge presented to an anonymous request](act3-iap-identity-challenge.png) + +**Allow path — IAP-asserted identity, rendered by the app:** + +![The application rendering an IAP-asserted identity on the allow path](act3-iap-asserted-identity.png) + +**Deny path — an authenticated Google account, absent from the access list, receives 403:** + +![A 403 for an authenticated but unauthorized account](act3-iap-403-unauthorized.png) + +### 11 · Reproducibility +The paid layer destroyed through the same pipeline that built it. + +![Teardown plan destroying the paid layer through the pipeline](act3-teardown-plan.png) + +## Part 2 — self-hosted AI-agent gateway audit + +Rows p2-01–07 are from the **production** gateway; p2-08–10 are from a +**non-production staging** host (the container validation). + +### p2-01/02 · SSH password authentication disabled +Effective `sshd` config moved from password-auth `yes` to `no` — verified against +the effective configuration, not just the file. + +**Before:** + +![sshd effective config showing PasswordAuthentication yes](p2-01-ssh-passwordauth-before.png) + +**After:** + +![sshd effective config showing PasswordAuthentication no](p2-02-ssh-passwordauth-after.png) + +### p2-03/04 · Control-UI insecure auth disabled +`allowInsecureAuth` moved `true` → `false`; gateway confirmed active after the +change. + +**Before:** + +![Gateway config showing allowInsecureAuth true](p2-03-insecureauth-before.png) + +**After:** + +![Gateway config showing allowInsecureAuth false, service active](p2-04-insecureauth-after.png) + +### p2-05 · Exec authorization — the baseline finding (High) +`security=full, ask=off`, empty allowlist: any command over the channel executed +immediately, unattended, at the service user's privilege. + +![Exec approvals showing security full, ask off, no allowlist — ungated](p2-05-exec-approvals-before.png) + +### p2-06 · Exec-approval gate configured +Policy set to require approval per invocation (`ask=always`). + +![Exec approvals showing ask always configured](p2-06-exec-approvals-after.png) + +### p2-07 · Gate enforced, fail-closed +The decisive test: the same shell command over the channel now returns an +approval prompt (Allow Once / Deny) and does **not** execute without explicit +approval. *Verified behaviorally — configuration state alone did not reflect +enforcement until the fail-open fallback was corrected.* + +![A message-delivered command returning an approval-required prompt instead of executing](p2-07-exec-gate-approval-required-telegram.png) + +### p2-08 · Container isolation (staging) +Runtime host-config verified: read-only root filesystem, all Linux capabilities +dropped, non-root uid 1000, `no-new-privileges`, not privileged. + +![docker inspect output confirming read-only rootfs, cap-drop ALL, non-root, no-new-privileges](p2-08-container-isolation-hardening.png) + +### p2-09 · Exec-approval gate operates in-container (staging) +The B5 gate confirmed operating identically inside the container: +`security=full, ask=always, askFallback=deny`, mounted on the state volume so it +does not revert. + +![Exec approvals inside the container showing ask always, askFallback deny](p2-09-exec-gate-in-container.png) + +### p2-10 · Image credential-free (staging) +Image layers scanned for credential strings — zero matches. No credential +material baked into the image. + +![Image layer scan showing zero credential-string matches](p2-10-image-credential-free.png)