Linux setup experience agent (#32172)

#32053
This commit is contained in:
Dante Catalfamo
2025-09-05 10:07:03 -04:00
committed by GitHub
parent 6ed3e69c67
commit dde5aedbd8
8 changed files with 158 additions and 0 deletions
@@ -0,0 +1 @@
- Added agent support for setup experience on Linux
+5
View File
@@ -125,6 +125,11 @@ func packageCommand() *cli.Command {
Usage: "Disable auto updates on the generated package",
Destination: &opt.DisableUpdates,
},
&cli.BoolFlag{
Name: "disable-setup-experience",
Usage: "Disable setup experience for Linux hosts",
Destination: &opt.DisableSetupExperience,
},
&cli.StringFlag{
Name: "update-url",
Usage: "URL for update server",
+133
View File
@@ -241,6 +241,11 @@ func main() {
Usage: "Configures fleetd to use TPM-backed key to sign HTTP requests. This functionality is licensed under the Fleet EE License. Usage requires a current Fleet EE subscription.",
EnvVars: []string{"ORBIT_FLEET_MANAGED_HOST_IDENTITY_CERTIFICATE"},
},
&cli.BoolFlag{
Name: "disable-setup-experience",
Usage: "Disables checking for setup experience on Linux hosts",
EnvVars: []string{"ORBIT_DISABLE_SETUP_EXPERIENCE"},
},
}
app.Before = func(c *cli.Context) error {
// handle old installations, which had default root dir set to /var/lib/orbit
@@ -1514,6 +1519,60 @@ func main() {
go sigusrListener(c.String("root-dir"))
isLinux := runtime.GOOS == "linux"
serverHasWebSetup := orbitClient.GetServerCapabilities().Has(fleet.CapabilityWebSetupExperience)
setupExperienceNotDisabled := !c.Bool("disable-setup-experience")
runSetupExperience := isLinux && serverHasWebSetup && setupExperienceNotDisabled
log.Debug().
Bool("isLinux", isLinux).
Bool("serverHasSetup", serverHasWebSetup).
Bool("notDisabled", setupExperienceNotDisabled).
Msg("checking setup experience preflight values")
openMyDevicePage := func() error {
log.Debug().Msg("launching browser for my device page")
token, err := trw.Read()
if err != nil {
return fmt.Errorf("getting device token: %w", err)
}
// My Device page
browserURL := deviceClient.BrowserDeviceURL(token)
switch runtime.GOOS {
case "linux":
loggedInUser, err := user.UserLoggedInViaGui()
if err != nil {
return fmt.Errorf("get logged in user: %w", err)
}
if loggedInUser == nil {
return errors.New("no user logged in")
}
var opts []execuser.Option
opts = append(opts, execuser.WithUser(*loggedInUser))
opts = append(opts, execuser.WithArg(browserURL, ""))
if _, err := execuser.Run("/usr/bin/xdg-open", opts...); err != nil {
return fmt.Errorf("opening browser with xdg-open: %w", err)
}
default:
log.Debug().Msg("could not open browser, unsupported OS: " + runtime.GOOS)
return errors.New("opening setup experience browser page not supported on " + runtime.GOOS)
}
return nil
}
if runSetupExperience {
log.Debug().Msg("web setup experience enabled")
setupExpPath := path.Join(c.String("root-dir"), constant.SetupExperienceFilename)
if err := processSetupExperience(orbitClient, setupExpPath, openMyDevicePage); err != nil {
log.Error().Err(err).Msg("initiating setup experience")
}
} else {
log.Debug().Msg("not running setup experience")
}
if err := g.Run(); err != nil {
log.Error().Err(err).Msg("unexpected exit")
}
@@ -1531,6 +1590,80 @@ func main() {
}
}
func processSetupExperience(oc *service.OrbitClient, setupExperienceStatusPath string, openMyDevicePage func() error) error {
log.Debug().Msg("checking setup experience file")
exp, err := readSetupExperienceStatusFile(setupExperienceStatusPath)
if err != nil {
return fmt.Errorf("read setup experience file: %w", err)
}
// Setup experience has been completed
if exp != nil && exp.TimeInitiated != nil {
log.Debug().Msg("setup experience already completed")
return nil
}
log.Debug().Msg("initiating setup experience")
resp, err := oc.InitiateSetupExperience()
if err != nil {
return fmt.Errorf("initializing server-side setup experience: %w", err)
}
// Setup experience enabled for us and is now kicked off, open a browser
if resp.Enabled {
if err := openMyDevicePage(); err != nil {
return fmt.Errorf("opening my device page: %w", err)
}
} else {
log.Debug().Msg("setup experience not enabled on team")
}
// Even if it wasn't enabled, mark it as complete so we don't start it again later
log.Debug().Msg("writing setup experience file")
initTime := time.Now()
if err := writeSetupExperienceStatusFile(setupExperienceStatusPath, &SetupExperienceInfo{
TimeInitiated: &initTime,
}); err != nil {
return fmt.Errorf("writing setup experience file: %w", err)
}
return nil
}
type SetupExperienceInfo struct {
TimeInitiated *time.Time `json:"time_initiated,omitempty"`
}
// Returns the time setup experience was completed, or nil if it hasn't
func readSetupExperienceStatusFile(experienceCompletedPath string) (*SetupExperienceInfo, error) {
f, err := os.Open(experienceCompletedPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("read setup experience file: %w", err)
}
var exp *SetupExperienceInfo
if err := json.NewDecoder(f).Decode(&exp); err != nil {
return nil, fmt.Errorf("decoding setup experience file: %w", err)
}
return exp, nil
}
func writeSetupExperienceStatusFile(experienceCompletedPath string, exp *SetupExperienceInfo) error {
f, err := os.Create(experienceCompletedPath)
if err != nil {
return fmt.Errorf("create setup experience completed file: %w", err)
}
if err := json.NewEncoder(f).Encode(exp); err != nil {
return fmt.Errorf("write setup experience completed file: %w", err)
}
return nil
}
func deleteSecretPathIfExists(enrollSecretPath string) {
// Since the secret is in the keystore, we can delete the original secret file if it exists
if _, err := os.Stat(enrollSecretPath); err == nil {
+2
View File
@@ -75,6 +75,8 @@ const (
DesktopTUFTargetName = "desktop"
// FleetURLFileName is the file where Fleet URL is stored after being read from Apple config profile.
FleetURLFileName = "fleet_url.txt"
// SetupExperienceComplete is a file created when Linux (and soon Windows) completes setup experience
SetupExperienceFilename = "setup_experience.json"
FleetHTTPSignatureCertificateFileName = "host_identity.crt"
// FleetHTTPSignatureTPMKeyFileName is the filename for the TPM key used for HTTP signature authentication
+1
View File
@@ -353,6 +353,7 @@ ORBIT_FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST={{ .FleetDesktopAlternativeBrowserH
{{ if .OsqueryDB }}ORBIT_OSQUERY_DB={{.OsqueryDB}}{{ end }}
{{ 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 }}
`))
func writeEnvFile(opt Options, rootPath string) error {
+2
View File
@@ -61,6 +61,8 @@ type Options struct {
FleetDesktopAlternativeBrowserHost string
// DisableUpdates disables auto updates on the generated package.
DisableUpdates bool
// DisableSetupExperience disables setup experience for Linux hosts
DisableSetupExperience bool
// OrbitChannel is the update channel to use for Orbit.
OrbitChannel string
// OsquerydChannel is the update channel to use for Osquery (osqueryd).
+4
View File
@@ -88,6 +88,9 @@ const (
// the ability of the client to show the corresponding UI to support that
// flow.
CapabilitySetupExperience Capability = "setup_experience"
// CapabilityWebSetupExperience denotes the ability of the server to support installing software
// as part of a non-blocking setup experience for Linux and Windows
CapabilityWebSetupExperience Capability = "web_setup_experience"
)
func GetServerOrbitCapabilities() CapabilityMap {
@@ -98,6 +101,7 @@ func GetServerOrbitCapabilities() CapabilityMap {
CapabilityEscrowBuddy: {},
CapabilityLinuxDiskEncryptionEscrow: {},
CapabilitySetupExperience: {},
CapabilityWebSetupExperience: {},
}
}
+10
View File
@@ -737,3 +737,13 @@ func (oc *OrbitClient) SendLinuxKeyEscrowResponse(lr luks.LuksResponse) error {
return nil
}
func (oc *OrbitClient) InitiateSetupExperience() (fleet.SetupExperienceInitResult, error) {
verb, path := "POST", "/api/fleet/orbit/setup_experience/init"
var resp orbitSetupExperienceInitResponse
if err := oc.authenticatedRequest(verb, path, &orbitSetupExperienceInitRequest{}, &resp); err != nil {
return fleet.SetupExperienceInitResult{}, err
}
return resp.Result, nil
}