diff --git a/changes/46644-bypass-end-user-auth b/changes/46644-bypass-end-user-auth new file mode 100644 index 00000000000..c8664c5413c --- /dev/null +++ b/changes/46644-bypass-end-user-auth @@ -0,0 +1 @@ +- Added a `--bypass-end-user-auth` flag to `fleetctl package` that configures the generated fleetd installer to skip the end-user authentication prompt during enrollment on Linux and Windows hosts (e.g. when the end user already authenticated via another MDM). Requires fleetd v1.60.0 or higher. diff --git a/client/orbit_client.go b/client/orbit_client.go index 024e6269a47..2f8d48ed50f 100644 --- a/client/orbit_client.go +++ b/client/orbit_client.go @@ -201,6 +201,8 @@ var ( // - addr is the address of the Fleet server. // - orbitHostInfo is the host system information used for enrolling to Fleet. // - onGetConfigErrFns can be used to handle errors in the GetConfig request. +// - bypassEndUserAuth, when true, omits the end-user auth capability so the server enrolls the +// host without prompting for end-user authentication (only meaningful on Linux and Windows). func NewOrbitClient( rootDir string, addr string, @@ -212,8 +214,13 @@ func NewOrbitClient( onGetConfigErrFns *OnGetConfigErrFuncs, httpSignerWrapper func(*http.Client) *http.Client, hostIdentityCertPath string, + bypassEndUserAuth bool, ) (*OrbitClient, error) { orbitCapabilities := fleet.GetOrbitClientCapabilities() + if bypassEndUserAuth { + // Don't advertise the end-user auth capability so the Fleet server enrolls this host without prompting for EUA. + delete(orbitCapabilities, fleet.CapabilityEndUserAuth) + } bc, err := NewBaseClient(addr, insecureSkipVerify, rootCA, "", fleetClientCert, orbitCapabilities, httpSignerWrapper) if err != nil { return nil, err diff --git a/client/orbit_client_test.go b/client/orbit_client_test.go index fc031e3c523..ece5d922644 100644 --- a/client/orbit_client_test.go +++ b/client/orbit_client_test.go @@ -37,6 +37,32 @@ func TestGetConfig(t *testing.T) { ) } +func TestNewOrbitClientBypassEndUserAuth(t *testing.T) { + newClient := func(bypassEndUserAuth bool) *OrbitClient { + oc, err := NewOrbitClient( + t.TempDir(), + "https://fleet.example.com", + "", + true, + "secret", + nil, + fleet.OrbitHostInfo{HardwareUUID: "uuid", Hostname: "host"}, + nil, + nil, + "", + bypassEndUserAuth, + ) + require.NoError(t, err) + return oc + } + + // With bypass enabled, orbit must not advertise the end-user auth capability. + require.NotContains(t, newClient(true).ClientCapabilities, fleet.CapabilityEndUserAuth) + + // Without bypass, capabilities are unchanged from the default set. + require.Equal(t, fleet.GetOrbitClientCapabilities(), newClient(false).ClientCapabilities) +} + func clientWithConfig(cfg *fleet.OrbitConfig) *OrbitClient { ctx, cancel := context.WithCancel(context.Background()) oc := &OrbitClient{ diff --git a/cmd/fleetctl/fleetctl/package.go b/cmd/fleetctl/fleetctl/package.go index 008b0ce8669..b586eecadc6 100644 --- a/cmd/fleetctl/fleetctl/package.go +++ b/cmd/fleetctl/fleetctl/package.go @@ -130,6 +130,11 @@ func packageCommand() *cli.Command { Usage: "Disable setup experience for Linux or Windows hosts", Destination: &opt.DisableSetupExperience, }, + &cli.BoolFlag{ + Name: "bypass-end-user-auth", + Usage: "Skip the end-user authentication prompt during fleetd enrollment (applies to Linux and Windows hosts; macOS only handles end-user auth during MDM enrollment)", + Destination: &opt.BypassEndUserAuth, + }, &cli.StringFlag{ Name: "update-url", Usage: "URL for update server", diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index fbaff01eb85..16f0bc57c04 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -1101,6 +1101,7 @@ func (a *agent) runOrbitLoop() { nil, signerWrapper, "", + false, ) if err != nil { log.Println("creating orbit client: ", err) diff --git a/orbit/changes/46644-bypass-end-user-auth b/orbit/changes/46644-bypass-end-user-auth new file mode 100644 index 00000000000..05c1ca4dba5 --- /dev/null +++ b/orbit/changes/46644-bypass-end-user-auth @@ -0,0 +1 @@ +- Added a `--bypass-end-user-auth` flag (env `ORBIT_BYPASS_END_USER_AUTH`) that skips the end-user authentication prompt during enrollment on Linux and Windows by not advertising the end-user auth capability to the Fleet server. When a Windows MDM EUA token is present, it takes precedence and end-user auth is still processed. diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 3975b5406df..c1d515de865 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -251,6 +251,11 @@ func main() { Usage: "Disables checking for setup experience on Linux or Windows hosts", EnvVars: []string{"ORBIT_DISABLE_SETUP_EXPERIENCE"}, }, + &cli.BoolFlag{ + Name: "bypass-end-user-auth", + Usage: "Bypasses end-user authentication during fleetd enrollment on Linux and Windows", + EnvVars: []string{"ORBIT_BYPASS_END_USER_AUTH"}, + }, } app.Before = func(c *cli.Context) error { // handle old installations, which had default root dir set to /var/lib/orbit @@ -1169,6 +1174,14 @@ func orbitAction(c *cli.Context) error { ) } + // Bypass end-user authentication only when there is no EUA token to process. When the Windows MDM installer supplies + // an EUA token, the user already authenticated during MDM enrollment and the server links the host's IdP account + // from that token. Processing the token requires that orbit keep advertising the end-user auth capability, so a + // present token takes precedence over the bypass flag. + euaToken := c.String("eua-token") + hasEUAToken := euaToken != "" && euaToken != constant.UnusedFlagKeyword + bypassEndUserAuth := c.Bool("bypass-end-user-auth") && !hasEUAToken + orbitClient, err = fleetclient.NewOrbitClient( c.String("root-dir"), fleetURL, @@ -1187,6 +1200,7 @@ func orbitAction(c *cli.Context) error { }, signerWrapper, hostIdentityCertificatePath, + bypassEndUserAuth, ) if err != nil { return fmt.Errorf("error new orbit client: %w", err) @@ -1204,7 +1218,7 @@ func orbitAction(c *cli.Context) error { // Set the EUA token from the MSI installer (Windows MDM enrollment). // Must be set before any authenticated request triggers enrollment. - if euaToken := c.String("eua-token"); euaToken != "" && euaToken != constant.UnusedFlagKeyword { + if hasEUAToken { orbitClient.SetEUAToken(euaToken) } @@ -1448,6 +1462,7 @@ func orbitAction(c *cli.Context) error { }, nil, "", + bypassEndUserAuth, ) if err != nil { return fmt.Errorf("new client for capabilities checker: %w", err) diff --git a/orbit/pkg/packaging/bypass_end_user_auth_test.go b/orbit/pkg/packaging/bypass_end_user_auth_test.go new file mode 100644 index 00000000000..c856bbb9b9e --- /dev/null +++ b/orbit/pkg/packaging/bypass_end_user_auth_test.go @@ -0,0 +1,61 @@ +package packaging + +import ( + "bytes" + "strings" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestBypassEndUserAuthTemplates verifies the --bypass-end-user-auth switch is wired into the generated Linux env file +// and Windows MSI arguments when enabled, and absent when not. macOS is intentionally excluded. +func TestBypassEndUserAuthTemplates(t *testing.T) { + baseOpt := Options{ + FleetURL: "https://fleet.example.com", + EnrollSecret: "secret", + OrbitChannel: "stable", + OsquerydChannel: "stable", + DesktopChannel: "stable", + NativePlatform: "windows", + Architecture: ArchAmd64, + } + + // render executes tmpl with the bypass option toggled and returns the generated output. + render := func(t *testing.T, tmpl *template.Template, bypass bool) string { + t.Helper() + opt := baseOpt + opt.BypassEndUserAuth = bypass + var buf bytes.Buffer + require.NoError(t, tmpl.Execute(&buf, opt)) + return buf.String() + } + + t.Run("linux env file", func(t *testing.T) { + assert.Contains(t, render(t, envTemplate, true), "ORBIT_BYPASS_END_USER_AUTH=true") + assert.NotContains(t, render(t, envTemplate, false), "ORBIT_BYPASS_END_USER_AUTH") + }) + + t.Run("windows msi args", func(t *testing.T) { + // The flag is one of many appended to the service's ServiceInstall Arguments; isolate that line. + argsLine := func(output string) string { + t.Helper() + for line := range strings.SplitSeq(output, "\n") { + if strings.Contains(line, "Arguments=") && strings.Contains(line, "--fleet-url") { + return line + } + } + t.Fatal("ServiceInstall Arguments line not found in template output") + return "" + } + assert.Contains(t, argsLine(render(t, windowsWixTemplate, true)), "--bypass-end-user-auth") + assert.NotContains(t, argsLine(render(t, windowsWixTemplate, false)), "--bypass-end-user-auth") + }) + + // Guard the deliberate macOS exclusion: the flag must never leak into the launchd plist. + t.Run("macos launchd plist excluded", func(t *testing.T) { + assert.NotContains(t, render(t, macosLaunchdTemplate, true), "ORBIT_BYPASS_END_USER_AUTH") + }) +} diff --git a/orbit/pkg/packaging/linux_shared.go b/orbit/pkg/packaging/linux_shared.go index cf674f74f1f..700f06010af 100644 --- a/orbit/pkg/packaging/linux_shared.go +++ b/orbit/pkg/packaging/linux_shared.go @@ -393,6 +393,7 @@ ORBIT_FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST={{ .FleetDesktopAlternativeBrowserH {{ if .EndUserEmail }}ORBIT_END_USER_EMAIL={{.EndUserEmail}}{{ end }} {{ if .FleetManagedHostIdentityCertificate }}ORBIT_FLEET_MANAGED_HOST_IDENTITY_CERTIFICATE=true{{ end }} {{ if .DisableSetupExperience }}ORBIT_DISABLE_SETUP_EXPERIENCE=true{{ end }} +{{ if .BypassEndUserAuth }}ORBIT_BYPASS_END_USER_AUTH=true{{ end }} `)) func writeEnvFile(opt Options, rootPath string) error { diff --git a/orbit/pkg/packaging/packaging.go b/orbit/pkg/packaging/packaging.go index 500b6362c50..37d12e6db21 100644 --- a/orbit/pkg/packaging/packaging.go +++ b/orbit/pkg/packaging/packaging.go @@ -67,6 +67,9 @@ type Options struct { DisableUpdates bool // DisableSetupExperience disables setup experience for Linux hosts DisableSetupExperience bool + // BypassEndUserAuth configures fleetd to skip end-user authentication during enrollment by not + // advertising the end-user auth capability to the Fleet server. + BypassEndUserAuth bool // OrbitChannel is the update channel to use for Orbit. OrbitChannel string // OsquerydChannel is the update channel to use for Osquery (osqueryd). diff --git a/orbit/pkg/packaging/windows_templates.go b/orbit/pkg/packaging/windows_templates.go index fe89a35a488..1619d613e53 100644 --- a/orbit/pkg/packaging/windows_templates.go +++ b/orbit/pkg/packaging/windows_templates.go @@ -114,7 +114,7 @@ var windowsWixTemplate = template.Must(template.New("").Option("missingkey=error Start="auto" Type="ownProcess" Description="This service runs Fleet's osquery runtime and autoupdater (Orbit)." - Arguments='--root-dir "[ORBITROOT]." --log-file "[System64Folder]config\systemprofile\AppData\Local\FleetDM\Orbit\Logs\orbit-osquery.log" --fleet-url "[FLEET_URL]"{{ if .FleetCertificate }} --fleet-certificate "[ORBITROOT]fleet.pem"{{ end }}{{ if .EnrollSecret }} --enroll-secret-path "[ORBITROOT]secret.txt"{{ end }}{{if .Insecure }} --insecure{{ end }}{{ if .Debug }} --debug{{ end }}{{ if .UpdateURL }} --update-url "{{ .UpdateURL }}"{{ end }}{{ if .UpdateTLSServerCertificate }} --update-tls-certificate "[ORBITROOT]update.pem"{{ end }}{{ if .DisableUpdates }} --disable-updates{{ end }} --fleet-desktop="[FLEET_DESKTOP]" --desktop-channel {{ .DesktopChannel }}{{ if .FleetDesktopAlternativeBrowserHost }} --fleet-desktop-alternative-browser-host {{ .FleetDesktopAlternativeBrowserHost }}{{ end }} --orbit-channel "{{ .OrbitChannel }}" --osqueryd-channel "{{ .OsquerydChannel }}" --enable-scripts="[ENABLE_SCRIPTS]" {{ if and (ne .HostIdentifier "") (ne .HostIdentifier "uuid") }}--host-identifier={{ .HostIdentifier }}{{ end }}{{ $endUserEmailArg }}{{ $euaTokenArg }}{{ if .OsqueryDB }} --osquery-db="{{ .OsqueryDB }}"{{ end }}{{ if .DisableSetupExperience }} --disable-setup-experience{{ end }}' + Arguments='--root-dir "[ORBITROOT]." --log-file "[System64Folder]config\systemprofile\AppData\Local\FleetDM\Orbit\Logs\orbit-osquery.log" --fleet-url "[FLEET_URL]"{{ if .FleetCertificate }} --fleet-certificate "[ORBITROOT]fleet.pem"{{ end }}{{ if .EnrollSecret }} --enroll-secret-path "[ORBITROOT]secret.txt"{{ end }}{{if .Insecure }} --insecure{{ end }}{{ if .Debug }} --debug{{ end }}{{ if .UpdateURL }} --update-url "{{ .UpdateURL }}"{{ end }}{{ if .UpdateTLSServerCertificate }} --update-tls-certificate "[ORBITROOT]update.pem"{{ end }}{{ if .DisableUpdates }} --disable-updates{{ end }} --fleet-desktop="[FLEET_DESKTOP]" --desktop-channel {{ .DesktopChannel }}{{ if .FleetDesktopAlternativeBrowserHost }} --fleet-desktop-alternative-browser-host {{ .FleetDesktopAlternativeBrowserHost }}{{ end }} --orbit-channel "{{ .OrbitChannel }}" --osqueryd-channel "{{ .OsquerydChannel }}" --enable-scripts="[ENABLE_SCRIPTS]" {{ if and (ne .HostIdentifier "") (ne .HostIdentifier "uuid") }}--host-identifier={{ .HostIdentifier }}{{ end }}{{ $endUserEmailArg }}{{ $euaTokenArg }}{{ if .OsqueryDB }} --osquery-db="{{ .OsqueryDB }}"{{ end }}{{ if .DisableSetupExperience }} --disable-setup-experience{{ end }}{{ if .BypassEndUserAuth }} --bypass-end-user-auth{{ end }}' >