Files
Audiobuzz 6fe623c727 tpm-enrol: move NV cert index off OEM-occupied 0x01800001 → 0x01800100
The Dell 820 kept "finding a cert" and bypassing enrolment despite repeated BIOS
TPM clears. tpm2_nvreadpublic revealed 0x01800001 on that hardware is a FIRMWARE
platform index (platformcreate|policydelete|writelocked, 70 bytes — not our
cert; siblings at 0x01800003/4 + 0x01810000). platformcreate survives TPM2_Clear
and policydelete needs the platform's secret policy, so it's permanent and
unusable by us — the blind check false-positived on it, and enrolment could
never write there anyway. Same class of bug as the EK-handle collision.

Move the NV cert index default 0x01800001 -> 0x01800100 (free on the 820, owner
range, overridable via rd.dot1x.nvindex): cmdline (extract-artifacts.sh),
fleet-enrol -nvindex, the dracut hooks, bench scripts, CLAUDE.md invariant #1,
and design/SSP docs. The signing-key handle (0x81000100) is unchanged.

Harden WriteNVCert: refuse a platformcreate index with a clear "pick a free
owner index via rd.dot1x.nvindex" error instead of a confusing undefine
failure. Regression simtest/TestWriteNVCertRefusesPlatformIndex; NV roundtrip +
ReadNVCert tests moved to 0x01800100. All go tests + packer validate pass.
2026-07-03 02:51:38 +00:00

127 lines
6.3 KiB
Markdown

# 802.1X EAP-TLS + MACsec secure-path bench
Bench harness for the **secure path** (`disable_dot1x=false`): wired 802.1X
EAP-TLS with a **TPM-held client key**, link encryption via **MACsec/MKA**, then
DHCP + NVMe/TCP root over the encrypted link. This proves the half of the system
the open-port (`rd.dot1x=0`) lab build deliberately skips. Tracks TODO #8 / SSP
residual risk R-01.
> **Status: scaffold.** Configs marked **BENCH-VERIFY** below are best-effort and
> need confirming on your wpa_supplicant / hostapd / tpm2 builds (see CLAUDE.md
> "Known bench-verify items"). This is a lab rig, expected to be iterated.
## The three components
| Component | Runs on | What it does |
|---|---|---|
| **step-ca** (`step-ca/`) | **serving host** | the fleet CA — signs each host's **TPM-generated CSR**, issues the RADIUS server cert, and is the trust anchor baked into the image as `radius-ca.pem`. |
| **FreeRADIUS** (`freeradius/`) | **serving host** | terminates EAP-TLS, validates the client cert against step-ca's root, returns the MSK so MKA can key MACsec. |
| **authenticator** (`authenticator/`) | **a node *between* each workstation and the serving network** | `hostapd` wired 802.1X + MACsec; bridges the decrypted link to the serving VLAN. One per-workstation P2P link. |
The CA + RADIUS sit on the **trusted side** (the serving host). The authenticator
is the only thing on the **untrusted** link with each workstation. Workstations
never reach DHCP/NVMe except *through* an authenticated, MACsec-encrypted link.
## Why a separate isolated-bridge topology (not your flat VLAN)
802.1X EAPOL (`01:80:c2:00:00:03`) is a reserved multicast that switches/bridges
**drop by default**, and **MACsec is point-to-point per link** — so a single
shared bridge can't authenticate or encrypt per host, and if the serving host
shares that bridge there's no authenticator in the path at all. Keep the flat
VLAN for the open-port build; build this sandwich alongside it.
```
VM-A (supplicant) ──[vmbr-secA]── eth1 ┐
│ authenticator node
VM-B (supplicant) ──[vmbr-secB]── eth2 ┤ (hostapd-wired + MACsec;
│ bridges macsecN ↔ eth0)
820 ───[inline box / MACsec switch]── ┘
eth0 │
[vmbr-serve]──────┴── serving host (step-ca, FreeRADIUS,
dnsmasq, NVMe/TCP)
```
Per supplicant-facing bridge, EAPOL must be allowed through:
```bash
echo 8 > /sys/class/net/<bridge>/bridge/group_fwd_mask # forward 01:80:c2:00:00:03
```
### Proxmox commands (VM legs)
```bash
# One isolated bridge per workstation link (vmbr-secA, vmbr-secB) — see your
# /etc/network/interfaces or the Proxmox UI. Then, after each is up:
for b in vmbr-secA vmbr-secB; do echo 8 > /sys/class/net/$b/bridge/group_fwd_mask; done
# Attach the workstation VM's SECURE nic to its isolated bridge:
qm set <vmid-A> --net1 virtio,bridge=vmbr-secA
qm set <vmid-B> --net1 virtio,bridge=vmbr-secB
# Authenticator VM: one nic per workstation link + one on the serving bridge:
qm set <vmid-auth> --net1 virtio,bridge=vmbr-secA \
--net2 virtio,bridge=vmbr-secB \
--net0 virtio,bridge=vmbr-serve
# swtpm for each workstation VM (Proxmox vTPM):
qm set <vmid-A> --tpmstate0 <storage>:1,version=v2.0
```
## Run order
1. **Bring up the CA + RADIUS** on the serving host:
```bash
cd serving/dot1x-bench
docker compose up -d step-ca
./step-ca/bootstrap.sh # init CA, make a provisioner, issue the RADIUS cert
docker compose up -d freeradius
```
`bootstrap.sh` writes the **root** to `pki/root_ca.crt` — copy it to
`files/etc/dot1x/radius-ca.pem` for the image build (this is what the
supplicant uses to trust the RADIUS server).
2. **Build a secure-path image** — `DISABLE_DOT1X=false` (CI var or
`-var disable_dot1x=false`), with `radius-ca.pem` = step-ca root and a
`nvmet_traddr` that resolves over the serving VLAN.
3. **Enrol each workstation's TPM** (one-shot, on a provisioning network that can
reach step-ca) — see `enrol/tpm-provision.sh`: generates the key **in** the TPM
at `0x81000100`, builds a CSR with it, has step-ca sign it, writes the cert to
NV `0x01800100`. Each host gets a unique cert → unique hostnqn.
4. **Set up the authenticator** — `authenticator/setup-link.sh` (bridge + macsec
plumbing per link) then run `hostapd authenticator/hostapd-wired-<link>.conf`.
5. **Boot the workstations** on the secure-path image and check the criteria
below.
## Pass criteria (per booted workstation)
```bash
wpa_cli -p /run/wpa_supplicant -i <nic> status | grep suppPortStatus # = Authorized
ip macsec show # macsec0: GCM-AES cipher, encrypt on, TX/RX SC
ip -s macsec show macsec0 # encrypted-pkts counters incrementing
ip -4 addr show macsec0 # DHCP lease ON macsec0
cat /etc/nvme/hostnqn # unique per host: …:host-<cert-serial>
findmnt / ; nvme list # NVMe root connected + verity-mapped, overlay writable
ps -o args= -C wpa_supplicant # @wpa_supplicant ALIVE after switch-root (invariant #3)
```
Plus the **renewal** check (the reason for the network-manager port): leave it up
past the DHCP lease and confirm `macsec0` keeps its address and the link doesn't
drop (~6 s without MKA keepalives = dead link). On the authenticator,
`tcpdump -i <link> ether proto 0x88e5` should show **only** MACsec frames after
auth — proof the wire is actually encrypted.
## Multi-host point
The whole reason for 2 VMs + the 820: one **identical** image, three **distinct**
TPM certs → three **distinct** hostnqns, each authenticating independently. That
verifies invariant #1 (no per-host state in the image) and lets you test NVMe
host-filtering per NQN.
## Next step: real TPM attestation
This starting solution *signs a TPM-generated CSR* — the key never leaves the
TPM, but step-ca doesn't yet *prove* TPM residency. The upgrade is step-ca's ACME
**`device-attest-01`** provisioner (EK/AK attestation): switch the enrolment from
`step ca sign` to `step ca acme` with a TPM attestation provisioner on the same
CA. That closes the "is the key really in a TPM" gap and strengthens SSP R-01.
See `step-ca/bootstrap.sh` (the commented `acme-attest` provisioner block).