Skip to content

Don't require end user auth on orbit re-enrollment (#46300) - #47740

Merged
getvictor merged 3 commits into
mainfrom
fix-46300-orbit-reenroll-eua
Jun 18, 2026
Merged

Don't require end user auth on orbit re-enrollment (#46300)#47740
getvictor merged 3 commits into
mainfrom
fix-46300-orbit-reenroll-eua

Conversation

@getvictor

@getvictor getvictor commented Jun 17, 2026

Copy link
Copy Markdown
Member

Windows and Linux hosts that had already orbit-enrolled were prompted for end user authentication (an SSO browser tab) when fleetd re-enrolled after a service restart, node key file loss, or osquery DB rebuild. Hosts enrolled before EUA was enabled have no host_mdm_idp_accounts row, so the service-layer EUA gate treated every re-enroll like a brand-new device.

Before returning END_USER_AUTH_REQUIRED, EnrollOrbit now checks whether a host matching the enrollment identifiers already exists and previously held an orbit node key (HostPreviouslyOrbitEnrolled, reusing matchHostDuringEnrollment's semantics). If so, the re-enroll proceeds without prompting. Genuinely new devices, and hosts moved to a different Fleet server, are still gated.

Related issue: Resolves #46300

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.

Testing

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

Summary by CodeRabbit

Summary by CodeRabbit

Bug Fixes

  • Fixed unnecessary end-user authentication prompts for Windows and Linux hosts during fleetd re-enrollment after a service restart. Previously enrolled devices can now re-enroll without being prompted for SSO authentication, while new devices still require the appropriate authentication.

Windows and Linux hosts that had already orbit-enrolled were prompted for
end user authentication (an SSO browser tab) when fleetd re-enrolled after a
service restart, node key file loss, or osquery DB rebuild. Hosts enrolled
before EUA was enabled have no host_mdm_idp_accounts row, so the service-layer
EUA gate treated every re-enroll like a brand-new device.

Before returning END_USER_AUTH_REQUIRED, EnrollOrbit now checks whether a host
matching the enrollment identifiers already exists and previously held an orbit
node key (HostPreviouslyOrbitEnrolled, reusing matchHostDuringEnrollment's
semantics). If so, the re-enroll proceeds without prompting. Genuinely new
devices, and hosts moved to a different Fleet server, are still gated.
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 17, 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 17, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. EUA bypass via spoofing 🐞 Bug ⛨ Security
Description
Service.EnrollOrbit now skips END_USER_AUTH_REQUIRED when HostPreviouslyOrbitEnrolled returns true
(existing matched host row with orbit_node_key set), so a client that can spoof matching enrollment
identifiers can re-enroll without end-user authentication. This enables unauthorized re-enrollment
and rotates/overwrites the matched host’s orbit_node_key, effectively taking over that host’s Orbit
credentials if the attacker also has a valid enroll secret.
Code

server/service/orbit.go[R273-293]

+					// A host that already exists in Fleet and was previously orbit-enrolled is re-enrolling (e.g. after a
+					// service restart, node key file loss, or osquery DB rebuild), not enrolling for the first time. We must not
+					// prompt for end user authentication again: it would interrupt the user on every restart, and it also
+					// grandfathers hosts that enrolled before end user authentication was enabled (those have no
+					// host_mdm_idp_accounts row). A genuinely new device, or one moved to a different Fleet server (where it
+					// won't be found here), is not matched and is still gated. See https://github.com/fleetdm/fleet/issues/46300.
+					//
+					// Security note: matching by hardware UUID is not a strong identity check, but this does not introduce a new
+					// attack vector. EnrollOrbit already takes over an existing host row on a duplicate-UUID enrollment
+					// regardless of this gate, and hosts that already have an IdP account skip it. Requiring the matched host to
+					// have previously held an orbit node key keeps the exemption to hosts that genuinely enrolled before.
+					previouslyEnrolled, err := svc.ds.HostPreviouslyOrbitEnrolled(ctx, hostInfo, appConfig.MDM.EnabledAndConfigured)
+					if err != nil {
+						return "", fleet.OrbitError{Message: "failed to check for prior orbit enrollment: " + err.Error()}
+					}
+					if !previouslyEnrolled {
+						// Otherwise report the unauthenticated host and let Orbit handle it (e.g. by prompting the user to authenticate).
+						return "", fleet.NewOrbitIDPAuthRequiredError()
+					}
+					svc.logger.InfoContext(ctx, "allowing re-enrollment without end-user authentication: host previously orbit-enrolled",
+						"host_uuid", hostInfo.HardwareUUID)
Evidence
The service EUA gate now calls HostPreviouslyOrbitEnrolled and proceeds without EUA when it returns
true. HostPreviouslyOrbitEnrolled uses the same enrollment matching as EnrollOrbit and returns
whether the matched row has orbit_node_key set (NodeKeySet), where matching is primarily by
hosts.osquery_host_id derived from request identifiers; EnrollOrbit then overwrites orbit_node_key
for the matched host row, enabling credential takeover when identifiers are spoofed.

server/service/orbit.go[235-305]
server/datastore/mysql/hosts.go[2521-2549]
server/datastore/mysql/hosts.go[2265-2279]
server/datastore/mysql/hosts.go[2364-2410]

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

## Issue description
Orbit enrollment now exempts “previously orbit-enrolled” hosts from End User Authentication (EUA) purely based on datastore host matching and `orbit_node_key IS NOT NULL`. Because enrollment matching is based on spoofable identifiers (primarily `osquery_host_id` derived from `hostInfo.OsqueryIdentifier`/`HardwareUUID`), a party with an enroll secret can impersonate an existing previously-orbit-enrolled host and bypass EUA, causing the server to overwrite/rotate that host’s `orbit_node_key`.
## Issue Context
- The service handler uses `HostPreviouslyOrbitEnrolled` to decide whether to return `END_USER_AUTH_REQUIRED`.
- The datastore match uses `matchHostDuringEnrollment` which matches by `hosts.osquery_host_id = ?` (derived from request identifiers) and considers `orbit_node_key IS NOT NULL` as “previously enrolled”.
- `EnrollOrbit` updates `hosts.orbit_node_key = ?` for the matched row.
## Fix Focus Areas
- server/service/orbit.go[273-294]
- server/datastore/mysql/hosts.go[2521-2549]
- server/datastore/mysql/hosts.go[2244-2279]
- server/datastore/mysql/hosts.go[2364-2410]
## Suggested fix directions
Choose one (or combine):
1. **Require stronger proof before skipping EUA**: only allow the exemption when the request is authenticated by a strong device identity signal (e.g., TPM identity cert + HTTP signature already validated earlier in the handler). If no strong signal is present, keep returning `END_USER_AUTH_REQUIRED`.
2. **Bind exemption to a server-issued re-enroll credential**: mint and persist a dedicated long-lived “re-enroll token” on first successful enrollment and require it for future re-enroll exemptions (distinct from `orbit_node_key` if you need to handle node key loss).
3. **Constrain exemption to safer cases**: e.g., require the matched host to be recently active (last_enrolled_at/seen_time within a bounded window) and/or ensure team/enroll secret association matches the existing host row before exempting.
Implement the chosen approach and add/adjust tests to cover the spoofing/bypass scenario (negative test) and the legitimate re-enroll scenario (positive test).

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


Grey Divider

Qodo Logo

Comment thread server/service/orbit.go
@coderabbitai

coderabbitai Bot commented Jun 17, 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: 4054aaa6-4567-469d-8556-708836d1359b

📥 Commits

Reviewing files that changed from the base of the PR and between 0c89b1c and e645ab3.

📒 Files selected for processing (5)
  • server/datastore/mysql/hosts.go
  • server/datastore/mysql/hosts_test.go
  • server/fleet/datastore.go
  • server/service/integration_enterprise_test.go
  • server/service/orbit.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/datastore/mysql/hosts_test.go
  • server/service/integration_enterprise_test.go
  • server/datastore/mysql/hosts.go
  • server/fleet/datastore.go

Walkthrough

The PR fixes Windows/Linux hosts being prompted for end-user authentication (EUA) when fleetd restarts and re-enrolls with the same credentials. A new HostPreviouslyOrbitEnrolled method is added to the Datastore interface and implemented in the MySQL datastore; it uses the existing matchHostDuringEnrollment logic with Windows serial-matching disabled to check whether a host already has an orbit node key. In EnrollOrbit, the existing EUA gate that fires when no IdP account is found for a host UUID now calls this method first: already-enrolled hosts are allowed through, while genuinely new devices still receive END_USER_AUTH_REQUIRED. Mock wiring, datastore unit tests, an integration test covering both linux and windows re-enrollment, and a changelog entry are also included.

Possibly related PRs

  • fleetdm/fleet#45951: Both PRs modify the end-user authentication flow in EnrollOrbit, directly controlling when EUA prompts are triggered during Orbit enrollment.
  • fleetdm/fleet#46008: Modifies matchHostDuringEnrollment in server/datastore/mysql/hosts.go, the same host-matching function that HostPreviouslyOrbitEnrolled relies on to determine if a host was previously enrolled.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 concisely summarizes the main change: preventing end user auth requirement during orbit re-enrollment, directly addressing issue #46300.
Description check ✅ Passed The description is substantially complete with context explaining the bug, the solution, and relevant checklist items completed (changes file and testing). However, it lacks specific validation details about input data and database migration checks required by the template.
Linked Issues check ✅ Passed The PR directly addresses issue #46300 by implementing HostPreviouslyOrbitEnrolled to skip EUA prompts for previously-enrolled hosts, allowing re-enrollment without authentication unless the host is genuinely new.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the objective: adding HostPreviouslyOrbitEnrolled datastore method, updating EnrollOrbit logic, adding integration tests, and including a release note. No unrelated changes detected.

✏️ 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 fix-46300-orbit-reenroll-eua

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.43.0)
server/service/integration_enterprise_test.go

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)
server/service/integration_enterprise_test.go (1)

31733-31748: ⚡ Quick win

Make EUA reset unconditional to avoid cross-subtest state leakage.

If any require.* fails before Line 31745, the manual reset block never runs, so the next subtest can inherit EnableEndUserAuthentication=true and fail for the wrong reason. Register cleanup immediately after enabling EUA so reset always happens.

Suggested diff
 		team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true
 		_, err = s.ds.SaveTeam(ctx, team)
 		require.NoError(t, err)
+		t.Cleanup(func() {
+			team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = false
+			_, cleanupErr := s.ds.SaveTeam(context.Background(), team)
+			require.NoError(t, cleanupErr)
+		})
@@
-		// Reset team EUA for the next subtest's first enrollment.
-		team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = false
-		_, err = s.ds.SaveTeam(ctx, team)
-		require.NoError(t, err)
 	}
🤖 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 `@server/service/integration_enterprise_test.go` around lines 31733 - 31748,
The manual reset of EnableEndUserAuthentication to false at the end of the test
block does not run if any require statement fails before it, causing the next
subtest to inherit the enabled state and fail for the wrong reason. Register a
cleanup function using defer immediately after setting
team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true to ensure that the
reset of EnableEndUserAuthentication to false and the subsequent SaveTeam call
always execute, regardless of assertion failures in the test logic that follows.
🤖 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 `@server/service/integration_enterprise_test.go`:
- Around line 31733-31748: The manual reset of EnableEndUserAuthentication to
false at the end of the test block does not run if any require statement fails
before it, causing the next subtest to inherit the enabled state and fail for
the wrong reason. Register a cleanup function using defer immediately after
setting team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true to ensure
that the reset of EnableEndUserAuthentication to false and the subsequent
SaveTeam call always execute, regardless of assertion failures in the test logic
that follows.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 837feeea-f2e4-488d-966b-5ed394070988

📥 Commits

Reviewing files that changed from the base of the PR and between dcf5203 and 0c89b1c.

📒 Files selected for processing (7)
  • changes/46300-orbit-reenroll-eua
  • server/datastore/mysql/hosts.go
  • server/datastore/mysql/hosts_test.go
  • server/fleet/datastore.go
  • server/mock/datastore_mock.go
  • server/service/integration_enterprise_test.go
  • server/service/orbit.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 adjusts Orbit enrollment behavior so that Linux/Windows hosts that have previously Orbit-enrolled are not re-prompted for End User Authentication (EUA) during re-enrollment events (service restart, node key loss, osquery DB rebuild), while still gating genuinely new enrollments.

Changes:

  • Add a “previously Orbit-enrolled” check (based on existing host match + prior orbit node key) before returning END_USER_AUTH_REQUIRED.
  • Introduce HostPreviouslyOrbitEnrolled on the datastore interface with a MySQL implementation that mirrors EnrollOrbit host-matching semantics.
  • Add integration and datastore-level tests covering the re-enrollment EUA bypass behavior.

Reviewed changes

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

Show a summary per file
File Description
server/service/orbit.go Skip EUA prompt for re-enrolling hosts that previously had an orbit node key.
server/service/integration_enterprise_test.go Adds an integration test covering the regression scenario from #46300.
server/mock/datastore_mock.go Extends datastore mock with HostPreviouslyOrbitEnrolled.
server/fleet/datastore.go Adds HostPreviouslyOrbitEnrolled to the datastore interface contract.
server/datastore/mysql/hosts.go Implements HostPreviouslyOrbitEnrolled using matchHostDuringEnrollment semantics.
server/datastore/mysql/hosts_test.go Adds unit tests for HostPreviouslyOrbitEnrolled.
changes/46300-orbit-reenroll-eua Release note entry (content excluded by policy).
Files excluded by content exclusion policy (1)
  • changes/46300-orbit-reenroll-eua

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

Comment thread server/service/orbit.go
Comment thread server/service/orbit.go
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.81818% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.20%. Comparing base (96fa2d9) to head (e645ab3).
⚠️ Report is 54 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/hosts.go 86.66% 2 Missing ⚠️
server/service/orbit.go 71.42% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #47740      +/-   ##
==========================================
+ Coverage   67.15%   67.20%   +0.05%     
==========================================
  Files        3616     3619       +3     
  Lines      229030   229475     +445     
  Branches    11787    11787              
==========================================
+ Hits       153805   154229     +424     
- Misses      61370    61381      +11     
- Partials    13855    13865      +10     
Flag Coverage Δ
backend 68.85% <81.81%> (+0.06%) ⬆️

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
getvictor marked this pull request as ready for review June 17, 2026 13:31
@getvictor
getvictor requested a review from a team as a code owner June 17, 2026 13:31

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

serialToMatch = ""
}

matched, err := matchHostDuringEnrollment(ctx, ds.reader(ctx), orbitEnroll, isMDMEnabled, hostInfo.OsqueryIdentifier,

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.

Is it safe to use the reader here? Doesn't EnrollOrbit write the orbit_node_key to the primary so if orbit crashes and turns back on is there a possibility the EUA will come back incorrectly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, it is safe. The timing makes this almost impossible in practice. Even if it did happen, we fail-safe to a EUA prompt. We never wrongly skip auth.

Comment thread server/service/orbit.go
// Otherwise report the unauthenticated host and let Orbit handle it (e.g. by prompting the user to authenticate).
return "", fleet.NewOrbitIDPAuthRequiredError()
}
svc.logger.InfoContext(ctx, "allowing re-enrollment without end-user authentication: host previously orbit-enrolled",

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.

One other thing, should this be info or debug? Does this log every single time orbit restarts?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This only fires on re-enroll, and only when re-enrolling the same device (with EUA enabled). Under normal operation, orbit should never re-enroll. I put it at info because it would help catch unexpected re-enrollments in prod (which doesn't run with debug).

@getvictor
getvictor requested a review from ksykulev June 18, 2026 05:49
@getvictor

Copy link
Copy Markdown
Member Author

@ksykulev ready for re-review

@getvictor
getvictor merged commit a2757a1 into main Jun 18, 2026
55 of 77 checks passed
@getvictor
getvictor deleted the fix-46300-orbit-reenroll-eua branch June 18, 2026 15:22
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.

On Windows, Fleet Desktop asks for end user auth after the service is intentionally restarted

3 participants