mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Orbit passes EUA token during enrollment (#43369)
**Related issue:** Resolves #41379 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [ ] Verified that fleetd runs on macOS, Linux and Windows - [ ] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added EUA token support to Orbit enrollment workflow * Introduced `--eua-token` CLI flag for Windows MDM enrollment * Windows MSI packages now support EUA_TOKEN property (Orbit v1.55.0+) * **Tests** * Added tests for EUA token handling in enrollment and Windows packaging * **Documentation** * Added changelog entry documenting EUA token inclusion in enrollment requests <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
7bcc2c6894
commit
2245359ad1
8 changed files with 173 additions and 1 deletions
|
|
@ -61,6 +61,10 @@ type OrbitClient struct {
|
|||
// receiverUpdateCancelFunc is used to cancel receiverUpdateContext.
|
||||
receiverUpdateCancelFunc context.CancelFunc
|
||||
|
||||
// euaToken is a one-time Fleet-signed JWT from Windows MDM enrollment,
|
||||
// sent during orbit enrollment to link the IdP account without prompting.
|
||||
euaToken string
|
||||
|
||||
// hostIdentityCertPath is the file path to the host identity certificate issued using SCEP.
|
||||
//
|
||||
// If set then it will be deleted on HTTP 401 errors from Fleet and it will cause ExecuteConfigReceivers
|
||||
|
|
@ -211,6 +215,11 @@ func NewOrbitClient(
|
|||
}, nil
|
||||
}
|
||||
|
||||
// SetEUAToken sets a one-time EUA token to include in the enrollment request.
|
||||
func (oc *OrbitClient) SetEUAToken(token string) {
|
||||
oc.euaToken = token
|
||||
}
|
||||
|
||||
// TriggerOrbitRestart triggers a orbit process restart.
|
||||
func (oc *OrbitClient) TriggerOrbitRestart(reason string) {
|
||||
log.Info().Msgf("orbit restart triggered: %s", reason)
|
||||
|
|
@ -512,6 +521,7 @@ func (oc *OrbitClient) enroll() (string, error) {
|
|||
OsqueryIdentifier: oc.hostInfo.OsqueryIdentifier,
|
||||
ComputerName: oc.hostInfo.ComputerName,
|
||||
HardwareModel: oc.hostInfo.HardwareModel,
|
||||
EUAToken: oc.euaToken,
|
||||
}
|
||||
var resp fleet.EnrollOrbitResponse
|
||||
err := oc.request(verb, path, params, &resp)
|
||||
|
|
|
|||
80
client/orbit_client_eua_test.go
Normal file
80
client/orbit_client_eua_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEnrollSendsEUAToken(t *testing.T) {
|
||||
// nolint:gosec // not a real credential, test-only JWT fragment
|
||||
euaTokenValue := "eyJhbGciOiJSUzI1NiJ9.test-eua-token"
|
||||
const testNodeKey = "test-node-key-abc"
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
token string
|
||||
assert func(t *testing.T, receivedBody fleet.EnrollOrbitRequest, rawBody []byte)
|
||||
}{
|
||||
{
|
||||
name: "eua_token included in enroll request when set",
|
||||
token: euaTokenValue,
|
||||
assert: func(t *testing.T, receivedBody fleet.EnrollOrbitRequest, rawBody []byte) {
|
||||
require.Equal(t, euaTokenValue, receivedBody.EUAToken)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "eua_token omitted from enroll request when empty",
|
||||
token: "",
|
||||
assert: func(t *testing.T, receivedBody fleet.EnrollOrbitRequest, rawBody []byte) {
|
||||
// Verify the eua_token key is not present in the JSON body (omitempty).
|
||||
require.Falsef(t, bytes.Contains(rawBody, []byte(`"eua_token"`)),
|
||||
"eua_token should not appear in JSON when empty, got: %s", string(rawBody))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var receivedBody fleet.EnrollOrbitRequest
|
||||
var rawBody []byte
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
rawBody, err = io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, json.Unmarshal(rawBody, &receivedBody))
|
||||
|
||||
resp := fleet.EnrollOrbitResponse{OrbitNodeKey: testNodeKey}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
assert.NoError(t, err)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
oc := &OrbitClient{
|
||||
enrollSecret: "secret",
|
||||
hostInfo: fleet.OrbitHostInfo{HardwareUUID: "uuid-1", Platform: "windows"},
|
||||
}
|
||||
oc.SetEUAToken(tc.token)
|
||||
bc, err := NewBaseClient(srv.URL, true, "", "", nil, fleet.CapabilityMap{}, nil)
|
||||
require.NoError(t, err)
|
||||
oc.BaseClient = bc
|
||||
|
||||
nodeKey, err := oc.enroll()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testNodeKey, nodeKey)
|
||||
require.Equal(t, "secret", receivedBody.EnrollSecret)
|
||||
require.Equal(t, "uuid-1", receivedBody.HardwareUUID)
|
||||
|
||||
tc.assert(t, receivedBody, rawBody)
|
||||
})
|
||||
}
|
||||
}
|
||||
1
orbit/changes/41379-orbit-eua
Normal file
1
orbit/changes/41379-orbit-eua
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Orbit passes EUA token during enrollment request
|
||||
|
|
@ -228,6 +228,12 @@ func main() {
|
|||
Usage: "Sets the email address of the user associated with the host when enrolling to Fleet. (requires Fleet >= v4.43.0)",
|
||||
EnvVars: []string{"ORBIT_END_USER_EMAIL"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "eua-token",
|
||||
Hidden: true,
|
||||
Usage: "EUA token from Windows MDM enrollment, used during orbit enrollment to link IdP account",
|
||||
EnvVars: []string{"ORBIT_EUA_TOKEN"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "disable-keystore",
|
||||
Usage: "Disables the use of the keychain on macOS and Credentials Manager on Windows",
|
||||
|
|
@ -1150,6 +1156,12 @@ func orbitAction(c *cli.Context) error {
|
|||
return nil
|
||||
})
|
||||
|
||||
// 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 != unusedFlagKeyword {
|
||||
orbitClient.SetEUAToken(euaToken)
|
||||
}
|
||||
|
||||
// If the server can't be reached, we want to fail quickly on any blocking network calls
|
||||
// so that desktop can be launched as soon as possible.
|
||||
serverIsReachable := orbitClient.Ping() == nil
|
||||
|
|
|
|||
|
|
@ -128,6 +128,8 @@ type Options struct {
|
|||
// EndUserEmail is the email address of the end user that uses the host on
|
||||
// which the agent is going to be installed.
|
||||
EndUserEmail string
|
||||
// EnableEUATokenProperty is a boolean indicating whether to enable EUA_TOKEN property in Windows MSI package.
|
||||
EnableEUATokenProperty bool
|
||||
// DisableKeystore disables the use of the keychain on macOS and Credentials Manager on Windows
|
||||
DisableKeystore bool
|
||||
// OsqueryDB is the directory to use for the osquery database.
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ func BuildMSI(opt Options) (string, error) {
|
|||
if semver.Compare(orbitVersion, "v1.28.0") >= 0 {
|
||||
opt.EnableEndUserEmailProperty = true
|
||||
}
|
||||
// v1.55.0 introduced EUA_TOKEN property for MSI package: https://github.com/fleetdm/fleet/issues/41379
|
||||
if semver.Compare(orbitVersion, "v1.55.0") >= 0 {
|
||||
opt.EnableEUATokenProperty = true
|
||||
}
|
||||
|
||||
// Write files
|
||||
|
||||
|
|
|
|||
58
orbit/pkg/packaging/windows_eua_test.go
Normal file
58
orbit/pkg/packaging/windows_eua_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package packaging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWindowsWixTemplateEUAToken(t *testing.T) {
|
||||
baseOpt := Options{
|
||||
FleetURL: "https://fleet.example.com",
|
||||
EnrollSecret: "secret",
|
||||
OrbitChannel: "stable",
|
||||
OsquerydChannel: "stable",
|
||||
DesktopChannel: "stable",
|
||||
NativePlatform: "windows",
|
||||
Architecture: ArchAmd64,
|
||||
}
|
||||
|
||||
t.Run("EUA_TOKEN property and flag included when enabled", func(t *testing.T) {
|
||||
opt := baseOpt
|
||||
opt.EnableEUATokenProperty = true
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := windowsWixTemplate.Execute(&buf, opt)
|
||||
require.NoError(t, err)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, `<Property Id="EUA_TOKEN" Value="dummy"/>`)
|
||||
|
||||
var argsLine string
|
||||
for line := range strings.SplitSeq(output, "\n") {
|
||||
if strings.Contains(line, "Arguments=") && strings.Contains(line, "--fleet-url") {
|
||||
argsLine = line
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, argsLine, "ServiceInstall Arguments line not found in template output")
|
||||
assert.Contains(t, argsLine, `--eua-token="[EUA_TOKEN]"`,
|
||||
"eua-token flag should be in ServiceInstall Arguments")
|
||||
})
|
||||
|
||||
t.Run("EUA_TOKEN property and flag absent when disabled", func(t *testing.T) {
|
||||
opt := baseOpt
|
||||
opt.EnableEUATokenProperty = false
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := windowsWixTemplate.Execute(&buf, opt)
|
||||
require.NoError(t, err)
|
||||
|
||||
output := buf.String()
|
||||
assert.NotContains(t, output, `EUA_TOKEN`)
|
||||
assert.NotContains(t, output, `--eua-token`)
|
||||
})
|
||||
}
|
||||
|
|
@ -66,6 +66,11 @@ var windowsWixTemplate = template.Must(template.New("").Option("missingkey=error
|
|||
{{ else if .EndUserEmail }}
|
||||
{{ $endUserEmailArg = printf " --end-user-email \"%s\"" .EndUserEmail }}
|
||||
{{ end }}
|
||||
{{ $euaTokenArg := "" }}
|
||||
{{ if .EnableEUATokenProperty }}
|
||||
<Property Id="EUA_TOKEN" Value="dummy"/>
|
||||
{{ $euaTokenArg = " --eua-token=\"[EUA_TOKEN]\"" }}
|
||||
{{ end }}
|
||||
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
|
|
@ -109,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 }}{{ 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 }}'
|
||||
>
|
||||
<util:ServiceConfig
|
||||
FirstFailureActionType="restart"
|
||||
|
|
|
|||
Loading…
Reference in a new issue