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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user