Skip to content

Commit 2b80cc4

Browse files
committed
Add SCEP certificate enrollment and 802.1x port-based network access control (PNAC)
SCEP (Simple Certificate Enrollment Protocol) enables automated certificate enrollment from a CA server. IEEE 802.1x is a port-based network access control (PNAC) standard that restricts network access on a switch port until the device authenticates using a certificate (or other EAP method). The 802.1x authentication is implemented using wpa_supplicant. The SCEP client is implemented using github.com/smallstep/scep. The end-to-end workflow is as follows: 1. Device sends a DHCP request with a vendor class identifier that identifies it as EVE OS. 2. The network switch places the port into a non-authenticated VLAN. Because it detects EVE OS, it allows the device to reach the controller and fetch network and SCEP configuration (needed to bootstrap the enrollment). 3. Device follows the SCEP profile to enroll a certificate, either by talking directly to the SCEP server or through a controller-provided SCEP proxy (essentially an HTTP proxy). 4. Device uses the enrolled certificate to authenticate the port via 802.1x. The switch then moves the port to the authenticated VLAN. 5. The device retries the DHCP request with exponential backoff (2s, 4s, 8s, ...) to obtain an IP address from the authenticated VLAN. Retries continue until the IP subnet changes (indicating the VLAN transition completed) or the configured maximum number of retries (pnac.dhcp.reacquire.max.retries, default 4) is reached. Setting this to 0 disables DHCP reacquire. EVE publishes PNAC status, enrolled certificate status and PNAC metrics to the controller. Signed-off-by: Milan Lenco <milan@zededa.com>
1 parent 344506d commit 2b80cc4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+11629
-207
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
/pkg/pillar/cmd/downloader/ @milan-zededa @rouming
3232
/pkg/pillar/cmd/ledmanager/ @rucoder @rene
3333
/pkg/pillar/cmd/nim/ @milan-zededa
34+
/pkg/pillar/cmd/scepclient/ @milan-zededa
3435
/pkg/pillar/cmd/tpmmgr/ @rucoder @shjala
3536
/pkg/pillar/cmd/volumemgr/ @OhmSpectator @rouming @europaul
3637
/pkg/pillar/cmd/zedagent/ @OhmSpectator @milan-zededa @rouming @uncleDecart

docs/CONFIG-PROPERTIES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@
9191
| diag.probe.remote.http.endpoint | string | `"http://www.google.com"` | - | - | Remote endpoint (URL, IP instead of hostname is accepted) queried over HTTP to assess the state of network connectivity whenever the controller is not reachable. Used only for diagnostics (no functional impact). Set to an empty string to disable. |
9292
| diag.probe.remote.https.endpoint | string | `"https://www.google.com"` | - | - | Remote endpoint (URL, IP instead of hostname is NOT accepted) queried over HTTPS to assess the state of network connectivity whenever the controller is not reachable. Used only for diagnostics (no functional impact). Set to an empty string to disable. |
9393
| app.enable.tcp.mss.clamping | bool | true | - | - | Configuration property that enables EVE to automatically adjust (clamp) the TCP MSS on forwarded application traffic to match the path MTU, preventing fragmentation and connectivity issues on lower-MTU links. |
94+
| scep.retry.interval | timer in seconds | 300 (5 minutes) | 60 (1 minute) | 3600 (1 hour) | Interval between retry attempts for certificates that previously failed to enroll/renew or returned PENDING from the SCEP server. |
95+
| pnac.dhcp.reacquire.max.retries | integer | 4 | 0 | 8 | Maximum number of DHCP reacquire retries after a PNAC (802.1X) port authentication state change. When the network switch reassigns the port to a different access VLAN, EVE retries with exponential backoff (2s, 4s, 8s, ...) until the IP subnet changes or the retry limit is reached. Setting this value to 0 disables DHCP reacquire. |
96+
| dhcp.enable.vendorclassid | bool | true | - | - | Enables sending the DHCP Vendor Class Identifier (Option 60) to identify the device as EVE OS. This allows networks or DHCP servers to apply policies such as VLAN assignment or granting access to the EVE controller. Some badly configured DHCP servers may reject unknown vendor class IDs. Setting this to false disables sending the vendor class ID. |
9497

9598
## Log levels
9699

@@ -156,3 +159,4 @@ Right now the following agents support per-agent log level settings:
156159
* msrv
157160
* domainmgr
158161
* diag
162+
* scepclient

docs/DEVICE-CONNECTIVITY.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,104 @@ There are two levels of errors:
604604
- A particular management port could not be used to reach the controller. In that case
605605
the `ErrorInfo` for the particular `DevicePort` is set to indicate the error and timestamp.
606606

607+
## Port-Based Network Access Control (802.1X) and SCEP Certificate Enrollment
608+
609+
EVE supports IEEE 802.1X Port-Based Network Access Control (PNAC), allowing network switches
610+
to restrict port-level access until the device authenticates with a valid certificate.
611+
IEEE 802.1X is a standard for port-based network access control that works at Layer 2
612+
of the network stack. A switch port starts in an unauthorized state and only grants full
613+
network access after the connected device (the supplicant) successfully authenticates
614+
against an authentication server (typically a RADIUS server) via the switch (the authenticator).
615+
616+
To obtain the certificate required for authentication, EVE implements SCEP (Simple Certificate
617+
Enrollment Protocol), a protocol designed for automated certificate enrollment from
618+
a Certificate Authority (CA). SCEP allows a device to generate a key pair, submit
619+
a Certificate Signing Request (CSR) to a SCEP server, and receive a signed certificate
620+
in return.
621+
622+
The 802.1X supplicant is implemented using [wpa_supplicant](https://w1.fi/wpa_supplicant/)
623+
with EAP-TLS as the authentication method. The SCEP client is implemented using
624+
the [github.com/smallstep/scep](https://github.com/smallstep/scep) Go library.
625+
626+
### Bootstrapping workflow
627+
628+
![SCEP + PNAC Workflow](images/scep-pnac-workflow.png)
629+
630+
The full workflow from an unauthenticated device to an authenticated network port is:
631+
632+
1. **DHCP with vendor class identification**: The device sends a DHCP request that includes
633+
a Vendor Class Identifier (DHCP Option 60) set to `LFEDGE-EVE`. This identifies the device
634+
as running EVE OS to the network infrastructure.
635+
636+
2. **Non-authenticated VLAN access**: The network switch places the port into a non-authenticated
637+
(bootstrap) VLAN. Because the switch detects the EVE OS vendor class identifier, it allows
638+
the device to reach the controller and fetch the network configuration including the SCEP
639+
enrollment profile. This step is critical for bootstrapping — the device needs connectivity
640+
to obtain the certificate it will later use for authentication.
641+
642+
3. **SCEP certificate enrollment**: The device follows the SCEP profile received from
643+
the controller to enroll a certificate. It can communicate with the SCEP server in one
644+
of two ways:
645+
- **Directly**: The device contacts the SCEP server URL specified in the profile.
646+
- **Via controller proxy**: The device routes SCEP requests through a controller-provided
647+
SCEP proxy (essentially an HTTP proxy), which is useful when the SCEP server is not
648+
directly reachable from the bootstrap VLAN.
649+
650+
4. **802.1X port authentication**: Once the certificate is enrolled, the device uses it
651+
to authenticate the port via 802.1X EAP-TLS. Upon successful authentication, the switch
652+
moves the port to the authenticated VLAN, granting full network access.
653+
654+
5. **DHCP reacquisition**: After the port authentication state changes, the device retries
655+
the DHCP request with exponential backoff (2s, 4s, 8s, ...) to obtain an IP address from
656+
the authenticated VLAN. Retries continue until the IP subnet changes (indicating the VLAN
657+
transition completed) or the configured maximum number of retries
658+
([`pnac.dhcp.reacquire.max.retries`](CONFIG-PROPERTIES.md), default 4) is reached.
659+
660+
### Configuration
661+
662+
PNAC and SCEP are configured through the controller using the device API:
663+
664+
- **SCEP profiles** are defined in `EdgeDevConfig.ScepProfiles` and specify the SCEP server URL,
665+
whether to use the controller proxy, a challenge password (encrypted), trusted CA certificates,
666+
and CSR parameters (subject DN, SANs, key type, hash algorithm, renewal period).
667+
668+
- **PNAC configurations** are defined in `EdgeDevConfig.Pnacs`, each referencing network adapter
669+
and a SCEP profile by logical names. They specify the EAP method (currently EAP-TLS),
670+
an optional EAP identity. If no EAP identity is configured, EVE will derive the identity from
671+
the enrolled certificate, preferring the subject common name (CN), or the SAN URI if CN is absent.
672+
673+
Relevant [configuration properties](CONFIG-PROPERTIES.md):
674+
675+
| Property | Default | Description |
676+
|---|---|---|
677+
| `scep.retry.interval` | 300s (5 min) | Interval between retry attempts for failed or pending SCEP enrollments |
678+
| `pnac.dhcp.reacquire.max.retries` | 4 | Max DHCP reacquire retries (with exponential backoff) after 802.1X authentication state change. Set to 0 to disable |
679+
| `dhcp.enable.vendorclassid` | true | Enables sending DHCP Vendor Class Identifier (Option 60) as `LFEDGE-EVE` |
680+
681+
### Certificate lifecycle
682+
683+
The enrolled certificate is stored on the device along with its private key (kept in the vault
684+
for protection). EVE monitors the certificate's validity and automatically initiates renewal
685+
when the configured percentage of the certificate's lifetime has elapsed
686+
(controlled by `RenewPeriodPercent` in the CSR profile). If the SCEP server or CSR profile
687+
configuration changes, EVE will re-enroll the certificate against the new parameters.
688+
689+
### Status and metrics reporting
690+
691+
EVE publishes the following information to the controller:
692+
693+
- **PNAC status** (per-port): Whether 802.1X is enabled, the current supplicant state
694+
(e.g. connecting, authenticating, authenticated, failed), the timestamp of the last
695+
successful authentication, and any authentication errors.
696+
697+
- **Enrolled certificate status**: Details of the installed certificate including subject,
698+
issuer, SANs, validity period, SHA-256 fingerprint, key type, and current certificate status
699+
(e.g. valid, expired, pending enrollment).
700+
701+
- **PNAC metrics** (per-port): EAPOL frame counters including frames received/transmitted,
702+
EAPOL-Start and EAPOL-Logoff frames, EAP-Request/Response frames, and counts of invalid
703+
or malformed frames.
704+
607705
## Air-Gap Mode
608706

609707
Air-Gap mode allows a device to operate without connectivity to the main controller,

docs/images/scep-pnac-workflow.mmd

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
sequenceDiagram
2+
title EVE: SCEP Certificate Enrollment + IEEE 802.1x PNAC
3+
4+
participant Controller
5+
participant zedagent
6+
participant scepclient
7+
participant nim
8+
participant netmonitor
9+
participant dhcpcd
10+
participant Switch as Network Switch<br/>(+ RADIUS)
11+
participant SCEP as SCEP Server<br/>(CA)
12+
13+
note over Controller, SCEP: 1. BOOT & DHCP BOOTSTRAP (onboarding VLAN)
14+
15+
nim ->> dhcpcd: start dhcpcd
16+
dhcpcd ->> Switch: DHCP DISCOVER (Option 60: "LFEDGE-EVE")
17+
note right of Switch: Switch detects EVE vendor class,<br/>assigns onboarding VLAN<br/>providing access to EVE controller
18+
Switch -->> dhcpcd: DHCP OFFER (onboarding VLAN IP)
19+
20+
note over Controller, SCEP: 2. CONFIG FETCH (controller reachable via onboarding VLAN)
21+
22+
zedagent ->> Controller: HTTPS GET /config
23+
Controller -->> zedagent: EdgeDevConfig { ScepProfiles, Pnacs }
24+
zedagent ->> scepclient: pubsub: SCEPProfile
25+
zedagent ->> nim: pubsub: DevicePortConfig { Port.PNAC }
26+
27+
note over Controller, SCEP: 3. SCEP CERTIFICATE ENROLLMENT
28+
29+
note over scepclient: Generate RSA key + CSR<br/>(subject DN, SAN from CSRProfile)
30+
scepclient ->> SCEP: PKIOperation=PKCSReq { CSR, challenge }
31+
note over scepclient, SCEP: (direct or via Controller SCEP proxy)
32+
33+
alt CertRep SUCCESS
34+
SCEP -->> scepclient: Signed certificate
35+
else CertRep PENDING
36+
SCEP -->> scepclient: PENDING
37+
note over scepclient: Retry (scep.retry.interval)
38+
scepclient ->> SCEP: PKIOperation=GetCertInitial
39+
SCEP -->> scepclient: Signed certificate
40+
end
41+
42+
note over scepclient: Save cert -> /persist/pnac/<profile>.cert.pem<br/>Save key -> /persist/vault/pnac/<profile>.key.pem
43+
scepclient ->> zedagent: pubsub: EnrolledCertificateStatus
44+
scepclient ->> nim: pubsub: EnrolledCertificateStatus
45+
46+
note over Controller, SCEP: 4. IEEE 802.1x EAP-TLS AUTHENTICATION
47+
48+
note over nim: Reconcile: create WpaSupplicant item<br/>with enrolled cert + CA chain.<br/>Start wpa_supplicant (-D wired)<br/>Start wpa_cli event watcher
49+
50+
nim ->> Switch: EAPOL-Start
51+
Switch -->> nim: EAP-Request/Identity
52+
nim ->> Switch: EAP-Response/Identity (from cert CN)
53+
nim -->> Switch: EAP-TLS handshake (mutual certificate auth)
54+
Switch -->> nim: EAP-TLS handshake
55+
note right of Switch: RADIUS validates client cert
56+
Switch -->> nim: EAP-Success
57+
58+
note right of Switch: Port moved to<br/>authenticated VLAN
59+
60+
note over nim: wpa_cli watcher script writes<br/>/run/pnac/<ifName>:<br/>STATE: CONNECTED<br/>TIMESTAMP: <unix_time>
61+
62+
note over Controller, SCEP: 5. DHCP REACQUIRE (authenticated VLAN)
63+
64+
netmonitor ->> nim: PNACEvent { IsAuthenticated: true }<br/>(fsnotify on /run/pnac/<ifName>)
65+
note over nim: Record current IPv4 subnet,<br/>schedule DHCP reacquire retry
66+
67+
loop Exponential backoff (2s, 4s, 8s, ...)
68+
nim ->> dhcpcd: Increment ReacquireCounter<br/>(triggers dhcpcd restart)
69+
dhcpcd ->> dhcpcd: release old lease, request new one
70+
dhcpcd ->> Switch: DHCP DISCOVER
71+
Switch -->> dhcpcd: DHCP OFFER (new VLAN IP)
72+
netmonitor ->> nim: AddrChange event
73+
alt Subnet changed
74+
note over nim: VLAN transition complete, stop retries
75+
else Same subnet & retries < pnac.dhcp.reacquire.max.retries (default 4)
76+
note over nim: Schedule next retry
77+
else Max retries reached
78+
note over nim: Give up
79+
end
80+
end
81+
82+
note over Controller, SCEP: 6. STATUS REPORTING & CERT RENEWAL
83+
84+
zedagent ->> Controller: ZInfoMsg { PNACStatus, EnrolledCertStatus }
85+
zedagent ->> Controller: ZMetricMsg { EAPOLFramesRx/Tx, ... }
86+
87+
note over scepclient: Monitor cert lifetime.<br/>At RenewPeriodPercent threshold:<br/>re-enroll -> re-authenticate

docs/images/scep-pnac-workflow.png

343 KB
Loading

pkg/edgeview/src/network.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1415,7 +1415,7 @@ func runWireless() {
14151415
_, _ = runCmd(prog, args, true)
14161416

14171417
retbytes, err = os.ReadFile(
1418-
fmt.Sprintf("/run/nim/wpa_supplicant.%s.conf", port.IfName))
1418+
fmt.Sprintf("/run/nim/wpa_supplicant-%s.conf", port.IfName))
14191419
if err != nil {
14201420
continue
14211421
}

pkg/pillar/cipher/cipher.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func getEncryptionBlock(
3131
decBlock.ProtectedUserData = zconfigDecBlockPtr.ProtectedUserData
3232
decBlock.ClusterToken = zconfigDecBlockPtr.ClusterToken
3333
decBlock.GzipRegistrationManifestYaml = zconfigDecBlockPtr.GzipRegistrationManifestYaml
34+
decBlock.SCEPChallengePassword = zconfigDecBlockPtr.ScepChallengePassword
3435
return decBlock
3536
}
3637

0 commit comments

Comments
 (0)