diff --git a/.vale.ini b/.vale.ini index 3f2c11d7..226585b9 100644 --- a/.vale.ini +++ b/.vale.ini @@ -51,6 +51,8 @@ Google.DateFormat = NO TokenIgnores = (@smallstep/[a-zA-Z0-9-]+), \ + (Device\.[A-Za-z]+), \ + (<<[A-Za-z0-9.-]+>>), \ (_[a-zA-Z0-9_]+_), \ ([a-zA-Z]+_[a-zA-Z_]+) diff --git a/.vale/styles/config/vocabularies/Smallstep/accept.txt b/.vale/styles/config/vocabularies/Smallstep/accept.txt index ba10480e..0d8a7dbc 100644 --- a/.vale/styles/config/vocabularies/Smallstep/accept.txt +++ b/.vale/styles/config/vocabularies/Smallstep/accept.txt @@ -956,3 +956,51 @@ nasIPs autojoin serverHostname affordances +wifi +deviceMetadata +managementMode +operatingSystem +Wlan +Xml +streetAddress +postalCode +extraNames +matchAddresses +radiusServerCA +radiusServerDomain +clientCA +serverCA +serverIP +serverPort +authorityID +credentialID +managedRadiusID +wifiID +ethernetID +browserID +idpClientID +trustRoots +redirectURI +lifecycleFailureURI +authorizeEndpoint +tokenEndpoint +jwksEndpoint +extendedKeyUsage +LanXML +WlanXml +WiredNetwork +OMA +URI +OMA-URI +dot3svc +mdmclient +QueryCertificates +keychain +certlm +certmgr +regedit +AutoSelectCertificateForUrls +EAM +EAMs +Wlan +authMode diff --git a/manifest.json b/manifest.json index 9971be42..363637aa 100644 --- a/manifest.json +++ b/manifest.json @@ -75,11 +75,21 @@ }, { "title": "Wi-Fi", - "path": "/tutorials/protect-wireless-networks.mdx" + "path": "/use-cases/wifi.mdx" + }, + { + "title": "Wi-Fi (previous guide)", + "path": "/tutorials/protect-wireless-networks.mdx", + "hideFromSidebar": true }, { "title": "Wired", - "path": "/tutorials/protect-wired-networks.mdx" + "path": "/use-cases/wired.mdx" + }, + { + "title": "Wired (previous guide)", + "path": "/tutorials/protect-wired-networks.mdx", + "hideFromSidebar": true }, { "title": "VPN", @@ -108,7 +118,12 @@ }, { "title": "Web apps (mTLS)", - "path": "/tutorials/browser-certificate-setup-guide.mdx" + "path": "/use-cases/web-apps.mdx" + }, + { + "title": "Web apps (previous guide)", + "path": "/tutorials/browser-certificate-setup-guide.mdx", + "hideFromSidebar": true }, { "title": "SSO device factor", @@ -431,10 +446,134 @@ "title": "Install the agent", "path": "/platform/smallstep-agent.mdx" }, + { + "title": "Access points", + "path": "/reference/access-points.mdx" + }, + { + "title": "RADIUS authorization webhooks", + "path": "/reference/radius-webhooks.mdx" + }, + { + "title": "Windows network profile XML", + "path": "/reference/wlan-profile-xml.mdx" + }, { "heading": true, "title": "Troubleshooting" }, + { + "title": "Troubleshooting", + "path": "/troubleshooting/README.mdx" + }, + { + "title": "Wi-Fi", + "routes": [ + { + "title": "unknown CA on the RADIUS server", + "path": "/troubleshooting/wifi/unknown-ca.mdx" + }, + { + "title": "No RADIUS traffic reaches Smallstep", + "path": "/troubleshooting/wifi/no-radius-traffic.mdx" + }, + { + "title": "New site gets no answer", + "path": "/troubleshooting/wifi/new-site-no-answer.mdx" + }, + { + "title": "Certificate with the wrong issuer", + "path": "/troubleshooting/wifi/wrong-issuer.mdx" + }, + { + "title": "mobileconfig installed by hand", + "path": "/troubleshooting/wifi/mobileconfig-installed-by-hand.mdx" + }, + { + "title": "SCEP payload failing for every device", + "path": "/troubleshooting/wifi/scep-payload-failing.mdx" + }, + { + "title": "SCEP errors after Intune profiles install", + "path": "/troubleshooting/wifi/intune-intermediate-store.mdx" + }, + { + "title": "ACME certificate not in Keychain Access", + "path": "/troubleshooting/wifi/acme-cert-not-in-keychain.mdx" + }, + { + "title": "Windows needs a restart for the profile", + "path": "/troubleshooting/wifi/windows-profile-needs-restart.mdx" + }, + { + "title": "Can't connect on an agent-managed device", + "path": "/troubleshooting/wifi/cannot-connect-agent-device.mdx" + } + ] + }, + { + "title": "Wired", + "routes": [ + { + "title": "Port never authorizes", + "path": "/troubleshooting/wired/port-never-authorizes.mdx" + }, + { + "title": "Windows does not attempt 802.1X", + "path": "/troubleshooting/wired/windows-wired-autoconfig-stopped.mdx" + }, + { + "title": "Devices without certificates lose connectivity", + "path": "/troubleshooting/wired/devices-without-certificates.mdx" + }, + { + "title": "LAN profile does not present the certificate", + "path": "/troubleshooting/wired/lan-profile-wrong-issuer-hash.mdx" + } + ] + }, + { + "title": "Web apps", + "routes": [ + { + "title": "Certificate selection dialog appears", + "path": "/troubleshooting/web-apps/certificate-selection-dialog.mdx" + }, + { + "title": "Browser does not see the certificate", + "path": "/troubleshooting/web-apps/browser-does-not-see-certificate.mdx" + }, + { + "title": "Auto-select policy not applied", + "path": "/troubleshooting/web-apps/policy-not-applied.mdx" + }, + { + "title": "Certificate expired or invalid", + "path": "/troubleshooting/web-apps/certificate-expired.mdx" + } + ] + }, + { + "title": "SSO device factor", + "routes": [ + { + "title": "OpenID Connect IdP missing in Okta", + "path": "/troubleshooting/sso-device-factor/okta-oidc-idp-missing.mdx" + }, + { + "title": "Certificate email does not match the username", + "path": "/troubleshooting/sso-device-factor/email-san-mismatch.mdx" + }, + { + "title": "Certificate picker at sign-in", + "path": "/troubleshooting/sso-device-factor/certificate-picker-at-sign-in.mdx" + }, + { + "title": "Lifecycle failure page", + "path": "/troubleshooting/sso-device-factor/lifecycle-failure.mdx" + } + ] + }, { "title": "Agent", "path": "/platform/troubleshooting-agent.mdx" diff --git a/reference/access-points.mdx b/reference/access-points.mdx new file mode 100644 index 00000000..c7adacef --- /dev/null +++ b/reference/access-points.mdx @@ -0,0 +1,190 @@ +--- +updated_at: September 13, 2026 +title: Access points +html_title: Access point reference for EAP-TLS Wi-Fi +description: Where each access point vendor takes the RADIUS server IP, port, shared secret, and the WPA Enterprise setting. Companion to the Wi-Fi guide. +kind: reference +--- + +Every access point takes the same values from the [Wi-Fi guide](../use-cases/wifi.mdx#configure-external): + +| Setting | Value | +|---|---| +| Security | WPA2 Enterprise or WPA3 Enterprise | +| RADIUS server IP | <> | +| RADIUS server port | <> | +| Shared secret | <> | +| Server certificate name (where asked) | <> | + +The sections below say where each vendor's interface takes them. +The guide's vendor tabs cover Meraki, Ubiquiti UniFi, Aruba, Cisco WLC, and Juniper Mist; they are repeated here so this page is complete. + +## Ubiquiti UniFi {#ubiquiti-unifi} + +Create a RADIUS profile in the UniFi Network app: + +1. Go to **Settings → Profiles → RADIUS → Create New**. +2. Name the profile. +3. Under **Authentication servers**, add <>, <>, and <>. +4. Save. + +Create the network and link the profile: + +1. Go to **Settings → WiFi → Create New**. +2. Enter <>. +3. Under **Advanced Configuration**, choose **Manual**. +4. Under **Security**, select **WPA-3 Enterprise** and, for **RADIUS Profile**, the profile you created. +5. Save. + +## MikroTik {#mikrotik} + +For a MikroTik access point running RouterOS, from WebFig or the terminal. + +Add the RADIUS client: + +1. Go to **RADIUS → Add New**. +2. For **Service**, select **wireless**. +3. Enter <> as the address and <> as the secret. +4. Set **Timeout** to 5000 ms and confirm. + +```bash +/radius +add address=<> secret="<>" \ +service=wireless timeout=5s +``` + +Add a security profile: + +1. Go to **Wireless → Security Profiles → Add New** and name it `EAP_AP`. +2. For **Mode**, choose **dynamic keys**; for **Authentication Types**, select **WPA2 EAP**; for **Supplicant Identity**, enter `Mikrotik`. +3. Confirm. + +```bash +/interface wireless security-profiles +add authentication-types=wpa2-eap eap-method=passthrough mode=dynamic-keys name=EAP_AP supplicant-identity=Mikrotik +``` + +Attach the profile to the wireless interface: + +1. Go to **Wireless Interfaces**, open the interface, and set **Security Profile** to `EAP_AP`. + +```bash +/interface/wireless +set [find] security-profile=EAP_AP +``` + +See the [MikroTik documentation](https://help.mikrotik.com/docs/display/ROS/Enterprise+wireless+security+with+User+Manager+v5) for more. + +## Aerohive {#aerohive} + +Create the RADIUS profile: + +1. Go to **Configuration → Common Objects → Authentication → External RADIUS Servers** and add a server. +2. Name it and enter <>, <>, and <>. +3. Save. + +Create the network: + +1. Go to **Configure → Network Policies → Add Network Policy**, select **Wireless**, name the policy, and continue. +2. Add a wireless SSID with the SSID name and broadcast name <>. +3. Under **SSID Usage**: authentication **Enterprise WPA/WPA2 802.1X**, key management **WPA2-(WPA2 Enterprise)-802.1X**, encryption **CCMP (AES)**. +4. Under **Authentication Settings**, add the RADIUS profile to the **Default RADIUS Server Group** and save. + +![Aerohive EAP-TLS setup](/graphics/Aerohive.png) + +## Aruba {#aruba} + +These steps follow an Aruba mobility controller; see [Aruba's WLAN configuration documentation](https://arubanetworking.hpe.com/techdocs/VSG/docs/020-campus-deploy/esp-campus-deploy-110-configuring-wireless-access/) for other products. + +Create the server group: + +1. Go to **Configuration → Authentication → Auth Servers**. +2. Add a **Server Group** and submit. +3. Open the group, add a new server, enter <> and <>, set **Type** to **RADIUS**, and submit. + +Create the WLAN: + +1. Go to **Configuration → WLAN** and add a WLAN. +2. On **General**: name <>, primary usage **Employee**, the AP groups to broadcast on, forwarding mode **tunnel**. +3. On **VLANs**: your VLAN ID. +4. On **Security → Enterprise**: key management **WPA-3 Enterprise**; under **Auth servers**, add the server group. +5. On **Access**: the default role for authenticated devices; finish. +6. Open **Pending Changes** and deploy. + +## Meraki {#meraki} + +1. Go to **Wireless → Configure → SSIDs** and enable an unconfigured SSID. +2. Rename it to <> and save. +3. Open **edit settings** to reach the SSID's Access control tab. +4. Set **Association requirements** to **Enterprise with my RADIUS server**. +5. Under **RADIUS servers**, add <>, <>, and <>. +6. Save. + +![Meraki EAP-TLS Wi-Fi setup](/graphics/meraki.png) + +## Cisco Wireless LAN Controller {#cisco-wlc} + +1. Go to **Security → RADIUS → Authentication**, choose **New**, and enter <>, <>, and <>; apply. +2. On the **WLANs** tab, choose **Create New**, name the WLAN <>, and apply. +3. On **General**, set **Status** to **Enabled**. +4. On **Security → AAA Servers**, select the server as **Server 1** under **Authentication Servers** and apply. + +## Extreme Networks {#extreme-networks} + +Create the RADIUS server: + +1. Go to **ONBOARD → AAA**. +2. Under **RADIUS Servers** on the default AAA configuration, choose **Add** and enter <>, <>, and <>. +3. Save. + +![Extreme EAP-TLS setup](/graphics/Extreme.png) + +Create the network: + +1. Go to **Networks → Add**: a network name, SSID <>, status **Enable**, auth type **WPA2 Enterprise w/ RADIUS**, authentication method **RADIUS**, primary RADIUS the server you added, a backup RADIUS if you have one, default auth role **Enterprise User**, and a default VLAN. +2. Save. + +## Juniper Mist {#juniper-mist} + +1. Go to **Organization → WLAN Templates** and open or create a template. +2. Choose **Add WLAN** and enter <>. +3. Under **Security**, select **WPA3** or **WPA2**, then **Enterprise (802.1X)**. +4. Under **Authentication Servers**, choose **Add Server** and enter <> and <>. +5. Save. + +See [Juniper Mist's RADIUS documentation](https://www.juniper.net/documentation/us/en/software/mist/mist-wireless/topics/topic-map/radius-configuration.html) for more. + +## Sophos UTM {#sophos-utm} + +Create the authentication server: + +1. Go to **Definitions & Users → Authentication Services**. +2. On **Servers**, choose **New Authentication Server**: backend **RADIUS**, position **Top**, server <>, shared secret <>. +3. Save. + +Use it for enterprise authentication: + +1. Go to **Wireless Protection → Global Settings → Advanced**. +2. Under **Enterprise Authentication**, select the server and apply. + +Create the network: + +1. Go to **Wireless Protection → Wireless Networks** and choose **Add Wireless Network**. +2. Enter a network name and the SSID <>, set **Encryption mode** to **WPA2/WPA Enterprise**, and choose the client traffic option that fits (see the [Sophos UTM administrator guide](https://docs.sophos.com/nsg/sophos-utm/utm/9.717/help/en-us/Content/utm/utmAdminGuide/WirelessNetworks.htm)). +3. Save and assign the network to your access points. + +## Asus {#asus} + +These steps follow the RT-AX1800S; current Asus routers have a similar interface. +Set up a separate band with a WPA2 password first so you keep a way in if the enterprise band misbehaves. + +1. Go to **Advanced Settings → Wireless**. +2. On **General**: network name <>, authentication method **WPA2-Enterprise**, and the server IP address, server port, and connection string set to <>, <>, and <>. +3. Apply. + +![ASUS EAP-TLS setup](/graphics/asus-eaptls.png) + +## Not listed? + +Any access point that supports WPA Enterprise with an external RADIUS server works with the values at the top of this page. +Open an issue on [the docs repository](https://github.com/smallstep/docs/issues) to ask for a vendor. diff --git a/reference/radius-webhooks.mdx b/reference/radius-webhooks.mdx new file mode 100644 index 00000000..5ccac759 --- /dev/null +++ b/reference/radius-webhooks.mdx @@ -0,0 +1,155 @@ +--- +updated_at: September 13, 2026 +title: RADIUS authorization webhooks +html_title: RADIUS authorization webhook specification +description: The request Smallstep RADIUS sends to your webhook on every authentication, how to verify its signature, and the response it expects. +kind: reference +--- + +With an authorization webhook, Smallstep RADIUS consults your own HTTP server on every EAP-TLS authentication after the client certificate has been verified. +Your server evaluates or logs the presented certificate and returns an authorization decision, so you can layer device posture or your own rules on top of certificate verification. + +Webhooks are part of the dedicated Smallstep RADIUS deployment described in the [Wi-Fi guide](../use-cases/wifi.mdx#create). +Smallstep configures a webhook for your team today; the webhook's ID and signing secret are handed to you when it is created. +Smallstep authenticates to your server with a bearer token or HTTP basic authentication if you configure one. + +## Requirements + +Your webhook server must present a TLS certificate issued by a public Web PKI CA. + +## Request + +- Method `POST`, content type `application/json`. +- Headers: + - `X-Smallstep-Webhook-ID`: the UUID of the webhook making the request. + - `X-Smallstep-Signature`: hex-encoded HMAC-SHA256 of the raw request body, keyed with the webhook's signing secret. + - `Authorization`: optional; `Bearer ` or HTTP basic auth when configured. +- Body: + - `timestamp`: the RFC 8222 timestamp of the request. + - `x509Certificate`: a JSON representation of the client certificate following [this structure](https://github.com/smallstep/crypto/blob/master/x509util/certificate.go#L17), plus a `raw` field holding the base64-encoded DER certificate. + +Example body: + +```json +{ + "timestamp": "2024-01-15T10:30:00Z", + "x509Certificate": { + "subject": { + "country": ["US"], + "organization": ["Example Corp"], + "organizationalUnit": ["Engineering"], + "locality": ["San Francisco"], + "province": ["CA"], + "streetAddress": ["123 Main St"], + "postalCode": ["94105"], + "serialNumber": "123456", + "commonName": "craig@smallstep.com", + "names": [ + { + "type": "2.5.4.3", + "value": "craig@smallstep.com" + } + ], + "extraNames": [] + }, + "issuer": { + "country": ["US"], + "organization": ["Example CA"], + "organizationalUnit": ["CA Unit"], + "locality": ["San Francisco"], + "province": ["CA"], + "streetAddress": ["456 CA St"], + "postalCode": ["94105"], + "serialNumber": "CA123", + "commonName": "Example Root CA", + "names": [], + "extraNames": [] + }, + "serialNumber": "270390854734985720984572058347298347234", + "sans": [ + { + "type": "email", + "value": "craig@smallstep.com" + } + ], + "emailAddresses": ["craig@smallstep.com"], + "ipAddresses": [], + "uris": [], + "extensions": [], + "keyUsage": ["digitalSignature", "keyEncipherment"], + "extKeyUsage": ["serverAuth", "clientAuth"], + "unknownExtKeyUsage": [], + "subjectKeyId": "base64EncodedSKID==", + "authorityKeyId": "base64EncodedAKID==", + "ocspServer": ["http://ocsp.example.com"], + "issuingCertificateURL": ["http://ca.example.com/ca.crt"], + "dnsNames": ["example.com", "www.example.com"], + "permittedDNSDomainsCritical": false, + "permittedDNSDomains": [], + "excludedDNSDomains": [], + "permittedIPRanges": [], + "excludedIPRanges": [], + "permittedEmailAddresses": [], + "excludedEmailAddresses": [], + "permittedURIDomains": [], + "excludedURIDomains": [], + "crlDistributionPoints": ["http://crl.example.com/ca.crl"], + "policyIdentifiers": [], + "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...", + "publicKeyAlgorithm": "RSA", + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z", + "raw": "MIIDXTCCAkWgAwIBAgIJAKb..." + } +} +``` + +## Signature verification + +You need the signing secret associated with the `X-Smallstep-Webhook-ID`. + +1. Compute HMAC-SHA256 over the raw request body bytes with the secret. +2. Hex-encode the result and compare it to the `X-Smallstep-Signature` header. + +## Response + +- Content type `application/json`. +- Status `200` means the webhook was processed; any other status denies the authentication. +- Body: + - `allow` (boolean): whether the client may join. + - `error` (object, optional): a `message` and `code` that appear in your Smallstep event log. + +Minimal allow: + +```json +{ "allow": true } +``` + +Deny with a reason: + +```json +{ + "allow": false, + "error": { + "message": "Device non-compliant with posture check", + "code": "E1002" + } +} +``` + +## Example server + +Smallstep publishes an [example RADIUS webhook server](https://github.com/smallstep/radius-webhooks/) in Go as a starting point. + +## Operating a webhook + +- Several webhooks can be configured. + They are called after Smallstep verifies the client certificate, sequentially, with no guarantee of order. +- A timeout (10 seconds) or a non-`200` status is a denial. + Build for availability and fast failover: at least two replicas behind a load balancer. +- Smallstep may retry briefly on a transient `5xx`. +- Deny known-bad cases with `"allow": false`; reserve non-`200` statuses for unexpected failures, so an outage does not read as a policy decision. +- Store the signing secret securely. + Rotate it by creating a new webhook, distributing its secret, then retiring the old one. +- Log request IDs, the webhook ID, and your decision for auditing. + Avoid logging full certificates where you can. diff --git a/reference/wlan-profile-xml.mdx b/reference/wlan-profile-xml.mdx new file mode 100644 index 00000000..71aa2b33 --- /dev/null +++ b/reference/wlan-profile-xml.mdx @@ -0,0 +1,181 @@ +--- +updated_at: September 13, 2026 +title: Windows network profile XML +html_title: Windows WLAN and LAN profile XML for EAP-TLS with an agent credential +description: The WLAN and LAN profile XML that Intune and Workspace ONE deploy when the agent owns the certificate and the MDM owns the network settings. +kind: reference +--- + +These profiles are used by the [Wi-Fi guide](../use-cases/wifi.mdx#deliver) and the [wired guide](../use-cases/wired.mdx#deliver) when the agent manages the credential and Intune or Workspace ONE delivers the Windows network profile. +The profile selects the certificate by the SHA-1 fingerprint of its issuing CA and validates the RADIUS server by the SHA-1 fingerprint of the RADIUS CA. + +## The two fingerprints + +```bash +# The RADIUS server CA (radius_ca.crt from the guide) +openssl x509 -in radius_ca.crt -noout -fingerprint -sha1 + +# The CA that issues the client certificates: the *intermediate* of the authority +# you chose; download it from the authority's page in the Console +openssl x509 -in issuing_ca.crt -noout -fingerprint -sha1 +``` + +Remove the colons before pasting the values. + +## Values to set + +| Element | Value | +|---|---| +| `IssuerHash` | The issuing (intermediate) CA fingerprint | +| `TrustedRootCA` | The RADIUS server CA fingerprint | +| `ServerNames` | <> | +| `authMode` | `machine` for credentials with `HARDWARE_ATTESTED` key protection (the agent stores them in the computer certificate store); `user` for every other protection level | +| `authentication` and `encryption` (WLAN only) | Must match the access point: `WPA2` and `AES` for a WPA2 Enterprise network | + +See [Microsoft's OneX schema documentation](https://learn.microsoft.com/en-us/windows/win32/nativewifi/onexschema-onex-element) for the elements. + +## WLAN profile (Wi-Fi) {#wlan} + +Deployed at the OMA-URI `./Vendor/MSFT/WiFi/Profile/<>/WlanXml` (URL-encode spaces in the SSID as `%20`). +Microsoft documents the format in [Wi-Fi settings import](https://learn.microsoft.com/en-us/intune/intune-service/configuration/wi-fi-settings-import-windows-8-1). + +```xml + + + <> + + + <> + + false + + ESS + auto + false + + + + WPA3ENT192 + GCMP256 + true + false + + enabled + 720 + 128 + disabled + + true + machine + + + + 13 + 0 + 0 + 0 + + + + 13 + + + + true + + + + true + <> + RADIUS_CA_SHA1_FINGERPRINT + + false + true + true + + + + ISSUING_CA_SHA1_FINGERPRINT + + + + + + + + + + + + +``` + +## LAN profile (wired) {#lan} + +Deployed at the OMA-URI `./Device/Vendor/MSFT/WiredNetwork/LanXML`. +Microsoft documents the format in the [LAN profile schema](https://learn.microsoft.com/en-us/windows/win32/nativewifi/lan-profileschema-lanprofile-element). +The EKU filter restricts selection to certificates with the Client Authentication extended key usage. + +```xml + + + + + false + true + + machine + + + + 13 + 0 + 0 + 0 + + + + 13 + + + + true + + + + false + <> + RADIUS_CA_SHA1_FINGERPRINT + + false + true + true + + + false + + ISSUING_CA_SHA1_FINGERPRINT + + + + Client Authentication + 1.3.6.1.5.5.7.3.2 + + + + + Client Authentication + + + + + + + + + + + + + +``` diff --git a/skills/sso-device-factor/SKILL.md b/skills/sso-device-factor/SKILL.md new file mode 100644 index 00000000..5f2d6071 --- /dev/null +++ b/skills/sso-device-factor/SKILL.md @@ -0,0 +1,69 @@ +--- +name: sso-device-factor +description: Make an identity provider require a Smallstep device certificate as a sign-in factor. Creates the device factor, its client, and the browser credential through the Smallstep API, and hands the Okta or Entra ID configuration to a person with the exact values they need. +--- + +# SSO device factor + +This skill walks the nine steps of the [SSO device factor guide](https://smallstep.com/docs/use-cases/sso-device-factor). +The guide is being validated against the product; tell the person that before starting. +Steps 3, 6, 7, and 9 can be done through the Smallstep API. +Step 4 (the identity provider) and the browser policy in step 5 must be handed to a person. +Show every API request to the person before sending it; the client secret is returned once. + +Requirements: an API token in `SMALLSTEP_API_TOKEN`, `curl`, and `jq`. +Every request carries `Authorization: Bearer $SMALLSTEP_API_TOKEN` and `x-smallstep-api-version: 2025-01-01`, against `https://gateway.smallstep.com/api`. + +## 1. What you get + +Tell the person: their identity provider redirects sign-ins to Smallstep as a second factor; Smallstep checks the device's certificate over mutual TLS and answers. +Objects: the device factor (one per team), a client per identity provider, and a browser credential plus web app resource for the device factor's address. +About 30 minutes plus the identity-provider change. + +## 2. Before you start + +- An approved device with a bound user and the agent (`GET /devices`). +- Users synced from the identity provider (`GET /platforms`, or ask). +- The authority whose credentials carry the person's email (`GET /authorities`); its root as `accounts_root.crt`. +- From the person: the identity provider and its tenant address (for Okta, the tenant domain). +- If Protect → SSO in the Console shows a request form, the team is not entitled; say so and stop. + +## 3. Create it in Smallstep (agent can do this) + +1. `PUT /sso` with `trustRoots` (the contents of `accounts_root.crt`; the root alone trusts every intermediate under it). + Save `issuer`, `authorizeEndpoint`, `tokenEndpoint`, `jwksEndpoint`. + Fetch `{issuer}/.well-known/openid-configuration` to confirm discovery works. +2. `POST /sso/clients` with `redirectURI`: `https:///oauth2/v1/authorize/callback` for Okta, `https://login.microsoftonline.com/common/federation/externalauthprovider` for Entra ID. + Save `id` and `secret`; show the secret once. +3. Create the browser credential and web app as the `web-apps` skill's step 3 does, with `matchAddresses` `[issuer]`, SANs with `smallstep:identity` first, and a duration up to `8760h0m0s`. + +## 4. Configure the identity provider (hand to a person) + +Give the person the issuer, the three endpoints, the client ID, and the client secret, plus the guide's Okta steps: add an OpenID Connect identity provider with usage **Factor only**, add it as an **IdP Authenticator**, and attach it to an authentication policy for a pilot group. +For Entra ID, say the mechanism is an external authentication method and that the exact menu steps are not documented yet. +Do not change the identity provider yourself. + +## 5. Deliver to devices + +The agent installs the certificate. +Hand the person the browser auto-select policy for the issuer address from the `web-apps` skill. + +## 6. Verify + +Ask the person to sign in to a pilot app from the enrolled device and choose Smallstep as the factor; the device check completes and the app opens. +Then from a device that is not enrolled; the factor is not satisfied. + +## 7. Roll out and operate (agent can do this) + +- The person widens the identity-provider policy; widen the credential's `policy` with `PUT /credential/{credentialID}`. +- Revoke with `POST /certificates/{serialNumber}/revoke`. +- Rotate the secret: create a new client, hand it over, then `DELETE /sso/clients/{idpClientID}` for the old one. +- More authorities: `PUT /sso` with a PEM bundle of roots. + +## 8. Troubleshoot + +Match the symptom to an entry under Troubleshooting → SSO device factor (and Web apps for the certificate itself) and follow its fix. + +## 9. Automate (agent can do this) + +Terraform: `smallstep_identity_provider`, `smallstep_identity_provider_client` (with `store_secret` or `write_secret_file`), plus `smallstep_credential` and `smallstep_browser`. diff --git a/skills/web-apps/SKILL.md b/skills/web-apps/SKILL.md new file mode 100644 index 00000000..305e797b --- /dev/null +++ b/skills/web-apps/SKILL.md @@ -0,0 +1,67 @@ +--- +name: web-apps +description: Protect a web app with browser certificates (mutual TLS) using Smallstep. Creates the credential and the web app resource through the Smallstep API, and hands the app configuration and the browser policy to a person with the exact values they need. +--- + +# Web apps with browser certificates + +This skill walks the nine steps of the [web apps guide](https://smallstep.com/docs/use-cases/web-apps). +Steps 3, 6, 7, and 9 can be done through the Smallstep API. +Step 4 (the app or proxy) and the browser policy in step 5 must be handed to a person. +Show every API request to the person before sending it. + +Requirements: an API token in `SMALLSTEP_API_TOKEN`, `curl`, and `jq`. +Every request carries `Authorization: Bearer $SMALLSTEP_API_TOKEN` and `x-smallstep-api-version: 2025-01-01`, against `https://gateway.smallstep.com/api`. + +## 1. What you get + +Tell the person: the browser on an enrolled device presents a device certificate to the app during the TLS handshake; the app verifies it against the team's authority. +Two objects: a credential and a web app resource. +The verifier is the person's own app or proxy. +About 15 minutes plus the app change and a browser policy. +If the app sits behind single sign-on instead, use the `sso-device-factor` skill. + +## 2. Before you start + +- An approved device with a bound user and the agent (`GET /devices`). +- The authority (`GET /authorities`); the person downloads its root and intermediate from the Console for step 4. +- From the person: the URLs that will require a certificate. + +## 3. Create it in Smallstep (agent can do this) + +1. `POST /credentials`: `slug` `browser`, `certificate.type` `X509`, `authorityID`, `duration` `168h0m0s`, common name from `smallstep:identity`, SANs `["smallstep:identity", "Device.Serial"]` (email first if the app maps users; serial first if it maps devices), `extendedKeyUsage` `["clientAuth"]`, key `ECDSA_P256` with `HARDWARE_ATTESTED`, `managementMode` `agent`, a narrow `policy`. + Save the `id`. +2. `POST /protect/browser`: `name`, `matchAddresses` (the URLs), `credentials` `[credential id]`. + Save the `id`. + +## 4. Configure the app (hand to a person) + +Tell the person to require a client certificate on the protected URLs, trust the authority's root for client verification, and map the identity from the first SAN (email) or second SAN (serial). +Name the server setting if they say which server they run (`ssl_verify_client` and `ssl_client_certificate` in NGINX, `SSLVerifyClient` and `SSLCACertificateFile` in Apache, `client_auth` in Caddy). +Do not change the app yourself. +Verifier-specific steps (Entra certificate-based authentication, Cloudflare Access, Auth0, Salesforce, Okta) are not documented yet; say so rather than inventing them. + +## 5. Deliver to devices (certificate is automatic; the policy is handed to a person) + +The agent installs the certificate. +Give the person the auto-select policy from the guide's Deliver step for their OS and browser, with the URL pattern and the exact intermediate CA name from the authority's page (for a hosted authority, `Smallstep Accounts Intermediate CA`). +Remind them that Chrome does not merge several `AutoSelectCertificateForUrls` policies. + +## 6. Verify + +Ask the person to confirm the certificate in the browser's certificate list, the policy at `chrome://policy`, then to open the app: no picker, and the app shows the identity. +Ask for `sudo step-agent doctor` output; every row `PASS`. + +## 7. Roll out and operate (agent can do this) + +- Widen the credential's `policy` with `PUT /credential/{credentialID}`. +- Add URLs with `PUT /protect/browser/{browserID}` and tell the person to add the same URLs to the policy. +- Revoke with `POST /certificates/{serialNumber}/revoke`; whether the app notices depends on its revocation checking. + +## 8. Troubleshoot + +Match the symptom to an entry under Troubleshooting → Web apps and follow its fix. + +## 9. Automate (agent can do this) + +Terraform: `smallstep_credential` and `smallstep_browser`; the guide's step 9 has the configuration. diff --git a/skills/wifi/SKILL.md b/skills/wifi/SKILL.md new file mode 100644 index 00000000..c30ef34d --- /dev/null +++ b/skills/wifi/SKILL.md @@ -0,0 +1,87 @@ +--- +name: wifi +description: Set up certificate-based Wi-Fi (802.1X EAP-TLS) with Smallstep. Creates the credential, the RADIUS server, and the Wi-Fi network through the Smallstep API, and hands the access-point and MDM steps to a person with the exact values they need. +--- + +# Wi-Fi with device certificates + +This skill walks the nine steps of the [Wi-Fi guide](https://smallstep.com/docs/use-cases/wifi). +Steps 3, 6, 7, and 9 can be done through the Smallstep API. +Steps 4 and 5 (access points, MDM) must be handed to a person with the values from step 3. +Show every API request to the person before sending it; a credential or a RADIUS server is a real object once created. + +Requirements: an API token in `SMALLSTEP_API_TOKEN`, `curl`, and `jq`. +Every request carries `Authorization: Bearer $SMALLSTEP_API_TOKEN` and `x-smallstep-api-version: 2025-01-01`, against `https://gateway.smallstep.com/api`. + +## 1. What you get + +Tell the person: a credential (client certificate per device, key in hardware), a RADIUS server run by Smallstep that verifies it, and a Wi-Fi network resource that tells devices which SSID to join and which RADIUS CA to trust. +About 20 minutes plus the access-point change. +Recommend a test SSID. + +## 2. Before you start + +Confirm with the person, or check with the API: + +- At least one approved device with the agent: `GET /devices` and look for an approved device on macOS, Windows, or Linux. +- The authority that signs client certificates: `GET /authorities`; note its `id` and download its root certificate (save as `client_ca.crt`). + If there is none, stop and point to the hosted authority guide. +- If profiles will be delivered by an MDM: the MDM is connected (`GET /platforms`). +- From the person: the public IP address the access points send RADIUS traffic from, and the test SSID. + +## 3. Create it in Smallstep (agent can do this) + +In order: + +1. `POST /credentials` with the body from the guide's step 3: `slug` `wifi`, `certificate.type` `X509`, `authorityID`, `duration` `24h0m0s`, common name from `smallstep:identity` with a static fallback, SANs from `smallstep:identity` and `Device.Serial`, `extendedKeyUsage` `["clientAuth"]`, key `ECDSA_P256` with `HARDWARE_ATTESTED` protection (or `HARDWARE_WITH_FALLBACK` if the person accepts software keys), `managementMode` `agent`, and a narrow `policy` (a pilot tag, or `assurance: ["high"]` with the operating systems). + Save the `id`. +2. `POST /managed-radius` with `name`, `nasIPs` (the person's public IPs), and `clientCA` (the contents of `client_ca.crt`). + Save `id`, `serverIP`, `serverPort`, `serverHostname`, and `serverCA` (save as `radius_ca.crt`). +3. `GET /managed-radius/{id}?secret=true` and keep `secret` for step 4 without printing it more than once. +4. `POST /protect/wifi` with `name`, `ssid`, `hidden` false, `autojoin` true, `radiusServerCA` (the contents of `radius_ca.crt`), `radiusServerDomain` (`serverHostname`), and `credentials` `[credential id]`. + Save the `id`. + +If the person brings their own RADIUS server, skip 2 and 3, tell them to add `client_ca.crt` to that server's trusted client CAs, and use their server's CA as `radiusServerCA`. + +## 4. Configure the access points (hand to a person) + +Give the person these values and the vendor steps from the guide's step 4 (Meraki, Ubiquiti UniFi, Aruba, Cisco WLC, Juniper Mist; others in the access point reference): + +- Security: WPA2 Enterprise or WPA3 Enterprise +- RADIUS server IP: `serverIP` +- RADIUS server port: `serverPort` +- Shared secret: `secret` +- Server certificate name: `serverHostname` + +Do not change the access point yourself. +Wait for the person to say it is done. + +## 5. Deliver to devices (agent-managed needs nothing; MDM is handed to a person) + +- Agent-managed devices: nothing to do; the agent requests the certificate, trusts `radius_ca.crt`, creates the profile, and renews. +- MDM-delivered profiles: hand the person the guide's tab for their MDM (Jamf Pro, Intune, Workspace ONE, ChromeOS). + The profile downloads and the SCEP settings are on the network's page under Protect → Wi-Fi in the Console; the agent cannot download them. + +## 6. Verify (agent can check the API side) + +- Ask the person to select the SSID on the test device; it should connect without a password. +- `GET /protect/wifi/{id}` confirms the resource; the issued certificate and the authentication activity are on the network's page in the Console. +- Ask the person to run `sudo step-agent doctor` on the device and paste the table; every row should be `PASS`. + +## 7. Roll out and operate (agent can do this) + +- Widen the assignment policy: `PUT /credential/{credentialID}` with the full object and a broader `policy` (for example `ownership: ["company"]`). +- Renewals are automatic on agent-managed devices. +- Revoke a device's certificate with `POST /certificates/{serialNumber}/revoke`; RADIUS checks revocation on every authentication. +- New site: `PUT /managed-radius/{id}` with the new public IP added to `nasIPs`. +- The same credential and RADIUS server serve a wired network (`POST /protect/ethernet`). + +## 8. Troubleshoot + +Match the symptom to an entry under Troubleshooting → Wi-Fi in the docs and follow its fix; the entries name the guide step to return to. +Do not guess at a cause the entries do not list. + +## 9. Automate (agent can do this) + +The same objects as Terraform: `smallstep_credential`, `smallstep_managed_radius`, `smallstep_wifi`, with the shared secret from the `smallstep_managed_radius_secret` data source. +The guide's step 9 has the configuration. diff --git a/skills/wired/SKILL.md b/skills/wired/SKILL.md new file mode 100644 index 00000000..d4c326b8 --- /dev/null +++ b/skills/wired/SKILL.md @@ -0,0 +1,66 @@ +--- +name: wired +description: Set up certificate-based wired networking (802.1X EAP-TLS) with Smallstep. Reuses the Wi-Fi credential and RADIUS server, creates the wired network through the Smallstep API, and hands the switch and MDM steps to a person with the exact values they need. +--- + +# Wired networks with device certificates + +This skill walks the nine steps of the [wired guide](https://smallstep.com/docs/use-cases/wired). +Steps 3, 6, 7, and 9 can be done through the Smallstep API. +Step 4 (switches) and the MDM part of step 5 must be handed to a person. +Show every API request to the person before sending it. + +Requirements: an API token in `SMALLSTEP_API_TOKEN`, `curl`, and `jq`. +Every request carries `Authorization: Bearer $SMALLSTEP_API_TOKEN` and `x-smallstep-api-version: 2025-01-01`, against `https://gateway.smallstep.com/api`. + +## 1. What you get + +Tell the person: the switch port opens only after the RADIUS server accepts the device's certificate. +The credential and the RADIUS server are the same objects as for Wi-Fi; only the wired network resource is new. +About 15 minutes plus the switch change. + +## 2. Before you start + +- An approved device with the agent and a wired interface (`GET /devices`). +- The authority (`GET /authorities`) and its root as `client_ca.crt`. +- If Wi-Fi is set up: the existing credential (`GET /credentials`) and RADIUS server (`GET /managed-radius`); reuse them. +- From the person: the public IP the switches send RADIUS traffic from, and a test port. + +## 3. Create it in Smallstep (agent can do this) + +1. If there is no credential or RADIUS server, create them as the `wifi` skill's step 3 does, with `slug` `dot1x` and static common name `Corporate Network`; `nasIPs` are the switches' public IPs. + Save `radius_ca.crt` (the server's `serverCA`). +2. `POST /protect/ethernet` with `name`, `autojoin` true, `radiusServerCA` (the contents of `radius_ca.crt`), and `credentials` `[credential id]`. + Save the `id`. + +## 4. Configure the switches (hand to a person) + +Give the person `serverIP`, `serverPort`, and the shared secret (`GET /managed-radius/{id}?secret=true`), and the shape from the guide: register the RADIUS server, enable 802.1X globally, enable port authentication on one test port first, plan a guest or unauthenticated VLAN for devices without certificates (MAC Authentication Bypass is not supported). +Do not change the switch yourself; wait for the person. + +## 5. Deliver to devices + +- Agent-managed devices: nothing to do. + Tell the person that on Windows the Wired AutoConfig service (`dot3svc`) must be running. +- Intune or Workspace ONE LAN profile (agent owns the certificate): hand the person the guide's tab and the profile reference; the two fingerprints come from `radius_ca.crt` and the authority's intermediate. + +## 6. Verify + +- Ask the person to plug the test device into the port and to run `show authentication sessions` on the switch; expect **Authorized**. +- `GET /protect/ethernet/{id}` confirms the resource; the activity is on the resource's page in the Console. +- Ask for `sudo step-agent doctor` output from the device; every row `PASS`. + +## 7. Roll out and operate (agent can do this) + +- Widen the credential's `policy` with `PUT /credential/{credentialID}`. +- The person enables port authentication on more ports in batches. +- Revoke with `POST /certificates/{serialNumber}/revoke`. +- A second RADIUS server for failover: register both public IPs in `nasIPs`. + +## 8. Troubleshoot + +Match the symptom to an entry under Troubleshooting → Wired (and the RADIUS-side entries under Wi-Fi) and follow its fix. + +## 9. Automate (agent can do this) + +Terraform: `smallstep_ethernet` beside the `smallstep_credential` and `smallstep_managed_radius` from the Wi-Fi configuration. diff --git a/troubleshooting/README.mdx b/troubleshooting/README.mdx new file mode 100644 index 00000000..b43e7b4b --- /dev/null +++ b/troubleshooting/README.mdx @@ -0,0 +1,49 @@ +--- +updated_at: September 13, 2026 +title: Troubleshooting +html_title: Troubleshooting +description: One entry per symptom, with its cause, fix, and the guide step to return to. +kind: troubleshooting +--- + +Each entry is one symptom: the error text or the failure you can see, its cause, the fix, a check that proves the fix, and the guide step to return to. +Start from the symptom you have. +For an agent that is not healthy on any use case, start with the [agent troubleshooting page](../platform/troubleshooting-agent.mdx) and `step-agent doctor`. + +## Wi-Fi + +- [`unknown CA` or `unable to get local issuer certificate` on the RADIUS server](./wifi/unknown-ca.mdx) +- [No RADIUS traffic reaches Smallstep when a device tries to join](./wifi/no-radius-traffic.mdx) +- [Access points at a new site get no answer from Smallstep RADIUS](./wifi/new-site-no-answer.mdx) +- [The device presents a certificate with the wrong issuer](./wifi/wrong-issuer.mdx) +- [The `.mobileconfig` installed by hand does not connect](./wifi/mobileconfig-installed-by-hand.mdx) +- [`The SCEP Certificate payload is failing for every device`](./wifi/scep-payload-failing.mdx) +- [SCEP enrollment errors after the Intune trusted certificate profiles install](./wifi/intune-intermediate-store.mdx) +- [The ACME certificate does not appear in Keychain Access](./wifi/acme-cert-not-in-keychain.mdx) +- [Windows does not pick up the profile until it restarts](./wifi/windows-profile-needs-restart.mdx) +- [`I can't connect to Wi-Fi` on an agent-managed device](./wifi/cannot-connect-agent-device.mdx) + +## Wired + +- [The port never authorizes and `show authentication sessions` shows no session](./wired/port-never-authorizes.mdx) +- [Windows does not attempt 802.1X on the wired interface](./wired/windows-wired-autoconfig-stopped.mdx) +- [Printers and other devices lose connectivity after port authentication is enabled](./wired/devices-without-certificates.mdx) +- [The Intune wired profile installs but Windows does not present the certificate](./wired/lan-profile-wrong-issuer-hash.mdx) + +## Web apps + +- [The browser shows a certificate selection dialog](./web-apps/certificate-selection-dialog.mdx) +- [Chrome or Firefox does not see the Smallstep certificate](./web-apps/browser-does-not-see-certificate.mdx) +- [`chrome://policy` does not list `AutoSelectCertificateForUrls`](./web-apps/policy-not-applied.mdx) +- [The app rejects the certificate as expired or invalid](./web-apps/certificate-expired.mdx) + +## SSO device factor + +- [`OpenID Connect IdP` is not in Okta's list of identity providers](./sso-device-factor/okta-oidc-idp-missing.mdx) +- [The identity provider rejects the factor because the certificate's email does not match the username](./sso-device-factor/email-san-mismatch.mdx) +- [A certificate selection dialog appears during the device check](./sso-device-factor/certificate-picker-at-sign-in.mdx) +- [The person lands on the lifecycle failure page](./sso-device-factor/lifecycle-failure.mdx) + +## Agent + +- [Agent troubleshooting](../platform/troubleshooting-agent.mdx): the doctor command, each of its checks, and the platform-specific commands and file locations. diff --git a/troubleshooting/sso-device-factor/certificate-picker-at-sign-in.mdx b/troubleshooting/sso-device-factor/certificate-picker-at-sign-in.mdx new file mode 100644 index 00000000..4a4dd0dc --- /dev/null +++ b/troubleshooting/sso-device-factor/certificate-picker-at-sign-in.mdx @@ -0,0 +1,27 @@ +--- +updated_at: September 13, 2026 +title: A certificate selection dialog appears during the device check +html_title: "SSO device factor: a certificate selection dialog appears during the device check" +description: The browser has no auto-select rule for the device factor's address, so it asks the person which certificate to present. +kind: troubleshooting +--- + +When the identity provider redirects to Smallstep, the browser shows a certificate picker before the device check completes. + +## Cause + +The browser's auto-select policy does not cover the device factor's address (`https://<>.id.smallstep.com`), or names an issuer that does not match the certificate. +The check still succeeds when the person picks the Smallstep certificate. + +## Fix + +1. Add the device factor's address to the `AutoSelectCertificateForUrls` policy (Chrome, Edge) or the Firefox preferences, as the [web apps guide](../../use-cases/web-apps.mdx#deliver) shows, with the exact intermediate CA name as the issuer. +2. Restart the browser. + +## Check + +The next sign-in completes the device check with no dialog. + +## Go back to + +[Deliver to devices](../../use-cases/sso-device-factor.mdx#deliver) in the SSO device factor guide. diff --git a/troubleshooting/sso-device-factor/email-san-mismatch.mdx b/troubleshooting/sso-device-factor/email-san-mismatch.mdx new file mode 100644 index 00000000..ef63815f --- /dev/null +++ b/troubleshooting/sso-device-factor/email-san-mismatch.mdx @@ -0,0 +1,33 @@ +--- +updated_at: September 13, 2026 +title: The identity provider rejects the factor because the certificate's email does not match the username +html_title: "SSO device factor: the certificate's email does not match the sign-in username" +description: The device factor identifies the person by the certificate's first email SAN, which must equal the username they signed in with at the identity provider. +kind: troubleshooting +--- + +The device check completes, but the identity provider reports that the factor was not satisfied, or that the identity returned does not match the signed-in user. + +## Cause + +The device factor takes the person's identity from the **first email SAN** of the browser certificate. +The credential's SANs do not put `smallstep:identity` first, or the device's bound user is not the same person who signed in, or the user synced from the identity provider has a different email than the sign-in username. + +## Fix + +1. Inspect the certificate on the device and read its first email SAN: + + ```bash + step certificate inspect --bundle /path/to/cert.crt + ``` + +2. If the SAN is not the sign-in username: put `smallstep:identity` first in the credential's `sans` (`PUT /credential/{credentialID}`) and let the agent renew, or fix the device's bound user under **Devices**. +3. If the synced user's email differs from the sign-in username, correct the mapping in the user sync for [Okta](../../tutorials/sync-okta-users-to-smallstep.mdx) or [Entra ID](../../tutorials/sync-entra-id-users-to-smallstep.mdx). + +## Check + +The certificate's first email SAN equals the username, and the sign-in completes with Smallstep as the factor. + +## Go back to + +[Create it in Smallstep](../../use-cases/sso-device-factor.mdx#create) in the SSO device factor guide. diff --git a/troubleshooting/sso-device-factor/lifecycle-failure.mdx b/troubleshooting/sso-device-factor/lifecycle-failure.mdx new file mode 100644 index 00000000..d097ba86 --- /dev/null +++ b/troubleshooting/sso-device-factor/lifecycle-failure.mdx @@ -0,0 +1,26 @@ +--- +updated_at: September 13, 2026 +title: The person lands on the lifecycle failure page +html_title: "SSO device factor: the person lands on the lifecycle failure page" +description: The device presented a certificate but its lifecycle in Smallstep is not active, so the device factor sent the person to the client's lifecycle failure address. +kind: troubleshooting +--- + +Instead of returning to the identity provider, the browser lands on the address you set as the client's `lifecycleFailureURI`, or the device check fails after the certificate is presented. + +## Cause + +The certificate verified, but the device's lifecycle state in Smallstep is not active: the device was unapproved, retired, or otherwise moved out of the active state, so the device factor declines it. + +## Fix + +1. Find the device under [Devices](https://smallstep.com/app/?next=/devices) and check its lifecycle state. +2. If the device should have access, return it to the active state; if it should not, the failure is correct and the person needs an enrolled device. + +## Check + +The sign-in from the device completes once its lifecycle state is active again. + +## Go back to + +[Roll out and operate](../../use-cases/sso-device-factor.mdx#operate) in the SSO device factor guide. diff --git a/troubleshooting/sso-device-factor/okta-oidc-idp-missing.mdx b/troubleshooting/sso-device-factor/okta-oidc-idp-missing.mdx new file mode 100644 index 00000000..21d5cf0a --- /dev/null +++ b/troubleshooting/sso-device-factor/okta-oidc-idp-missing.mdx @@ -0,0 +1,26 @@ +--- +updated_at: September 13, 2026 +title: "`OpenID Connect IdP` is not in Okta's list of identity providers" +html_title: "SSO device factor: OpenID Connect IdP is not in Okta's list" +description: Okta hides the generic OpenID Connect identity provider type behind a tenant feature that Okta support enables. +kind: troubleshooting +--- + +Under **Security → Identity Providers → Add identity provider**, Okta offers social and SAML providers but no **OpenID Connect IdP**. + +## Cause + +The generic OpenID Connect identity provider type is behind the `GENERIC_OIDC_IDP` feature on your Okta tenant, and it is not enabled. + +## Fix + +[Open a support case with Okta](https://support.okta.com/help/s/article/How-to-Create-a-Support-Case?language=en_US) and ask for the `GENERIC_OIDC_IDP` feature to be enabled for your tenant. +Nothing on the Smallstep side changes. + +## Check + +**Add identity provider** lists **OpenID Connect IdP**. + +## Go back to + +[Configure your identity provider, Okta](../../use-cases/sso-device-factor.mdx#configure-external) in the SSO device factor guide. diff --git a/troubleshooting/web-apps/browser-does-not-see-certificate.mdx b/troubleshooting/web-apps/browser-does-not-see-certificate.mdx new file mode 100644 index 00000000..4f7ccde1 --- /dev/null +++ b/troubleshooting/web-apps/browser-does-not-see-certificate.mdx @@ -0,0 +1,46 @@ +--- +updated_at: September 13, 2026 +title: Chrome or Firefox does not see the Smallstep certificate +html_title: "Web apps: Chrome or Firefox does not see the Smallstep certificate" +description: The certificate is issued but the browser's certificate list does not show it, because the browser has not reloaded its store or, on Linux, cannot reach the agent's PKCS#11 module. +kind: troubleshooting +--- + +The device's **Certificates** tab in the Console shows a certificate from the browser credential, but `chrome://settings/certificates`, Edge's certificate manager, or Firefox's `about:certificate` does not list it, and the app fails the handshake. + +## Cause + +On macOS and Windows, the browser was open when the certificate was installed and has not reread the keychain or certificate store. +On Linux, browsers reach the certificate through the agent's PKCS#11 interface, and the module is not reachable: the PKCS#11 socket is missing, `P11_KIT_SERVER_ADDRESS` is not set for the browser's environment, or p11-kit is not installed. + +## Fix + +1. Restart the browser. +2. On Linux, check the PKCS#11 path: + + ```bash + ls -l /run/step-agent/step-agent-pkcs11.sock + echo $P11_KIT_SERVER_ADDRESS + pkcs11-tool --module /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-client.so --list-slots + ``` + + If the variable is empty, set it for the session that starts the browser: + + ```bash + export P11_KIT_SERVER_ADDRESS=unix:path=/run/step-agent/step-agent-pkcs11.sock + ``` + + The [agent troubleshooting page](../../platform/troubleshooting-agent.mdx#pkcs11-not-working-linuxmacos) covers p11-kit installation and a failed `modutil` add. +3. Run the doctor; the **Enumerate PKCS#11 objects** row must pass on Linux and macOS: + + ```bash + sudo step-agent doctor + ``` + +## Check + +The browser's certificate list shows the certificate issued by your authority's intermediate, and the app connects. + +## Go back to + +[Verify](../../use-cases/web-apps.mdx#verify) in the web apps guide. diff --git a/troubleshooting/web-apps/certificate-expired.mdx b/troubleshooting/web-apps/certificate-expired.mdx new file mode 100644 index 00000000..15e4b6b1 --- /dev/null +++ b/troubleshooting/web-apps/certificate-expired.mdx @@ -0,0 +1,34 @@ +--- +updated_at: September 13, 2026 +title: The app rejects the certificate as expired or invalid +html_title: "Web apps: the app rejects the certificate as expired or invalid" +description: The certificate expired while the device was offline, or the device clock is wrong, so the browser presents a certificate the app will not accept. +kind: troubleshooting +--- + +The browser presents the certificate and the app or proxy returns a TLS error about an expired or invalid client certificate; the doctor's **Verify issued certificates** row fails. + +## Cause + +The certificate is outside its validity period from the app's point of view. +Either it expired because the agent was offline past the renewal window, or the device's clock is wrong so a valid certificate looks not yet valid or already expired. + +## Fix + +1. Check the device clock with `date` and correct it; restart the agent if it was wrong. +2. Force renewal of the device's certificates: + + ```bash + sudo step-agent doctor --renew + ``` + + Renewal briefly interrupts anything using those certificates. +3. If renewal fails, check the agent logs for the error and confirm the device can reach `<>.ca.smallstep.com`. + +## Check + +The doctor's **Verify issued certificates** row passes, the browser's certificate list shows the new validity dates, and the app connects. + +## Go back to + +[Roll out and operate](../../use-cases/web-apps.mdx#operate) in the web apps guide. diff --git a/troubleshooting/web-apps/certificate-selection-dialog.mdx b/troubleshooting/web-apps/certificate-selection-dialog.mdx new file mode 100644 index 00000000..72943681 --- /dev/null +++ b/troubleshooting/web-apps/certificate-selection-dialog.mdx @@ -0,0 +1,38 @@ +--- +updated_at: September 13, 2026 +title: The browser shows a certificate selection dialog +html_title: "Web apps: the browser shows a certificate selection dialog" +description: The auto-select policy is missing, does not cover this URL, or names an issuer that does not match the certificate. +kind: troubleshooting +--- + +Opening the app prompts the person to choose a certificate instead of connecting silently. +Choosing the Smallstep certificate works. + +## Cause + +The browser has the certificate but no rule that tells it to present it for this origin. +One of: + +- The `AutoSelectCertificateForUrls` policy (Chrome, Edge) or the Firefox preferences are not applied on this device. +- The policy's `pattern` does not match the URL; a pattern like `[.*]xample.com` does not match `example.com`. +- The policy's `ISSUER.CN` is not the exact common name of the authority's intermediate CA. +- Several `AutoSelectCertificateForUrls` policies were deployed. + Chrome does not merge them; only one applies. + +## Fix + +1. Open `chrome://policy` (or `edge://policy`) on the device and check that the policy is listed with the expected value. + If it is missing, see [`chrome://policy` does not list `AutoSelectCertificateForUrls`](./policy-not-applied.mdx). +2. Compare the `pattern` with the URL the person opened, following the [Chrome Enterprise URL pattern rules](https://chromeenterprise.google/policies/url-patterns/). +3. Compare `ISSUER.CN` with the intermediate CA name on the authority's page under **Authorities**. +4. Put every protected URL into one policy and remove the others. +5. Restart the browser. + +## Check + +Opening the app connects with no dialog. + +## Go back to + +[Deliver to devices](../../use-cases/web-apps.mdx#deliver) in the web apps guide. diff --git a/troubleshooting/web-apps/policy-not-applied.mdx b/troubleshooting/web-apps/policy-not-applied.mdx new file mode 100644 index 00000000..56241dd6 --- /dev/null +++ b/troubleshooting/web-apps/policy-not-applied.mdx @@ -0,0 +1,33 @@ +--- +updated_at: September 13, 2026 +title: "`chrome://policy` does not list `AutoSelectCertificateForUrls`" +html_title: "Web apps: chrome://policy does not list the auto-select policy" +description: The policy was written to the wrong registry key or file, or the browser has not been restarted since. +kind: troubleshooting +--- + +`chrome://policy` (or `edge://policy`) does not show `AutoSelectCertificateForUrls`, and the app prompts for a certificate. + +## Cause + +The policy is not where the browser reads it, or the browser has not restarted since it was written: + +- Windows: the string value is under the wrong key. + Chrome reads `HKEY_LOCAL_MACHINE\Software\Policies\Google\Chrome`; Edge reads `HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\Edge`; a missing `Google\Chrome` or `Edge` key was not created. +- Linux: the JSON file is not under `/etc/opt/chrome/policies/managed/`, or is not valid JSON. +- macOS: the configuration profile is not scoped to this device, or the preference domain is not `com.google.Chrome`. +- ChromeOS: the device has not synced since the Admin console change. + +## Fix + +1. Put the policy in the location for the browser and operating system as the guide's Deliver step shows. +2. Restart the browser. +3. Reload `chrome://policy` and choose **Reload policies** if it is offered. + +## Check + +`chrome://policy` lists `AutoSelectCertificateForUrls` with your pattern and issuer, with no error next to it. + +## Go back to + +[Deliver to devices](../../use-cases/web-apps.mdx#deliver) in the web apps guide. diff --git a/troubleshooting/wifi/acme-cert-not-in-keychain.mdx b/troubleshooting/wifi/acme-cert-not-in-keychain.mdx new file mode 100644 index 00000000..aef32115 --- /dev/null +++ b/troubleshooting/wifi/acme-cert-not-in-keychain.mdx @@ -0,0 +1,31 @@ +--- +updated_at: September 13, 2026 +title: The ACME certificate does not appear in Keychain Access +html_title: "Wi-Fi: the ACME device attestation certificate does not appear in Keychain Access" +description: Certificates issued through ACME device attestation on macOS are managed by the MDM client and are not shown in Keychain Access. +kind: troubleshooting +--- + +The Jamf profile with the ACME certificate payload installed, but Keychain Access shows no certificate from your device authority. + +## Cause + +This is how managed device attestation works on macOS. +The certificate is held by the MDM client, not the login or system keychain, so Keychain Access does not list it. + +## Fix + +Nothing to fix. +Inspect the certificate with the MDM client instead: + +```bash +sudo /usr/libexec/mdmclient QueryCertificates +``` + +## Check + +The profile appears under **Profiles** in System Settings with an ACME certificate whose subject common name is the device serial number and whose **Hardware Bound** property says **Yes**. + +## Go back to + +[Deliver to devices, Jamf Pro](../../use-cases/wifi.mdx#deliver) in the Wi-Fi guide. diff --git a/troubleshooting/wifi/cannot-connect-agent-device.mdx b/troubleshooting/wifi/cannot-connect-agent-device.mdx new file mode 100644 index 00000000..b2d823d2 --- /dev/null +++ b/troubleshooting/wifi/cannot-connect-agent-device.mdx @@ -0,0 +1,37 @@ +--- +updated_at: September 13, 2026 +title: "`I can't connect to Wi-Fi` on an agent-managed device" +html_title: "Wi-Fi: an agent-managed device cannot connect" +description: Where to look when a device with the agent cannot join the network, in the order that rules out the most causes first. +kind: troubleshooting +--- + +A device that runs the agent cannot join the network, or joined once and cannot rejoin. + +## Cause + +One of three things, in order of likelihood: the device is not in a healthy state in the Console, the device has no valid certificate for the network, or the certificate exists but the network profile on the device does not use it. + +## Fix + +1. Find the device under [Devices](https://smallstep.com/app/?next=/devices) and confirm it shows a green status, is awake, and is online. +2. Open the device's **Certificates** tab and look for the Wi-Fi credential's certificate. + - **No certificate**: the credential may not be assigned to this device; check the credential's assignment policy and the device's **Resources** tab. + Then run the doctor on the device. + - **A certificate exists**: the problem is between the agent and the network profile. + Run the doctor; a failing **Verify issued certificates** row means the certificate expired while the device was offline or the clock is wrong. +3. Run the doctor: + + ```bash + sudo step-agent doctor + ``` + + To force renewal of every certificate, add `--renew`. + +## Check + +Every doctor row reports `PASS`, the **Certificates** tab shows a current certificate, and selecting the SSID connects without a password. + +## Go back to + +[Verify](../../use-cases/wifi.mdx#verify) in the Wi-Fi guide; for doctor rows that fail, the [agent troubleshooting page](../../platform/troubleshooting-agent.mdx#understanding-doctor-output). diff --git a/troubleshooting/wifi/intune-intermediate-store.mdx b/troubleshooting/wifi/intune-intermediate-store.mdx new file mode 100644 index 00000000..a60529d0 --- /dev/null +++ b/troubleshooting/wifi/intune-intermediate-store.mdx @@ -0,0 +1,33 @@ +--- +updated_at: September 13, 2026 +title: SCEP enrollment errors after the Intune trusted certificate profiles install +html_title: "Wi-Fi: SCEP enrollment errors on Windows after the Intune profiles install" +description: Windows SCEP enrollment fails when the intermediate CA was placed in the intermediate certificate store instead of the root store, or when the SCEP profile points at the root instead of the intermediate. +kind: troubleshooting +--- + +The two trusted certificate profiles and the SCEP profile deployed, and Windows devices log SCEP enrollment errors in Event Viewer under **Applications and Services Logs → Microsoft → Windows → DeviceManagement-Enterprise-Diagnostics-Provider**. + +## Cause + +One of two profile settings is wrong: + +- The intermediate CA's trusted certificate profile has **Destination store** set to the intermediate store. + It must go in **Computer certificate store - root**. +- The SCEP certificate profile's **Root Certificate** is the root CA. + It must be the intermediate CA, because the enrolling device's SCEP client checks the fingerprint of the intermediate, not the root. + +## Fix + +1. Edit the intermediate CA's trusted certificate profile and set the destination store to **Computer certificate store - root**. +2. Edit the SCEP certificate profile and select the intermediate CA as its root certificate. +3. Sync the device from **Settings → Accounts → Access work or school → Info → Sync**. + +## Check + +Intune's reports show the device enrolled without errors, and the certificate appears in the Windows certificate store (`certlm.msc` for the computer store). +Microsoft's [SCEP troubleshooting documentation](https://learn.microsoft.com/en-us/troubleshoot/mem/intune/certificates/troubleshoot-scep-certificate-profiles) covers the remaining error codes. + +## Go back to + +[Deliver to devices, Intune](../../use-cases/wifi.mdx#deliver) in the Wi-Fi guide. diff --git a/troubleshooting/wifi/mobileconfig-installed-by-hand.mdx b/troubleshooting/wifi/mobileconfig-installed-by-hand.mdx new file mode 100644 index 00000000..7ba8c6fc --- /dev/null +++ b/troubleshooting/wifi/mobileconfig-installed-by-hand.mdx @@ -0,0 +1,28 @@ +--- +updated_at: September 13, 2026 +title: The `.mobileconfig` installed by hand does not connect +html_title: "Wi-Fi: the mobileconfig installed by hand does not connect" +description: The Jamf profile template only works when Jamf delivers it, because the SCEP challenge is fetched by Jamf through its webhook. +kind: troubleshooting +--- + +You downloaded the configuration profile template from the Wi-Fi network's page, double-clicked it on a Mac to test, and the device does not get a certificate or join the network. + +## Cause + +The template is meant to be uploaded to Jamf. +Jamf fills in the single-use SCEP challenge through the `SCEPChallenge` webhook when it delivers the profile; a profile installed by hand has no challenge, so the SCEP payload fails. + +## Fix + +1. Upload the `.mobileconfig` under **Configuration Profiles** in Jamf. +2. Scope it to a test device that already has a basic Jamf MDM profile. +3. Let Jamf deliver it. + +## Check + +The device shows the profile under **Profiles** in System Settings, a certificate is issued, and the network joins without a password. + +## Go back to + +[Deliver to devices, Jamf Pro](../../use-cases/wifi.mdx#deliver) in the Wi-Fi guide. diff --git a/troubleshooting/wifi/new-site-no-answer.mdx b/troubleshooting/wifi/new-site-no-answer.mdx new file mode 100644 index 00000000..96df4074 --- /dev/null +++ b/troubleshooting/wifi/new-site-no-answer.mdx @@ -0,0 +1,39 @@ +--- +updated_at: September 13, 2026 +title: Access points at a new site get no answer from Smallstep RADIUS +html_title: "Wi-Fi: access points at a new site get no answer" +description: Requests from a public IP that is not registered on the RADIUS server are not attributed to your team and go unanswered. +kind: troubleshooting +--- + +Devices at a new office, or at an office whose internet provider changed, cannot join, and the access points there report RADIUS timeouts. +Other sites work. + +## Cause + +Smallstep RADIUS attributes incoming requests to your team by the source IP address. +The new site's public IP is not in the server's `nasIPs` list, so its requests are not attributed to you. + +## Fix + +Add the site's public IP address to the RADIUS server: + +```bash +curl -sH @api_headers --request PUT \ + --url "https://gateway.smallstep.com/api/managed-radius/<>" \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data '{"name": "Corporate RADIUS", "nasIPs": ["<>", "203.0.113.20"], "clientCA": "..."}' | jq +``` + +Send the full object; `PUT` replaces it. +Each IP must be unique across Smallstep customers. + +## Check + +Access points at the new site receive Access-Accept, and the activity appears on the network's page under **Protect → Wi-Fi**. + +## Go back to + +[Create the RADIUS server](../../use-cases/wifi.mdx#create) in the Wi-Fi guide. diff --git a/troubleshooting/wifi/no-radius-traffic.mdx b/troubleshooting/wifi/no-radius-traffic.mdx new file mode 100644 index 00000000..05e3f10b --- /dev/null +++ b/troubleshooting/wifi/no-radius-traffic.mdx @@ -0,0 +1,36 @@ +--- +updated_at: September 13, 2026 +title: No RADIUS traffic reaches Smallstep when a device tries to join +html_title: "Wi-Fi: no RADIUS traffic reaches Smallstep" +description: The RADIUS server sees nothing when the device tries to join, so the problem is on the device or the access point, not the server. +kind: troubleshooting +--- + +The device shows the network, tries to join, and fails, and the RADIUS server records no request at all. + +## Cause + +The device never completed association, so the access point never relayed a handshake. +Two device-side causes account for most cases: + +- **Certificate selection failed.** + The profile's issuer pattern (ChromeOS), issuer hash (Windows), or identity certificate (macOS) does not match the certificate on the device, so the supplicant has nothing to present. +- **No certificate was issued.** + The device is not in the credential's assignment policy, the agent has not synced, or the MDM's certificate payload has not completed. + +A third cause is on the access point: the SSID is not configured for WPA Enterprise with the RADIUS server, or the access point sends from an IP that is not registered. + +## Fix + +1. Open the device in the Console and check its **Certificates** tab for a valid certificate from the credential. + If there is none, check the credential's assignment policy and, for MDM delivery, the MDM's payload status for the device. +2. If a certificate exists, recheck the selector in the profile: the issuer pattern must equal the intermediate CA's common name exactly, with locality, organization, organizational unit, and subject pattern left empty (ChromeOS); the `IssuerHash` must be the intermediate's SHA-1 fingerprint (Windows). +3. On the access point, confirm the SSID uses WPA2 or WPA3 Enterprise and the RADIUS server IP, port, and secret from the guide. + +## Check + +A join attempt now produces a request on the RADIUS server: either Access-Accept, or a reject with a reason you can act on. + +## Go back to + +[Deliver to devices](../../use-cases/wifi.mdx#deliver) in the Wi-Fi guide; for the access point, [Configure your access points](../../use-cases/wifi.mdx#configure-external). diff --git a/troubleshooting/wifi/scep-payload-failing.mdx b/troubleshooting/wifi/scep-payload-failing.mdx new file mode 100644 index 00000000..29e9f537 --- /dev/null +++ b/troubleshooting/wifi/scep-payload-failing.mdx @@ -0,0 +1,28 @@ +--- +updated_at: September 13, 2026 +title: "`The SCEP Certificate payload is failing for every device`" +html_title: "Wi-Fi: the SCEP certificate payload is failing for every device" +description: Every MDM-managed device fails the SCEP payload because the SCEP URL or the SCEP credentials differ between the MDM and Smallstep. +kind: troubleshooting +--- + +The MDM reports the SCEP certificate payload as failed on every device, not just one. + +## Cause + +The MDM and Smallstep disagree on the SCEP settings: the SCEP URL in the MDM does not match the one Smallstep shows for the network, or the SCEP username and password differ, or the credentials expired. + +## Fix + +1. Open the Wi-Fi network under **Protect → Wi-Fi** and compare the SCEP URL with the one in your MDM's certificate profile. +2. Compare the SCEP username and password in both places; if they expired, update them in both. +3. Retry the SCEP payload from the MDM. + +## Check + +The MDM's payload report shows success for a test device, and the device's **Certificates** tab in the Console lists the certificate. +A single device that still fails is a device problem, not a settings problem; continue with the [agent troubleshooting page](../../platform/troubleshooting-agent.mdx). + +## Go back to + +[Deliver to devices](../../use-cases/wifi.mdx#deliver) in the Wi-Fi guide. diff --git a/troubleshooting/wifi/unknown-ca.mdx b/troubleshooting/wifi/unknown-ca.mdx new file mode 100644 index 00000000..1e605fc8 --- /dev/null +++ b/troubleshooting/wifi/unknown-ca.mdx @@ -0,0 +1,35 @@ +--- +updated_at: September 13, 2026 +title: "`unknown CA` or `unable to get local issuer certificate` on the RADIUS server" +html_title: "Wi-Fi: unknown CA or unable to get local issuer certificate" +description: The RADIUS server rejects the EAP-TLS handshake because it cannot build a chain from the device's certificate to a root it trusts. +kind: troubleshooting +--- + +The device associates, RADIUS traffic reaches the server, and the server logs `unknown CA` or `unable to get local issuer certificate` and answers Access-Reject. + +## Cause + +The RADIUS server's client trust store is missing the root, or the intermediate, of the authority that issued the device's certificate. +On Smallstep RADIUS this is the `clientCA` you registered; on your own server it is the trusted client CA bundle. +ChromeOS devices send the intermediate in the handshake, so the root alone is enough for them; other supplicants may send only the leaf. + +## Fix + +1. Download the root certificate of the authority named in the credential's `authorityID`. +2. Smallstep RADIUS: update the server's `clientCA` with `PUT /managed-radius/{managedRadiusID}` so it holds that root. + Your own server: add the root (and the intermediate if your supplicants do not send it) to the trusted client CA bundle and reload the server. +3. Have the device reconnect. + +## Check + +The next authentication answers Access-Accept, and the activity appears on the network's page under **Protect → Wi-Fi**. +To confirm which authority signed the device's certificate, inspect it on the device: + +```bash +step certificate inspect --bundle /path/to/cert.crt +``` + +## Go back to + +[Create the RADIUS server](../../use-cases/wifi.mdx#create) in the Wi-Fi guide. diff --git a/troubleshooting/wifi/windows-profile-needs-restart.mdx b/troubleshooting/wifi/windows-profile-needs-restart.mdx new file mode 100644 index 00000000..dbfec842 --- /dev/null +++ b/troubleshooting/wifi/windows-profile-needs-restart.mdx @@ -0,0 +1,28 @@ +--- +updated_at: September 13, 2026 +title: Windows does not pick up the profile until it restarts +html_title: "Wi-Fi: Windows does not pick up the Intune profile until it restarts" +description: A Windows device that has been online or asleep for a long time can lose its push-notification channel and miss new profiles until it syncs or restarts. +kind: troubleshooting +--- + +Profiles assigned in Intune reach new devices, but a device that has been running for a while does not receive them and shows no error. + +## Cause + +The device has been disconnected from Windows push notifications (WNS), which happens after a long uptime or a long sleep, so it receives no notification that new profiles are waiting. + +## Fix + +1. On the device, go to **Settings → Accounts → Access work or school**, expand the signed-in account, choose **Info**, and select **Sync**. +2. If the profiles still do not arrive, restart Windows to re-establish the channel. + +Give it a minute after the sync for certificates to appear in the trust store. + +## Check + +The **Info** screen shows a recent successful sync, and the expected certificates are in the right stores: trusted roots and intermediates in the computer's root store, the client certificate in the personal store of the account the profile targets. + +## Go back to + +[Deliver to devices, Intune](../../use-cases/wifi.mdx#deliver) in the Wi-Fi guide. diff --git a/troubleshooting/wifi/wrong-issuer.mdx b/troubleshooting/wifi/wrong-issuer.mdx new file mode 100644 index 00000000..b2824b29 --- /dev/null +++ b/troubleshooting/wifi/wrong-issuer.mdx @@ -0,0 +1,27 @@ +--- +updated_at: September 13, 2026 +title: The device presents a certificate with the wrong issuer +html_title: "Wi-Fi: the device presents a certificate with the wrong issuer" +description: The RADIUS server sees a certificate from an authority it does not trust because the device kept a stale certificate after re-enrollment. +kind: troubleshooting +--- + +The RADIUS server logs a certificate whose issuer is not the authority you configured, and rejects it. + +## Cause + +The device is presenting a certificate issued before you changed its enrollment. +A ChromeOS device keeps presenting a stale certificate until it restarts, even after re-enrollment completes. + +## Fix + +1. Confirm the device's enrollment completed with the authority you chose; for ChromeOS, that [ChromeOS device identity certificates](../../tutorials/chromeos-device-identity-certificates.mdx) finished. +2. Restart the device so it stops presenting the old certificate. + +## Check + +The next authentication shows the expected issuer on the RADIUS server and answers Access-Accept. + +## Go back to + +[Deliver to devices](../../use-cases/wifi.mdx#deliver) in the Wi-Fi guide. diff --git a/troubleshooting/wired/devices-without-certificates.mdx b/troubleshooting/wired/devices-without-certificates.mdx new file mode 100644 index 00000000..69a4ce4a --- /dev/null +++ b/troubleshooting/wired/devices-without-certificates.mdx @@ -0,0 +1,29 @@ +--- +updated_at: September 13, 2026 +title: Printers and other devices lose connectivity after port authentication is enabled +html_title: "Wired: devices without certificates lose connectivity" +description: Devices that cannot do 802.1X need a guest VLAN, an unauthenticated VLAN, or separate ports, because Smallstep RADIUS does not do MAC Authentication Bypass. +kind: troubleshooting +--- + +After you enable port authentication on the access ports, printers, phones, or other devices without certificates drop off the network. + +## Cause + +Those devices cannot do 802.1X, so the switch never gets an Access-Accept for them. +MAC Authentication Bypass (MAB), which many switches offer for such devices, is not supported by Smallstep RADIUS: it authenticates certificates only. + +## Fix + +Give those devices a path that does not depend on RADIUS: + +- A guest VLAN or a dedicated unauthenticated VLAN that the switch assigns when 802.1X fails or times out, or +- Ports without port authentication reserved for them. + +## Check + +The affected devices are reachable on their VLAN, and devices with certificates still authorize on the 802.1X ports. + +## Go back to + +[Configure your switches](../../use-cases/wired.mdx#configure-external) in the wired guide. diff --git a/troubleshooting/wired/lan-profile-wrong-issuer-hash.mdx b/troubleshooting/wired/lan-profile-wrong-issuer-hash.mdx new file mode 100644 index 00000000..94ffc2ce --- /dev/null +++ b/troubleshooting/wired/lan-profile-wrong-issuer-hash.mdx @@ -0,0 +1,37 @@ +--- +updated_at: September 13, 2026 +title: The Intune wired profile installs but Windows does not present the certificate +html_title: "Wired: Windows does not present the certificate from the LAN profile" +description: The LAN profile selects the certificate by issuer fingerprint and store; a root fingerprint or the wrong authMode leaves Windows with nothing to present. +kind: troubleshooting +--- + +The custom OMA-URI profile shows as applied in Intune, the device has a certificate from the agent, and 802.1X fails on the port with no certificate presented. + +## Cause + +The LAN profile XML selects the certificate by two things, and one of them does not match: + +- `IssuerHash` must be the SHA-1 fingerprint of the **intermediate** (issuing) CA of the authority, not the root. +- `authMode` must match the store the agent put the certificate in: `machine` for a credential with `HARDWARE_ATTESTED` key protection (the computer store), `user` for every other protection level (the user store). + +A `TrustedRootCA` that is not the RADIUS server CA's fingerprint fails later, at server validation, rather than at certificate selection. + +## Fix + +1. Recompute the issuing CA fingerprint from the authority's intermediate certificate and remove the colons: + + ```bash + openssl x509 -in issuing_ca.crt -noout -fingerprint -sha1 + ``` + +2. Check the credential's `key.protection` and set `authMode` accordingly. +3. Update the OMA-URI value in Intune with the corrected [LAN profile XML](../../reference/wlan-profile-xml.mdx#lan) and sync the device. + +## Check + +Event Viewer's **Wired-AutoConfig** log shows the EAP-TLS exchange completing, and the switch shows the session as authorized. + +## Go back to + +[Deliver to devices, Intune](../../use-cases/wired.mdx#deliver) in the wired guide. diff --git a/troubleshooting/wired/port-never-authorizes.mdx b/troubleshooting/wired/port-never-authorizes.mdx new file mode 100644 index 00000000..0c0aaa66 --- /dev/null +++ b/troubleshooting/wired/port-never-authorizes.mdx @@ -0,0 +1,31 @@ +--- +updated_at: September 13, 2026 +title: The port never authorizes and `show authentication sessions` shows no session +html_title: "Wired: the port never authorizes" +description: The switch shows no 802.1X session for the port, so either the port is not doing 802.1X or the device is not answering the EAP request. +kind: troubleshooting +--- + +A device is plugged into an 802.1X-enabled port, gets no address on the corporate VLAN, and `show authentication sessions` (or `show dot1x interface ` on a Cisco-style command line) shows no session or an unauthorized one. + +## Cause + +Either the switch is not running 802.1X on that port, or the device's supplicant is not answering. +Check the switch first: 802.1X must be enabled globally and the port must have port authentication (`dot1x port-control auto` on Cisco IOS-style switches) and the RADIUS server registered. +If the switch is right, the device has no 802.1X profile for the wired interface yet, or its supplicant is not running. + +## Fix + +1. On the switch, confirm global 802.1X, port authentication on this port, and the RADIUS server entry with the IP, port, and secret from the guide. +2. On the device, confirm the wired network profile exists. + The agent creates it on its next sync for devices in the credential's assignment policy; check the device's **Resources** tab in the Console. +3. On Windows, confirm the **Wired AutoConfig** service is running; see [Windows does not attempt 802.1X on the wired interface](./windows-wired-autoconfig-stopped.mdx). +4. Unplug and plug the cable, or bounce the port, to start a new authentication. + +## Check + +`show authentication sessions` shows the device with an **Authorized** state, and the activity appears on the resource's page under **Protect → Ethernet**. + +## Go back to + +[Configure your switches](../../use-cases/wired.mdx#configure-external) in the wired guide. diff --git a/troubleshooting/wired/windows-wired-autoconfig-stopped.mdx b/troubleshooting/wired/windows-wired-autoconfig-stopped.mdx new file mode 100644 index 00000000..1eff0c8a --- /dev/null +++ b/troubleshooting/wired/windows-wired-autoconfig-stopped.mdx @@ -0,0 +1,34 @@ +--- +updated_at: September 13, 2026 +title: Windows does not attempt 802.1X on the wired interface +html_title: "Wired: Windows does not attempt 802.1X on the wired interface" +description: Windows only does 802.1X on Ethernet while the Wired AutoConfig service is running; it is stopped by default on many builds. +kind: troubleshooting +--- + +The wired profile is on the device, the certificate is issued, and the switch sees no EAP response from the Windows device. +Event Viewer shows nothing under **Applications and Services Logs → Microsoft → Windows → Wired-AutoConfig**. + +## Cause + +The **Wired AutoConfig** service (`dot3svc`) is not running. +Windows performs 802.1X on Ethernet only through this service, and it is stopped by default on many installations. + +## Fix + +Start the service and set it to start automatically, from an elevated PowerShell: + +```powershell +Set-Service -Name dot3svc -StartupType Automatic +Start-Service -Name dot3svc +``` + +Then unplug and plug the cable. + +## Check + +Event Viewer's **Wired-AutoConfig** log shows an authentication attempt and success, and the switch shows the session as authorized. + +## Go back to + +[Deliver to devices](../../use-cases/wired.mdx#deliver) in the wired guide. diff --git a/use-cases/sso-device-factor.mdx b/use-cases/sso-device-factor.mdx index 998b2c1c..d3d08c39 100644 --- a/use-cases/sso-device-factor.mdx +++ b/use-cases/sso-device-factor.mdx @@ -1,8 +1,215 @@ --- updated_at: September 13, 2026 title: SSO device factor -html_title: SSO device factor -description: Require a device certificate at sign-in with the Smallstep device factor. +html_title: SSO device factor (require an enrolled device at sign-in) +description: Make your identity provider require a Smallstep device certificate as a sign-in factor. Create the device factor and its client, add Smallstep to Okta or Entra ID, deliver the certificate, and verify a sign-in. +kind: guide +entitlement: team +skill: sso-device-factor --- -This guide will replace the page on howto.smallstep.com and follow the same nine steps as every use-case guide. It is being validated against the product before it is published. +Require an enrolled device at sign-in. +About 30 minutes in Smallstep, plus the change in your identity provider. + + + Being validated +
+ This guide is written from the API specification and the previous how-to article and has not yet been walked against the product end to end. + It is hidden from the sidebar until it has been. +
+
+ +## What you get {#what-you-get} + +When a person signs in to an app through your identity provider, the identity provider sends them to Smallstep as a second factor. +Smallstep's device factor asks the browser for the device's certificate over mutual TLS, checks it against your authority, and returns the person to the identity provider with a signed answer. +Only a person on an enrolled device with a current certificate completes the sign-in; the phishing-resistant part is the key that never left the device's hardware. + +```mermaid +sequenceDiagram + participant P as Person on an enrolled device + participant I as Identity provider (Okta, Entra ID) + participant S as Smallstep device factor + P->>I: Sign in to an app + I->>S: Redirect: verify the device (OpenID Connect) + S->>P: Request the device certificate (mutual TLS) + P->>S: Certificate from the device's hardware key + S->>I: Signed result + I->>P: Session issued +``` + +Three objects do the work: + +- The **device factor**: Smallstep's hosted OpenID Connect identity provider for your team, at `https://<>.id.smallstep.com`, configured with the trust roots of the authority that issues your device certificates. +- A **client** of the device factor: the client ID and secret your identity provider uses, with the redirect URI it needs. +- A **credential** and a **web app** resource, as in the [web apps guide](./web-apps.mdx), so the browser presents the certificate to the device factor's address. + +The verifier is your identity provider's policy; the device factor is the part that checks the certificate. +[Why device identity](../learn/why-device-identity.mdx) explains why this is stronger than a second password. + +## Before you start {#before-you-start} + +1. **Devices enrolled.** + At least one device in your [inventory](../platform/enrollment-guide.mdx), approved, with a user bound to it, and running the [agent](../platform/smallstep-agent.mdx). +2. **Users synced from the identity provider**, so the device's bound user matches the sign-in identity: [Okta](../tutorials/sync-okta-users-to-smallstep.mdx), [Entra ID](../tutorials/sync-entra-id-users-to-smallstep.mdx), or [Google Workspace](../tutorials/sync-google-workspace-users-to-smallstep.mdx). +3. **An authority** that issues the browser certificates: note its ID, <>, under [Authorities](https://smallstep.com/app/?next=/cm/authorities) and download its root certificate as `accounts_root.crt`. + The certificate's first email SAN must be the person's sign-in username at the identity provider, so use an authority whose credentials carry `smallstep:identity`. +4. **An API token** from [Settings → API tokens](https://smallstep.com/app/?next=/settings/api/tokens/add), stored in `api_headers` as the [Wi-Fi guide](./wifi.mdx#before-you-start) shows. + There is no form for the device factor in the Console today; **Protect → SSO** shows a request form for teams without the entitlement, and the objects are created with the API or Terraform. +5. **Admin access** to the identity provider, and its tenant address: <> for Okta. + +## Create it in Smallstep {#create} + +### 1. Create the device factor + +The device factor is one per team. +`trustRoots` is the CA bundle it verifies browser certificates against. +Use only the root if any intermediate under it should be trusted; use only the intermediate if only its leaves should be. +A bundle of root and intermediate trusts every intermediate under the root, whether or not it is in the bundle. + +```bash +jq -n --rawfile roots accounts_root.crt '{trustRoots: $roots}' \ +| curl -sH @api_headers --request PUT \ + --url https://gateway.smallstep.com/api/sso \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data @- | jq +``` + +The response holds the values your identity provider needs: `issuer` (<>), `authorizeEndpoint`, `tokenEndpoint`, and `jwksEndpoint`. +Confirm the discovery document is served at `<>/.well-known/openid-configuration`. + +### 2. Create the client + +One client per identity provider. +The redirect URI is where the identity provider expects to receive the answer: + +| Identity provider | Redirect URI | +|---|---| +| Okta | `https://<>/oauth2/v1/authorize/callback` | +| Entra ID (external authentication method) | `https://login.microsoftonline.com/common/federation/externalauthprovider` | + +```bash +curl -sH @api_headers --request POST \ + --url https://gateway.smallstep.com/api/sso/clients \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data '{"redirectURI": "https://<>/oauth2/v1/authorize/callback"}' | jq +``` + +Save `id` as <> and `secret` as <>. +The secret is returned only at creation. +`lifecycleFailureURI` is optional: where to send the person when their device's lifecycle is not active. + +### 3. Create the browser credential and web app + +The device factor asks the browser for a certificate at <>, so the device needs a browser certificate and a rule to present it there. +Follow the [web apps guide's step 3](./web-apps.mdx#create) with `matchAddresses` set to `["<>"]`, a credential whose first SAN is `smallstep:identity`, and, because the key is hardware bound, a lifetime of up to a year. + +## Configure your identity provider {#configure-external} + + + + Add Smallstep as an identity provider used only as a factor, make it an authenticator, and attach it to a policy. + + 1. In the Okta admin console, go to **Security → Identity Providers** and choose **Add identity provider → OpenID Connect IdP**. + If **OpenID Connect IdP** is not in the list, ask Okta to enable the `GENERIC_OIDC_IDP` feature for your tenant. + 2. Configure it: **Name** `Smallstep`; **IdP Usage** **Factor only**; **Client ID** <>; **Client Secret** <>; **Issuer** <>; and the **Authorization endpoint**, **Token endpoint**, and **JWKS endpoint** from step 3. + 3. Go to **Security → Authenticators**, choose **Add authenticator → IdP Authenticator**, and select the Smallstep identity provider. + 4. Go to **Security → Authentication Policies** and add the Smallstep authenticator to the policy for a pilot group or app. + On a fresh tenant the default policy is "Any two factors", which makes Smallstep available as an optional second factor. + + + Entra ID takes Smallstep as an **external authentication method**, Microsoft's name for an OpenID Connect second factor. + The client for it uses the redirect URI in step 3's table. + In the Entra admin center, the method is registered with <> and <>, then enabled in a Conditional Access or authentication-methods policy for a pilot group. + The exact Entra menu steps are not documented here yet; Microsoft's external authentication method documentation has them. + + + +Google Workspace and other identity providers that support an OpenID Connect external factor take the same issuer, client ID, secret, and endpoints; their menu steps are not documented here yet. + +## Deliver to devices {#deliver} + +The same as the [web apps guide](./web-apps.mdx#deliver): the agent installs the browser certificate, and the auto-select policy for <> keeps the certificate picker from appearing. + + + + Nothing to do for the certificate. + Set the auto-select policy for <> per the web apps guide; without it, the person sees a certificate picker at the device check and must choose the Smallstep certificate. + + + Deliver the browser policy profiles from the web apps guide's [Jamf Pro and Google Workspace tabs](./web-apps.mdx#deliver) with <> as the URL pattern. + + + +## Verify {#verify} + +1. On the test device, confirm the browser has a current certificate from the authority in step 2 whose first email SAN is the person's sign-in username. +2. Sign in to an app covered by the pilot policy. + When the identity provider asks for a second factor, choose Smallstep. + The device check completes without a prompt (or with one certificate picker if the policy is not set) and the app opens. +3. Sign in from a device that is not enrolled. + The Smallstep factor is not satisfied and the identity provider does not issue the session. + +## Roll out and operate {#operate} + +- **Widen the identity-provider policy** from the pilot group to everyone who should be device-bound; widen the credential's assignment policy in step. +- **Renewals** are automatic on agent-managed devices. +- **Revocation.** + Revoke a device's certificate with [`/certificates/{serialNumber}/revoke`](https://gateway.smallstep.com/v2025-01-01/operations/RevokeCertificate); the next device check fails. + Set `lifecycleFailureURI` on the client to land the person on a page that says why. +- **Rotate the client secret** by creating a new client, updating the identity provider, then deleting the old client with `DELETE /sso/clients/{idpClientID}`. +- **Several authorities.** + To trust more than one CA, put their roots in one PEM bundle and `PUT /sso` again with the bundle as `trustRoots`. + +## Troubleshoot {#troubleshoot} + +- [`OpenID Connect IdP` is not in Okta's list of identity providers](../troubleshooting/sso-device-factor/okta-oidc-idp-missing.mdx) +- [The identity provider rejects the factor because the certificate's email does not match the username](../troubleshooting/sso-device-factor/email-san-mismatch.mdx) +- [A certificate selection dialog appears during the device check](../troubleshooting/sso-device-factor/certificate-picker-at-sign-in.mdx) +- [The person lands on the lifecycle failure page](../troubleshooting/sso-device-factor/lifecycle-failure.mdx) + +For the certificate itself, use the [web apps troubleshooting entries](./web-apps.mdx#troubleshoot). + +## Automate {#automate} + +### REST + +| Object | Create | Read | +|---|---|---| +| Device factor | `PUT /sso` | `GET /sso` | +| Client | `POST /sso/clients` | `GET /sso/clients/{idpClientID}` | +| Credential | `POST /credentials` | `GET /credential/{credentialID}` | +| Web app | `POST /protect/browser` | `GET /protect/browser/{browserID}` | + +### Terraform + +```hcl +resource "smallstep_identity_provider" "device_factor" { + trust_roots = file("${path.module}/accounts_root.crt") +} + +resource "smallstep_identity_provider_client" "okta" { + redirect_uri = "https://<>/oauth2/v1/authorize/callback" + store_secret = true + depends_on = [smallstep_identity_provider.device_factor] +} + +output "issuer" { + value = smallstep_identity_provider.device_factor.issuer +} + +output "client_id" { + value = smallstep_identity_provider_client.okta.id +} + +output "client_secret" { + value = smallstep_identity_provider_client.okta.secret + sensitive = true +} +``` + +The resources are [`smallstep_identity_provider`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/resources/identity_provider) and [`smallstep_identity_provider_client`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/resources/identity_provider_client); the credential and web app are the [web apps guide's](./web-apps.mdx#automate). diff --git a/use-cases/web-apps.mdx b/use-cases/web-apps.mdx new file mode 100644 index 00000000..e1980cf8 --- /dev/null +++ b/use-cases/web-apps.mdx @@ -0,0 +1,343 @@ +--- +updated_at: September 13, 2026 +title: Web apps (mTLS) +html_title: Web apps with browser certificates (mutual TLS) +description: Let only enrolled devices reach a web app. Create the credential and the web app resource, give your app the trust roots, deliver the certificate and the browser policy, and verify from an enrolled device. +kind: guide +entitlement: team +skill: web-apps +--- + +Protect a web app with device certificates in the browser. +About 15 minutes in Smallstep, plus the change on your app or proxy and a browser policy. + +## What you get {#what-you-get} + +When a person on an enrolled device opens the app, the browser presents a certificate issued to that device during the TLS handshake. +Your app, or the proxy in front of it, verifies the certificate against your authority and knows which device and which user is on the other end before a single request is served. +A device without a certificate, or with a revoked one, never gets past the handshake. + +```mermaid +graph LR + cred["Credential
(browser certificate)"] -->|credential ID| app["Web app resource
(URL patterns)"] + app --> device["Device
(agent installs the certificate)"] + device -->|"mutual TLS"| verifier["Your app or proxy
(trusts your authority)"] +``` + +Two objects do the work: + +- A **credential**: which authority signs, what identity the certificate carries (the bound user's email, the device serial), how the key is protected, and which devices get it. +- A **web app** resource: the URLs that require the certificate and the credential to present there. + +The verifier is yours: the app, the proxy, or the identity provider that checks the certificate. +Smallstep supplies the certificate and the trust roots. +[Certificates for people, devices, and workloads](../learn/certificates-for-people-devices-and-workloads.mdx) explains why a browser certificate carries both a device and a person. +If the app is behind your single sign-on rather than checking certificates itself, use the [SSO device factor](./sso-device-factor.mdx) guide instead. + +## Before you start {#before-you-start} + +1. **Devices enrolled.** + At least one device in your [inventory](../platform/enrollment-guide.mdx), approved, with a user bound to it, and running the [agent](../platform/smallstep-agent.mdx) on macOS, Windows, or Linux. + ChromeOS devices use the Smallstep extension instead; see [ChromeOS device identity certificates](../tutorials/chromeos-device-identity-certificates.mdx). +2. **An MDM connected**, only if you will deliver the browser policy with one; the agent needs none. +3. **An authority** to sign the browser certificates: note its ID, <>, under [Authorities](https://smallstep.com/app/?next=/cm/authorities), and download its root and intermediate certificates; your app needs them in step 4. +4. **An API token** from [Settings → API tokens](https://smallstep.com/app/?next=/settings/api/tokens/add), stored in `api_headers` as the [Wi-Fi guide](./wifi.mdx#before-you-start) shows. + The same objects can be created under **Protect → Browsers (mTLS)** in the Console. +5. **The URLs** that will require a certificate, <>, and admin access to the app or proxy that serves them. + +## Create it in Smallstep {#create} + +### 1. Create the credential + +The recipe for a browser certificate: the bound user's email as the common name and the first SAN, the device serial as a second SAN, client authentication only, a hardware-attested key, and assignment to high-assurance devices. +A longer lifetime is reasonable here because the key cannot leave the hardware. + +{/* vale Google.Spacing = NO */} + +```bash +curl -sH @api_headers --request POST \ + --url https://gateway.smallstep.com/api/credentials \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data @- <<'EOF' | jq +{ + "slug": "browser", + "certificate": { + "type": "X509", + "authorityID": "<>", + "duration": "168h0m0s", + "fields": { + "commonName": { + "deviceMetadata": "smallstep:identity" + }, + "sans": { + "deviceMetadata": ["smallstep:identity", "Device.Serial"] + }, + "extendedKeyUsage": ["clientAuth"] + } + }, + "key": { + "type": "ECDSA_P256", + "protection": "HARDWARE_ATTESTED" + }, + "managementMode": "agent", + "policy": { + "assurance": ["high"], + "operatingSystem": ["macOS", "Windows", "Linux"] + } +} +EOF +``` + +{/* vale Google.Spacing = YES */} + +The [Wi-Fi guide](./wifi.mdx#create) explains every field. +Shape the SANs for your verifier: an app that maps users by email wants `smallstep:identity` first; an app that maps devices wants `Device.Serial` or `Device.PermanentIdentifier`. +Save the `id` as <>. + +### 2. Create the web app + +```bash +curl -sH @api_headers --request POST \ + --url https://gateway.smallstep.com/api/protect/browser \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data @- <<'EOF' | jq +{ + "name": "Intranet", + "matchAddresses": ["<>"], + "credentials": ["<>"] +} +EOF +``` + +- `matchAddresses` lists the URLs the certificate applies to. +- `credentials` lists the credentials the browser presents there. + +The resource appears under [Protect → Browsers (mTLS)](https://smallstep.com/app/?next=/protect/browser). +Its ID is <>. + +## Configure your app {#configure-external} + +Your app or proxy must ask for a client certificate on <>, verify it against your authority, and read the identity from it. + + + + 1. Download the authority's **root certificate** (and its intermediate, if your server does not build chains itself) from the authority's page under **Authorities**. + 2. Configure the server to require a client certificate on the protected paths and to trust that root for client verification. + Every web server and proxy has a setting for this: `ssl_verify_client` and `ssl_client_certificate` in NGINX, `SSLVerifyClient` and `SSLCACertificateFile` in Apache, `client_auth` in Caddy, the mutual TLS listener on a load balancer. + 3. Map the certificate to a user or device in your app: the email in the first SAN, or the serial in the second, as you shaped the credential in step 3. + + [Trust roots and chains](../learn/trust-roots-and-chains.mdx) explains what to give the server and why the root alone is usually right. + + + +Steps for specific verifiers (Entra certificate-based authentication, Cloudflare Access, Auth0, Salesforce, Okta) are not written yet; each takes the same root certificate and maps the same SANs. +For an identity provider that should require the device certificate as a sign-in factor, use the [SSO device factor](./sso-device-factor.mdx) guide. + +## Deliver to devices {#deliver} + +The agent installs the certificate where the browser looks: the keychain on macOS, the user's personal certificate store on Windows, and the NSS database through PKCS#11 on Linux. +What is left is the browser's **auto-select policy**, so the person sees no certificate picker. +Chrome and Edge take the `AutoSelectCertificateForUrls` policy; Firefox takes two preferences; Safari selects automatically and the agent sets its identity preferences. + +The policy names the issuing CA. +Copy the exact intermediate CA name from the authority's page; for a hosted authority it looks like `Smallstep <> Accounts Intermediate CA`. + + + + Nothing to do for the certificate. + On its next sync, the agent on every device that matches the credential's assignment policy requests the certificate with the key in the device's secure hardware, installs it for the browsers, and renews it before it expires. + + Set the auto-select policy per operating system: + + **Windows (Chrome and Edge).** + In the Registry Editor, under `HKEY_LOCAL_MACHINE\Software\Policies\Google\Chrome` for Chrome or `HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\Edge` for Edge (create the keys if they do not exist), add a string value named `AutoSelectCertificateForUrls`: + + ```text + ["{\"pattern\":\"<>\",\"filter\":{\"ISSUER\":{\"CN\":\"Smallstep <> Accounts Intermediate CA\"}}}"] + ``` + + Restart the browser and confirm the policy at `chrome://policy` or `edge://policy`. + + **Linux (Chrome).** + As root, create `/etc/opt/chrome/policies/managed/auto_select_cert.json`: + + ```json + { + "AutoSelectCertificateForUrls": ["{\"pattern\":\"<>\",\"filter\":{\"ISSUER\":{\"CN\":\"Smallstep <> Accounts Intermediate CA\"}}}"] + } + ``` + + Restart Chrome and confirm the policy at `chrome://policy`. + Firefox on Linux shows its client certificates at `about:certificate`. + + **macOS.** + Deliver the Chrome and Firefox preferences with your MDM (the Jamf Pro tab), or accept the picker on the first visit. + Safari needs nothing. + + + Two configuration profiles under **Computers → Configuration Profiles**, each an **Application & Custom Settings → Upload** payload. + Other MDMs take the same property lists. + + **Chrome.** + Preference domain `com.google.Chrome`, with `com.google.Chrome.plist`: + + ```xml + + + + + AutoSelectCertificateForUrls + + {"pattern":"<>","filter":{"ISSUER":{"CN":"Smallstep <> Accounts Intermediate CA"}}} + + + + ``` + + The pattern is a [Chrome Enterprise URL pattern](https://chromeenterprise.google/policies/url-patterns/): `[.*]xample.com` matches `xample.com` and `hello.xample.com` on any scheme, port, and path, and does not match `example.com`. + Chrome does not merge several `AutoSelectCertificateForUrls` policies; put every URL in one profile. + + **Firefox.** + Preference domain `org.mozilla.firefox`, with `org.mozilla.firefox.plist`: + + ```xml + + + + + EnterprisePoliciesEnabled + + PayloadDisplayName + Firefox ESR Policies + PayloadEnabled + + PayloadIdentifier + org.mozilla.firefox.BCADDC78-843E-4112-936A-DAB8EEEF514C + PayloadType + org.mozilla.firefox + PayloadUUID + BCADDC78-843E-4112-936A-DAB8EEEF514C + PayloadVersion + 1 + Preferences + + security.default_personal_cert + Select Automatically + security.osclientcerts.autoload + + + + + ``` + + Scope both profiles to a test device first. + Safari needs no profile. + + + On managed Chromebooks the certificate comes from the Smallstep extension, and the policy is set once in the Admin console for an organizational unit. + + 1. Complete [ChromeOS device identity certificates](../tutorials/chromeos-device-identity-certificates.mdx) so a certificate exists to select. + 2. Go to **Devices → Chrome → Settings → Users & browsers**, choose the organizational unit, and under **Client certificates** find **Auto-select client certificate for these sites**. + 3. Add an entry per protected URL: + + ```json + {"pattern":"<>","filter":{"ISSUER":{"CN":"Smallstep (<>) Devices Intermediate CA"}}} + ``` + + 4. Save. + + The policy reaches devices on their next sync with Google Workspace. + + + +## Verify {#verify} + +1. On the test device, confirm the certificate is there. + - Windows: run `certmgr` and look under **Certificates - Current User → Personal → Certificates** for a certificate issued by your authority's intermediate. + - Chrome or Edge on any OS: `chrome://settings/certificates` or **Manage certificates** under `edge://settings/privacy/securitySubPage`. + - Linux: `chrome://settings/certificates` after a restart; if the certificate is missing, the agent's PKCS#11 module is not reachable. +2. Confirm the policy applied at `chrome://policy` or `edge://policy`. +3. Restart the browser and open <>. + The app loads with no certificate picker and shows the identity from the certificate. +4. On the device, run the doctor and expect every row to pass: + + ```bash + sudo step-agent doctor + ``` + +## Roll out and operate {#operate} + +- **Widen the assignment policy** on the credential once one device works. +- **Add URLs** to `matchAddresses` with `PUT /protect/browser/{browserID}`, and to the auto-select policy in the same change; a URL in one and not the other produces a picker. +- **Renewals** are automatic; the agent renews before expiry and the browser picks up the new certificate on its next connection. +- **Revocation.** + Revoke a device's certificate with [`/certificates/{serialNumber}/revoke`](https://gateway.smallstep.com/v2025-01-01/operations/RevokeCertificate). + Whether your app notices before the certificate expires depends on whether it checks revocation; a short `duration` on the credential bounds the window either way. +- **What the person sees.** + With the policy in place, nothing. + Without it, the browser's certificate picker on the first visit to each origin; choosing the Smallstep certificate once is enough for that session. + +## Troubleshoot {#troubleshoot} + +- [The browser shows a certificate selection dialog](../troubleshooting/web-apps/certificate-selection-dialog.mdx) +- [Chrome or Firefox does not see the Smallstep certificate](../troubleshooting/web-apps/browser-does-not-see-certificate.mdx) +- [`chrome://policy` does not list `AutoSelectCertificateForUrls`](../troubleshooting/web-apps/policy-not-applied.mdx) +- [The app rejects the certificate as expired or invalid](../troubleshooting/web-apps/certificate-expired.mdx) + +For agent problems that are not specific to browsers, use the [agent troubleshooting page](../platform/troubleshooting-agent.mdx). + +## Automate {#automate} + +### REST + +| Object | Create | Read | +|---|---|---| +| Credential | `POST /credentials` | `GET /credential/{credentialID}` | +| Web app | `POST /protect/browser` | `GET /protect/browser/{browserID}` | + +### Terraform + +{/* vale Google.Spacing = NO */} + +```hcl +resource "smallstep_credential" "browser" { + slug = "browser" + + certificate = { + authority_id = "<>" + duration = "168h" + x509 = { + common_name = { + device_metadata = "smallstep:identity" + } + sans = { + device_metadata = ["smallstep:identity", "Device.Serial"] + } + } + } + + key = { + type = "ECDSA_P256" + protection = "HARDWARE_ATTESTED" + } + + policy = { + assurance = ["high"] + os = ["macOS", "Windows", "Linux"] + } +} + +resource "smallstep_browser" "intranet" { + name = "Intranet" + match_addresses = ["<>"] + credentials = [smallstep_credential.browser.id] +} +``` + +{/* vale Google.Spacing = YES */} + +The resource is [`smallstep_browser`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/resources/browser). diff --git a/use-cases/wifi.mdx b/use-cases/wifi.mdx new file mode 100644 index 00000000..a5cf4af3 --- /dev/null +++ b/use-cases/wifi.mdx @@ -0,0 +1,527 @@ +--- +updated_at: September 13, 2026 +title: Wi-Fi +html_title: Wi-Fi with device certificates (802.1X EAP-TLS) +description: Join your Wi-Fi with a certificate instead of a password. Create the credential, the RADIUS server, and the network, point your access points at Smallstep, deliver with the agent or your MDM, and verify. +kind: guide +entitlement: team +skill: wifi +--- + +Secure Wi-Fi with device certificates. +About 20 minutes in Smallstep, plus the change on your access points. + + +
+ Use a test SSID for this walk-through. + Do not change a production network until you have seen a device join. +
+
+ +## What you get {#what-you-get} + +A device joins the network by presenting a certificate that was issued to it and whose private key never left its hardware. +The access point relays the EAP-TLS handshake to a RADIUS server, the RADIUS server checks the certificate against your authority, and the port opens only on Access-Accept. +There is no password to phish, share, or rotate. + +```mermaid +graph LR + cred["Credential
(client certificate)"] -->|credential ID| wifi["Wi-Fi network
(SSID and trust)"] + radius["Smallstep RADIUS"] -->|server CA| wifi + radius -->|"IP, port, shared secret"| ap["Access point"] + wifi --> devices["Devices
(agent or MDM)"] +``` + +Three objects do the work, and you create all three in this guide: + +- A **credential**: which authority signs, what goes in the certificate, how the key is protected, and which devices get it. +- A **RADIUS server**: the verifier. + Your access points delegate authentication to it and it verifies the certificate during the EAP-TLS handshake. + Smallstep runs it for you, or you bring your own. +- A **Wi-Fi network** resource: the SSID, the RADIUS server CA that devices must trust, and the credential they authenticate with. + +[Why device identity](../learn/why-device-identity.mdx) explains the argument for certificates over passwords, and [RadSec](../learn/radsec.mdx) explains the transport between your access points and the RADIUS server. + +## Before you start {#before-you-start} + +Each of these must exist before step 3. + +1. **Devices enrolled.** + At least one device in your [inventory](../platform/enrollment-guide.mdx), approved, and running the [agent](../platform/smallstep-agent.mdx) on macOS, Windows, or Linux. + For MDM-delivered profiles without the agent, the device must be synced from your MDM instead. +2. **An MDM connected**, if you will deliver profiles with one: [Jamf Pro](../tutorials/connect-jamf-pro-to-smallstep.mdx), [Intune](../tutorials/connect-intune-to-smallstep.mdx), [Workspace ONE](../tutorials/connect-workspace-one-to-smallstep.mdx), [Mosyle](../tutorials/connect-mosyle-to-smallstep.mdx), [Fleet](../tutorials/connect-fleet-dm-to-smallstep.mdx), or [Google Workspace](../tutorials/connect-google-workspace-to-smallstep.mdx) for ChromeOS. + Agent-managed devices need none. +3. **An authority** to sign the client certificates. + Open [Authorities](https://smallstep.com/app/?next=/cm/authorities) in the Console (under the **Certificate Manager** menu today) and note its ID, <>. + Download its root certificate and save it as `client_ca.crt`; the RADIUS server needs it in step 3. + If you have no authority yet, [create a hosted authority](../certificate-manager/getting-started.mdx) first. +4. **An API token** from [Settings → API tokens](https://smallstep.com/app/?next=/settings/api/tokens/add). + This guide creates the objects with the API because every value you need comes back in one response. + The same objects can be created under **Protect → Wi-Fi** in the Console. +5. **Your network facts**: the public IP address your access points or wireless controller send RADIUS traffic from, and admin access to the access points and a test SSID. + +Store the token in a headers file for `curl`: + +```bash +set +o history +echo "Authorization: Bearer <>" > api_headers +set -o history +``` + +## Create it in Smallstep {#create} + +Three calls, in this order: the credential, the RADIUS server, and the Wi-Fi network. +Every object is visible afterwards: the credential under **Credentials** (labelled **Endpoints** under the **Certificate Manager** menu today), the RADIUS server and the network under **Protect → Wi-Fi**. + +### 1. Create the credential + +The credential names the authority, the certificate shape, the key protection, and the devices that get it. +The defaults below are the recipe for company laptops: a one-day certificate, an ECDSA P-256 key generated in the device's secure hardware with attestation, and assignment to high-assurance macOS, Windows, and Linux devices. + +{/* vale Google.Spacing = NO */} + +```bash +curl -sH @api_headers --request POST \ + --url https://gateway.smallstep.com/api/credentials \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data @- <<'EOF' | jq +{ + "slug": "wifi", + "certificate": { + "type": "X509", + "authorityID": "<>", + "duration": "24h0m0s", + "fields": { + "commonName": { + "deviceMetadata": "smallstep:identity", + "static": "Corporate Wi-Fi" + }, + "sans": { + "deviceMetadata": ["smallstep:identity", "Device.Serial"] + }, + "extendedKeyUsage": ["clientAuth"] + } + }, + "key": { + "type": "ECDSA_P256", + "protection": "HARDWARE_ATTESTED" + }, + "managementMode": "agent", + "policy": { + "assurance": ["high"], + "operatingSystem": ["macOS", "Windows", "Linux"] + } +} +EOF +``` + +{/* vale Google.Spacing = YES */} + +What each part does: + +- `certificate.fields` fills the subject. + Each field takes a `static` value, one or more `deviceMetadata` keys, or both; the static value is the fallback when the key is absent on a device. + Reserved keys are `smallstep:identity` (the email of the user bound to the device), `Device.Serial`, `Device.Hostname`, `Device.PermanentIdentifier`, and `Device.DisplayName`; your own metadata keys work too. +- `key.protection` is the issuance method. + `HARDWARE_ATTESTED` generates the key in the TPM or Secure Enclave and requires attestation before the authority signs. + `HARDWARE_WITH_FALLBACK` prefers hardware and allows a software key where there is none. + [Attestation explained](../learn/attestation-explained.mdx) says what each level proves. +- `policy` is the assignment policy: which devices receive the credential. + An empty policy assigns it to every device. + Match on `assurance` (`normal`, `high`), `ownership` (`company`, `user`), `operatingSystem`, discovery `source`, and `tags`. + Start narrow with a tag on your test devices and widen it in step 7. +- `managementMode: "agent"` makes the agent enroll, renew, and keep the key; use `mdm` when your MDM delivers and renews the certificate instead. + +Save the `id` from the response as <>. + + +
+ Automation built on the /accounts or /endpoint-configurations endpoints should move to /credentials and /protect/*. + The older endpoints are deprecated in the 2025-01-01 API version. +
+
+ +### 2. Create the RADIUS server + +Smallstep RADIUS verifies EAP-TLS only. +It validates the full chain against your client CA and checks revocation on every authentication. +Password methods (EAP-TTLS, PEAP-MSCHAPv2), MAC Authentication Bypass, RADIUS accounting, and Change of Authorization are not supported, by design. + +Register the public IP addresses your access points send from and the authority root you downloaded: + +```bash +jq -n --rawfile ca client_ca.crt \ + '{name: "Corporate RADIUS", nasIPs: ["<>"], clientCA: $ca}' \ +| curl -sH @api_headers --request POST \ + --url https://gateway.smallstep.com/api/managed-radius \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data @- | jq +``` + +- `nasIPs` is how the service attributes incoming requests to your team, so each IP must be unique across Smallstep customers. + (The Console labels this field **WAP/NAS IP address**.) + When an office changes ISP, update this list before anyone can join. +- `clientCA` is the bundle the server trusts to verify clients: the root of the authority from step 2. + +The response holds the values your access points need: `serverIP` (<>), `serverPort` (<>), and `serverHostname` (<>, the DNS name on the server's TLS certificate). +Save `serverCA` to a file; devices need it to trust the server: + +```bash +curl -sH @api_headers --request GET \ + --url "https://gateway.smallstep.com/api/managed-radius/<>" \ + --header 'Accept: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' | jq -r '.serverCA' > radius_ca.crt +``` + +The shared secret is not in responses by default. +Fetch it with the `secret` query parameter and treat it like a password: + +```bash +curl -sH @api_headers --request GET \ + --url "https://gateway.smallstep.com/api/managed-radius/<>?secret=true" \ + --header 'Accept: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' | jq -r '.secret' +``` + +That value is <>. + +**A dedicated deployment.** +Smallstep RADIUS is also offered as dedicated infrastructure with static IPs in the regions you choose, RadSec transport on a dedicated hostname, reply attributes for VLAN assignment, and [authorization webhooks](../reference/radius-webhooks.mdx). +Smallstep provisions it for your team today; it is not something you create in the Console or the API. +The [RadSec](../learn/radsec.mdx) article says when you want it. + +**Your own RADIUS server.** +FreeRADIUS, Cisco ISE, Aruba ClearPass, NPS, or any server that speaks EAP-TLS works with Smallstep certificates. +Add `client_ca.crt` to its trusted client CA bundle, and in the next call use *your* server's CA as the `radiusServerCA`. + +### 3. Create the Wi-Fi network + +```bash +jq -n --rawfile ca radius_ca.crt \ + '{ + name: "Corporate Wi-Fi", + ssid: "<>", + hidden: false, + autojoin: true, + radiusServerCA: $ca, + radiusServerDomain: "<>", + credentials: ["<>"] + }' \ +| curl -sH @api_headers --request POST \ + --url https://gateway.smallstep.com/api/protect/wifi \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data @- | jq +``` + +- `radiusServerCA` is what devices use to verify the RADIUS server: the `serverCA` you saved, or your own server's CA. +- `radiusServerDomain` requires the server certificate to carry this DNS name; Windows honors it. +- `credentials` lists the credentials devices authenticate with. + +The network appears under [Protect → Wi-Fi](https://smallstep.com/app/?next=/protect/wifi). +Its ID is <>. + +## Configure your access points {#configure-external} + +Every access point takes the same three values, plus the security mode. +Set the SSID to **WPA2 Enterprise** or **WPA3 Enterprise** and add the RADIUS server: + +| Setting | Value | +|---|---| +| RADIUS server IP | <> | +| RADIUS server port | <> | +| Shared secret | <> | +| Server certificate name | <> | + + + + 1. Go to **Wireless → Configure → SSIDs** and enable an unconfigured SSID. + 2. Rename it to <> and save. + 3. Open **edit settings** to reach the SSID's Access control tab. + 4. Set **Association requirements** to **Enterprise with my RADIUS server**. + 5. Under **RADIUS servers**, add <>, <>, and <>. + 6. Save. + + + Create a RADIUS profile in the UniFi Network app: + + 1. Go to **Settings → Profiles → RADIUS → Create New** and name it. + 2. Under **Authentication servers**, add <>, <>, and <>. + 3. Save. + + Then create the network: + + 1. Go to **Settings → WiFi → Create New** and enter <>. + 2. Under **Advanced Configuration**, choose **Manual**. + 3. Under **Security**, select **WPA-3 Enterprise** and the RADIUS profile you created. + 4. Save. + + + On an Aruba mobility controller: + + 1. Go to **Configuration → Authentication → Auth Servers**. + 2. Add a **Server Group**, open it, and add a server of type **RADIUS** with <> and <>. + 3. Go to **Configuration → WLAN** and add a WLAN named <>; choose the AP groups that broadcast it and your VLAN. + 4. On **Security → Enterprise**, set **Key management** to **WPA-3 Enterprise** and add the server group under **Auth servers**. + 5. On **Access**, pick the default role for authenticated devices, finish, and deploy the pending changes. + + + 1. Go to **Security → RADIUS → Authentication** and add a server with <>, <>, and <>. + 2. On the **WLANs** tab, create a WLAN named <> and enable it. + 3. On **Security → AAA Servers**, select the server you added as **Server 1** and apply. + + + 1. Go to **Organization → WLAN Templates** and open or create a template. + 2. Add a WLAN with <>. + 3. Under **Security**, select **WPA3** or **WPA2**, then **Enterprise (802.1X)**. + 4. Under **Authentication Servers**, add <> and <>. + 5. Save. + + + The [access point reference](../reference/access-points.mdx) has the same steps for MikroTik, Aerohive, Extreme Networks, Sophos UTM, and Asus. + Any access point that supports WPA Enterprise with an external RADIUS server works with the three values above. + + + +## Deliver to devices {#deliver} + +Every device needs three things: the client certificate, trust in the RADIUS server CA, and the network profile. +The agent does all three. +An MDM can deliver a profile that does all three without the agent, using its own SCEP or ACME flow for the certificate. +You can mix the two across a fleet. + + + + Nothing to do. + On its next sync, the agent on every device that matches the credential's assignment policy: + + 1. Requests the client certificate, generating the private key in the device's secure hardware. + 2. Trusts the RADIUS server CA for this network. + 3. Creates the network profile (SSID, WPA Enterprise, EAP-TLS) on macOS, Windows, or Linux. + 4. Renews the certificate before it expires. + + + Two ways. + Both start by [connecting Jamf Pro](../tutorials/connect-jamf-pro-to-smallstep.mdx). + + **SCEP (macOS and iOS).** + Jamf deploys a profile that trusts the CA, gets a certificate from Smallstep's SCEP service with single-use challenges, and configures the network. + No SCEP proxy is needed; the hosted CA is reachable from the internet. + + 1. Open the network under **Protect → Wi-Fi** and choose **Configuration Profile** to download the `.mobileconfig` template for Jamf. + The same page shows the **Jamf Settings** for this network: a webhook URL, username, and password. + 2. In Jamf, go to **Settings**, search for **Webhooks**, and add one: enabled, **Basic Authentication** with the username and password from Smallstep, the webhook URL, content type **JSON**, event **SCEPChallenge**. + You do this once per Jamf tenant. + 3. Go to **Configuration Profiles**, choose **Upload**, select the `.mobileconfig`, and scope it to a test device that already has a basic Jamf MDM profile. + + Do not install the `.mobileconfig` by hand to test it; it only works when Jamf delivers it. + Running your own RADIUS server? Point the Wi-Fi payload's certificate trust at your server's root CA instead, adding a Certificate payload for it if needed. + A JumpCloud RADIUS server needs an RSA authority: create an advanced authority with key type `RSA_SIGN_PKCS1_2048_SHA256` for root and intermediate. + + **ACME device attestation (macOS).** + A higher-assurance path: the key is hardware bound and Apple's attestation proves the request came from a genuine device. + The device must already be synced from Jamf into your inventory, have a user assigned, and be approved. + Create a computer-level profile, distribution **Install automatically**, with: + + - An **ACME Certificate** payload: directory URL <>, client identifier `$SERIALNUMBER`, key size 384, key type **ECSECPrimeRandom**, hardware bound, subject `CN=$SERIALNUMBER`, extended key usage `1.3.6.1.5.5.7.3.2`, attest, allow all apps access, key not extractable. + - **Certificate** payloads for your device authority's root and intermediate and for the RADIUS server CA (`radius_ca.crt`), each with **Allow all apps access**. + - A **Network** payload: interface Wi-Fi, SSID <>, security **WPA3 Enterprise** or **WPA2 Enterprise**, accepted EAP type **TLS**, identity certificate the ACME certificate, trusted certificate the RADIUS server CA, certificate common name <>. + + After install, the certificate shows under **Profiles** in System Settings with **Hardware Bound: Yes**. + It does not appear in Keychain Access; inspect it with `sudo /usr/libexec/mdmclient QueryCertificates`. + + + Two ways. + Both start by [connecting Intune](../tutorials/connect-intune-to-smallstep.mdx), which registers the Entra ID application Smallstep uses to validate SCEP enrollments. + Microsoft recommends a staged rollout; create an evaluation group first. + + **SCEP (Windows).** + Open the network under **Protect → Wi-Fi** and download the root CA, the intermediate CA, and copy the SCEP URL. + Then create four profiles in Intune: + + 1. A **Trusted certificate** profile for the root CA, destination **Computer certificate store - root**. + 2. A **Trusted certificate** profile for the intermediate CA, destination **Computer certificate store - root**. + Not the intermediate store: that causes enrollment errors. + 3. A **SCEP certificate** profile: type Device, validity equal to the credential's duration, key storage **Enroll to TPM KSP if available, Software KSP if not**, key usage digital signature and key encipherment, key size 2048, SHA-2, extended key usage **Client Authentication**, renewal threshold 20 percent, root certificate **the intermediate CA** (the SCEP client checks the intermediate's fingerprint), and the SCEP URL from Smallstep. + 4. A **Wi-Fi** profile from the Templates list: type Enterprise, SSID <>, EAP type **EAP - TLS**, certificate server name <>, trusted certificate the RADIUS server CA (`radius_ca.crt`, the same CA the API returned as `serverCA`), client authentication **SCEP Certificate** pointing at profile 3. + + Sync a device from **Settings → Accounts → Access work or school → Info → Sync** and check the Intune reports. + + **Agent credential with an Intune Wi-Fi profile (Windows).** + Use this when the agent should own the certificate but Intune should own the network settings. + Intune deploys a custom OMA-URI profile whose WLAN XML selects the certificate by the SHA-1 fingerprint of its issuing CA. + Get the two fingerprints and strip the colons: + + ```bash + openssl x509 -in radius_ca.crt -noout -fingerprint -sha1 + openssl x509 -in issuing_ca.crt -noout -fingerprint -sha1 # the authority's intermediate + ``` + + Create a **Custom** template policy for **Windows 10 and Later** with one OMA-URI setting: name **Wi-Fi Configuration**, OMA-URI `./Vendor/MSFT/WiFi/Profile/<>/WlanXml` (URL-encode spaces as `%20`), data type **String**, value the [WLAN profile XML](../reference/wlan-profile-xml.mdx) with `ServerNames` set to <>, `TrustedRootCA` the RADIUS CA fingerprint, and `IssuerHash` the issuing CA fingerprint. + `authMode` must match where the agent stores the credential: `machine` for `HARDWARE_ATTESTED` keys, `user` for every other protection level. + + + [Connect Workspace ONE UEM](../tutorials/connect-workspace-one-to-smallstep.mdx) first. + + **macOS.** + Open the network under **Protect → Wi-Fi** and download the configuration profile for Workspace ONE. + In Workspace ONE UEM go to **Resources → Profiles**, choose **Add → Upload Profile**, select **macOS** and **Device Profile**, upload the file, save, assign it to device groups, and publish. + + **Windows.** + Workspace ONE deploys the same WLAN profile XML as the Intune agent-credential path. + Under **Profiles**, add a **Windows** device profile, add the **WiFi** template, paste the [WLAN profile XML](../reference/wlan-profile-xml.mdx) into **Wlan Xml**, set assignments, and save and publish. + + + ChromeOS devices get their certificate through the Smallstep extension using ACME device attestation, and the network is configured in Google Admin. + Complete [Connect Google Workspace](../tutorials/connect-google-workspace-to-smallstep.mdx) and [ChromeOS device identity certificates](../tutorials/chromeos-device-identity-certificates.mdx) first; a Wi-Fi credential from step 3 is not used. + + 1. Upload the RADIUS server CA (`radius_ca.crt`) under **Devices → Networks → Certificates**. + 2. Under **Devices → Networks → Wi-Fi**, select the organizational unit and add a network: name <>, security **WPA/WPA2 Enterprise**, EAP method **EAP-TLS**, EAP identity `${DEVICE_ASSET_ID}` (device authority) or `${USER_EMAIL}` (user authority), CA certificate the one you uploaded. + 3. Set the **Issuer pattern → Common name** to the exact intermediate CA name, `Smallstep (<>) Devices Intermediate CA` or `Smallstep (<>) Accounts Intermediate CA`. + Leave locality, organization, organizational unit, and the subject pattern empty; any value there prevents a match. + + If your RADIUS server is your own, its client trust must include the **root** of the authority you chose; ChromeOS sends the intermediate in the handshake. + + + +## Verify {#verify} + +1. On the test device, select <>. + It connects without asking for a password. +2. Open the network under [Protect → Wi-Fi](https://smallstep.com/app/?next=/protect/wifi). + The issued certificate and the authentication activity for the device appear on the resource page. +3. On an agent-managed device, run the doctor and expect every row to pass: + + ```bash + sudo step-agent doctor + ``` + + On macOS the binary is `/Applications/SmallstepAgent.app/Contents/MacOS/SmallstepAgent`; on Windows it is `'C:\Program Files\Smallstep\SmallstepAgent\smallstep-agent.exe'`. + + ```text + +------------------------------------------+--------+-------------+ + | Check | Status | Description | + +------------------------------------------+--------+-------------+ + | Check runtime requirements | PASS | | + | Ping the agent | PASS | | + | Check attestation CA connectivity | PASS | | + | Check API gateway connectivity | PASS | | + | Check mission control connectivity | PASS | | + | Bootstrap certificate authorities | PASS | | + | Verify issued certificates | PASS | | + | Sign with endpoint keys | PASS | | + | Enumerate PKCS#11 objects | PASS | | + +------------------------------------------+--------+-------------+ + ``` + +4. On the access point or controller, confirm the client is associated with an 802.1X session. + +If the device does not connect, start at step 8, Troubleshoot. + +## Roll out and operate {#operate} + +- **Widen the assignment policy.** + Change the credential's `policy` from your pilot tag to the devices that should have the network, for example `ownership: ["company"]`. + The agent picks up new assignments on its next sync. +- **Renewals** are automatic on agent-managed devices; the agent renews before expiry. + MDM-delivered certificates renew through the MDM's SCEP or ACME flow at the threshold you set in the profile. +- **Revocation.** + Revoke a device's certificate with [`/certificates/{serialNumber}/revoke`](https://gateway.smallstep.com/v2025-01-01/operations/RevokeCertificate) or from the device's **Certificates** tab; Smallstep RADIUS checks revocation on every authentication, so the next join fails. +- **VLANs.** + Dynamic VLAN assignment uses RADIUS reply attributes (`Tunnel-Type`, `Tunnel-Medium-Type`, `Tunnel-Private-Group-ID`) whose values are static, taken from a certificate field by OID, or from device metadata. + Reply attributes are part of the dedicated deployment described in step 3. +- **New sites.** + Add the new public IP to `nasIPs` before the access points there are switched to the new SSID. +- **Reuse.** + The same credential and RADIUS server serve a [wired network](./wired.mdx). + +## Troubleshoot {#troubleshoot} + +Each entry is a symptom with its cause, fix, and the step to return to. + +- [`unknown CA` or `unable to get local issuer certificate` on the RADIUS server](../troubleshooting/wifi/unknown-ca.mdx) +- [No RADIUS traffic reaches Smallstep when a device tries to join](../troubleshooting/wifi/no-radius-traffic.mdx) +- [Access points at a new site get no answer from Smallstep RADIUS](../troubleshooting/wifi/new-site-no-answer.mdx) +- [The device presents a certificate with the wrong issuer](../troubleshooting/wifi/wrong-issuer.mdx) +- [The `.mobileconfig` installed by hand does not connect](../troubleshooting/wifi/mobileconfig-installed-by-hand.mdx) +- [`The SCEP Certificate payload is failing for every device`](../troubleshooting/wifi/scep-payload-failing.mdx) +- [SCEP enrollment errors after the Intune trusted certificate profiles install](../troubleshooting/wifi/intune-intermediate-store.mdx) +- [The ACME certificate does not appear in Keychain Access](../troubleshooting/wifi/acme-cert-not-in-keychain.mdx) +- [Windows does not pick up the profile until it restarts](../troubleshooting/wifi/windows-profile-needs-restart.mdx) +- [`I can't connect to Wi-Fi` on an agent-managed device](../troubleshooting/wifi/cannot-connect-agent-device.mdx) + +For agent problems that are not specific to Wi-Fi, use the [agent troubleshooting page](../platform/troubleshooting-agent.mdx). + +## Automate {#automate} + +The same three objects, as REST calls and as Terraform resources. +The REST calls are the ones in step 3; the Terraform resources are [`smallstep_credential`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/resources/credential), [`smallstep_managed_radius`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/resources/managed_radius), and [`smallstep_wifi`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/resources/wifi). + +### REST + +| Object | Create | Read | +|---|---|---| +| Credential | `POST /credentials` | `GET /credential/{credentialID}` | +| RADIUS server | `POST /managed-radius` | `GET /managed-radius/{managedRadiusID}?secret=true` | +| Wi-Fi network | `POST /protect/wifi` | `GET /protect/wifi/{wifiID}` | + +All three take the `x-smallstep-api-version: 2025-01-01` header; the request bodies are the ones in step 3. + +### Terraform + +{/* vale Google.Spacing = NO */} + +```hcl +resource "smallstep_credential" "wifi" { + slug = "wifi" + + certificate = { + authority_id = "<>" + duration = "24h" + x509 = { + common_name = { + device_metadata = "smallstep:identity" + static = "Corporate Wi-Fi" + } + sans = { + device_metadata = ["smallstep:identity", "Device.Serial"] + } + } + } + + key = { + type = "ECDSA_P256" + protection = "HARDWARE_ATTESTED" + } + + policy = { + assurance = ["high"] + os = ["macOS", "Windows", "Linux"] + } +} + +resource "smallstep_managed_radius" "corporate" { + name = "Corporate RADIUS" + nas_ips = ["<>"] + client_ca = file("${path.module}/client_ca.crt") +} + +resource "smallstep_wifi" "corporate" { + name = "Corporate Wi-Fi" + ssid = "<>" + radius_server_ca = smallstep_managed_radius.corporate.server_ca + radius_server_domain = smallstep_managed_radius.corporate.server_hostname + hidden = false + autojoin = true + credentials = [smallstep_credential.wifi.id] +} +``` + +{/* vale Google.Spacing = YES */} + +The shared secret is available as the [`smallstep_managed_radius_secret`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/data-sources/managed_radius_secret) data source. diff --git a/use-cases/wired.mdx b/use-cases/wired.mdx new file mode 100644 index 00000000..4d7edaf5 --- /dev/null +++ b/use-cases/wired.mdx @@ -0,0 +1,199 @@ +--- +updated_at: September 13, 2026 +title: Wired +html_title: Wired networks with device certificates (802.1X EAP-TLS) +description: Open a switch port only to devices with a certificate. Reuse the Wi-Fi credential and RADIUS server, create the wired network, configure your switches, deliver with the agent or your MDM, and verify. +kind: guide +entitlement: team +skill: wired +--- + +Secure wired ports with device certificates. +About 15 minutes in Smallstep if you already have Wi-Fi set up, plus the change on your switches. + +## What you get {#what-you-get} + +A device plugs into a port and the switch, acting as the 802.1X authenticator, relays the EAP-TLS handshake to a RADIUS server. +The RADIUS server verifies the device's certificate against your authority, and the switch forwards traffic only after Access-Accept. +Unplugging and plugging in a different device starts the check again. + +```mermaid +graph LR + device["Device
(client certificate)"] -->|EAP-TLS| switch["Switch port
(802.1X authenticator)"] + switch -->|RADIUS| radius["Smallstep RADIUS"] + radius -->|verifies against| ca["Your authority's root"] +``` + +The objects are the same as for [Wi-Fi](./wifi.mdx): a **credential**, a **RADIUS server**, and a **wired network** resource that tells devices to authenticate on their Ethernet interface with that credential and to trust that server. +If you have set up Wi-Fi, you reuse the first two and create only the third. + +## Before you start {#before-you-start} + +1. **Devices enrolled.** + At least one device in your [inventory](../platform/enrollment-guide.mdx), approved, running the [agent](../platform/smallstep-agent.mdx) on macOS, Windows, or Linux, with a wired interface. +2. **An MDM connected**, only if you will deliver the Windows profile with [Intune](../tutorials/connect-intune-to-smallstep.mdx) or [Workspace ONE](../tutorials/connect-workspace-one-to-smallstep.mdx) while the agent manages the certificate. +3. **An authority** to sign the client certificates: note its ID, <>, under [Authorities](https://smallstep.com/app/?next=/cm/authorities) and download its root as `client_ca.crt`. + If you did the [Wi-Fi guide](./wifi.mdx#before-you-start), you have this. +4. **An API token** from [Settings → API tokens](https://smallstep.com/app/?next=/settings/api/tokens/add), stored in `api_headers` as the Wi-Fi guide shows. +5. **Your network facts**: the public IP address your switches send RADIUS traffic from, and admin access to a switch with a spare access port for testing. + +## Create it in Smallstep {#create} + +### 1. The credential and the RADIUS server + +If they exist from Wi-Fi, skip to 2 and use the same <>, <>, and `radius_ca.crt`. +A single credential serves both networks. + +Otherwise create them exactly as the Wi-Fi guide's [step 3](./wifi.mdx#create) does, with one change: use `"slug": "dot1x"` and a static common name of `Corporate Network`. +For the RADIUS server, `nasIPs` are the public IP addresses your **switches** send from. +Smallstep RADIUS verifies EAP-TLS only and checks the full chain and revocation on every authentication; it does not do MAC Authentication Bypass, so plan for devices without certificates in step 4. + +### 2. Create the wired network + +```bash +jq -n --rawfile ca radius_ca.crt \ + '{ + name: "Corporate wired network", + autojoin: true, + radiusServerCA: $ca, + credentials: ["<>"] + }' \ +| curl -sH @api_headers --request POST \ + --url https://gateway.smallstep.com/api/protect/ethernet \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header 'x-smallstep-api-version: 2025-01-01' \ + --data @- | jq +``` + +- `radiusServerCA` is what devices use to verify the RADIUS server: the `serverCA` from the RADIUS server, or your own server's CA. +- `credentials` lists the credentials devices authenticate with. + +The resource appears under [Protect → Ethernet](https://smallstep.com/app/?next=/protect/ethernet). +Its ID is <>. + +## Configure your switches {#configure-external} + +Every switch vendor exposes 802.1X differently, but the configuration has the same shape. +The values are the same as for Wi-Fi: + +| Setting | Value | +|---|---| +| RADIUS server IP | <> | +| RADIUS server port | <> | +| Shared secret | <> | + + + + 1. **Register the RADIUS server** as an authentication server (often under AAA or RADIUS clients) with the three values above. + 2. **Enable 802.1X globally** on the switch. + 3. **Enable port authentication** on each access port. + On a Cisco IOS-style command line that is `dot1x port-control auto`. + 4. **Decide what happens to devices without certificates.** + Printers and other devices that cannot do 802.1X need a guest VLAN, a dedicated unauthenticated VLAN, or separate ports. + MAC Authentication Bypass is not supported by Smallstep RADIUS; it authenticates certificates only. + 5. **Assign VLANs dynamically** with RADIUS reply attributes (`Tunnel-Type`, `Tunnel-Medium-Type`, `Tunnel-Private-Group-ID`) instead of per port, if your RADIUS deployment supports reply attributes (see [Roll out and operate](#operate)). + + Consult your switch vendor's 802.1X documentation for the exact commands. + Per-vendor tabs for Cisco IOS-XE, Aruba CX, Juniper EX, Meraki MS, UniFi, and HPE are planned and not written yet; the shape above applies to all of them. + + + +Start with one test port. +Do not enable port authentication on every port until a device has authenticated. + +## Deliver to devices {#deliver} + + + + Nothing to do. + On its next sync, the agent on every device that matches the credential's assignment policy requests the certificate with the key in the device's secure hardware, trusts the RADIUS server CA, and configures the wired 802.1X profile: Wired AutoConfig on Windows, the Ethernet payload on macOS, NetworkManager on Linux. + + Two caveats. + On Windows, the **Wired AutoConfig** service (`dot3svc`) must be running for 802.1X on Ethernet; the agent's profile does nothing while it is stopped. + On Linux, keys held in the TPM through the agent's PKCS#11 interface have limits with some NetworkManager versions; check the [agent troubleshooting page](../platform/troubleshooting-agent.mdx#pkcs11-not-working-linuxmacos) if the supplicant cannot use the certificate. + + + Use this when the agent should own the certificate and Intune should own the network settings on Windows. + Intune deploys a custom OMA-URI profile whose LAN XML selects the certificate by the SHA-1 fingerprint of its issuing CA. + + 1. Get the two fingerprints (the RADIUS server CA and the authority's intermediate) as the [profile reference](../reference/wlan-profile-xml.mdx) shows, and remove the colons. + 2. In Intune, go to **Devices → Configuration**, choose **Create → New Policy**: platform **Windows 10 and Later**, profile type **Templates**, template **Custom**. + 3. Name the policy, for example **EAP-TLS Wired Network with Smallstep**. + 4. Add one OMA-URI setting: name **Wired Network Configuration**, OMA-URI `./Device/Vendor/MSFT/WiredNetwork/LanXML`, data type **String**, value the [LAN profile XML](../reference/wlan-profile-xml.mdx#lan) with `ServerNames` set to <>, `TrustedRootCA` the RADIUS CA fingerprint, and `IssuerHash` the issuing CA fingerprint. + 5. Save, assign to your test group, skip applicability rules, and create. + + `authMode` must match where the agent stores the credential: `machine` for `HARDWARE_ATTESTED` keys, `user` for every other protection level. + + + Workspace ONE UEM deploys the same LAN profile XML. + Create a **Windows** device profile, add the **Wired Network** template, paste the [LAN profile XML](../reference/wlan-profile-xml.mdx#lan), set the assignments, and save and publish. + + + +## Verify {#verify} + +1. Plug the test device into the 802.1X-enabled port. + It gets an address on the corporate VLAN without a prompt. +2. On the switch, check the port's authentication state; on a Cisco-style command line: + + ```text + show dot1x interface + show authentication sessions + ``` + + The session shows the device's identity and **Authorized**. +3. Open the resource under [Protect → Ethernet](https://smallstep.com/app/?next=/protect/ethernet); the issued certificate and the authentication activity for the device appear there. +4. On an agent-managed device, run the doctor and expect every row to pass: + + ```bash + sudo step-agent doctor + ``` + + On Windows, 802.1X events are in Event Viewer under **Applications and Services Logs → Microsoft → Windows → Wired-AutoConfig**. + +## Roll out and operate {#operate} + +- **Widen the assignment policy** on the credential once the test port works; it is the same credential as Wi-Fi, so devices that already have the certificate need no new issuance. +- **Enable port authentication** on the remaining access ports in batches, with the guest or unauthenticated VLAN in place for devices without certificates. +- **Renewals** are automatic on agent-managed devices. +- **Revocation.** + Revoke a device's certificate with [`/certificates/{serialNumber}/revoke`](https://gateway.smallstep.com/v2025-01-01/operations/RevokeCertificate); the next re-authentication on the port fails. +- **VLANs.** + Dynamic VLAN assignment uses RADIUS reply attributes whose values are static, taken from a certificate field by OID, or from device metadata. + Reply attributes are part of the dedicated Smallstep RADIUS deployment described in the [Wi-Fi guide](./wifi.mdx#create); with the multi-tenant service, assign VLANs per port on the switch. +- **A second RADIUS server** for failover on the switch is configured the same way; register both public IPs. + +## Troubleshoot {#troubleshoot} + +- [The port never authorizes and `show authentication sessions` shows no session](../troubleshooting/wired/port-never-authorizes.mdx) +- [Windows does not attempt 802.1X on the wired interface](../troubleshooting/wired/windows-wired-autoconfig-stopped.mdx) +- [Printers and other devices lose connectivity after port authentication is enabled](../troubleshooting/wired/devices-without-certificates.mdx) +- [The Intune wired profile installs but Windows does not present the certificate](../troubleshooting/wired/lan-profile-wrong-issuer-hash.mdx) + +RADIUS-side symptoms are the same as for Wi-Fi: [`unknown CA`](../troubleshooting/wifi/unknown-ca.mdx) and [no answer from a new site](../troubleshooting/wifi/new-site-no-answer.mdx). + +## Automate {#automate} + +### REST + +| Object | Create | Read | +|---|---|---| +| Credential | `POST /credentials` | `GET /credential/{credentialID}` | +| RADIUS server | `POST /managed-radius` | `GET /managed-radius/{managedRadiusID}?secret=true` | +| Wired network | `POST /protect/ethernet` | `GET /protect/ethernet/{ethernetID}` | + +### Terraform + +With the credential and RADIUS server from the [Wi-Fi guide](./wifi.mdx#automate): + +```hcl +resource "smallstep_ethernet" "corporate" { + name = "Corporate wired network" + radius_server_ca = smallstep_managed_radius.corporate.server_ca + autojoin = true + credentials = [smallstep_credential.wifi.id] +} +``` + +The resource is [`smallstep_ethernet`](https://registry.terraform.io/providers/smallstep/smallstep/latest/docs/resources/ethernet).