Skip to content

Commit 29776ec

Browse files
committed
Orbit passes EUA token during enrollment
1 parent b1395ef commit 29776ec

File tree

8 files changed

+197
-1
lines changed

8 files changed

+197
-1
lines changed

changes/41379-orbit-eua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Orbit passes EUA token during enrollment request

client/orbit_client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ type OrbitClient struct {
6161
// receiverUpdateCancelFunc is used to cancel receiverUpdateContext.
6262
receiverUpdateCancelFunc context.CancelFunc
6363

64+
// euaToken is a one-time Fleet-signed JWT from Windows MDM enrollment,
65+
// sent during orbit enrollment to link the IdP account without prompting.
66+
euaToken string
67+
6468
// hostIdentityCertPath is the file path to the host identity certificate issued using SCEP.
6569
//
6670
// If set then it will be deleted on HTTP 401 errors from Fleet and it will cause ExecuteConfigReceivers
@@ -211,6 +215,11 @@ func NewOrbitClient(
211215
}, nil
212216
}
213217

218+
// SetEUAToken sets a one-time EUA token to include in the enrollment request.
219+
func (oc *OrbitClient) SetEUAToken(token string) {
220+
oc.euaToken = token
221+
}
222+
214223
// TriggerOrbitRestart triggers a orbit process restart.
215224
func (oc *OrbitClient) TriggerOrbitRestart(reason string) {
216225
log.Info().Msgf("orbit restart triggered: %s", reason)
@@ -512,6 +521,7 @@ func (oc *OrbitClient) enroll() (string, error) {
512521
OsqueryIdentifier: oc.hostInfo.OsqueryIdentifier,
513522
ComputerName: oc.hostInfo.ComputerName,
514523
HardwareModel: oc.hostInfo.HardwareModel,
524+
EUAToken: oc.euaToken,
515525
}
516526
var resp fleet.EnrollOrbitResponse
517527
err := oc.request(verb, path, params, &resp)

client/orbit_client_eua_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/fleetdm/fleet/v4/server/fleet"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestEnrollSendsEUAToken(t *testing.T) {
16+
const (
17+
testToken = "eyJhbGciOiJSUzI1NiJ9.test-eua-token"
18+
testNodeKey = "test-node-key-abc"
19+
)
20+
21+
t.Run("eua_token included in enroll request when set", func(t *testing.T) {
22+
var receivedBody fleet.EnrollOrbitRequest
23+
24+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25+
body, err := io.ReadAll(r.Body)
26+
require.NoError(t, err)
27+
require.NoError(t, json.Unmarshal(body, &receivedBody))
28+
29+
resp := fleet.EnrollOrbitResponse{OrbitNodeKey: testNodeKey}
30+
w.Header().Set("Content-Type", "application/json")
31+
err = json.NewEncoder(w).Encode(resp)
32+
require.NoError(t, err)
33+
}))
34+
defer srv.Close()
35+
36+
oc := &OrbitClient{
37+
enrollSecret: "secret",
38+
hostInfo: fleet.OrbitHostInfo{HardwareUUID: "uuid-1", Platform: "windows"},
39+
euaToken: testToken,
40+
}
41+
bc, err := NewBaseClient(srv.URL, true, "", "", nil, fleet.CapabilityMap{}, nil)
42+
require.NoError(t, err)
43+
oc.BaseClient = bc
44+
45+
nodeKey, err := oc.enroll()
46+
require.NoError(t, err)
47+
require.Equal(t, testNodeKey, nodeKey)
48+
require.Equal(t, testToken, receivedBody.EUAToken)
49+
require.Equal(t, "secret", receivedBody.EnrollSecret)
50+
require.Equal(t, "uuid-1", receivedBody.HardwareUUID)
51+
})
52+
53+
t.Run("eua_token omitted from enroll request when empty", func(t *testing.T) {
54+
var rawBody []byte
55+
56+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
57+
var err error
58+
rawBody, err = io.ReadAll(r.Body)
59+
require.NoError(t, err)
60+
61+
resp := fleet.EnrollOrbitResponse{OrbitNodeKey: testNodeKey}
62+
w.Header().Set("Content-Type", "application/json")
63+
err = json.NewEncoder(w).Encode(resp)
64+
require.NoError(t, err)
65+
}))
66+
defer srv.Close()
67+
68+
oc := &OrbitClient{
69+
enrollSecret: "secret",
70+
hostInfo: fleet.OrbitHostInfo{HardwareUUID: "uuid-1", Platform: "windows"},
71+
// euaToken not set — should be omitted from JSON (omitempty)
72+
}
73+
bc, err := NewBaseClient(srv.URL, true, "", "", nil, fleet.CapabilityMap{}, nil)
74+
require.NoError(t, err)
75+
oc.BaseClient = bc
76+
77+
_, err = oc.enroll()
78+
require.NoError(t, err)
79+
80+
// Verify the eua_token key is not present in the JSON body.
81+
require.False(t, bytes.Contains(rawBody, []byte(`"eua_token"`)),
82+
"eua_token should not appear in JSON when empty, got: %s", string(rawBody))
83+
})
84+
}
85+
86+
func TestSetEUAToken(t *testing.T) {
87+
oc := &OrbitClient{}
88+
require.Empty(t, oc.euaToken)
89+
90+
oc.SetEUAToken("some-token")
91+
require.Equal(t, "some-token", oc.euaToken)
92+
93+
oc.SetEUAToken("")
94+
require.Empty(t, oc.euaToken)
95+
}

orbit/cmd/orbit/orbit.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ func main() {
228228
Usage: "Sets the email address of the user associated with the host when enrolling to Fleet. (requires Fleet >= v4.43.0)",
229229
EnvVars: []string{"ORBIT_END_USER_EMAIL"},
230230
},
231+
&cli.StringFlag{
232+
Name: "eua-token",
233+
Hidden: true,
234+
Usage: "EUA token from Windows MDM enrollment, used during orbit enrollment to link IdP account",
235+
EnvVars: []string{"ORBIT_EUA_TOKEN"},
236+
},
231237
&cli.BoolFlag{
232238
Name: "disable-keystore",
233239
Usage: "Disables the use of the keychain on macOS and Credentials Manager on Windows",
@@ -1150,6 +1156,12 @@ func orbitAction(c *cli.Context) error {
11501156
return nil
11511157
})
11521158

1159+
// Set the EUA token from the MSI installer (Windows MDM enrollment).
1160+
// Must be set before any authenticated request triggers enrollment.
1161+
if euaToken := c.String("eua-token"); euaToken != "" && euaToken != unusedFlagKeyword {
1162+
orbitClient.SetEUAToken(euaToken)
1163+
}
1164+
11531165
// If the server can't be reached, we want to fail quickly on any blocking network calls
11541166
// so that desktop can be launched as soon as possible.
11551167
serverIsReachable := orbitClient.Ping() == nil

orbit/pkg/packaging/packaging.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ type Options struct {
128128
// EndUserEmail is the email address of the end user that uses the host on
129129
// which the agent is going to be installed.
130130
EndUserEmail string
131+
// EnableEUATokenProperty is a boolean indicating whether to enable EUA_TOKEN property in Windows MSI package.
132+
EnableEUATokenProperty bool
131133
// DisableKeystore disables the use of the keychain on macOS and Credentials Manager on Windows
132134
DisableKeystore bool
133135
// OsqueryDB is the directory to use for the osquery database.

orbit/pkg/packaging/windows.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ func BuildMSI(opt Options) (string, error) {
104104
if semver.Compare(orbitVersion, "v1.28.0") >= 0 {
105105
opt.EnableEndUserEmailProperty = true
106106
}
107+
// v1.55.0 introduced EUA_TOKEN property for MSI package: https://github.com/fleetdm/fleet/issues/41379
108+
if semver.Compare(orbitVersion, "v1.55.0") >= 0 {
109+
opt.EnableEUATokenProperty = true
110+
}
107111

108112
// Write files
109113

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package packaging
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestWindowsWixTemplateEUAToken(t *testing.T) {
13+
baseOpt := Options{
14+
FleetURL: "https://fleet.example.com",
15+
EnrollSecret: "secret",
16+
OrbitChannel: "stable",
17+
OsquerydChannel: "stable",
18+
DesktopChannel: "stable",
19+
NativePlatform: "windows",
20+
Architecture: ArchAmd64,
21+
}
22+
23+
t.Run("EUA_TOKEN property and flag included when enabled", func(t *testing.T) {
24+
opt := baseOpt
25+
opt.EnableEUATokenProperty = true
26+
27+
var buf bytes.Buffer
28+
err := windowsWixTemplate.Execute(&buf, opt)
29+
require.NoError(t, err)
30+
31+
output := buf.String()
32+
assert.Contains(t, output, `<Property Id="EUA_TOKEN" Value="dummy"/>`)
33+
assert.Contains(t, output, `--eua-token="[EUA_TOKEN]"`)
34+
})
35+
36+
t.Run("EUA_TOKEN property and flag absent when disabled", func(t *testing.T) {
37+
opt := baseOpt
38+
opt.EnableEUATokenProperty = false
39+
40+
var buf bytes.Buffer
41+
err := windowsWixTemplate.Execute(&buf, opt)
42+
require.NoError(t, err)
43+
44+
output := buf.String()
45+
assert.NotContains(t, output, `EUA_TOKEN`)
46+
assert.NotContains(t, output, `--eua-token`)
47+
})
48+
49+
t.Run("EUA_TOKEN flag appears in ServiceInstall Arguments", func(t *testing.T) {
50+
opt := baseOpt
51+
opt.EnableEUATokenProperty = true
52+
53+
var buf bytes.Buffer
54+
err := windowsWixTemplate.Execute(&buf, opt)
55+
require.NoError(t, err)
56+
57+
// Find the ServiceInstall Arguments line and verify eua-token is in it.
58+
for _, line := range strings.Split(buf.String(), "\n") {
59+
if strings.Contains(line, "Arguments=") && strings.Contains(line, "--fleet-url") {
60+
assert.Contains(t, line, `--eua-token="[EUA_TOKEN]"`,
61+
"eua-token flag should be in ServiceInstall Arguments")
62+
return
63+
}
64+
}
65+
t.Fatal("ServiceInstall Arguments line not found in template output")
66+
})
67+
}

orbit/pkg/packaging/windows_templates.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ var windowsWixTemplate = template.Must(template.New("").Option("missingkey=error
6666
{{ else if .EndUserEmail }}
6767
{{ $endUserEmailArg = printf " --end-user-email \"%s\"" .EndUserEmail }}
6868
{{ end }}
69+
{{ $euaTokenArg := "" }}
70+
{{ if .EnableEUATokenProperty }}
71+
<Property Id="EUA_TOKEN" Value="dummy"/>
72+
{{ $euaTokenArg = " --eua-token=\"[EUA_TOKEN]\"" }}
73+
{{ end }}
6974
7075
<MediaTemplate EmbedCab="yes" />
7176
@@ -109,7 +114,7 @@ var windowsWixTemplate = template.Must(template.New("").Option("missingkey=error
109114
Start="auto"
110115
Type="ownProcess"
111116
Description="This service runs Fleet's osquery runtime and autoupdater (Orbit)."
112-
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 }}'
117+
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 }}'
113118
>
114119
<util:ServiceConfig
115120
FirstFailureActionType="restart"

0 commit comments

Comments
 (0)