Adding dot1x support
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# 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 `0x81010001`, 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).
|
||||
Reference in New Issue
Block a user