From f6cdefcc7961a8919099d3bbc2bce4e244818445 Mon Sep 17 00:00:00 2001 From: Chris Invictus Date: Sun, 19 Jul 2026 05:13:29 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20evidence=20index=20=E2=80=94=20at-a-gla?= =?UTF-8?q?nce=20tables=20+=20inline=20screenshot=20walkthrough?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 118 ----------------------- docs/evidence/README.md | 201 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 186 insertions(+), 133 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 473f951..0000000 --- a/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# paved-org πŸ›£οΈ - -![CI](https://github.com/ChrisInvictus/paved-org/actions/workflows/plan.yml/badge.svg) ![License](https://img.shields.io/github/license/ChrisInvictus/paved-org) ![Release](https://img.shields.io/github/v/release/ChrisInvictus/paved-org) - -*A GCP organization baseline as code: org-wide guardrails, a project factory that vends secure-by-default projects, and a zero-trust workload driving on the paved road β€” IAP-protected, deployed via a keyless WIF pipeline. -Prevention half of CloudCanary.* - -**At a glance:** org policies that make insecure states unrepresentable Β· a project factory vending secure-by-default projects Β· zero exported keys anywhere (keyless WIF/OIDC end-to-end) Β· a zero-trust workload (IAP) as living proof Β· extended into agentic AI: an ai-guardrails module, AI-workload detection in its sister project, and a published MCP trust-boundary threat model (v1.1 β€” STRIDE Γ— OWASP LLM Top 10 2025, multi-agent extension) with a two-part applied security runbook. Detection half: [CloudCanary](https://github.com/ChrisInvictus/CloudCanary). - -## The thesis - -Most organizations bolt security onto projects after they exist β€” a scanner here, a review checklist there, a policy document nobody reads. paved-org makes the **organization itself** the security boundary. Org policy constraints are enforced at the root and inherited by everything below; the project factory vends projects that are hardened before their first API call. Insecure states aren't forbidden by policy documents β€” they're **unrepresentable in the platform**. - -Everything here is Terraform, deployed through a GitOps pipeline (plan-on-PR, apply-on-merge) that authenticates to GCP with **zero exported service-account keys**. - -## Three acts - -``` -Act 1: THE PAVEMENT (org layer) - GCP Organization - β”œβ”€β”€ Org policies (Terraform): the secure-by-default constraint set - β”œβ”€β”€ Folder hierarchy: /workloads, /sandbox - └── Project factory module: vends pre-hardened projects - (baseline APIs, logging sink, no default network, - no default-SA editor grant, labels, budget guard) - -Act 2: THE KEYLESS ROAD (deployment identity) - GitHub Actions ──OIDC──> Workload Identity Federation pool - └── deploys everything with ZERO exported service-account keys - plan-on-PR Β· apply-on-merge Β· state in a versioned GCS bucket - -Act 3: THE WORKLOAD (zero trust in action) - A vended project runs a tiny demo app (Cloud Run) - └── fronted by Identity-Aware Proxy (IAP β€” Google's ZTNA, - BeyondCorp lineage): no VPN, no public ingress, - every request identity-checked -``` - -The paved road β€” and a workload driving on it. - -## The guardrails (Act 1) - -Seven organization policy constraints, each closing a class of misconfiguration ([`org/policies.tf`](org/policies.tf), one-line threat model on each): - -| Constraint | What it kills | -|---|---| -| `iam.disableServiceAccountKeyCreation` | The #1 GCP credential-theft vector: exported keys that never expire | -| `iam.automaticIamGrantsForDefaultServiceAccounts` | Default service accounts born with project Editor | -| `iam.allowedPolicyMemberDomains` | IAM grants to identities outside the org's directory | -| `compute.skipDefaultNetworkCreation` | The permissive default VPC in every new project | -| `compute.vmExternalIpAccess` | Internet-scannable VMs | -| `storage.uniformBucketLevelAccess` | Per-object ACL sprawl β€” the root cause of public-bucket incidents | -| `sql.restrictPublicIp` | Databases on the public internet | - -A note on identity governance: modern GCP organizations are born with several of these pre-enforced by Google. paved-org **imports** those defaults into Terraform state rather than leaving them unmanaged β€” an unmanaged default is drift waiting to happen, and one of these constraints is tightened beyond Google's default. The full story is in [`docs/bootstrap.md`](docs/bootstrap.md). - -## No keys were harmed (Act 2) - -The pipeline that enforces `iam.disableServiceAccountKeyCreation` authenticates **without any key itself**: GitHub's OIDC issuer vouches for each workflow run, and Workload Identity Federation exchanges that short-lived token for federated credentials. Trust is pinned to exactly one repository via an attribute condition; the deployer service account carries least-privilege org roles (org policy + folder administration β€” deliberately not `organizationAdmin`). - -A governance layer that violated its own policies would be theater. - -## Sister repo - -**paved-org prevents what [CloudCanary](https://github.com/ChrisInvictus/CloudCanary) detects.** Run both: prevention for the known, detection for the drift. The key-creation constraint above prevents the exact event CloudCanary's identity-drift canary alerts on. - - -## AI-workload security -Extending the prevention/detection thesis to AI workloads: -- [**MCP trust-boundary threat model**](docs/threat-models/mcp-trust-boundaries.md) β€” five boundaries (B1–B5), STRIDE Γ— OWASP LLM Top 10 (2025), with an IAM lens and a multi-agent (agent ↔ agent) extension (v1.1). -- [**MCP security runbook**](docs/runbooks/securing-mcp-setup.md) β€” a two-part applied audit: a managed-connector host and a self-hosted agent gateway, with host hardening, a fail-closed exec-approval gate, and a staging-validated container. -- [**`modules/ai-guardrails`**](modules/ai-guardrails/) β€” org-policy guardrails for AI workloads (verified canned constraints), the prevention half of what [CloudCanary](https://github.com/ChrisInvictus/CloudCanary) detects. -## Cost & reproducibility - -Built end-to-end on GCP free-trial credits. The org layer (policies, folders, WIF, state) is control-plane configuration and costs $0 to keep alive. The workload layer (Act 3) carries the one real cost β€” a global external HTTPS load balancer for IAP, ~$18/month β€” so it lives for roughly 48 hours: built, evidenced, then `terraform destroy`ed. - -The destroy/apply round trip is documented and repeatable. **Reproducibility is the point** β€” the repo's value is the code plus evidence, not a running demo. To rebuild from a bare domain, follow [`docs/bootstrap.md`](docs/bootstrap.md). - -## Evidence - -Tracked with status in [`docs/evidence/`](docs/evidence/README.md): - -- βœ… Converged plan: `No changes. Your infrastructure matches the configuration.` -- βœ… Plan-on-PR and apply-on-merge: proposals rendered, merges becoming infrastructure β€” keyless throughout -- βœ… The platform refusing violations: an SA key request denied by org policy; the default compute SA born with zero privilege -- βœ… The vending machine: a hardened project created by a pull request -- βœ… Zero trust, proven both ways: IAP identity challenge and asserted identity on the allow path; 403 for an authenticated-but-unauthorized account -- βœ… Reproducibility: the paid layer destroyed through the same pipeline that built it - -## Honest limitations - -This is a demo-scale organization, and says so: - -- **No multi-environment promotion.** One org, one state, one pipeline lane. Production would add dev/nonprod/prod folders with per-environment state and promotion gates. -- **No Security Command Center integration.** Production would wire SCC Premium for posture management and threat findings on top of these preventive controls. -- **No VPC Service Controls.** Data-exfiltration perimeters are the next layer of a real deployment; nothing here conflicts with adding them. -- **Single-person directory.** Group-based IAM (admin groups, breakglass procedures) is stubbed by design; the folder layout leaves room for it. - -The full threat model, including accepted risks, is in [SECURITY.md](SECURITY.md). - -## Repo map - -``` -org/ Act 1 + 2: policies, folders, WIF, org logging β€” live -modules/project-factory/ Act 1: the vending machine β€” landing in Act 3 -workloads/demo/ Act 3: vended project + Cloud Run + IAP β€” landing in Act 3 -docs/ architecture, bootstrap guide, evidence -.github/workflows/ plan-on-PR, apply-on-merge, lint -``` - -## Roadmap - -Multi-env promotion Β· SCC integration Β· VPC-SC perimeter Β· group-based IAM & breakglass Β· budget guard as a factory default Β· Terraform tests (`terraform test`) for the factory module. - -## License - -MIT - 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)