zero-trust-lab · live evidence

A Zero Trust access model on a real home network, built in the open. Every status below was read from GitHub Actions when this page was built, and links to the run that produced it. None of them is typed by hand. A check older than its schedule turns amber on its own.

Built · repository · status and plan · decision log

The one number

2 of ~40

devices in the house are reachable from the network overlay — each by one grant, on one port (measured live: 2 routes in effect, the widest /32). Everything else is refused by default.

Live, checked daily

The policy in force equals main. 42 policy assertions — 34 of them refusals — run against the live network. CI holds no stored key for the network or the cloud.

Measured once, not counted: after a 15-minute link loss the telemetry path came back in 11 s with no hands; the readings sent meanwhile were lost, not queued.

Not built, and why

Field units: deferred — the house's sensors cannot run a client, and the cloud host that would run simulated ones serves two projects. Device-management posture and multi-user sign-in: a paid tier ($8/user/mo) and one user. Just-in-time access and log streaming: $18/user/mo. The before-and-after exposure reading: missed, and stated, not reconstructed.

Read with care

Posture on this plan is reported by the client itself: it shows how a device is configured, not that it is intact. The break-glass path into production — the cloud provider's console — exists but has not yet been exercised (D-042).

Now

Reading GitHub…

This panel reads GitHub's public Actions API from your browser: what is running this minute, step by step, and the last runs. Everything else on the page moves forward with it.

The lab on one picture

Where each part lives, which tool manages it, and who may reach what. Contours and words are drawn by hand; every access arrow is parsed from the policy file, so the picture cannot show a path the policy does not grant. Counts on the tiles are live, counted on 2026-09-11T02:45:28Z.

Show:
CI · GitHub Actions no stored keys · OIDC per job Control plane · Tailscale SaaS · policy as code Cloud · AWS us-west-2 — managed by Terraform planned by CI · applied by a person Tailnet — WireGuard overlay · 100.64/10 · one identity per node · deny by default laptop + phones = one policy role grey: roles declared in the policy, no host yet — see "not built yet" below one /32 route per exposed device, through the hub · the /24 itself is never advertised connected over: Matter over Thread and Wi-Fi · HomeKit · Wi-Fi · a Thread / Zigbee radio Home network — flat private /24 · reached only through the hub The rest of the houseabout 40 devices · 2 reachable from the tailnet6 lights5 plugs and switches5 temperature sensors3 media and TV3 phones, as presence2 blinds2 buttons2 radios and bridges1 robot vacuum+ other, inactive or withheldcounted 2026-09-10 · cameras withheld Growth path — not built. What a multi-user or company deployment adds, what it plugs into, and what it would cost GitHub Actions — Runs every check. Holds no network or cloud key: each job gets a short-lived token by OIDC.GitHub Actionsvalidate · drift · planworkflowsTailscale — Evaluates the policy and refuses a save whose tests fail. The policy is not in Terraform: two tools writing one global file fight.Tailscalecontrol plane · 42 testspolicy as codeCI plan role — Lets CI run terraform plan and nothing more. Its trust names this repository by immutable id; wildcards are refused three ways.CI plan roleOIDC · 0 IAM usersterraformArchive bucket — Where the collector's data is meant to land. It exists so the certificate credential has something real to authorise.Archive bucketS3 · encrypted · GlacierterraformRoles Anywhere — Written, switched off: no certificate authority exists yet. Cloud access for a machine that holds a certificate, not a key.Roles Anywherecertificate → role · offterraformTerraform state — State joins the account's existing bucket rather than creating one; locking is the native lockfile.StateterraformOperator laptop — The physical console. Applies the policy by hand after CI has validated it. Its posture attributes are reported by the client on the device itself: they establish configuration, not integrity.Operator laptopposture: self-reportedlive · 4 user-ownedOperator phones — Posture subjects, and the way back in if everything else fails — verified off-network.Operator phonesbreak-glass pathby handHome server — Runs the camera recorder and the house's duty timers, and is someone's daily machine, so it stays user-owned with no machine identity. Members reach their own devices on SSH only -- the house's recovery path, restored after the lab's policy had cut it.Home serveruser-owned · untaggedby handMedia appliance — A device on the network that should reach nothing — and is tested to reach nothing.Media appliancetag:appliancelive · 1/1 onlineAutomation hub — The only door into the home: it advertises one /32 route per exposed device, never the subnet.Automation hubtag:gateway-homelive · 1/1 onlineCollector — Telemetry sink: a container on the cloud dev host, outside the house. The hub pushes readings to it on one port; it has no way in, and the tests assert that.Collectortag:collector · containerlive · 1/1 onlineMobile units — A unit that reports AND takes commands. The candidate is the house's robot vacuum, which cannot run a client: it would need an adapter with a path into the hub, which is a widening to decide, not a default.Mobile unitstag:drone · not builtdesign pendingSensors — Push-only field units. The house's own sensors cannot run a client, so their readings arrive through the hub. Simulated ones would run on the cloud dev host, which serves two projects on 2 GB and no swap: deferred. A real one is a single-board computer and a sensor, about $30.Sensorstag:sensor · not builtdeferredProduction VM — The production stand-in. A destination, never a source. Outside Terraform; also where a person runs terraform apply.Production VMtag:prod · Lightsaillive · 1/1 onlineSmart plug — The action tier: operators may switch it, only from a device whose client reports the required posture. On this plan the report comes from the device itself, so a compromised node is not constrained by it — which is what the device-management tile below would change.Smart pluggranted · postureone /32Smart plug — The same model on the same port, never granted. Proves least privilege is per host, not per protocol.Smart plugidentical · refusedone /32Cat camera — The one camera the owner released for the lab. Nothing on the tailnet can reach it: the recorder in the house watches it, and at the end of each visit the hub pushes the event -- id, times, scores, no image -- to the collector. Named in the recorder, never addressed.Cat camerareleased · events onlyno routeRobot vacuum — Run by the hub through its vendor's cloud, not the home network. The candidate for the mobile-unit role; it cannot run a client, so it would need an adapter with a path into the hub -- a widening to decide, not built.Robot vacuumvia the vendor's cloudno route · not builtIdentity provider — One login for many people; their groups become policy sources instead of every member. Plugs into the control plane and cloud sign-in.Identity providerSSO · users, groupsStandard $8/user/moMDM / EDR — Posture from the fleet's management system instead of the client's own report. An integration a paid plan adds.MDM / EDRposture from the fleetStandard $8/user/moJust-in-time — Standing access to the action tier replaced by grants that expire. Needs a paid plan: one time-boxed month.Just-in-timegrants that expirePremium $18/user/moLog streaming — Who connected to what, kept and searchable. The drills in Phase 6 would read it.Log streamingflow + audit → SIEMPremium $18/user/moIdentity Center — Cloud sign-in for people through the identity provider. Lives in the organisation's management account, not this stack.Identity Centerpeople's cloud sign-inAWS: no chargePrivate CA — The missing half of Roles Anywhere: machines get certificates, never keys. Switches the cloud tile above on.Private CAcerts for the collectorfree, or $50+/moread-onlyterraform plan · OIDCpolicy apply · a personenforcesterraform apply · a personplanned: certificate → archivevisit events · LAN✗ refused · 2 tested✗ refused · 3 tested✗ refused✗ operators refused · tested8123, 22222284438443844352432 · posture8444

Green arrows are grants parsed from the policy, with the port each names; dashed green goes to a role with no host yet. Red arrows are refusals asserted by the policy tests. Dotted green is data moving inside the house, where the tailnet has no path. Everything not drawn is refused by default.

What each part is for, and why it is managed the way it is
PartManaged by · costWhat it is forWhy
GitHub ActionsworkflowsRuns every check. Holds no network or cloud key: each job gets a short-lived token by OIDC.D-041
Tailscalepolicy as codeEvaluates the policy and refuses a save whose tests fail. The policy is not in Terraform: two tools writing one global file fight.D-001
CI plan roleterraformLets CI run terraform plan and nothing more. Its trust names this repository by immutable id; wildcards are refused three ways.D-028
Archive bucketterraformWhere the collector's data is meant to land. It exists so the certificate credential has something real to authorise.D-039
Roles AnywhereterraformWritten, switched off: no certificate authority exists yet. Cloud access for a machine that holds a certificate, not a key.Phase 5
Terraform stateterraformState joins the account's existing bucket rather than creating one; locking is the native lockfile.D-029
Operator laptopby handThe physical console. Applies the policy by hand after CI has validated it. Its posture attributes are reported by the client on the device itself: they establish configuration, not integrity.D-040
Operator phonesby handPosture subjects, and the way back in if everything else fails — verified off-network.D-016
Home serverby handRuns the camera recorder and the house's duty timers, and is someone's daily machine, so it stays user-owned with no machine identity. Members reach their own devices on SSH only -- the house's recovery path, restored after the lab's policy had cut it.D-051
Media appliancetagA device on the network that should reach nothing — and is tested to reach nothing.D-019
Automation hubtagThe only door into the home: it advertises one /32 route per exposed device, never the subnet.D-024
CollectortagTelemetry sink: a container on the cloud dev host, outside the house. The hub pushes readings to it on one port; it has no way in, and the tests assert that.D-049
Mobile unitsdesign pendingA unit that reports AND takes commands. The candidate is the house's robot vacuum, which cannot run a client: it would need an adapter with a path into the hub, which is a widening to decide, not a default.Phase 6
SensorsdeferredPush-only field units. The house's own sensors cannot run a client, so their readings arrive through the hub. Simulated ones would run on the cloud dev host, which serves two projects on 2 GB and no swap: deferred. A real one is a single-board computer and a sensor, about $30.Phase 6
Production VMby handThe production stand-in. A destination, never a source. Outside Terraform; also where a person runs terraform apply.D-036
Smart plugone /32The action tier: operators may switch it, only from a device whose client reports the required posture. On this plan the report comes from the device itself, so a compromised node is not constrained by it — which is what the device-management tile below would change.D-040
Smart plugone /32The same model on the same port, never granted. Proves least privilege is per host, not per protocol.D-010
Cat camerano routeThe one camera the owner released for the lab. Nothing on the tailnet can reach it: the recorder in the house watches it, and at the end of each visit the hub pushes the event -- id, times, scores, no image -- to the collector. Named in the recorder, never addressed.D-049
Robot vacuumno route · not builtRun by the hub through its vendor's cloud, not the home network. The candidate for the mobile-unit role; it cannot run a client, so it would need an adapter with a path into the hub -- a widening to decide, not built.Phase 6
Identity providerStandard $8/user/moOne login for many people; their groups become policy sources instead of every member. Plugs into the control plane and cloud sign-in.multi-user
MDM / EDRStandard $8/user/moPosture from the fleet's management system instead of the client's own report. An integration a paid plan adds.D-040
Just-in-timePremium $18/user/moStanding access to the action tier replaced by grants that expire. Needs a paid plan: one time-boxed month.Phase 4
Log streamingPremium $18/user/moWho connected to what, kept and searchable. The drills in Phase 6 would read it.Phase 6
Identity CenterAWS: no chargeCloud sign-in for people through the identity provider. Lives in the organisation's management account, not this stack.D-030
Private CAfree, or $50+/moThe missing half of Roles Anywhere: machines get certificates, never keys. Switches the cloud tile above on.Phase 5
The rest of the houseno routeAbout 40 devices by type, counted by hand on 2026-09-10 from the hub's registry. None is reachable from the tailnet; the hub talks to them locally. The camera count is withheld, and categories that would let it be subtracted are folded together.D-008

Checked against the live network

The policy in force is the one on main

pass

The live tailnet policy is read back and compared byte for byte with the render of main. The console stores the file verbatim, so any difference is real.

commit 130ee6c · push

· open the run →

Every policy test passes on the live network

pass

The tailnet itself parses the policy and runs its tests — including every “this must be refused” assertion — against the real devices. Read-only; no credential is stored anywhere.

commit 130ee6c · push

· open the run →

The tests can fail

refused, as it should be

A pull request that deliberately asserted a forbidden path. The pipeline refused it. A check that has only ever been seen passing proves nothing.

PR #8: failure, closed without merging · commit aebaa0b · pull_request

· open the run →

Cloud infrastructure matches its code

pass

A read-only plan against the cloud account, authenticated by federation — no access keys exist. CI can plan; only a person can apply.

terraform plan: no changes · commit 75f5a00 · workflow_dispatch

· open the run →

Checked on every change

No real address, hostname or account id can merge

pass

Every file and every commit message is swept for real-world values before merge, including a private list of this network's own names.

commit 74d3573 · push

· open the run →

No secrets in history

pass

The whole git history is scanned for credentials on every change.

commit 74d3573 · push

· open the run →

main accepts only changes that passed its checks

pass

GitHub enforces the checks above on main for everyone, the owner included.

required: disclosure sweep, secret scanner, policy template lint, terraform guards · applies to: everyone · commit 74d3573 · read from GitHub at build

· see the branch →

Measured by watching the system

Not a check that passes or fails: numbers observed while something was deliberately broken, counted from both ends, and written down once. They change only when the drill is run again.

Lost comms, collector side · 2026-09-11

11 sfor the network path to come back after a 15 min 9 s outage, with no hands
7 of 7readings sent during the outage were lost; 0 replayed — nothing is buffered, by design
nonemanual steps to recover

Not tested: the hub's own link, and a node restart · runbook, with the timeline

From the code, not measured

Counted from the repository's files when the page was built. The live checks above are what show these are the rules actually in force.

8access grants, each naming one port
42policy assertions — 34 of them must be refused
2real addresses in the whole policy
nonewildcard grants
51recorded decisions
4of them correct an earlier one

policy template · decision log

In the project, not built yet

What this lab intends and does not have, why, and what each would take. An item leaves this list when it is built; nothing here is drawn on the diagram as if it existed. The full plan is in STATUS.md.

WhatStateWhy not yet What it takesRef
Sensors as tailnet nodesdeferredThe house's sensors cannot run a client. Simulated ones would share a cloud host that serves two projects on 2 GB with no swap.a single-board computer and a sensor, about $30Phase 6
A mobile unit that takes commandsdesign pendingThe candidate, the house's robot vacuum, cannot run a client; an adapter would need a path into the hub. That is a widening to decide, not a default.a design, then the owner's decisionPhase 6
The kiosk tablet in a role of its ownowner decisionIt is a member today, so formally it may do everything an operator may. A role would leave it the dashboard and nothing else.one tag and one grant; the owner's yesPhase 2
Lost comms on the household sideplannedMeasured so far only where the lab could break its own link. The hub's link and the control plane are the house's.an agreed window with the householdPhase 6
A button that starts a check from this pagenot builtA public button needs an endpoint that holds a credential to start a workflow. This page holds none, on purpose.a small cloud function with a capped, audited dispatchD-044
Two stale route advertisementsowner decisionTwo user devices advertise a whole /24 that was never approved, so nothing is routed. Harmless, and a finding.the owners switch the advertisement offD-047
Just-in-time access to the action tierneeds a paid planStanding access to the socket would become a grant that expires.one time-boxed month, $18/userPhase 4