# Chapter 12 Labs: Zero Trust Architecture

This pack contains five hands-on labs that follow the chapter's pillar sequence:
identity and application (Lab 1), network at the workload layer (Lab 2), device
posture (Lab 3), the maturity model as a whole (Lab 4), and a first-principles
demonstration of why segmentation matters (Lab 5).

Labs 1 and 5 run entirely in Docker Compose. Labs 2, 3, and 4 require host-side
tools (KIND/kubectl, osquery, and Python/Jupyter respectively) and are documented
below with canonical upstream install instructions.

**Safety / Authorisation:** Labs 1, 2, and 5 are offensive or network-altering in
character. Run them only against the provided lab targets or systems you own and are
authorised to test. Never point an identity-aware proxy at a third-party service.
Never apply a default-deny policy to a production network without a tested roadmap
and a working break-glass path.

---

## Prerequisites

| Requirement | Labs | Notes |
|---|---|---|
| Docker Desktop or Colima (Docker Engine + Compose v2) | 1, 5 | Compose plugin must support `profiles:` |
| KIND v0.31.0 | 2 | `brew install kind` or binary from GitHub |
| kubectl | 2 | `brew install kubectl` |
| Linkerd CLI (edge-26.5.1) | 2 | `curl -sL https://run.linkerd.io/install-edge | sh` |
| osquery 5.23.0 | 3 | Package or `brew install --cask osquery` |
| Python 3.11+ with pandas, matplotlib, jupyterlab | 4 | `pip install pandas matplotlib jupyterlab` |
| OIDC credentials (IdP) | 1 | Google, GitHub, or Keycloak; see Lab 1 setup |

Disk space: under 2 GB total (images are slim; no large binaries).

---

## Quick start (containerised labs)

```bash
# Lab 1 - identity-aware proxy (choose one proxy)
docker compose --profile lab1-pomerium up -d   # Pomerium v0.32.7
# or
docker compose --profile lab1-oauth2proxy up -d  # OAuth2-Proxy v7.15.2

# Lab 5 - flat vs segmented network
docker compose --profile lab5 up -d

# Shared tools container (curl, nc, ping on all lab networks)
docker compose --profile tools up -d

# Stop everything
docker compose down -v
```

---

## Lab 1: Identity-Aware Proxy (the BeyondCorp pattern)

**Objective:** Stand up an identity-aware proxy in front of a backend service so
that no request reaches the backend without first being authenticated and authorised.
The proxy is the Policy Enforcement Point (PEP); the IdP is the Policy Engine input.

### Setup

The backend service (`traefik/whoami`) is deliberately not published to any host
port. All traffic must flow through the proxy.

**Option A - Pomerium v0.32.7:**

1. Edit `lab1/pomerium-config.yaml` with your IdP credentials. For a zero-credential
   first run, use Pomerium's hosted authenticate service
   (`authenticate_service_url: https://authenticate.pomerium.app`) and its built-in
   demo IdP.
2. Start: `docker compose --profile lab1-pomerium up -d`
3. Browse to `https://whoami.localhost.pomerium.io:8443` (add a hosts entry or use
   the `*.localhost.pomerium.io` wildcard DNS that resolves to 127.0.0.1).

**Option B - OAuth2-Proxy v7.15.2:**

1. Edit the `command:` block in `docker-compose.yml` (the `lab1-oauth2proxy` service)
   and replace all `REPLACE_WITH_*` placeholders with real OIDC values. Generate the
   cookie secret with: `openssl rand -base64 32`
2. Start: `docker compose --profile lab1-oauth2proxy up -d`
3. Browse to `http://localhost:4180`

### Walkthrough

1. With the proxy running, open the proxy URL in a browser without authenticating.
   The proxy redirects you to the IdP login page (the PEP at work).
2. Authenticate with a valid identity. The proxy inspects the returned ID token,
   evaluates the policy (allowed email / domain), and only then forwards the request
   to the backend.
3. Tighten the policy: change `allowed_users` to an email that is NOT yours, or
   restrict `allowed_domains` to a domain you do not have. Try again - the proxy
   blocks at the PEP, the backend receives nothing.
4. Confirm the key property: `docker logs lab1-backend` shows requests only from the
   proxy, never direct from the client.

### Expected output

Unauthenticated request: HTTP 302 to the IdP login URL.  
Authenticated, authorised request: the `whoami` backend response (headers, IP, etc.)  
Authenticated, unauthorised identity: HTTP 403 from the proxy.

### Verify completion

```bash
# The backend is NOT reachable directly from the host:
curl -s http://localhost:80  # must fail or time out

# The proxy endpoint redirects to the IdP (HTTP 302 or 401):
curl -sv http://localhost:4180 2>&1 | grep "< HTTP"
```

### Cleanup

```bash
docker compose --profile lab1-pomerium down -v
# or
docker compose --profile lab1-oauth2proxy down -v
```

---

## Lab 2: Service-Mesh Zero Trust with Linkerd

**Host setup (not containerised here)**

Linkerd runs on a real Kubernetes cluster. The OSS `linkerd/linkerd2` project ships
edge releases only; there is no free stable channel (that is the commercial Buoyant
Enterprise for Linkerd). This lab uses the OSS edge channel.

**Upstream resources:**
- KIND: https://kind.sigs.k8s.io/
- Linkerd edge channel: https://github.com/linkerd/linkerd2

**Objective:** Bring up a local Kubernetes cluster, inject Linkerd to get automatic
mutual TLS between all services, then write authorisation policies that default-deny.

### Setup

```bash
# 1. Create the cluster (KIND v0.31.0 default node image)
kind create cluster --name zt-mesh \
  --image kindest/node:v1.35.0@sha256:452d707d4862f52530247495d180205e029056831160e22870e37e3f6c1ac31f

# 2. Install Linkerd CLI (edge channel)
curl -sL https://run.linkerd.io/install-edge | LINKERD2_VERSION=edge-26.5.1 sh
export PATH=$PATH:$HOME/.linkerd2/bin

# 3. Pre-flight check
linkerd check --pre

# 4. Install Linkerd (CRDs first, then control plane)
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -
linkerd check
```

### Walkthrough

```bash
# 5. Deploy the emojivoto demo app
kubectl apply -f https://run.linkerd.io/emojivoto.yml

# 6. Inject the Linkerd proxy (enables automatic mTLS immediately)
kubectl get deploy -n emojivoto -o yaml | linkerd inject - | kubectl apply -f -

# 7. Install the visualisation extension and confirm mTLS edges
linkerd viz install | kubectl apply -f -
linkerd viz dashboard &
```

To write a default-deny authorisation policy:

```yaml
# server.yaml - select the voting-svc port
apiVersion: policy.linkerd.io/v1beta3
kind: Server
metadata:
  name: voting-svc-grpc
  namespace: emojivoto
spec:
  podSelector:
    matchLabels:
      app: voting-svc
  port: grpc
  proxyProtocol: gRPC
---
# authz.yaml - allow only the web-svc mesh identity
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: web-to-voting
  namespace: emojivoto
spec:
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: voting-svc-grpc
  requiredAuthenticationRefs:
    - name: web-svc-identity
      kind: MeshTLSAuthentication
      group: policy.linkerd.io
---
apiVersion: policy.linkerd.io/v1alpha1
kind: MeshTLSAuthentication
metadata:
  name: web-svc-identity
  namespace: emojivoto
spec:
  identities:
    - "web.emojivoto.serviceaccount.identity.linkerd.cluster.local"
```

Apply, then prove the deny works from an unauthorised workload:

```bash
kubectl apply -f server.yaml -f authz.yaml
# From an unrelated pod, try to reach voting-svc:grpc - the mesh refuses it.
```

### Expected output

`linkerd viz dashboard` shows green lock icons on all meshed edges (mTLS active).  
Calls from an unauthorised workload return a gRPC status code 7 (PERMISSION_DENIED).

### Verify completion

```bash
linkerd viz stat deploy -n emojivoto
# "SECURED" column should show 100% for all deployments.
```

### Cleanup

```bash
kind delete cluster --name zt-mesh
```

---

## Lab 3: Device Posture with osquery

**Host setup (not containerised here)**

osquery reads live OS tables on the host it runs on. It cannot be meaningfully
containerised for this purpose (a container only sees its own namespace, not the
host's disk encryption or real OS version).

**Upstream resource:** https://www.osquery.io/ (official packages + Homebrew cask)

**Objective:** Use osquery to query real endpoint posture signals and wire them into
a Python access decision - mapping directly to SP 800-207 tenet 5 and the CDM input
of the Policy Engine (section 3.1).

### Setup

```bash
# macOS
brew install --cask osquery

# Ubuntu / Debian (follow https://osquery.io/downloads/official/5.23.0)
export OSQUERY_KEY=1484120AC4E9F8A1A577AEEE97A80C63C9D8B80B
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys $OSQUERY_KEY
sudo add-apt-repository 'deb [arch=amd64] https://pkg.osquery.io/deb deb main'
sudo apt-get update && sudo apt-get install osquery=5.23.0-1.linux

# Verify
osqueryi --version
```

### Walkthrough

Open the interactive shell: `osqueryi`

```sql
-- 1. Disk encryption status
SELECT name, encrypted, encryption_status FROM disk_encryption;
-- encrypted = 1 means protected. macOS also exposes filevault_status.

-- 2. OS and patch level
SELECT name, version, major, minor, patch, platform FROM os_version;
-- Compare major against your minimum supported build.

-- 3. EDR / agent process running?
SELECT name, path, pid FROM processes
WHERE name LIKE '%falcon%' OR name LIKE '%osqueryd%';

-- 4. Explore other posture tables
SELECT * FROM system_info;
SELECT * FROM listening_ports LIMIT 10;
SELECT * FROM users LIMIT 5;
```

**Python decision shim:**

```bash
# From the lab3/ directory:
python3 lab3/posture-check.py --os-floor 13
```

The script runs the `disk_encryption` and `os_version` queries via `osqueryi --json`,
parses the output, and prints ALLOW or DENY with remediation hints.

### Expected output

Encrypted disk, patched OS:
```
VERDICT: ALLOW
  Both posture conditions met - forward the request.
```

Unencrypted or out-of-date OS:
```
VERDICT: DENY - route to remediation
  ACTION: enable disk encryption ...
```

### Verify completion

Run `osqueryi "SELECT encrypted FROM disk_encryption;"` and confirm the output
matches the verdict from the Python shim.

### Cleanup

No containers to stop. Remove osquery via your package manager if desired.

---

## Lab 4: Score an Org Against the CISA ZTMM

**Host setup (not containerised here)**

The notebook runs in a standard Python virtual environment with no network access
required.

**Objective:** Score a fictional organisation against the five CISA ZTMM v2.0 pillars
and three cross-cutting capabilities, visualise the gaps, and produce a prioritised
roadmap - the discipline the chapter calls "assess, find the biggest gap, fix that
first."

### Setup

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install pandas matplotlib jupyterlab

# Open the notebook
jupyter lab lab4/12_zt_maturity_score.ipynb
```

### Walkthrough

1. In the notebook's "Step 1" cell, edit the `current` dict to reflect the org you
   are assessing. The fictional baseline already filled in matches the lab description
   in the chapter: Identity at Initial, Networks and Data at Traditional, etc.
2. Run all cells. The "Step 2" cell computes gaps. "Step 3" renders the heat-map
   (red = Traditional, green = Optimal). "Step 4" prints the ranked roadmap.
3. Read the roadmap: the two Traditional pillars (Networks and Data) appear at the
   top. The rule is: fix the largest gap first. Resist the urge to round up any
   pillar to a stage it has not yet earned.

### Expected output

A heat-map PNG (`lab4/ztmm_heatmap.png`) and console output such as:

```
Prioritised roadmap (largest gap first):
  Networks                           advance Traditional -> Initial -> Advanced (gap = 2 stage(s))
  Data                               advance Traditional -> Initial -> Advanced (gap = 2 stage(s))
  Automation & Orchestration         advance Traditional -> Initial -> Advanced (gap = 2 stage(s))
  ...
```

### Verify completion

The notebook runs to completion without errors. The heat-map shows red cells for
Traditional pillars and yellow-green for Initial.

### Cleanup

```bash
deactivate
# Optionally: rm -rf .venv lab4/ztmm_heatmap.png
```

---

## Lab 5: Flat Network Falls; Segmented Network Contains

**Objective:** Demonstrate from first principles why micro-segmentation is the
network pillar's whole point. Build a flat topology, perform lateral movement, then
rebuild as segmented and prove the same attack is contained.

**Scope check:** This lateral-movement demonstration runs only inside your local
Docker lab network. Never run it against a network you do not own.

### Setup

```bash
docker compose --profile lab5 up -d
# Starts six containers: flat-web, flat-app, flat-db, seg-web, seg-app, seg-db
```

### Walkthrough

**Topology A - flat (the breach)**

All three `lab5-flat-*` containers share `lab5-flat`. From `flat-web`, you can
reach `flat-db` directly - one foothold, full lateral movement.

```bash
# Step inside the "compromised" web tier:
docker exec -it lab5-flat-web bash

# From inside flat-web, reach the database directly:
curl http://lab5-flat-db:5432
# Output: "flat-db: SENSITIVE DATA REACHED"

# Also reach the app tier:
curl http://lab5-flat-app:8080
# Output: "flat-app OK"
```

**Topology B - segmented (the containment)**

`lab5-seg-web` is on `lab5-web-app` only. `lab5-seg-db` is on `lab5-app-db` only.
`lab5-seg-app` bridges both. `web` and `db` are on different networks - Docker's
bridge driver enforces the boundary.

```bash
# Step inside the "compromised" segmented web tier:
docker exec -it lab5-seg-web bash

# Can still reach app (expected, intended path):
curl http://lab5-seg-app:8080
# Output: "seg-app OK"

# CANNOT reach the database - different network, route does not exist:
curl --connect-timeout 3 http://lab5-seg-db:5432
# Output: connection timeout / network unreachable
```

The blast radius is contained to the web segment.

### Expected output

Flat topology: `curl http://lab5-flat-db:5432` returns `flat-db: SENSITIVE DATA REACHED`  
Segmented topology: `curl http://lab5-seg-db:5432` times out or refuses connection.

### Verify completion

```bash
# Confirm web cannot reach db in the segmented topology:
docker exec lab5-seg-web curl --connect-timeout 3 http://lab5-seg-db:5432
# Must fail (exit code != 0)
```

### Cleanup

```bash
docker compose --profile lab5 down -v
```

---

## Teardown (all labs)

```bash
docker compose down -v
docker network prune -f
```

This removes all containers, volumes, and the lab Docker networks.

---

## Safety / Authorisation

These labs are for learning on systems you own. Specifically:

- **Lab 1:** Proxy only your own backend. Do not point the proxy at a third-party
  service without its operator's consent.
- **Lab 2:** Apply Linkerd policies only to your own KIND cluster. Never flip a
  shared or production network to default-deny without a staged rollout plan and a
  tested break-glass path. A default-deny policy applied to production without
  preparation is not a maturity milestone - it is an outage.
- **Lab 5:** Run the lateral-movement demonstration only inside the local Docker
  network defined in this compose file. Never run it against infrastructure you do
  not own.
