Bench (Proxmox vTPM, host cygnus-cruiser-99) confirmed via tpm2_readpublic that
0x81010001 — the handle our design used for the fleet signing key — is the
TCG-standard RSA Endorsement Key slot: the object there is the swtpm-provisioned
EK (restricted|decrypt|adminwithpolicy, RSA-2048/AES128CFB, canonical EK
authPolicy 837197…, EK cert at NV 0x1c00002). Most firmware/swtpm TPMs occupy
that slot, so EnsureKey could never create our key there — it errored reading
the EK as ECC and enrolment died before the CSR/POST.
Move the fleet signing key to 0x81000100, a non-reserved owner persistent
handle (clear of EK 0x81010001/2 and SRK 0x81000001/2). Threaded through:
- fleet-enrol -handle default + new rd.dot1x.handle= cmdline knob (baked by
extract-artifacts.sh, overridable via SMBIOS)
- files/etc/dot1x/wpa_supplicant-dot1x.conf private_key handle
- the dracut hook, bench tpm-provision.sh / tpm-selftest.sh
- CLAUDE.md invariant #1 + design/SSP docs + TODO #11
NV index 0x01800001 (owner range) is unchanged.
EnsureKey hardening: refuse to evict any restricted-decrypt object (EK/SRK) as
a safety net — never clobber the TPM endorsement/storage identity (also
protects the future EK/AK attestation path). Still self-heals a genuine stale
signing key. New regression TestEnsureKeyRefusesToEvictProtectedKey; the
stale-key regression and handle constants moved to 0x81000100.
Design note records the IDevID (0x81020001) option for the later EK/AK
attestation work. Follow-up items logged: content-aware NV presence check
(verify the cert chains to the fleet CA, so leftover/foreign NV can't suppress
enrolment) and a de-enrol/TPM-reset procedure.
127 lines
6.3 KiB
Markdown
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 `0x01800001`. 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).
|