Skip to content

Tighten conditional-access SCEP challenge validation#48041

Open
sharon-fdm wants to merge 3 commits into
mainfrom
worktree-fix-condaccess-scep-scope
Open

Tighten conditional-access SCEP challenge validation#48041
sharon-fdm wants to merge 3 commits into
mainfrom
worktree-fix-condaccess-scep-scope

Conversation

@sharon-fdm

@sharon-fdm sharon-fdm commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Related issue: N/A

Checklist for submitter

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

  • Timeouts are implemented and retries are limited to avoid infinite loops

  • If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes

Testing

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

Summary

Tightens validation in the conditional-access SCEP challenge middleware to reject enrollment requests that use a secret outside the expected scope. Adds unit test coverage for the middleware.

Reproduction

The challengeMiddleware in ee/server/service/condaccess/scep.go calls ds.VerifyEnrollSecret() but discards the returned secret (_, err :=), so it only checks that some valid enroll secret exists. It does not verify the secret's scope. Any valid secret from any scope passes the challenge.

This was confirmed with a unit test using mock secrets scoped to different teams. Before the fix, the middleware accepted all of them indiscriminately. The server-side profile generation (server/service/conditional_access_idp.go) only ever embeds a global-scope secret as the SCEP challenge, so only global secrets should be accepted.

How it was tested

  1. Unit tests (scep_test.go) - table-driven test with 5 cases exercising the challengeMiddleware directly:

    • Empty challenge -> rejected ("missing challenge")
    • Unknown secret -> rejected ("invalid challenge")
    • Team-scoped secret (team A) -> rejected ("invalid challenge") [new behavior]
    • Team-scoped secret (team B) -> rejected ("invalid challenge") [new behavior]
    • Global secret (team_id = nil) -> accepted, signer invoked, cert returned
  2. Ran go test -v ./ee/server/service/condaccess/ - all tests pass.

  3. Ran make lint-go-incremental - 0 issues.

Summary by CodeRabbit

  • Bug Fixes

    • Improved validation for conditional access SCEP enrollment, including rejecting team-scoped enrollment secrets.
    • Updated challenge middleware behavior to better handle enrollment secret verification outcomes and associated error messaging.
  • Tests

    • Added comprehensive coverage for conditional access SCEP challenge validation, including cases for missing, unknown, team-scoped, and global enrollment secrets, plus signer invocation expectations.

Restrict SCEP enrollment to global enroll secrets only, rejecting
team-scoped secrets. Add unit tests for the challenge middleware.
@sharon-fdm sharon-fdm marked this pull request as ready for review June 22, 2026 18:55
@sharon-fdm sharon-fdm requested a review from a team as a code owner June 22, 2026 18:55
Copilot AI review requested due to automatic review settings June 22, 2026 18:55

@claude claude Bot left a comment

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Jun 22, 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: 5ebd336c-3538-4b63-8fba-e196ce5cbc56

📥 Commits

Reviewing files that changed from the base of the PR and between ab8786b and 31cbfbd.

📒 Files selected for processing (1)
  • ee/server/service/condaccess/scep_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • ee/server/service/condaccess/scep_test.go

Walkthrough

challengeMiddleware in ee/server/service/condaccess/scep.go is updated to capture the record returned by ds.VerifyEnrollSecret and reject the challenge with "invalid challenge" when secret.TeamID is non-nil, ensuring only global enrollment secrets are accepted. A new table-driven test (TestChallengeMiddleware) covers empty, unknown, team-scoped, and global-secret challenge inputs, verifying error content and whether the CSR signer is invoked. A changelog entry records the validation improvement.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 summarizes the main change—tightening SCEP challenge validation by rejecting team-scoped secrets.
Description check ✅ Passed The description fully covers all required sections: related issue, completed checklist items, clear summary, reproduction steps, and testing details with specific test cases.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 worktree-fix-condaccess-scep-scope

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.

@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)
ee/server/service/condaccess/scep_test.go (1)

21-67: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a generic datastore-error test case to cover the remaining middleware branch.

The table currently misses the err != nil && !IsNotFound(err) path ("verifying enrollment secret: %w"), so a regression there would be untested.

Suggested patch
 import (
 	"context"
 	"crypto/x509"
+	"errors"
 	"testing"
@@
 	cases := []struct {
 		name           string
 		challenge      string
 		wantErr        string
 		wantSignCalled bool
 	}{
@@
 		{
 			name:      "unknown secret is rejected",
 			challenge: "unknown-secret",
 			wantErr:   "invalid challenge",
 		},
+		{
+			name:      "datastore error is returned as verification failure",
+			challenge: "datastore-error",
+			wantErr:   "verifying enrollment secret",
+		},
@@
 			ds.VerifyEnrollSecretFunc = func(_ context.Context, secret string) (*fleet.EnrollSecret, error) {
 				switch secret {
 				case "secret-team-a":
 					return &fleet.EnrollSecret{Secret: secret, TeamID: &teamAID}, nil
 				case "secret-team-b":
 					return &fleet.EnrollSecret{Secret: secret, TeamID: &teamBID}, nil
 				case "global-secret":
 					return &fleet.EnrollSecret{Secret: secret, TeamID: nil}, nil
+				case "datastore-error":
+					return nil, errors.New("db unavailable")
 				default:
 					return nil, common_mysql.NotFound("enroll_secret")
 				}
 			}
🤖 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 `@ee/server/service/condaccess/scep_test.go` around lines 21 - 67, Add a new
test case to the cases table that covers the generic datastore error path. Add a
test case with a unique challenge value (like "db-error") that returns a
non-NotFound error from the VerifyEnrollSecretFunc mock. In the
VerifyEnrollSecretFunc switch statement, handle this challenge value by
returning a generic error (not common_mysql.NotFound), and set the wantErr field
in the test case to match the expected error message format "verifying
enrollment secret:". This will ensure the err != nil && !IsNotFound(err) code
path is tested for regressions.
🤖 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 `@ee/server/service/condaccess/scep_test.go`:
- Around line 21-67: Add a new test case to the cases table that covers the
generic datastore error path. Add a test case with a unique challenge value
(like "db-error") that returns a non-NotFound error from the
VerifyEnrollSecretFunc mock. In the VerifyEnrollSecretFunc switch statement,
handle this challenge value by returning a generic error (not
common_mysql.NotFound), and set the wantErr field in the test case to match the
expected error message format "verifying enrollment secret:". This will ensure
the err != nil && !IsNotFound(err) code path is tested for regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a1b75e16-5849-40fc-90c0-df0b396debde

📥 Commits

Reviewing files that changed from the base of the PR and between 59f33dc and ab8786b.

📒 Files selected for processing (3)
  • changes/fix-condaccess-scep-validation
  • ee/server/service/condaccess/scep.go
  • ee/server/service/condaccess/scep_test.go

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.

Warning

  • Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.

Pull request overview

This PR tightens conditional-access SCEP challenge validation by ensuring only global enrollment secrets (team_id NULL) are accepted by the conditional-access SCEP middleware, preventing team-scoped secrets from being used to satisfy the SCEP challenge.

Changes:

  • Update challengeMiddleware to reject team-scoped enroll secrets even if they are otherwise valid.
  • Add unit tests covering missing/unknown/team-scoped/global secret challenge scenarios.
  • Add a changes entry (changes/fix-condaccess-scep-validation) (content excluded by policy).

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 1 comment.

File Description
ee/server/service/condaccess/scep.go Enforces that conditional-access SCEP challenges only accept global (teamless) enroll secrets.
ee/server/service/condaccess/scep_test.go Adds table-driven unit coverage for the middleware’s new secret-scope validation behavior.
changes/fix-condaccess-scep-validation User-visible changes entry (not reviewed here due to content exclusion policy).
Files excluded by content exclusion policy (1)
  • changes/fix-condaccess-scep-validation

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

Comment on lines +83 to +92
if tc.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
assert.False(t, signCalled, "signer should not be called on error")
assert.Nil(t, cert)
} else {
require.NoError(t, err)
assert.True(t, signCalled)
assert.NotNil(t, cert)
}
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.31%. Comparing base (59f33dc) to head (31cbfbd).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main   #48041    +/-   ##
========================================
  Coverage   67.31%   67.31%            
========================================
  Files        3655     3655            
  Lines      231242   231241     -1     
  Branches    12224    12074   -150     
========================================
+ Hits       155658   155660     +2     
  Misses      61618    61618            
+ Partials    13966    13963     -3     
Flag Coverage Δ
backend 68.94% <100.00%> (+<0.01%) ⬆️

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.

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.

2 participants