fleet/server/mdm/scep/SCEP.md

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:

  1. 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
  2. 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

Architecture Components

  1. 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

  2. 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:

  1. GET ?operation=GetCACaps

    • Fleet validates the identifier (checks host/profile exist)
    • Proxies to upstream CA
    • Returns CA capabilities
  2. GET ?operation=GetCACert

    • Fleet proxies to CA
    • Returns CA certificate
  3. 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

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)