fleet
fleet copied to clipboard
Certificates from custom SCEP CA: one-time code
Goal
| User story |
|---|
| As a Security Engineer, |
| I want Fleet to use a one-time code before it sends certificates from my custom SCEP certificate authority (CA) |
| so that I can be sure that only my hosts can get a certificate. |
Key result
None. Prioritized to unblock pingali's MDM migration.
Original requests
- #13420
Context
- Product Designer: @noahtalerman
- Engineer: @getvictor
Changes
Product
- [ ] When the host requests a certificate from a custom SCEP certificate authority (Fleet proxy), the URL includes a one-time secret. The URL's path will look like this:
/mdm/scep/proxy/{host_uuid,profile_uuid,CA_name,one_time_secret}
- The one-time secret expires after 1 hour. If the one-time secret is expired, Fleet resends the profile.
- Support the old URL path without the one-time secret for backwards compatibility.
- [ ] UI changes: No changes
- [ ] CLI (fleetctl) usage changes: No changes
- [ ] YAML changes: No changes
- [ ] REST API changes: Figma here.
- [ ] Fleet's agent (fleetd) changes: No changes
- [ ] GitOps changes: No changes
- [ ] Activity changes: No changes
- [ ] Permissions changes: No changes
- [ ] Changes to paid features or tiers: No changes
- [ ] My device and fleetdm.com/better changes: No changes
- [x] First draft of test plan added
- [ ] Other reference documentation changes: No changes
- [ ] Once shipped, requester has been notified
- [ ] Once shipped, dogfooding issue has been filed
Engineering
- [ ] Test plan is finalized
- [ ] Contributor API changes: N/A
- [ ] Feature guide changes: Update the existing "Connect end users to Wi-Fi or VPN" guide here.
- @noahtalerman: In the guide, explain what happens when an end user comes back from vacation and their SCEP cert is expired and one-time code for the new cert is expired. The end user will have to connect to the corporate guest or their home Wi-Fi to have Fleet use a new one-time code to deliver a new certificate. Once the profile with the SCEP payload is "Verified" the user can connect to the company Wi-Fi or VPN.
- [ ] Database schema migrations: N/A
- [ ] Load testing: N/A
ℹ️ Please read this issue carefully and understand it. Pay special attention to UI wireframes, especially "dev notes".
QA
Risk assessment
- Requires load testing: no
- Risk level: Low
- Risk description: adding a feature to existing functionality
Test plan
Make sure to go through the list and consider all events that might be related to this story, so we catch edge cases earlier.
- @noahtalerman: The steps below were pulled from the "Deploy certificates from DigiCert and custom SCEP certificate authority on macOS" story (#25822) and tweaked for this story.
Happy path (custom SCEP)
- Go to Settings > Integrations > Certificates
- Select Add CA and select Custom Simple Certificate Enrollment Protocol (SCEP)
- Fill the form with necessary information and select Add CA. Use
SCEP_WIFIas name. - Create a configuration profile (SCEP), using an example from Apple docs here
- Replace challenge field with
$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_SCEP_WIFIand replace URL field with$FLEET_VAR_CUSTOM_SCEP_PROXY_URL_SCEP_WIFI. - Upload configuration profile to Fleet
- Verify that when the host requests a certificate, the request URL includes the one-time secret
- Go to host details and verify that the profile is installed
- Use a query to check if a SCEP certificate is installed on the host
API
- [ ] Make sure that if you hit
/mdm/scep/proxy/{host_uuid,profile_uuid,CA_name,one_time_secret}with an invalid secret, you get an easy to understand error message.
Testing notes
Confirmation
- [ ] Engineer: Added comment to user story confirming successful completion of test plan.
- [x] QA: Added comment to user story confirming successful completion of test plan.
For NDES, the URL path looks like /mdm/scep/proxy/{host_uuid,profile_uuid},NDES. So, the SCEP proxy needs to parse the path and check the profile to see if the one-time secret is needed.
Although we could also add this one-time secret to the NDES path, we probably shouldn't. NDES already has a one-time secret that it gets via admin URL, so adding a second one would be confusing and hard to maintain.
@getvictor, I'm confused by the terminology. Aren't these one-time codes essentially a form of dynamic SCEP challenge?
Are these one-time secrets being generated and managed entirely by Fleet? Or does Fleet depend on the IT admin-configured, upstream SCEP-provider for these secrets? If the latter, is there any API definition for how Fleet server will communicate with the upstream provider?
Yes, these are essentially dynamic SCEP challenges managed entirely by Fleet.
Proposed technical specifications:
DATASTORE
Reference implementation for dynamic SCEP challenges: https://github.com/jessepeterson/mysqlscepserver
We can try to follow some basic patterns from the reference implementation, in anticipation of potentially implementing dynamic SCEP challenges for Fleet-issued SCEP (e.g., enrollment profiles).
In our case, we'll also include time-based validations that aren't part of the reference implementation.
-- The scep_challenges table contains generated challenges created using the
-- API. If the challenge is successfully validated it is deleted from
-- this table (i.e. no longer valid). Ideally no outstanding unverified
-- challenges would exist in the table.
CREATE TABLE scep_challenges (
challenge CHAR(32),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (challenge)
);
// NewSCEPChallenge generates a random, base64-encoded challenge and insert it into the challenges
// table and insert new challenge into the challenges table
NewSCEPChallenge() (string, error)
// HasChallenge checks if a valid challenge exists in the challenges table
// and deletes it if it does. If the challenge does not exist or is not valid (i.e. expired),
// an error is returned.
HasChallenge(pw string) (bool, error)
// Optional: CleanupExpiredChallenges removes expired challenges from the challenges table,
// intended to be run as a cron job.
CleanupExpiredSCEPChallenges() (int, error)
SCEP PROXY SERVICE
In validateIdentifier, update the CAConfigCustomSCEPProxy case to check the SCEP challenge. If not valid, retry sending the MDM SCEP profile to the device. Specs say to only retry once, which will require some additional technical design to nail down.
PROFILE RECONCILIATION / FLEET VARIABLES
Update Fleet variable processing to include the dynamic challenge for SCEP proxy. Whenever InstallProfile with this variable is processed, must call NewSCEPChallenge to generate a new challenge and add it to the profile before sending it.
QUESTIONS:
Per initial product specs:
The one-time secret expires after 1 hour. If the one-time secret is expired, mark the profile as "Failed" in the UI and API (see error message in Figma). Fleet will try again once. If it fails again, the IT admin can resend the profile to try again.
- Does Fleet need to do this automatically, i.e. before the host tries to actually install the profile and use the challenge? That's more work and not consistent with the NDES implementation.
Per "happy path" from initial product specs:
Replace challenge field with $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_SCEP_WIFI and replace URL field with $$FLEET_VAR_CUSTOM_SCEP_PROXY_URL_SCEP_WIFI.
- Shouldn't we have a new variable for dynamic SCEP challenges? If not, what are we doing with the old challenge field?
Needs to be discussed further at DR to set expectations.
Needs to be discussed further at DR to set expectations.
@georgekarrv @ghernandez345 added this to #g-mdm design review for Tues (2025-05-26)
Because this stories subtasks are estimated, I moved them and this story to the estimated column. I removed the T-shirt size (S).
We already have a table where we store custom CA details, so I recommend adding this there. We might even have a challenge field already that we only use for NDES.
The static challenge is identified by a Fleet var and stored encrypted as part of app config.
The new dynamic challenge does not need a new var. we simply add it to the end of the SCEP proxy URL. This dynamic challenge is unique per host/profile/CA.
NDES currently gets the one time challenge in 2 places -- when profile goes to pending and then again when profile is sent to device. This seems more complex than needed. We should just generate the dynamic challenge right before we send it to the host.
QA Test Results
I was able to run thru the Happy Path test plan above and confirmed the host received the profile and cert as expected. Web traffic reveals that a one-time code was used
Upon resending the profile we can see that a new code was generated
@noahtalerman @allenhouchins Do we want to dogfood this issue? I assume we don't have the infrastructure to test this currently.
@marko-lisica we do! We stood up EJBCA: https://github.com/fleetdm/confidential/issues/10366
@marko-lisica, please file a dogfooding issue when you get the chance.
One-time secret code, Secures certificates' path, Trust in Fleet unfolds.