Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions client/orbit_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
80 changes: 80 additions & 0 deletions client/orbit_client_eua_test.go
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions orbit/changes/41379-orbit-eua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Orbit passes EUA token during enrollment request
Comment thread
ksykulev marked this conversation as resolved.
12 changes: 12 additions & 0 deletions orbit/cmd/orbit/orbit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions orbit/pkg/packaging/packaging.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions orbit/pkg/packaging/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
58 changes: 58 additions & 0 deletions orbit/pkg/packaging/windows_eua_test.go
Original file line number Diff line number Diff line change
@@ -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`)
})
}
7 changes: 6 additions & 1 deletion orbit/pkg/packaging/windows_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" />

Expand Down Expand Up @@ -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"
Expand Down
Loading