Skip to content

Fixing Windows SCEP issues - #47255

Merged
getvictor merged 9 commits into
mainfrom
victor/fix-windows-scep-challenge-and-validator-panic
Jun 15, 2026
Merged

Fixing Windows SCEP issues#47255
getvictor merged 9 commits into
mainfrom
victor/fix-windows-scep-challenge-and-validator-panic

Conversation

@getvictor

@getvictor getvictor commented Jun 10, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #47492 and Resolves #46982

  • Fixed panic when uploading bad profile
  • Added validation for SCEP challenge to exclude underscore (and other non-printable characters).

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • Bug Fixes

    • Prevented a server panic during Windows configuration profile validation when SCEP and non-SCEP elements are mixed; such profiles are now rejected with a clear validation error.
  • New Features

    • Enforced Windows-compatible printable characters for Custom SCEP proxy challenge values; rejects disallowed characters while preserving legacy values unless changed.
  • UI / Validation

    • Improved form validation feedback for the Custom SCEP challenge field, showing errors and disabling submit for invalid input while allowing masked/unchanged values.
  • Tests

    • Added regression and unit tests covering profile validation and challenge character validation.

@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Masked challenge overwrites secret 🐞 Bug ≡ Correctness
Description
validateCustomSCEPProxyUpdate treats fleet.MaskedPassword as an unchanged challenge, but
UpdateCertificateAuthority still forwards that value into caToUpdate.Challenge for persistence. If a
client submits "********" it can overwrite the real encrypted challenge, causing subsequent SCEP
enrollments to fail.
Code

ee/server/service/certificate_authorities.go[R1513-1518]

+	// Only validate the challenge characters when a new challenge value is provided. A nil or masked
+	// challenge means it is unchanged, so challenges stored before this validation existed keep working.
+	if customSCEP.Challenge != nil && *customSCEP.Challenge != fleet.MaskedPassword &&
+		!challengeHasOnlyPrintableStringChars(*customSCEP.Challenge) {
+		return &fleet.BadRequestError{Message: fmt.Sprintf("%s%s", errPrefix, scepChallengePrintableErrMsg)}
+	}
Evidence
The service’s update validator documents masked challenges as “unchanged” and skips
printable-character validation in that case, but the update handler still assigns the challenge
pointer directly to the outgoing update struct. The MySQL datastore update logic updates
challenge_encrypted for any non-nil challenge pointer, so a masked value can be persisted and
overwrite the true secret.

ee/server/service/certificate_authorities.go[1513-1518]
ee/server/service/certificate_authorities.go[1228-1243]
server/datastore/mysql/certificate_authorities.go[567-583]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`validateCustomSCEPProxyUpdate` explicitly treats a masked challenge (`fleet.MaskedPassword`, i.e. `"********"`) as “unchanged”, but the update flow still assigns that value into `caToUpdate.Challenge` and passes it to the datastore.
Because the MySQL datastore updates `challenge_encrypted` whenever `ca.Challenge != nil`, a client that submits the masked placeholder can inadvertently replace the real challenge with the masked placeholder, breaking Windows SCEP enrollment.
## Issue Context
- The service layer comment says masked == unchanged.
- The service layer does not normalize/strip masked secrets before persistence.
- The datastore layer has no special handling for `fleet.MaskedPassword` and will encrypt+persist any non-nil challenge.
## Fix Focus Areas
- ee/server/service/certificate_authorities.go[1228-1243]
- ee/server/service/certificate_authorities.go[1493-1518]
- server/datastore/mysql/certificate_authorities.go[567-583]
## Suggested fix approach
1. In `UpdateCertificateAuthority` (Custom SCEP proxy case), if `p.CustomSCEPProxyCAUpdatePayload.Challenge != nil` and equals `fleet.MaskedPassword`, treat it as unchanged for persistence (e.g., set `caToUpdate.Challenge = nil` so the datastore doesn’t update `challenge_encrypted`).
2. Optionally add a defensive guard in the datastore update builder to ignore `fleet.MaskedPassword` for secret fields to prevent similar issues across CA types.
3. Add/adjust tests to assert that sending a masked challenge does not change the stored challenge value.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Stale challenge schema comment ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The challenges table migration still documents challenges as base64.URLEncoding even though
NewChallenge now generates base32 challenges, which can mislead future maintenance/debugging and
schema reasoning.
Code

server/datastore/mysql/challenges.go[R15-18]

+// NewChallenge generates a random, base32-encoded challenge and inserts it into the challenges
// table. It returns the generated challenge or an error if the insertion fails.
func (ds *Datastore) NewChallenge(ctx context.Context) (string, error) {
return newChallenge(ctx, ds.writer(ctx))
Evidence
The datastore now encodes challenges with base32, but the table-creation migration comment still
states base64.URLEncoding, creating a direct contradiction in the codebase documentation.

server/datastore/mysql/challenges.go[15-33]
server/datastore/mysql/migrations/tables/20250609112613_AddChallengesTable.go[14-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `challenges` table migration comment says the `challenge` value is encoded with `base64.URLEncoding`, but the current implementation now generates **base32** challenges. This is stale documentation and can mislead future engineers.
### Issue Context
- `NewChallenge` was changed to generate base32 challenges to satisfy Windows SCEP CSP requirements.
- The schema migration that created the table still describes base64.
### Fix Focus Areas
- Update schema/migration comment to base32: 
- server/datastore/mysql/migrations/tables/20250609112613_AddChallengesTable.go[14-22]
- (Optional) Ensure any other inline docs match base32 wording:
- server/datastore/mysql/challenges.go[15-33]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copilot AI left a comment

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.

Pull request overview

This PR fixes two Windows SCEP-related issues in Fleet: (1) a server panic during Windows SCEP profile validation when non-SCEP <LocURI> elements appear before SCEP nodes, and (2) Windows enrollment failures caused by proxy-generated challenges containing base64url characters (_, -) that the Windows SCEP CSP rejects.

Changes:

  • Prevent windowsSCEPProfileValidator from indexing into placeholder empty LocURI arrays by reinitializing the “valid/required” LocURI lists when the first SCEP LocURI is encountered (even if earlier non-SCEP LocURIs were seen).
  • Generate custom SCEP proxy dynamic challenges using base32 (32 chars, [A-Z2-7]) to ensure the challenge is alphanumeric and compatible with Windows.
  • Add regression tests for both issues and add changes/ entries.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
server/fleet/windows_mdm.go Fixes SCEP profile validation logic to avoid panic on mixed LocURI ordering and ensure clean validation errors.
server/fleet/windows_mdm_test.go Adds regression tests covering the non-SCEP-first mixed LocURI panic scenarios.
server/fleet/datastore.go Updates Datastore interface comment to reflect base32 challenge encoding.
server/datastore/mysql/challenges.go Switches generated challenges from base64url to base32 to avoid Windows CSP “non-printable character” failures.
server/datastore/mysql/challenges_test.go Adds tests to enforce the generated challenge alphabet and basic consume behavior.
changes/46990-windows-scep-proxy-challenge-alphabet Release note entry for the Windows SCEP proxy challenge fix.
changes/46982-windows-scep-profile-validation-panic Release note entry for the Windows SCEP validation panic fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/datastore/mysql/challenges.go Outdated
Comment on lines 29 to 33
// Base32 keeps the challenge strictly alphanumeric: the Windows ClientCertificateInstall/SCEP CSP rejects
// base64url's '_' and '-' as non-printable characters, failing certificate enrollment. 20 random bytes
// (160 bits) encode to exactly 32 base32 characters with no padding, fitting the CHAR(32) column.
challenge := base32.StdEncoding.EncodeToString(key)
_, err = exec.ExecContext(ctx, `INSERT INTO challenges (challenge) VALUES (?)`, challenge)
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: d6f41bdf-78f1-4d9f-a15a-e0144d8b7557

📥 Commits

Reviewing files that changed from the base of the PR and between a88fbf2 and c31485a.

📒 Files selected for processing (2)
  • ee/server/service/certificate_authorities.go
  • frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/helpers.ts
  • ee/server/service/certificate_authorities.go

Walkthrough

This PR fixes two Windows SCEP issues. First, setLocURIArrays now reinitializes SCEP arrays when the first SCEP is encountered so mixed profiles with a non-SCEP LocURI first no longer cause an index-out-of-range panic and instead yield a validation error. Second, custom SCEP proxy CA challenges are validated against an ASN.1 PrintableString-compatible regex: the backend enforces this on creation and when a challenge is changed (including batch apply), the update path skips masked/unchanged challenges, and the frontend form shows the same validation message.

Possibly related issues

  • #46990: The PrintableString challenge validation directly addresses Windows enrollment failures caused by non-printable characters (e.g., underscore) in custom SCEP proxy challenges.

Possibly related PRs

  • fleetdm/fleet#46029: Modifies Windows MDM LocURI parsing/validation in server/fleet/windows_mdm.go and is closely related to the SCEP LocURI validation changes in this PR.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and does not clearly convey the specific changes or primary objectives of the PR. Use a more descriptive title that specifically mentions the two issues being fixed, such as 'Fix Windows SCEP panic and validate challenge characters' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description references both linked issues, indicates changes files were added, confirms input validation was performed, and states tests were added and manual QA was completed.
Linked Issues check ✅ Passed Code changes comprehensively address both issue objectives: #46982 panic fix with improved validation logic in windows_mdm.go and regression tests, #47492 character validation with UI and backend checks plus extensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the two linked issues: SCEP profile validation panic fix, printable character validation for challenges, and associated test coverage.

✏️ 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 victor/fix-windows-scep-challenge-and-validator-panic

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.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.19%. Comparing base (ed42b7b) to head (c31485a).
⚠️ Report is 96 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #47255      +/-   ##
==========================================
- Coverage   67.19%   67.19%   -0.01%     
==========================================
  Files        3068     3489     +421     
  Lines      226815   228550    +1735     
  Branches    11721    11912     +191     
==========================================
+ Hits       152418   153577    +1159     
- Misses      60656    61148     +492     
- Partials    13741    13825      +84     
Flag Coverage Δ
backend 68.83% <100.00%> (+0.01%) ⬆️
frontend 57.97% <100.00%> (+0.14%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 12, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9f8bf3c

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Comment thread ee/server/service/certificate_authorities_test.go
Comment thread ee/server/service/certificate_authorities_test.go

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/CustomSCEPForm.tests.tsx (1)

118-118: ⚡ Quick win

Use the shared unchanged-password constant instead of a hardcoded mask literal.

"********" in this test can drift from runtime sentinel behavior. Import and use UNCHANGED_PASSWORD_API_RESPONSE to keep tests coupled to the real contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/CustomSCEPForm.tests.tsx`
at line 118, Replace the hardcoded mask literal "********" with the shared
sentinel constant by importing UNCHANGED_PASSWORD_API_RESPONSE and passing it to
createTestFormData for the formData prop; update the test in
CustomSCEPForm.tests.tsx to import UNCHANGED_PASSWORD_API_RESPONSE and use
createTestFormData({ challenge: UNCHANGED_PASSWORD_API_RESPONSE }) so the test
stays consistent with the runtime API contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/CustomSCEPForm.tests.tsx`:
- Line 118: Replace the hardcoded mask literal "********" with the shared
sentinel constant by importing UNCHANGED_PASSWORD_API_RESPONSE and passing it to
createTestFormData for the formData prop; update the test in
CustomSCEPForm.tests.tsx to import UNCHANGED_PASSWORD_API_RESPONSE and use
createTestFormData({ challenge: UNCHANGED_PASSWORD_API_RESPONSE }) so the test
stays consistent with the runtime API contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 80ad700d-aa95-4442-975c-6caf902db01f

📥 Commits

Reviewing files that changed from the base of the PR and between ed42b7b and 9f8bf3c.

📒 Files selected for processing (8)
  • changes/46982-windows-scep-profile-validation-panic
  • ee/server/service/certificate_authorities.go
  • ee/server/service/certificate_authorities_test.go
  • frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/CustomSCEPForm.tests.tsx
  • frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/CustomSCEPForm.tsx
  • frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CustomSCEPForm/helpers.ts
  • server/fleet/windows_mdm.go
  • server/fleet/windows_mdm_test.go

Comment thread ee/server/service/certificate_authorities.go Outdated
@getvictor
getvictor marked this pull request as ready for review June 12, 2026 09:27
@getvictor
getvictor requested review from a team as code owners June 12, 2026 09:27
// Only validate the challenge characters when the challenge is new or changed, so that challenges stored before this validation
// existed continue to work.
existing, exists := existingByName[name]
challengeChanged := !exists || existing == nil || incoming.Challenge != existing.Challenge

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.

non-blocking but there is no code path where exists = true AND existing == nil, so we could probably shorten this to challengeChanged := !exists || incoming.Challenge != existing.Challenge

@getvictor
getvictor merged commit 4fdd4bd into main Jun 15, 2026
45 checks passed
@getvictor
getvictor deleted the victor/fix-windows-scep-challenge-and-validator-panic branch June 15, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants