5.6 KiB
Overview
Fleet implements a SCEP proxy that sits between devices and Certificate Authorities. For Android, only Custom SCEP Proxy is supported. The proxy validates requests and forwards them to the CA.
Two Challenge Types
Custom SCEP uses two different challenges:
-
Fleet Challenge (one-time use)
- Generated by Fleet and stored in the database
- Embedded in the SCEP proxy URL identifier
- Validated by Fleet via
ConsumeChallenge()during PKIOperation - Prevents replay attacks and ensures request authenticity
- Consumed (deleted) after successful validation
-
Static Challenge (from CA configuration)
- Configured in the CA settings (
certificate_authorities.challenge_encrypted) - Substituted into profile via
$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_<CA_NAME> - Embedded by the device in its encrypted CSR
- Validated by the SCEP CA itself
- Same challenge used across all requests to that CA
- Configured in the CA settings (
Architecture Components
-
Handler Registration (server/service/handler.go)
RegisterSCEPProxy(...)sets up two HTTP endpoints:- GET /mdm/scep/proxy/{identifier} - For GetCACaps and GetCACert operations
- POST /mdm/scep/proxy/{identifier} - For PKIOperation (certificate signing)
The {identifier} is a comma-separated string:
hostUUID,profileUUID,caName,fleetChallenge -
SCEP Operations (ee/server/service/scep_proxy.go)
- GetCACaps: Returns CA capabilities (SHA-256, AES, etc.). Pass-through to upstream.
- GetCACert: Returns CA certificate(s) in PKCS#7 format. Pass-through to upstream.
- PKIOperation: Certificate signing request. This is where Fleet validates the Fleet challenge before forwarding.
Flow Diagram (Custom SCEP for Android)
sequenceDiagram
participant Device
participant Fleet as Fleet SCEP Proxy
participant DB as Fleet Database
participant CA as Custom SCEP CA
Note over Device,CA: Step 1: Certificate Template Delivery
Fleet->>DB: NewChallenge() - generate Fleet challenge
DB-->>Fleet: Fleet challenge token (one-time use)
Fleet->>DB: Get Static challenge from CA config
DB-->>Fleet: Static challenge (from certificate_authorities table)
Fleet->>Device: Certificate template with:<br/>• SCEP URL containing Fleet challenge<br/>• Static challenge in profile payload
Note over Device,CA: Step 2: Device Makes SCEP Requests
Device->>Fleet: GET /mdm/scep/proxy/{identifier}?operation=GetCACaps<br/>(identifier contains Fleet challenge)
Fleet->>Fleet: Validate identifier (host, profile exist)
Fleet->>CA: Forward GetCACaps
CA-->>Fleet: CA capabilities
Fleet-->>Device: CA capabilities (SHA-256, AES, etc.)
Device->>Fleet: GET /mdm/scep/proxy/{identifier}?operation=GetCACert
Fleet->>CA: Forward GetCACert
CA-->>Fleet: CA certificate (PKCS#7)
Fleet-->>Device: CA certificate
Note over Device,CA: Step 3: Certificate Signing
Device->>Fleet: POST /mdm/scep/proxy/{identifier}?operation=PKIOperation<br/>(encrypted CSR containing Static challenge)
Fleet->>DB: ConsumeChallenge(fleetChallenge)<br/>Validates & deletes Fleet challenge
alt Fleet challenge valid
Fleet->>CA: Forward CSR (CA validates Static challenge)
CA-->>Fleet: Signed certificate
Fleet->>DB: Update status to "verifying"
Fleet-->>Device: Signed certificate
else Fleet challenge invalid/consumed
Note over Fleet,DB: Profile Requeue Flow
Fleet->>DB: ResendHostCertificateProfile()<br/>Sets status = NULL
Fleet-->>Device: Error: "custom scep challenge failed"
Note over Fleet,DB: On next cron run
DB-->>Fleet: Find profiles with status = NULL
Fleet->>DB: NewChallenge() - generate fresh Fleet challenge
Fleet->>Device: Re-send template with new SCEP URL
end
How a Device Enrolls (Step-by-step)
Step 1: Certificate Template Delivery
When Fleet sends a certificate template to a device:
// Generate one-time Fleet challenge
fleetChallenge, err := ds.NewChallenge(ctx)
// Build SCEP proxy URL with Fleet challenge embedded
proxyURL := fmt.Sprintf("%s/mdm/scep/proxy/%s",
appConfig.MDMUrl(),
url.PathEscape(fmt.Sprintf("%s,%s,%s,%s", hostUUID, templateID, caName, fleetChallenge)))
// Static challenge is substituted via $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_<CA_NAME>
The template contains:
- SCEP URL:
https://fleet.example.com/mdm/scep/proxy/abc123,tmpl-456,MyCA,xyz789 - Challenge field: The static challenge from CA configuration
Step 2: Device Makes SCEP Requests
The device makes 3 requests to the proxy URL:
-
GET ?operation=GetCACaps
- Fleet validates the identifier (checks host/profile exist)
- Proxies to upstream CA
- Returns CA capabilities
-
GET ?operation=GetCACert
- Fleet proxies to CA
- Returns CA certificate
-
POST ?operation=PKIOperation (with encrypted CSR in body)
- Fleet validates and consumes the Fleet challenge via
ConsumeChallenge() - If valid, forwards CSR to CA (CA validates the static challenge inside the CSR)
- Returns signed certificate
- Fleet validates and consumes the Fleet challenge via
Step 3: Challenge Validation
For Custom SCEP:
- Fleet challenge is one-time use, validated via
ConsumeChallenge(ctx, fleetChallenge) - If the Fleet challenge was already used or doesn't exist, the request fails
- Fleet calls
ResendHostCertificateProfile()to requeue the profile with a fresh Fleet challenge
Step 4: Status Tracking
- Profile must be in "pending" state to proceed
- After successful certificate issuance, status updates to "verifying"
- If Fleet challenge validation fails, profile is requeued (status set to NULL)