Skip to content

Apple non-proxied SCEP profile validation (PR 7/9)#45364

Merged
mostlikelee merged 18 commits into
40639-cert-renewfrom
pr-2.3b-raw-scep-validation
May 13, 2026
Merged

Apple non-proxied SCEP profile validation (PR 7/9)#45364
mostlikelee merged 18 commits into
40639-cert-renewfrom
pr-2.3b-raw-scep-validation

Conversation

@mostlikelee
Copy link
Copy Markdown
Contributor

@mostlikelee mostlikelee commented May 13, 2026

Related issue: Resolves #45223

What this PR does

Adds additionalNonProxiedSCEPValidation: any profile containing a com.apple.security.scep payload AND no Fleet proxy variables must carry $FLEET_VAR_CERTIFICATE_RENEWAL_ID in the cert Subject OU, or the upload is rejected. Profiles using Fleet proxy variables (NDES / Custom SCEP / Smallstep) keep their existing per-CA validators.

Why this is a follow-up to #45043

PR #45043 (2.3) added the ACME validator. Same renewal-ID requirement, same Decision 2.7 rationale, but it only fires on com.apple.security.acme payloads. Raw-SCEP profiles (literal Challenge/URL, no Fleet proxy vars — the Okta SCEP / Okta Verify-static-challenge flows named in #40639) still bypass all renewal-ID enforcement on main. This PR closes that gap.

What's new vs PR 2.3

  • OU-only: the marker must live in Subject OU. CN placement is rejected. Net-new validator owes no back-compat for CN placement; aligns behavior with the "must be in the OU" error message that every existing validator already shows.
  • Preferred-only: only $FLEET_VAR_CERTIFICATE_RENEWAL_ID is accepted. The legacy $FLEET_VAR_SCEP_RENEWAL_ID is rejected. Same net-new reasoning.
  • Bypass broadening: validateConfigProfileFleetVariables's payload check now recognizes both ACME and SCEP via mobileconfig.HasPayloadType so the new validator runs before the fleetVars early-return.

Checklist for submitter

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements)

Testing

  • Added/updated automated tests

Summary by CodeRabbit

  • Bug Fixes

    • Improved validation for SCEP certificate configuration profiles to ensure renewal ID markers are properly configured.
    • Enhanced detection of invalid SCEP configurations to prevent misconfigurations.
  • Tests

    • Added comprehensive test coverage for SCEP profile validation across multiple configuration scenarios.

Review Change Stack

mostlikelee added 17 commits May 8, 2026 11:17
Two related changes that prepare Apple profile uploads for the non-proxied
renewal flow:

1. Add `$FLEET_VAR_CERTIFICATE_RENEWAL_ID` as the preferred name for the
   renewal-ID marker variable (per docs PR #44069). The legacy
   `$FLEET_VAR_SCEP_RENEWAL_ID` is still accepted by validation and
   substitution for back-compat with existing profiles. Both names
   substitute to the same value: "fleet-" + profile_uuid.

2. Add additionalACMEValidation: any profile containing a
   com.apple.security.acme payload must include either renewal-ID
   variable in the cert Subject's CN or OU, otherwise the upload is
   rejected. Without the marker the resulting cert can't be linked back
   to its profile and would never auto-renew.

The shared SCEP-renewal-ID-without-URL/Challenge guard in
mdm_profiles.go skips its check when an ACME payload is present —
ACME profiles use the marker variable alone (no SCEP URL/Challenge
companions).

Validation error messages reference only the new
CERTIFICATE_RENEWAL_ID name to steer new authoring toward it.
The validation error messages in mdm_profiles.go switched to reference
the preferred CERTIFICATE_RENEWAL_ID name. The unit and integration
test assertions still required the legacy SCEP_RENEWAL_ID text.
Updating them to match.
Decision 2.7 requires profile validation to keep accepting the
pre-rename variable name. The existing failure-case tests don't
guard that contract because they all exercise error paths. Add a
happy-path case asserting that a Custom SCEP profile authored with
$FLEET_VAR_SCEP_RENEWAL_ID validates successfully when all other
required variables are present.
The renewal-ID-only bypass in validateProfileCertificateAuthorityVariables
checked the profile body with strings.Contains, which would false-positive
on "com.apple.security.acme" appearing in a description, identifier, or
custom string field. Switch to the existing Mobileconfig.HasPayloadType
helper for a structured plist parse. Parse failure (e.g., Windows-path
content that isn't a mobileconfig) is treated as no-ACME, so the SCEP-only
error fires normally on that path.
…aw-scep-validation

# Conflicts:
#	server/fleet/mdm.go
#	server/service/apple_mdm.go
#	server/service/apple_mdm_test.go
Reject raw-SCEP profiles (com.apple.security.scep payload, no Fleet
proxy variables) whose Subject OU lacks $FLEET_VAR_CERTIFICATE_RENEWAL_ID.
Profiles using Fleet proxy variables (NDES/Custom SCEP/Smallstep) keep
their existing per-CA validators.

Net-new validator: OU placement only, preferred variable name only —
no back-compat owed on a surface that didn't previously enforce
renewal-ID at all.

Also broadens the validateConfigProfileFleetVariables bypass to recognize
both ACME and SCEP payloads via mobileconfig.HasPayloadType.
@mostlikelee
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc201bfd-9104-4468-ac6b-b8a41cae6984

📥 Commits

Reviewing files that changed from the base of the PR and between 430df5d and c354fba.

📒 Files selected for processing (3)
  • server/service/apple_mdm.go
  • server/service/apple_mdm_test.go
  • server/service/mdm_profiles.go

Walkthrough

This PR implements validation for non-proxied SCEP profiles to require a certificate auto-renewal marker ($FLEET_VAR_CERTIFICATE_RENEWAL_ID in the SCEP certificate Subject OrganizationalUnit). The change introduces a new helper function additionalNonProxiedSCEPValidation invoked during profile upload validation, which parses raw SCEP payloads and rejects those lacking the renewal-ID requirement while deferring to per-CA validators for profiles using Fleet proxy CA variables. Related validation bypass logic in mdm_profiles.go now recognizes both ACME and raw SCEP payloads when determining renewal-ID-only profiles. Comprehensive test coverage validates the preferred renewal-ID variable, rejects the legacy variable name, and confirms correct behavior across proxy and non-proxy scenarios.

Possibly related PRs

  • fleetdm/fleet#45043: Introduces the new preferred renewal-marker variable name ($FLEET_VAR_CERTIFICATE_RENEWAL_ID) and associated regex that this PR now enforces in non-proxied SCEP certificate validation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding validation for Apple non-proxied SCEP profiles with a renewal-ID requirement.
Description check ✅ Passed The description covers the main objective, relates it to prior work (#45043), explains what's new, provides rationale, and marks required checklist items; however, it is missing the 'Changes file added' checklist item and some optional testing sections.
Linked Issues check ✅ Passed The PR implements the coding requirement from #45223: rejecting non-proxied SCEP profiles lacking the renewal-ID marker in cert Subject OU, while preserving per-CA validation for proxied profiles.
Out of Scope Changes check ✅ Passed All changes (new validation function, test coverage, payload detection refactor) are directly in scope for implementing non-proxied SCEP profile validation per #45223.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-2.3b-raw-scep-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mostlikelee mostlikelee marked this pull request as ready for review May 13, 2026 13:24
@mostlikelee mostlikelee requested a review from a team as a code owner May 13, 2026 13:24
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 83.87097% with 5 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (40639-cert-renew@b649599). Learn more about missing BASE report.

Files with missing lines Patch % Lines
server/service/apple_mdm.go 83.33% 2 Missing and 2 partials ⚠️
server/service/mdm_profiles.go 85.71% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##             40639-cert-renew   #45364   +/-   ##
===================================================
  Coverage                    ?   66.74%           
===================================================
  Files                       ?     2730           
  Lines                       ?   218567           
  Branches                    ?    10703           
===================================================
  Hits                        ?   145879           
  Misses                      ?    59467           
  Partials                    ?    13221           
Flag Coverage Δ
backend 68.60% <83.87%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@sgress454 sgress454 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! It feels like when we're done here there's gonna be a fair amount of overlap/duplicated code between proxy-less SCEP and proxy-less ACME, that might be worth pulling into shared helpers at some point.

Comment on lines +795 to +798
strings.HasPrefix(v, string(fleet.FleetVarCustomSCEPChallengePrefix)) ||
strings.HasPrefix(v, string(fleet.FleetVarCustomSCEPProxyURLPrefix)) ||
strings.HasPrefix(v, string(fleet.FleetVarSmallstepSCEPChallengePrefix)) ||
strings.HasPrefix(v, string(fleet.FleetVarSmallstepSCEPProxyURLPrefix)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duck-typing is now repeated both here and in validateProfileCertificateAuthorityVariables, so they'll have to be kept in sync if any new kind of SCEP proxy is implemented. Not sure how likely that is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, i'll make a note

@mostlikelee mostlikelee merged commit 2f73475 into 40639-cert-renew May 13, 2026
46 of 48 checks passed
@mostlikelee mostlikelee deleted the pr-2.3b-raw-scep-validation branch May 13, 2026 16:58
mostlikelee added a commit that referenced this pull request May 14, 2026
… 1.4)

Status sweep:
- PR 2.1 (#44344), PR 2.2 (#44345), PR 2.3 (#45043), PR 2.3b
  (#45364), PR 2.4 (#45237) all merged — task lists in §5/§6/§7/§7b/§8
  now reflect completion. Items deferred to PR 2.5 (release notes) stay
  open.
- §10.1 annotated with the 2026-05-14 local end-to-end validation
  result. Hydrant verification still required for customer readiness.

New refinement — Decision 1.4 and §7c (PR 2.3c):
- Surfaced during local validation: a user-installed Root CA ended up
  with origin=mdm because MDM CertificateList returned the entire
  keychain on InstallProfile ack, beating the next osquery sync.
- Decision: osquery sync downgrades existing origin=mdm rows to
  origin=osquery when it also observes them. One-way (mdm → osquery,
  never reverse).
- Rationale: osquery sees a strict superset of MDM CertificateList,
  so observation by both implies the cert is in the device keychain
  by some path — not evidence of MDM delivery. Origin should reflect
  "did Fleet deliver this?", not "who saw it first?"
- Renewal correctness is unaffected; matcher keys on the
  fleet-<profile_uuid> marker, not on origin.
mostlikelee added a commit that referenced this pull request May 15, 2026
…ision 2.6)

Removes the upload-time renewal-ID enforcement that PR 2.3 (#45043) and
PR 2.3b (#45364) initially shipped. Marker becomes purely advisory:
profiles that include it activate auto-renewal; profiles that don't
continue to work as in 4.85, unchanged.

Rationale (Decision 2.6 reversal):

- GitOps trap: `fleetctl gitops apply` on existing customer profiles
  (Conditional Access, Okta Verify, ACME) would fail after upgrade
  because pre-4.86 profiles lack the marker.
- UI-edit trap: opening an existing profile and saving an unrelated
  change would be rejected.
- Conditional Access: Fleet's own generated profile lacks the marker
  (#45580), so customers following the published guide can't deploy.

The cost of hard rejection — broken upgrades for customers who don't
even use auto-renewal — exceeded the benefit of catching the
misconfiguration that's only relevant to opt-in customers.

Spec updates:

- Decision 2.6: reversed entirely. Documents validator removal,
  GitOps-continuity rationale, and alternatives considered.
- Decision 2.7: collapsed the "net-new surfaces accept preferred-only,
  OU-only" enforcement rules. Variable rename + substitution back-compat
  still documented.
- Decision 1.2, 2.5: softened "customers must redeploy" to "customers
  may opt in by redeploying."
- §7 / §7b task lists: struck through reverted tasks, kept history.
- §7d added: revert tasks for removing the two Apple validators.
- §9 (docs): framing shifted from "required migration" to "opt-in
  capability." Existing customers continue unchanged unless they want
  auto-renewal.
- §10.6 / §10.7: validation-rejection QA replaced with opt-out path
  verification.

Code revert work (§7d) will follow on a separate PR branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Apple non-proxied SCEP profile validation requires renewal-ID marker (sub-task of #40639)

2 participants