Base Orbit enrollment end user auth on server policy

EnrollOrbit now determines end user authentication requirements from
server policy rather than the client-supplied X-Fleet-Capabilities
header, which is treated as an informational hint.
    
Adds the mdm.allow_orbit_end_user_auth_bypass setting (enabled by
default) controlling whether hosts that do not complete end user
authentication may enroll into a team that requires it; set it to false
to strictly enforce end user auth. The setting also governs installers
built with fleetctl package --bypass-end-user-auth.
This commit is contained in:
Juan Fernandez
2026-08-03 07:40:48 -04:00
committed by GitHub
parent 7abc0f6e7f
commit 6c3de12f69
6 changed files with 169 additions and 10 deletions
+1
View File
@@ -0,0 +1 @@
- Orbit enrollment now determines end user authentication requirements from server policy rather than client-advertised capabilities. The `mdm.allow_orbit_end_user_auth_bypass` server setting (enabled by default) controls whether hosts that do not complete end user authentication may enroll into a team that requires it; set it to `false` to strictly enforce end user authentication for all Orbit enrollments.
@@ -3728,6 +3728,22 @@ Enabling this bypasses checks for forbidden declaration types, reserved identifi
allow_all_declarations: true
```
### mdm.allow_orbit_end_user_auth_bypass
When a team requires [end user authentication](https://fleetdm.com/guides/end-user-authentication), Fleet gates Linux and Windows Orbit enrollment on end user authentication. `fleetd`/Orbit versions that predate end user authentication support cannot complete that flow, and installers built with `fleetctl package --bypass-end-user-auth` intentionally skip it.
By default (`true`), Fleet allows those hosts to enroll into a team that requires end user authentication without completing it. Set this to `false` to strictly enforce end user authentication for all Orbit enrollments — hosts that do not complete end user authentication (including `--bypass-end-user-auth` installers and pre-end-user-auth agents) are then blocked.
Hosts that already enrolled before end user authentication was enabled are always allowed to re-enroll regardless of this setting. Windows hosts that present a valid end-user-auth token from MDM enrollment always complete end user authentication regardless of this setting.
- Default value: `true`
- Environment variable: `FLEET_MDM_ALLOW_ORBIT_END_USER_AUTH_BYPASS`
- Config file format:
```yaml
mdm:
allow_orbit_end_user_auth_bypass: false
```
### fleet_allow_bootstrap_package_during_migration
When set to `1` or `true`, this environment variable enables Fleet to install bootstrap packages on hosts during MDM migration enrollments (i.e. non-DEP enrollments). By default, bootstrap packages are only installed for DEP-enrolled hosts. Setting this variable restores the previous behavior, ensuring all new enrollments receive the bootstrap package.
+13
View File
@@ -949,6 +949,14 @@ type MDMConfig struct {
EnableCustomDiskEncryption bool `yaml:"enable_custom_disk_encryption"`
AllowAllDeclarations bool `yaml:"allow_all_declarations"`
// AllowOrbitEndUserAuthBypass controls whether an Orbit/fleetd host that does
// not complete end user authentication is allowed to enroll into a team that
// requires it. Defaults to true so that agents predating end user
// authentication (and installers built with `fleetctl package
// --bypass-end-user-auth`) can still enroll. Set to false to strictly enforce
// end user authentication for all Orbit enrollments.
AllowOrbitEndUserAuthBypass bool `yaml:"allow_orbit_end_user_auth_bypass"`
AndroidAgent AndroidAgentConfig `yaml:"android_agent"`
AndroidBatchSize int `yaml:"android_batch_size"`
}
@@ -1806,6 +1814,7 @@ func (man Manager) addConfigs() {
man.addConfigBool("mdm.enable_custom_filevault", false, "Allows usage of custom Apple MDM profiles for FileVault (Fleet Premium required)")
man.addConfigBool("mdm.enable_custom_disk_encryption", false, "Allows usage of custom Apple MDM profiles for FileVault and custom Windows profiles for BitLocker (Fleet Premium required)")
man.addConfigBool("mdm.allow_all_declarations", false, "Allows all MDM declaration types to be sent, bypassing safety checks")
man.addConfigBool("mdm.allow_orbit_end_user_auth_bypass", true, "Allow Orbit hosts that do not complete end user authentication to enroll into teams that require it; set to false to strictly enforce end user authentication for Orbit enrollments")
man.addConfigString("mdm.android_agent.package", "com.fleetdm.agent", "Package name for the Fleet Android agent")
man.addConfigString("mdm.android_agent.signing_sha256", "x+IyvrwVbQEBYV/ojWmLavJE0VIZE1RAT2JmxeI5sFw=", "Signing certificate SHA256 fingerprint for the Fleet Android agent")
man.hideConfig("mdm.android_agent.package")
@@ -2152,6 +2161,7 @@ func (man Manager) LoadConfig() FleetConfig {
EnableCustomFileVault: man.getConfigBool("mdm.enable_custom_filevault"),
EnableCustomDiskEncryption: man.getConfigBool("mdm.enable_custom_disk_encryption"),
AllowAllDeclarations: man.getConfigBool("mdm.allow_all_declarations"),
AllowOrbitEndUserAuthBypass: man.getConfigBool("mdm.allow_orbit_end_user_auth_bypass"),
AndroidAgent: AndroidAgentConfig{
Package: man.getConfigString("mdm.android_agent.package"),
SigningSHA256: man.getConfigString("mdm.android_agent.signing_sha256"),
@@ -2569,6 +2579,9 @@ func TestConfig() FleetConfig {
Vulnerabilities: VulnerabilitiesConfig{
OSVForVulnerabilities: true,
},
MDM: MDMConfig{
AllowOrbitEndUserAuthBypass: true,
},
}
}
@@ -33072,9 +33072,9 @@ func (s *integrationEnterpriseTestSuite) TestOrbitEnrollWithIdPPopulatesDeviceMa
require.NoError(t, s.ds.ApplyEnrollSecrets(ctx, &team.ID, []*fleet.EnrollSecret{{Secret: enrollSecret}}))
// Orbit client capabilities — Linux and Windows orbit builds advertise
// CapabilityEndUserAuth. Without this header the EnrollOrbit handler
// short-circuits past the EUA gating (with a logged warning) and the bug
// would not be exercised.
// CapabilityEndUserAuth. The X-Fleet-Capabilities header is an
// informational hint only: EUA gating must hold regardless of what
// the client advertises.
var caps fleet.CapabilityMap
caps.PopulateFromString(string(fleet.CapabilityEndUserAuth))
capsHeaders := map[string]string{fleet.CapabilitiesHeader: caps.String()}
+26 -7
View File
@@ -253,13 +253,17 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
// Orbit enrollment is only gated by end user auth for Linux and Windows hosts.
// For macOS hosts the MDM enrollment process handles end user auth.
if platform == "linux" || platform == "windows" {
// If the Orbit client doesn't support end user auth, complain loudly and let the host enroll.
mp, ok := capabilities.FromContext(ctx)
// Enforcement is based solely on server policy. The client-supplied
// X-Fleet-Capabilities header is an informational hint and must not
// gate this decision.
//
// The AllowOrbitEndUserAuthBypass escape hatch lets clients that do not
// advertise the end-user auth capability enroll anyway — either pre-EUA
// agents, or installers built with `fleetctl package --bypass-end-user-auth`.
// It defaults to true; set it to false to strictly enforce end user auth.
mp, capsOK := capabilities.FromContext(ctx)
clientSupportsEUA := capsOK && mp.Has(fleet.CapabilityEndUserAuth)
switch {
case !ok:
svc.logger.ErrorContext(ctx, "allowing unauthenticated enrollment: could not determine orbit end-user auth capability", "host_uuid", hostInfo.HardwareUUID)
case !mp.Has(fleet.CapabilityEndUserAuth):
svc.logger.WarnContext(ctx, "allowing unauthenticated enrollment: orbit version does not support end-user authentication", "host_uuid", hostInfo.HardwareUUID)
case platform == "windows" && euaToken != "":
// A Windows host already authenticated during MDM enrollment and the
// EUA token was passed by the MSI installer.
@@ -271,6 +275,10 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
euaDeviceID = deviceID
euaIdpAcctUUID = idpAcctUUID
// Continue enrollment — do not return END_USER_AUTH_REQUIRED.
case svc.config.MDM.AllowOrbitEndUserAuthBypass && !clientSupportsEUA:
svc.logger.WarnContext(ctx, "allowing enrollment without end-user authentication: end-user auth bypass is enabled and the client does not support end-user auth",
"host_uuid", hostInfo.HardwareUUID)
// Continue enrollment — do not return END_USER_AUTH_REQUIRED.
default:
// 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
@@ -280,7 +288,18 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
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).
// Report the unauthenticated host and let Orbit handle it (e.g. by prompting the user to authenticate).
// Dereference the team ID so the log shows the numeric value; leave it nil for a global enroll secret.
var teamID any
if secret.TeamID != nil {
teamID = *secret.TeamID
}
svc.logger.WarnContext(ctx, "blocking enrollment: end-user authentication required but not completed",
"host_uuid", hostInfo.HardwareUUID,
"hardware_serial", hostInfo.HardwareSerial,
"platform", platform,
"team_id", teamID,
)
return "", fleet.NewOrbitIDPAuthRequiredError()
}
svc.logger.InfoContext(ctx, "allowing re-enrollment without end-user authentication: host previously orbit-enrolled",
+110
View File
@@ -9,6 +9,7 @@ import (
"testing"
"time"
hostidentity_types "github.com/fleetdm/fleet/v4/ee/pkg/hostidentity/types"
"github.com/fleetdm/fleet/v4/pkg/optjson"
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
"github.com/fleetdm/fleet/v4/server/config"
@@ -1654,3 +1655,112 @@ func TestResolveOrbitDebugLogging(t *testing.T) {
})
}
}
func TestEnrollOrbitEndUserAuthBypass(t *testing.T) {
// When end user authentication is required and the enrolling agent does not
// advertise the end_user_auth capability (for example an older agent that
// does not set the X-Fleet-Capabilities header), the
// AllowOrbitEndUserAuthBypass config flag decides whether enrollment is
// blocked or allowed.
newSvc := func(t *testing.T, allowBypass bool) (*mock.DataStore, fleet.Service, context.Context) {
// mock.Store hard-codes EnrollOrbit to return (nil, nil), which would make
// the bypass-allowed success path panic. Use the underlying mock.DataStore so
// EnrollOrbitFunc is honored.
ds := new(mock.DataStore)
cfg := config.TestConfig()
cfg.MDM.AllowOrbitEndUserAuthBypass = allowBypass
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
// Global enroll secret (no team) with end user auth required at the app-config level.
ds.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
return &fleet.EnrollSecret{Secret: secret}, nil
}
ds.GetHostIdentityCertByNameFunc = func(ctx context.Context, name string) (*hostidentity_types.HostIdentityCertificate, error) {
return nil, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
ac := &fleet.AppConfig{}
ac.MDM.EnabledAndConfigured = true
ac.MDM.MacOSSetup.EnableEndUserAuthentication = true
return ac, nil
}
// No IdP account linked and not previously enrolled: a genuine first-time enrollment.
ds.GetMDMIdPAccountByHostUUIDFunc = func(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) {
return nil, nil
}
ds.HostPreviouslyOrbitEnrolledFunc = func(ctx context.Context, hostInfo fleet.OrbitHostInfo, isMDMEnabled bool) (bool, error) {
return false, nil
}
ds.EnrollOrbitFunc = func(ctx context.Context, opts ...fleet.DatastoreEnrollOrbitOption) (*fleet.Host, error) {
return &fleet.Host{ID: 1, UUID: "host-uuid-1", Platform: "ubuntu"}, nil
}
ds.MaybeAssociateHostWithScimUserFunc = func(ctx context.Context, hostID uint) error {
return nil
}
return ds, svc, ctx
}
hostInfo := fleet.OrbitHostInfo{
HardwareUUID: "host-uuid-1",
HardwareSerial: "serial-1",
Hostname: "host-1",
Platform: "ubuntu",
PlatformLike: "debian",
}
// noEUACtx builds a request context advertising only unrelated capabilities,
// simulating an agent that does not support end user auth.
noEUACtx := func(ctx context.Context) context.Context {
req := httptest.NewRequest("POST", "/api/fleet/orbit/enroll", nil)
req.Header.Set(fleet.CapabilitiesHeader, "foo,bar")
return capabilities.NewContext(ctx, req)
}
t.Run("flag disabled blocks enrollment", func(t *testing.T) {
ds, svc, ctx := newSvc(t, false)
_, err := svc.EnrollOrbit(noEUACtx(ctx), hostInfo, "secret", "")
require.Error(t, err)
require.Contains(t, err.Error(), "END_USER_AUTH_REQUIRED")
require.False(t, ds.EnrollOrbitFuncInvoked, "no host must be enrolled when EUA is required and the flag is off")
})
t.Run("flag enabled allows enrollment", func(t *testing.T) {
ds, svc, ctx := newSvc(t, true)
nodeKey, err := svc.EnrollOrbit(noEUACtx(ctx), hostInfo, "secret", "")
require.NoError(t, err)
require.NotEmpty(t, nodeKey)
require.True(t, ds.EnrollOrbitFuncInvoked)
})
t.Run("flag enabled still gates agents that support EUA", func(t *testing.T) {
// The escape hatch only applies to agents that do not support end user
// auth. A modern agent that advertises the capability must still go
// through the SSO flow even when the flag is on.
ds, svc, ctx := newSvc(t, true)
euaCtx := func(ctx context.Context) context.Context {
req := httptest.NewRequest("POST", "/api/fleet/orbit/enroll", nil)
req.Header.Set(fleet.CapabilitiesHeader, string(fleet.CapabilityEndUserAuth))
return capabilities.NewContext(ctx, req)
}
_, err := svc.EnrollOrbit(euaCtx(ctx), hostInfo, "secret", "")
require.Error(t, err)
require.Contains(t, err.Error(), "END_USER_AUTH_REQUIRED")
require.False(t, ds.EnrollOrbitFuncInvoked)
})
t.Run("windows EUA token takes precedence over the flag", func(t *testing.T) {
// A Windows host presenting an EUA token must go through the token path even when
// the flag is on and the client omits the capability — the token case is ordered
// first. wstepCertManager is unset in this harness, so the token path falls back to
// END_USER_AUTH_REQUIRED; the point is that the flag's bypass does not fire (no host
// is enrolled), proving the token case wins.
ds, svc, ctx := newSvc(t, true)
winHost := hostInfo
winHost.Platform = "windows"
winHost.PlatformLike = ""
_, err := svc.EnrollOrbit(noEUACtx(ctx), winHost, "secret", "some-eua-token")
require.Error(t, err)
require.Contains(t, err.Error(), "END_USER_AUTH_REQUIRED")
require.False(t, ds.EnrollOrbitFuncInvoked, "the flag bypass must not fire when an EUA token is present")
})
}