Files
fleet/orbit/cmd/orbit/orbit.go
T
Roberto Dip 3cfe583ea0 fix issue with disk encryption banner (#21385)
for #21381

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [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/Committing-Changes.md#changes-files)
for more information.
- [x] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [x] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [x] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
2024-08-19 12:02:43 -03:00

1917 lines
68 KiB
Go

package main
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/fleetdm/fleet/v4/orbit/pkg/augeas"
"github.com/fleetdm/fleet/v4/orbit/pkg/build"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
"github.com/fleetdm/fleet/v4/orbit/pkg/insecure"
"github.com/fleetdm/fleet/v4/orbit/pkg/installer"
"github.com/fleetdm/fleet/v4/orbit/pkg/keystore"
"github.com/fleetdm/fleet/v4/orbit/pkg/logging"
"github.com/fleetdm/fleet/v4/orbit/pkg/osquery"
"github.com/fleetdm/fleet/v4/orbit/pkg/osservice"
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
"github.com/fleetdm/fleet/v4/orbit/pkg/profiles"
"github.com/fleetdm/fleet/v4/orbit/pkg/table"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/fleetd_logs"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/orbit_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/token"
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
"github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore"
"github.com/fleetdm/fleet/v4/orbit/pkg/user"
"github.com/fleetdm/fleet/v4/pkg/certificate"
"github.com/fleetdm/fleet/v4/pkg/file"
retrypkg "github.com/fleetdm/fleet/v4/pkg/retry"
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/google/uuid"
"github.com/oklog/run"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"gopkg.in/natefinch/lumberjack.v2"
)
// unusedFlagKeyword is used by the MSI installer to populate parameters, which cannot be empty
const unusedFlagKeyword = "dummy"
func main() {
app := cli.NewApp()
app.Name = "Orbit osquery"
app.Usage = "A powered-up, (near) drop-in replacement for osquery"
app.Commands = []*cli.Command{
versionCommand,
shellCommand,
}
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "root-dir",
Usage: "Root directory for Orbit state",
Value: "", // need to check if explicitly set
EnvVars: []string{"ORBIT_ROOT_DIR"},
},
&cli.BoolFlag{
Name: "insecure",
Usage: "Disable TLS certificate verification",
EnvVars: []string{"ORBIT_INSECURE"},
},
&cli.StringFlag{
Name: "fleet-url",
Usage: "URL (host:port) of Fleet server",
EnvVars: []string{"ORBIT_FLEET_URL"},
},
&cli.StringFlag{
Name: "fleet-certificate",
Usage: "Path to the Fleet server certificate chain",
EnvVars: []string{"ORBIT_FLEET_CERTIFICATE"},
},
&cli.StringFlag{
Name: "fleet-desktop-alternative-browser-host",
Usage: "Alternative host:port to use for Fleet Desktop in the browser (this may be required when using TLS client authentication in the Fleet server)",
EnvVars: []string{"ORBIT_FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST"},
},
&cli.StringFlag{
Name: "update-url",
Usage: "URL for update server",
Value: "https://tuf.fleetctl.com",
EnvVars: []string{"ORBIT_UPDATE_URL"},
},
&cli.StringFlag{
Name: "update-tls-certificate",
Usage: "Path to the update server TLS certificate chain",
EnvVars: []string{"ORBIT_UPDATE_TLS_CERTIFICATE"},
},
&cli.StringFlag{
Name: "enroll-secret",
Usage: "Enroll secret for authenticating to Fleet server",
EnvVars: []string{"ORBIT_ENROLL_SECRET"},
},
&cli.StringFlag{
Name: "enroll-secret-path",
Usage: "Path to file containing enroll secret. On macOS and Windows, this file will be deleted and secret will be stored in the system keystore",
EnvVars: []string{"ORBIT_ENROLL_SECRET_PATH"},
},
&cli.StringFlag{
Name: "osqueryd-channel",
Usage: "Update channel of osqueryd to use",
Value: "stable",
EnvVars: []string{"ORBIT_OSQUERYD_CHANNEL"},
},
&cli.StringFlag{
Name: "orbit-channel",
Usage: "Update channel of Orbit to use",
Value: "stable",
EnvVars: []string{"ORBIT_ORBIT_CHANNEL"},
},
&cli.StringFlag{
Name: "desktop-channel",
Usage: "Update channel of Fleet Desktop to use",
Value: "stable",
EnvVars: []string{"ORBIT_DESKTOP_CHANNEL"},
},
&cli.DurationFlag{
Name: "update-interval",
Usage: "How often to check for updates. Note: fleetd checks for updates at startup. The next update check adds some randomization and may take up to 10 minutes longer",
Value: 15 * time.Minute,
EnvVars: []string{"ORBIT_UPDATE_INTERVAL"},
},
&cli.BoolFlag{
Name: "disable-updates",
Usage: "Disables auto updates",
EnvVars: []string{"ORBIT_DISABLE_UPDATES"},
},
&cli.BoolFlag{
Name: "dev-mode",
Usage: "Runs in development mode",
EnvVars: []string{"ORBIT_DEV_MODE"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "Enable debug logging",
EnvVars: []string{"ORBIT_DEBUG"},
},
&cli.BoolFlag{
Name: "version",
Usage: "Get Orbit version",
},
&cli.StringFlag{
Name: "log-file",
Usage: "Log to this file path in addition to stderr",
EnvVars: []string{"ORBIT_LOG_FILE"},
},
&cli.BoolFlag{
Name: "fleet-desktop",
Usage: "Launch Fleet Desktop application (flag currently only used on darwin)",
EnvVars: []string{"ORBIT_FLEET_DESKTOP"},
},
// Note: this flag doesn't have any effect anymore. I'm keeping
// it just for backwards compatibility since some users were
// using it because softwareupdated was causing problems and I
// don't want to break their setups.
//
// For more context please check out: https://github.com/fleetdm/fleet/issues/11777
&cli.BoolFlag{
Name: "disable-kickstart-softwareupdated",
Usage: "(Deprecated) Disable periodic execution of 'launchctl kickstart -k softwareupdated' on macOS",
EnvVars: []string{"ORBIT_FLEET_DISABLE_KICKSTART_SOFTWAREUPDATED"},
Hidden: true,
},
&cli.BoolFlag{
Name: "use-system-configuration",
Usage: "Try to read --fleet-url and --enroll-secret using configuration in the host (currently only macOS profiles are supported)",
EnvVars: []string{"ORBIT_USE_SYSTEM_CONFIGURATION"},
Hidden: true,
},
&cli.BoolFlag{
Name: "enable-scripts",
Usage: "Enable script execution",
EnvVars: []string{"ORBIT_ENABLE_SCRIPTS"},
},
&cli.StringFlag{
Name: "host-identifier",
Usage: "Sets the host identifier that orbit and osquery will use when enrolling to Fleet. Options: 'uuid' and 'instance' (requires Fleet >= v4.42.0)",
EnvVars: []string{"ORBIT_HOST_IDENTIFIER"},
Value: "uuid",
},
&cli.StringFlag{
Name: "end-user-email",
Hidden: true, // experimental feature, we don't want to show it for now
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.BoolFlag{
Name: "disable-keystore",
Usage: "Disables the use of the keychain on macOS and Credentials Manager on Windows",
EnvVars: []string{"ORBIT_DISABLE_KEYSTORE"},
},
&cli.StringFlag{
Name: "osquery-db",
Usage: "Sets a custom osquery database directory, it must be an absolute path",
EnvVars: []string{"ORBIT_OSQUERY_DB"},
},
}
app.Before = func(c *cli.Context) error {
// handle old installations, which had default root dir set to /var/lib/orbit
if c.String("root-dir") == "" {
rootDir := update.DefaultOptions.RootDirectory
executable, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get orbit executable: %w", err)
}
if strings.HasPrefix(executable, "/var/lib/orbit") {
rootDir = "/var/lib/orbit"
}
if err := c.Set("root-dir", rootDir); err != nil {
return fmt.Errorf("failed to set root-dir: %w", err)
}
}
return nil
}
app.Action = func(c *cli.Context) error {
if c.Bool("version") {
fmt.Println("orbit " + build.Version)
return nil
}
startTime := time.Now()
var logFile io.Writer
if logf := c.String("log-file"); logf != "" {
if logDir := filepath.Dir(logf); logDir != "." {
if err := secure.MkdirAll(logDir, constant.DefaultDirMode); err != nil {
panic(err)
}
}
logFile = &lumberjack.Logger{
Filename: logf,
MaxSize: 25, // megabytes
MaxBackups: 3,
MaxAge: 28, // days
}
if runtime.GOOS == "windows" {
// On Windows, Orbit runs as a "Windows Service", which fails to write to os.Stderr with
// "write /dev/stderr: The handle is invalid" (see
// #3100). Thus, we log to the logFile only.
log.Logger = log.Output(zerolog.MultiLevelWriter(
zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true},
&fleetd_logs.Logger,
))
} else {
log.Logger = log.Output(zerolog.MultiLevelWriter(
zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true},
zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true},
&fleetd_logs.Logger,
))
}
} else {
log.Logger = log.Output(zerolog.MultiLevelWriter(
zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true},
&fleetd_logs.Logger,
))
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if c.Bool("debug") {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
// Override flags with values retrieved from Fleet.
fallbackServerOverridesCfg := setServerOverrides(c)
if !fallbackServerOverridesCfg.empty() {
log.Debug().Msgf("fallback settings: %+v", fallbackServerOverridesCfg)
}
if c.Bool("insecure") && c.String("fleet-certificate") != "" {
return errors.New("insecure and fleet-certificate may not be specified together")
}
if c.Bool("insecure") && c.String("update-tls-certificate") != "" {
return errors.New("insecure and update-tls-certificate may not be specified together")
}
if odb := c.String("osquery-db"); odb != "" && !filepath.IsAbs(odb) {
return fmt.Errorf("the osquery database must be an absolute path: %q", odb)
}
enrollSecretPath := c.String("enroll-secret-path")
if enrollSecretPath != "" {
if c.String("enroll-secret") != "" {
return errors.New("enroll-secret and enroll-secret-path may not be specified together")
}
// Read secret from file. If secret is found and keystore enabled, write/overwrite the secret to the keystore and delete the file.
b, err := os.ReadFile(enrollSecretPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) || !keystore.Supported() || c.Bool("disable-keystore") {
return fmt.Errorf("read enroll secret file: %w", err)
}
} else {
secret := strings.TrimSpace(string(b))
if err = c.Set("enroll-secret", secret); err != nil {
return fmt.Errorf("set enroll secret from file: %w", err)
}
if keystore.Supported() && !c.Bool("disable-keystore") {
// Check if secret is already in the keystore.
secretFromKeystore, err := keystore.GetSecret()
if err != nil {
log.Warn().Err(err).Msgf("failed to retrieve enroll secret from %v", keystore.Name())
} else if secretFromKeystore == "" {
// Keystore secret not found, so we will add it to the keystore.
if err = keystore.AddSecret(secret); err != nil {
log.Warn().Err(err).Msgf("failed to add enroll secret to %v", keystore.Name())
} else {
// Sanity check that the secret was added to the keystore.
checkSecret, err := keystore.GetSecret()
if err != nil {
log.Warn().Err(err).Msgf("failed to check that enroll secret was saved in %v", keystore.Name())
} else if checkSecret != secret {
log.Warn().Msgf("enroll secret was not saved correctly in %v", keystore.Name())
} else {
log.Info().Msgf("added enroll secret to keystore: %v", keystore.Name())
deleteSecretPathIfExists(enrollSecretPath)
}
}
} else if secretFromKeystore != secret {
// Keystore secret found, but needs to be updated.
if err = keystore.UpdateSecret(secret); err != nil {
log.Warn().Err(err).Msgf("failed to update enroll secret in %v", keystore.Name())
} else {
// Sanity check that the secret was updated in the keystore.
checkSecret, err := keystore.GetSecret()
if err != nil {
log.Warn().Err(err).Msgf("failed to check that enroll secret was updated in %v", keystore.Name())
} else if checkSecret != secret {
log.Warn().Msgf("enroll secret was not updated correctly in %v", keystore.Name())
} else {
log.Info().Msgf("updated enroll secret in keystore: %v", keystore.Name())
deleteSecretPathIfExists(enrollSecretPath)
}
}
} else {
// Keystore secret found, and it matches the secret from the file.
deleteSecretPathIfExists(enrollSecretPath)
}
}
}
}
if c.String("enroll-secret") == "" && keystore.Supported() && !c.Bool("disable-keystore") &&
!(runtime.GOOS == "darwin" && c.Bool("use-system-configuration")) {
secret, err := keystore.GetSecret()
if err != nil || secret == "" {
return fmt.Errorf("failed to retrieve enroll secret from %v: %w", keystore.Name(), err)
}
log.Info().Msgf("found enroll secret in keystore: %v", keystore.Name())
if err = c.Set("enroll-secret", secret); err != nil {
return fmt.Errorf("set enroll secret from keystore: %w", err)
}
}
if hostIdentifier := c.String("host-identifier"); hostIdentifier != "uuid" && hostIdentifier != "instance" {
return fmt.Errorf("--host-identifier=%s is not supported, currently supported values are 'uuid' and 'instance'", hostIdentifier)
}
if email := c.String("end-user-email"); email != "" && email != unusedFlagKeyword && !fleet.IsLooseEmail(email) {
return fmt.Errorf("the provided end-user email address %q is not a valid email address", email)
}
if err := secure.MkdirAll(c.String("root-dir"), constant.DefaultDirMode); err != nil {
return fmt.Errorf("initialize root dir: %w", err)
}
// if neither are set, this might be an agent deployed via MDM, try to read
// both configs from a configuration profile
if runtime.GOOS == "darwin" && c.Bool("use-system-configuration") {
log.Info().Msg("trying to read fleet-url and enroll-secret from a configuration profile")
for {
config, err := profiles.GetFleetdConfig()
switch {
// handle these errors separately as debug messages to not raise false
// alarms when users look into the orbit logs, it's perfectly normal to
// not have a configuration profile, or to get into this situation in
// operating systems that don't have profile support.
case err != nil:
log.Error().Err(err).Msg("reading configuration profile")
case config.EnrollSecret == "" || config.FleetURL == "":
log.Debug().Msg("enroll secret or fleet url are empty in configuration profile, not setting either")
default:
log.Info().Msg("setting enroll-secret and fleet-url configs from configuration profile")
if err := c.Set("enroll-secret", config.EnrollSecret); err != nil {
return fmt.Errorf("set enroll secret from configuration profile: %w", err)
}
if err := c.Set("fleet-url", config.FleetURL); err != nil {
return fmt.Errorf("set fleet URL from configuration profile: %w", err)
}
if err := writeSecret(config.EnrollSecret, c.String("root-dir")); err != nil {
return fmt.Errorf("write enroll secret: %w", err)
}
}
if c.String("fleet-url") != "" && c.String("enroll-secret") != "" {
log.Info().Msg("found configuration values in system profile")
break
}
log.Info().Msg("didn't find configuration values in system profile, trying again in 30 seconds")
time.Sleep(30 * time.Second)
}
}
localStore, err := filestore.New(filepath.Join(c.String("root-dir"), "tuf-metadata.json"))
if err != nil {
log.Fatal().Err(err).Msg("create local metadata store")
}
opt := update.DefaultOptions
if c.Bool("fleet-desktop") {
switch runtime.GOOS {
case "darwin":
opt.Targets["desktop"] = update.DesktopMacOSTarget
case "windows":
opt.Targets["desktop"] = update.DesktopWindowsTarget
case "linux":
if runtime.GOARCH == "arm64" {
opt.Targets["desktop"] = update.DesktopLinuxArm64Target
} else {
opt.Targets["desktop"] = update.DesktopLinuxTarget
}
default:
log.Fatal().Str("GOOS", runtime.GOOS).Msg("unsupported GOOS for desktop target")
}
// Override default channel with the provided value.
opt.Targets.SetTargetChannel("desktop", c.String("desktop-channel"))
}
// Override default channels with the provided values.
opt.Targets.SetTargetChannel("orbit", c.String("orbit-channel"))
opt.Targets.SetTargetChannel("osqueryd", c.String("osqueryd-channel"))
opt.RootDirectory = c.String("root-dir")
opt.ServerURL = c.String("update-url")
opt.LocalStore = localStore
opt.InsecureTransport = c.Bool("insecure")
opt.ServerCertificatePath = c.String("update-tls-certificate")
var (
osquerydPath string
desktopPath string
g run.Group
appDoneCh chan struct{} // closed when runner run.group.Run() returns
)
// Setting up the system service management early on the process lifetime
appDoneCh = make(chan struct{})
// Initializing windows service runner and system service manager.
if runtime.GOOS == "windows" {
systemChecker := newSystemChecker()
addSubsystem(&g, "system checker", systemChecker)
go osservice.SetupServiceManagement(constant.SystemServiceName, systemChecker.svcInterruptCh, appDoneCh)
}
// sofwareupdated is a macOS daemon that automatically updates Apple software.
if c.Bool("disable-kickstart-softwareupdated") && runtime.GOOS == "darwin" {
log.Warn().Msg("fleetd no longer automatically kickstarts softwareupdated. The --disable-kickstart-softwareupdated flag, which was previously used to disable this behavior, has been deprecated and will be removed in a future version")
}
updateClientCrtPath := filepath.Join(c.String("root-dir"), constant.UpdateTLSClientCertificateFileName)
updateClientKeyPath := filepath.Join(c.String("root-dir"), constant.UpdateTLSClientKeyFileName)
updateClientCrt, err := certificate.LoadClientCertificateFromFiles(updateClientCrtPath, updateClientKeyPath)
if err != nil {
return fmt.Errorf("error loading update client certificate: %w", err)
}
if updateClientCrt != nil {
log.Info().Msg("Found TLS client certificate and key. Using them to authenticate to the update server.")
opt.ClientCertificate = &updateClientCrt.Crt
}
// NOTE: When running in dev-mode, even if `disable-updates` is set,
// it fetches osqueryd once as part of initialization.
var updater *update.Updater
var updateRunner *update.Runner
if !c.Bool("disable-updates") || c.Bool("dev-mode") {
updater, err = update.NewUpdater(opt)
if err != nil {
return fmt.Errorf("create updater: %w", err)
}
if err := updater.UpdateMetadata(); err != nil {
log.Info().Err(err).Msg("update metadata. using saved metadata")
}
targets := []string{"orbit", "osqueryd"}
if c.Bool("fleet-desktop") {
targets = append(targets, "desktop")
}
if c.Bool("dev-mode") {
targets = targets[1:] // exclude orbit itself on dev-mode.
}
updateRunner, err = update.NewRunner(updater, update.RunnerOptions{
CheckInterval: c.Duration("update-interval"),
Targets: targets,
})
if err != nil {
return err
}
// Get current version of osquery
log.Info().Msgf("orbit version: %s", build.Version)
osquerydPath, err = updater.ExecutableLocalPath("osqueryd")
if err != nil {
log.Info().Err(err).Msg("Could not find local osqueryd executable")
} else {
version, err := update.GetVersion(osquerydPath)
if err == nil && version != "" {
log.Info().Msgf("Found osquery version: %s", version)
updateRunner.OsqueryVersion = version
}
}
// Perform early check for updates before starting any sub-system.
// This is to prevent bugs in other sub-systems to mess up with
// the download of available updates.
didUpdate, err := updateRunner.UpdateAction()
if err != nil {
log.Info().Err(err).Msg("early update check failed")
}
if didUpdate && !c.Bool("dev-mode") {
log.Info().Msg("exiting due to successful early update")
return nil
}
addSubsystem(&g, "update runner", updateRunner)
// if getting any of the targets fails, keep on
// retrying, the `updater.Get` method has built-in backoff functionality.
//
// NOTE: it used to be the case that we would return an
// error on the first attempt here, causing orbit to
// restart. This was changed to have control over
// how/when we want to retry to download the packages.
err = retrypkg.Do(func() error {
var err error
osquerydPath, desktopPath, err = getFleetdComponentPaths(c, updater, fallbackServerOverridesCfg)
if err != nil {
return err
}
return nil
},
// retry every 5 minutes to not flood the logs,
// but actual pings to the remote server are
// handled by `updater.Get`
retrypkg.WithInterval(5*time.Minute),
)
if err != nil {
// this should never happen because `retry.Do` is
// executed without a defined number of max attempts
return fmt.Errorf("getting targets after retry: %w", err)
}
} else {
log.Info().Msg("running with auto updates disabled")
updater = update.NewDisabled(opt)
osquerydPath, err = updater.ExecutableLocalPath("osqueryd")
if err != nil {
log.Fatal().Err(err).Msg("locate osqueryd")
}
if c.Bool("fleet-desktop") {
if runtime.GOOS == "darwin" {
desktopPath, err = updater.DirLocalPath("desktop")
if err != nil {
return fmt.Errorf("get desktop target: %w", err)
}
} else {
desktopPath, err = updater.ExecutableLocalPath("desktop")
if err != nil {
return fmt.Errorf("get desktop target: %w", err)
}
}
}
}
// Clear leftover files from updates
if err := filepath.Walk(c.String("root-dir"), func(path string, info fs.FileInfo, err error) error {
// Ignore anything not containing .old extension
if !strings.HasSuffix(path, ".old") {
return nil
}
if err := os.RemoveAll(path); err != nil {
log.Info().Err(err).Msg("remove .old")
return nil
}
log.Debug().Str("path", path).Msg("cleaned up old")
return nil
}); err != nil {
return fmt.Errorf("cleanup old files: %w", err)
}
// Kill any pre-existing instances of osqueryd (otherwise getHostInfo will fail
// because of osqueryd's lock over its database).
//
// This can happen for instance when orbit is killed via the Windows Task Manager
// (there's no SIGTERM on Windows) and the osqueryd child processes are left orphaned.
killedProcesses, err := platform.KillAllProcessByName("osqueryd")
if err != nil {
log.Error().Err(err).Msg("failed to kill pre-existing instances of osqueryd")
} else if len(killedProcesses) > 0 {
log.Debug().Str("processes", fmt.Sprintf("%+v", killedProcesses)).Msg("existing osqueryd processes killed")
}
osqueryDB := filepath.Join(c.String("root-dir"), "osquery.db")
if odb := c.String("osquery-db"); odb != "" {
osqueryDB = odb
}
osqueryHostInfo, err := getHostInfo(osquerydPath, osqueryDB)
if err != nil {
return fmt.Errorf("get UUID: %w", err)
}
log.Debug().Str("info", fmt.Sprint(osqueryHostInfo)).Msg("retrieved host info from osquery")
orbitHostInfo := fleet.OrbitHostInfo{
HardwareSerial: osqueryHostInfo.HardwareSerial,
HardwareUUID: osqueryHostInfo.HardwareUUID,
Hostname: osqueryHostInfo.Hostname,
Platform: osqueryHostInfo.Platform,
}
if runtime.GOOS == "darwin" {
// Get the hardware UUID. We use a temporary osquery DB location in order to guarantee that
// we're getting true UUID, not a cached UUID. See
// https://github.com/fleetdm/fleet/issues/17934 and
// https://github.com/osquery/osquery/issues/7509 for more details.
tmpDBPath := filepath.Join(os.TempDir(), strings.Join([]string{uuid.NewString(), "tmp-db"}, "-"))
oi, err := getHostInfo(osquerydPath, tmpDBPath)
if err != nil {
return fmt.Errorf("get UUID from temp db: %w", err)
}
if err := os.RemoveAll(tmpDBPath); err != nil {
log.Info().Err(err).Msg("failed to remove temporary osquery db")
}
if oi.HardwareUUID != orbitHostInfo.HardwareUUID {
// Then we have moved to a new physical machine, so we should restart!
// Removing the osquery DB should trigger a re-enrollment when fleetd is restarted.
if err := os.RemoveAll(osqueryDB); err != nil {
return fmt.Errorf("removing old osquery.db: %w", err)
}
// We can remove this because we want it to be regenerated during the re-enrollment.
if err := os.RemoveAll(filepath.Join(c.String("root-dir"), constant.OrbitNodeKeyFileName)); err != nil {
return fmt.Errorf("removing old orbit node key file: %w", err)
}
return errors.New("found a new hardware uuid, restarting")
}
}
// Only send osquery's `instance_id` if the user is running orbit with `--host-identifier=instance`.
// When not set, orbit and osquery will be matched using the hardware UUID (orbitHostInfo.HardwareUUID).
if c.String("host-identifier") == "instance" {
orbitHostInfo.OsqueryIdentifier = osqueryHostInfo.InstanceID
}
// The hardware serial was not sent when Windows MDM was implemented,
// thus we clear its value here to not break any existing enroll functionality
// on the server.
if runtime.GOOS == "windows" {
orbitHostInfo.HardwareSerial = ""
}
var (
options []osquery.Option
// optionsAfterFlagfile is populated with options that will be set after the '--flagfile' argument
// to not allow users to change their values on their flagfiles.
optionsAfterFlagfile []osquery.Option
)
options = append(options, osquery.WithDataPath(c.String("root-dir"), ""))
options = append(options, osquery.WithLogPath(filepath.Join(c.String("root-dir"), "osquery_log")))
optionsAfterFlagfile = append(optionsAfterFlagfile, osquery.WithFlags(
[]string{"--database_path", osqueryDB},
))
if logFile != nil {
// If set, redirect osqueryd's stderr to the logFile.
options = append(options, osquery.WithStderr(logFile))
}
fleetURL := c.String("fleet-url")
if !strings.HasPrefix(fleetURL, "http") {
fleetURL = "https://" + fleetURL
}
enrollSecret := c.String("enroll-secret")
if enrollSecret != "" {
const enrollSecretEnvName = "ENROLL_SECRET"
options = append(options,
osquery.WithEnv([]string{enrollSecretEnvName + "=" + enrollSecret}),
osquery.WithFlags([]string{"--enroll_secret_env", enrollSecretEnvName}),
)
}
var certPath string
if fleetURL != "https://" && c.Bool("insecure") {
proxy, err := insecure.NewTLSProxy(fleetURL)
if err != nil {
return fmt.Errorf("create TLS proxy: %w", err)
}
addSubsystem(&g, "insecure proxy", &wrapSubsystem{
execute: func() error {
log.Info().
Str("addr", fmt.Sprintf("localhost:%d", proxy.Port)).
Str("target", c.String("fleet-url")).
Msg("using insecure TLS proxy")
err := proxy.InsecureServeTLS()
return err
},
interrupt: func(err error) {
if err := proxy.Close(); err != nil {
log.Error().Err(err).Msg("close proxy")
}
},
})
// Directory to store proxy related assets
proxyDirectory := filepath.Join(c.String("root-dir"), "proxy")
if err := secure.MkdirAll(proxyDirectory, constant.DefaultDirMode); err != nil {
return fmt.Errorf("there was a problem creating the proxy directory: %w", err)
}
certPath = filepath.Join(proxyDirectory, "fleet.crt")
// Write cert that proxy uses
err = os.WriteFile(certPath, []byte(insecure.ServerCert), os.ModePerm)
if err != nil {
return fmt.Errorf("write server cert: %w", err)
}
// Rewrite URL to the proxy URL. Note the proxy handles any URL
// prefix so we don't need to carry that over here.
parsedURL := &url.URL{
Scheme: "https",
Host: fmt.Sprintf("localhost:%d", proxy.Port),
}
// Check and log if there are any errors with TLS connection.
pool, err := certificate.LoadPEM(certPath)
if err != nil {
return fmt.Errorf("load certificate: %w", err)
}
if err := certificate.ValidateConnection(pool, fleetURL); err != nil {
log.Info().Err(err).Msg("Failed to connect to Fleet server. Osquery connection may fail.")
}
options = append(options,
osquery.WithFlags(osquery.FleetFlags(parsedURL)),
osquery.WithFlags([]string{"--tls_server_certs", certPath}),
)
} else if fleetURL != "https://" {
if enrollSecret == "" {
return errors.New("enroll secret must be specified to connect to Fleet server")
}
parsedURL, err := url.Parse(fleetURL)
if err != nil {
return fmt.Errorf("parse URL: %w", err)
}
options = append(options,
osquery.WithFlags(osquery.FleetFlags(parsedURL)),
)
if certPath = c.String("fleet-certificate"); certPath != "" {
// Check and log if there are any errors with TLS connection.
pool, err := certificate.LoadPEM(certPath)
if err != nil {
return fmt.Errorf("load certificate: %w", err)
}
if err := certificate.ValidateConnection(pool, fleetURL); err != nil {
log.Info().Err(err).Msg("Failed to connect to Fleet server. Osquery connection may fail.")
}
options = append(options,
osquery.WithFlags([]string{"--tls_server_certs", certPath}),
)
} else {
certPath = filepath.Join(c.String("root-dir"), "certs.pem")
if exists, err := file.Exists(certPath); err == nil && exists {
_, err = certificate.LoadPEM(certPath)
if err != nil {
return fmt.Errorf("load certs.pem: %w", err)
}
options = append(options, osquery.WithFlags([]string{"--tls_server_certs", certPath}))
} else {
log.Info().Msg("No cert chain available. Relying on system store.")
}
}
}
fleetClientCertPath := filepath.Join(c.String("root-dir"), constant.FleetTLSClientCertificateFileName)
fleetClientKeyPath := filepath.Join(c.String("root-dir"), constant.FleetTLSClientKeyFileName)
fleetClientCrt, err := certificate.LoadClientCertificateFromFiles(fleetClientCertPath, fleetClientKeyPath)
if err != nil {
return fmt.Errorf("error loading fleet client certificate: %w", err)
}
var fleetClientCertificate *tls.Certificate
if fleetClientCrt != nil {
log.Info().Msg("Found TLS client certificate and key. Using them to authenticate to Fleet.")
fleetClientCertificate = &fleetClientCrt.Crt
options = append(options, osquery.WithFlags([]string{
"--tls_client_cert", fleetClientCertPath,
"--tls_client_key", fleetClientKeyPath,
}))
}
orbitClient, err := service.NewOrbitClient(
c.String("root-dir"),
fleetURL,
c.String("fleet-certificate"),
c.Bool("insecure"),
enrollSecret,
fleetClientCertificate,
orbitHostInfo,
&service.OnGetConfigErrFuncs{
DebugErrFunc: func(err error) {
log.Debug().Err(err).Msg("get config")
},
OnNetErrFunc: func(err error) {
log.Info().Err(err).Msg("network error")
},
},
)
if err != nil {
return fmt.Errorf("error new orbit client: %w", err)
}
// create the notifications middleware that wraps the orbit client
// (must be shared by all runners that use a ConfigFetcher).
const (
renewEnrollmentProfileCommandFrequency = 3 * time.Minute
windowsMDMEnrollmentCommandFrequency = time.Hour
windowsMDMBitlockerCommandFrequency = time.Hour
)
scriptConfigReceiver, scriptsEnabledFn := update.ApplyRunScriptsConfigFetcherMiddleware(
c.Bool("enable-scripts"), orbitClient,
)
orbitClient.RegisterConfigReceiver(scriptConfigReceiver)
switch runtime.GOOS {
case "darwin":
orbitClient.RegisterConfigReceiver(update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(
orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL))
const nudgeLaunchInterval = 30 * time.Minute
orbitClient.RegisterConfigReceiver(update.ApplyNudgeConfigReceiverMiddleware(update.NudgeConfigFetcherOptions{
UpdateRunner: updateRunner, RootDir: c.String("root-dir"), Interval: nudgeLaunchInterval,
}))
orbitClient.RegisterConfigReceiver(update.ApplySwiftDialogDownloaderMiddleware(updateRunner))
case "windows":
orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMEnrollmentFetcherMiddleware(windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient))
orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMBitlockerFetcherMiddleware(windowsMDMBitlockerCommandFrequency, orbitClient))
}
flagUpdateReceiver := update.NewFlagReceiver(orbitClient.TriggerOrbitRestart, update.FlagUpdateOptions{
RootDir: c.String("root-dir"),
})
orbitClient.RegisterConfigReceiver(flagUpdateReceiver)
if !c.Bool("disable-updates") {
serverOverridesReceiver := newServerOverridesReceiver(
c.String("root-dir"),
fallbackServerOverridesConfig{
OsquerydPath: osquerydPath,
DesktopPath: desktopPath,
},
c.Bool("fleet-desktop"),
orbitClient.TriggerOrbitRestart,
)
orbitClient.RegisterConfigReceiver(serverOverridesReceiver)
}
// only setup extensions autoupdate if we have enabled updates
// for extensions autoupdate, we can only proceed after orbit is enrolled in fleet
// and all relevant things for it (like certs, enroll secrets, tls proxy, etc) is configured
if !c.Bool("disable-updates") || c.Bool("dev-mode") {
extRunner := update.NewExtensionConfigUpdateRunner(update.ExtensionUpdateOptions{
RootDir: c.String("root-dir"),
}, updateRunner, orbitClient.TriggerOrbitRestart)
// call UpdateAction on the updateRunner after we have fetched extensions from Fleet
_, err := updateRunner.UpdateAction()
if err != nil {
// OK, initial call may fail, ok to continue
logging.LogErrIfEnvNotSet(constant.SilenceEnrollLogErrorEnvVar, err, "initial extensions update action failed")
}
extensionAutoLoadFile := filepath.Join(c.String("root-dir"), "extensions.load")
stat, err := os.Stat(extensionAutoLoadFile)
// we only want to add the extensions_autoload flag to osquery, if the file exists and size > 0
switch {
case err == nil:
if stat.Size() > 0 {
log.Debug().Msg("adding --extensions_autoload flag for file " + extensionAutoLoadFile)
// We set this option after the --flagfile to prevent users from changing it on their flagfiles.
optionsAfterFlagfile = append(optionsAfterFlagfile, osquery.WithFlags([]string{"--extensions_autoload", extensionAutoLoadFile}))
} else {
// OK, expected as well when extensions are unloaded, just debug log
log.Debug().Msg("found empty extensions.load file at " + extensionAutoLoadFile)
}
case errors.Is(err, os.ErrNotExist):
// OK, nothing to do.
default:
logging.LogErrIfEnvNotSet(constant.SilenceEnrollLogErrorEnvVar, err, "error with extensions.load file at "+extensionAutoLoadFile)
}
orbitClient.RegisterConfigReceiver(extRunner)
}
// Run a early check of fleetd configuration to check if orbit needs to
// restart before proceeding to start the sub-systems.
//
// E.g. the administrator has updated the following agent options for this device:
// - `update_channels`
// - `extensions` were removed/unset
// - `command_line_flags` (osquery startup flags)
if err := orbitClient.RunConfigReceivers(); err != nil {
log.Error().Msgf("failed initial config fetch: %s", err)
} else {
if orbitClient.RestartTriggered() {
log.Info().Msg("exiting after early config fetch")
return nil
}
}
addSubsystem(&g, "config receivers", &wrapSubsystem{
execute: orbitClient.ExecuteConfigReceivers,
interrupt: orbitClient.InterruptConfigReceivers,
})
var trw *token.ReadWriter
if c.Bool("fleet-desktop") {
trw = token.NewReadWriter(filepath.Join(c.String("root-dir"), "identifier"))
if err := trw.LoadOrGenerate(); err != nil {
return fmt.Errorf("initializing token read writer: %w", err)
}
log.Info().Msg("token rotation is enabled")
// we enable remote updates only if the server supports them by setting
// this function.
trw.SetRemoteUpdateFunc(
func(token string) error {
return orbitClient.SetOrUpdateDeviceToken(token)
},
)
// Note that the deviceClient used by orbit must not define a retry on
// invalid token, because its goal is to detect invalid tokens when
// making requests with this client.
deviceClient, err := service.NewDeviceClient(
fleetURL,
c.Bool("insecure"),
c.String("fleet-certificate"),
fleetClientCertificate,
c.String("fleet-desktop-alternative-browser-host"),
)
if err != nil {
return fmt.Errorf("initializing client: %w", err)
}
// Check if token is not expired and still good.
// If not, rotate the token.
expired, _ := trw.HasExpired()
if expired || deviceClient.CheckToken(trw.GetCached()) != nil {
if err := trw.Rotate(); err != nil {
return fmt.Errorf("rotating token: %w", err)
}
}
go func() {
// This timer is used to check if the token should be rotated if at
// least one hour has passed since the last modification of the token
// file.
//
// This is better than using a ticker that ticks every hour because the
// we can't ensure the tick actually runs every hour (eg: the computer is
// asleep).
rotationDuration := 30 * time.Second
rotationTicker := time.NewTicker(rotationDuration)
defer rotationTicker.Stop()
// This timer is used to periodically check if the token is valid. The
// server might deem a toked as invalid for reasons out of our control,
// for example if the database is restored to a back-up or if somebody
// manually invalidates the token in the db.
remoteCheckDuration := 5 * time.Minute
remoteCheckTicker := time.NewTicker(remoteCheckDuration)
defer remoteCheckTicker.Stop()
for {
select {
case <-rotationTicker.C:
rotationTicker.Reset(rotationDuration)
log.Debug().Msgf("checking if token has changed or expired, cached mtime: %s", trw.GetMtime())
hasChanged, err := trw.HasChanged()
if err != nil {
log.Error().Err(err).Msg("error checking if token has changed")
}
exp, remain := trw.HasExpired()
// rotate if the token file has been modified, if the token is
// expired or if it is very close to expire.
if hasChanged || exp || remain <= time.Second {
log.Info().Msg("token TTL expired, rotating token")
if err := trw.Rotate(); err != nil {
log.Error().Err(err).Msg("error rotating token")
}
} else if remain > 0 && remain < rotationDuration {
// check again when the token will expire, which will happen
// before the next rotation check
rotationTicker.Reset(remain)
log.Debug().Msgf("token will expire soon, checking again in: %s", remain)
}
case <-remoteCheckTicker.C:
log.Debug().Msgf("initiating remote token check after %s", remoteCheckDuration)
if err := deviceClient.CheckToken(trw.GetCached()); err != nil {
log.Info().Err(err).Msg("periodic check of token failed, initiating rotation")
if err := trw.Rotate(); err != nil {
log.Error().Err(err).Msg("error rotating token")
}
}
}
}
}()
}
// On Windows, where augeas doesn't work, we have a stubbed CopyLenses that always returns
// `"", nil`. Therefore there's no platform-specific stuff required here
augeasPath, err := augeas.CopyLenses(c.String("root-dir"))
if err != nil {
log.Warn().Err(err).Msg("failed to copy augeas lenses, augeas may not be available")
} else if augeasPath != "" {
options = append(options, osquery.WithFlags([]string{"--augeas_lenses", augeasPath}))
}
// --force is sometimes needed when an older osquery process has not
// exited properly
options = append(options, osquery.WithFlags([]string{"--force"}))
if c.Bool("debug") {
options = append(options,
osquery.WithFlags([]string{"--verbose", "--tls_dump"}),
)
}
// Provide the flagfile to osquery if it exists. This comes after the other flags set by
// Orbit so that users can override those flags. Note this means users may unintentionally
// break things by overriding Orbit flags in incompatible ways. That's the price to pay for
// flexibility.
flagfilePath := filepath.Join(c.String("root-dir"), "osquery.flags")
if exists, err := file.Exists(flagfilePath); err == nil && exists {
options = append(options, osquery.WithFlags([]string{"--flagfile", flagfilePath}))
}
// These options must go after '--flagfile' to not allow users to change their values
// on their flagfiles.
hostIdentifier := c.String("host-identifier")
options = append(options, osquery.WithFlags([]string{"--host-identifier", hostIdentifier}))
for _, option := range optionsAfterFlagfile {
options = append(options, option)
}
// Handle additional args after '--' in the command line. These are added last and should
// override all other flags and flagfile entries.
options = append(options, osquery.WithFlags(c.Args().Slice()))
// Create an osquery runner with the provided options.
r, err := osquery.NewRunner(osquerydPath, options...)
if err != nil {
return fmt.Errorf("create osquery runner: %w", err)
}
addSubsystem(&g, "osqueryd runner", r)
// rootDir string, addr string, rootCA string, insecureSkipVerify bool, enrollSecret, uuid string
checkerClient, err := service.NewOrbitClient(
c.String("root-dir"),
fleetURL,
c.String("fleet-certificate"),
c.Bool("insecure"),
enrollSecret,
fleetClientCertificate,
orbitHostInfo,
&service.OnGetConfigErrFuncs{
DebugErrFunc: func(err error) {
log.Debug().Err(err).Msg("get config")
},
OnNetErrFunc: func(err error) {
log.Info().Err(err).Msg("network error")
},
},
)
if err != nil {
return fmt.Errorf("new client for capabilities checker: %w", err)
}
capabilitiesChecker := newCapabilitiesChecker(checkerClient)
// We populate the known capabilities so that the capability checker does not need to do the initial check on startup.
checkerClient.GetServerCapabilities().Copy(orbitClient.GetServerCapabilities())
addSubsystem(&g, "capabilities checker", capabilitiesChecker)
var desktopVersion string
if c.Bool("fleet-desktop") {
runPath := desktopPath
if runtime.GOOS == "darwin" {
runPath = filepath.Join(desktopPath, "Contents", "MacOS", constant.DesktopAppExecName)
}
desktopVersion, err = update.GetVersion(runPath)
if err == nil && desktopVersion != "" {
log.Info().Msgf("Found fleet-desktop version: %s", desktopVersion)
} else {
desktopVersion = "unknown"
}
}
registerExtensionRunner(
&g,
r.ExtensionSocketPath(),
table.WithExtension(orbit_info.New(
orbitClient,
c.String("orbit-channel"),
c.String("osqueryd-channel"),
c.String("desktop-channel"),
desktopVersion,
trw,
startTime,
scriptsEnabledFn,
)),
)
if c.Bool("fleet-desktop") {
var (
rawClientCrt []byte
rawClientKey []byte
)
if fleetClientCrt != nil {
rawClientCrt = fleetClientCrt.RawCrt
rawClientKey = fleetClientCrt.RawKey
}
desktopRunner := newDesktopRunner(
desktopPath,
fleetURL,
c.String("fleet-certificate"),
c.Bool("insecure"),
trw,
rawClientCrt,
rawClientKey,
c.String("fleet-desktop-alternative-browser-host"),
opt.RootDirectory,
)
addSubsystem(&g, "desktop runner", desktopRunner)
}
// --end-user-email is only supported on Windows and Linux (for macOS it gets the
// email from the enrollment profile)
endUserEmail := c.String("end-user-email")
if (runtime.GOOS == "windows" || runtime.GOOS == "linux") && endUserEmail != "" && endUserEmail != unusedFlagKeyword {
if orbitClient.GetServerCapabilities().Has(fleet.CapabilityEndUserEmail) {
log.Debug().Msg("sending end-user email to Fleet")
if err := orbitClient.SetOrUpdateDeviceMappingEmail(endUserEmail); err != nil {
log.Error().Err(err).Msg("error sending end-user email to Fleet")
}
} else {
log.Info().Msg("an end-user email is provided, but the Fleet server doesn't have the capability to set it.")
}
}
// For macOS hosts, check if MDM enrollment profile is present and if it contains the
// custom end user email field. If so, report it to the server.
if runtime.GOOS == "darwin" {
log.Info().Msg("checking for custom mdm enrollment profile with end user email")
email, err := profiles.GetCustomEnrollmentProfileEndUserEmail()
if err != nil {
if errors.Is(err, profiles.ErrNotFound) {
// This is fine. Many hosts will not have this profile so just log and continue.
log.Info().Msg(fmt.Sprintf("get custom enrollment profile end user email: %s", err))
} else {
log.Error().Err(err).Msg("get custom enrollment profile end user email")
}
}
if email != "" {
log.Info().Msg(fmt.Sprintf("found custom end user email: %s", email))
if err := orbitClient.SetOrUpdateDeviceMappingEmail(email); err != nil {
log.Error().Err(err).Msg(fmt.Sprintf("set or update device mapping: %s", email))
}
}
}
softwareRunner := installer.NewRunner(orbitClient, r.ExtensionSocketPath(), scriptsEnabledFn)
orbitClient.RegisterConfigReceiver(softwareRunner)
if runtime.GOOS == "darwin" {
log.Info().Msgf("orbitClient.GetServerCapabilities() %+v", orbitClient.GetServerCapabilities())
if orbitClient.GetServerCapabilities().Has(fleet.CapabilityEscrowBuddy) {
orbitClient.RegisterConfigReceiver(update.NewEscrowBuddyRunner(updateRunner, 5*time.Minute))
} else {
orbitClient.RegisterConfigReceiver(
update.ApplyDiskEncryptionRunnerMiddleware(
orbitClient.GetServerCapabilities,
orbitClient.TriggerOrbitRestart,
),
)
}
}
// Install a signal handler
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
signalHandlerExecute, signalHandlerInterrupt := signalHandler(ctx)
addSubsystem(&g, "signal handler", &wrapSubsystem{
execute: signalHandlerExecute,
interrupt: signalHandlerInterrupt,
})
go sigusrListener(c.String("root-dir"))
if err := g.Run(); err != nil {
log.Error().Err(err).Msg("unexpected exit")
}
close(appDoneCh) // Signal to indicate runners have just ended
return nil
}
if len(os.Args) == 2 && os.Args[1] == "--help" {
platform.PreUpdateQuirks()
}
if err := app.Run(os.Args); err != nil {
log.Error().Err(err).Msg("run orbit failed")
}
}
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 {
log.Info().Msgf("deleting enroll secret file: %v", enrollSecretPath)
err = os.Remove(enrollSecretPath)
if err != nil {
log.Warn().Err(err).Msgf("failed to delete enroll secret file: %v", enrollSecretPath)
}
}
}
// setServerOverrides overrides specific variables in c with values fetched from Fleet.
func setServerOverrides(c *cli.Context) fallbackServerOverridesConfig {
overrideCfg, err := loadServerOverrides(c.String("root-dir"))
if err != nil {
log.Error().Err(err).Msg("failed to load server overrides")
return fallbackServerOverridesConfig{}
}
if overrideCfg.OrbitChannel != "" {
if err := c.Set("orbit-channel", overrideCfg.OrbitChannel); err != nil {
log.Error().Err(err).Str("component", "orbit").Msg("failed to set server overrides")
}
}
if overrideCfg.OsquerydChannel != "" {
if err := c.Set("osqueryd-channel", overrideCfg.OsquerydChannel); err != nil {
log.Error().Err(err).Str("component", "osqueryd").Msg("failed to set server overrides")
}
}
if overrideCfg.DesktopChannel != "" {
if err := c.Set("desktop-channel", overrideCfg.DesktopChannel); err != nil {
log.Error().Err(err).Str("component", "desktop").Msg("failed to set server overrides")
}
}
return overrideCfg.fallbackServerOverridesConfig
}
// getFleetdComponentPaths returns the paths of the fleetd components.
// If the path to the component cannot be fetched using the updater (e.g. channel doesn't exist yet)
// then it will use the fallbackCfg's paths (if set).
func getFleetdComponentPaths(
c *cli.Context,
updater *update.Updater,
fallbackCfg fallbackServerOverridesConfig,
) (osquerydPath string, desktopPath string, err error) {
if err := updater.UpdateMetadata(); err != nil {
log.Error().Err(err).Msg("update metadata before getting components")
}
// osqueryd
osquerydLocalTarget, err := updater.Get("osqueryd")
if err != nil {
if fallbackCfg.OsquerydPath == "" {
log.Info().Err(err).Msg("get osqueryd target failed")
return "", "", fmt.Errorf("get osqueryd target: %w", err)
}
log.Info().Err(err).Msgf("get osqueryd target failed, fallback to using %s", fallbackCfg.OsquerydPath)
osquerydPath = fallbackCfg.OsquerydPath
} else {
osquerydPath = osquerydLocalTarget.ExecPath
}
// Fleet Desktop
if c.Bool("fleet-desktop") {
fleetDesktopLocalTarget, err := updater.Get("desktop")
if err != nil {
if fallbackCfg.DesktopPath == "" {
log.Info().Err(err).Msg("get desktop target failed")
return "", "", fmt.Errorf("get desktop target: %w", err)
}
log.Info().Err(err).Msgf("get desktop target failed, fallback to using %s", fallbackCfg.DesktopPath)
desktopPath = fallbackCfg.DesktopPath
} else {
if runtime.GOOS == "darwin" {
desktopPath = fleetDesktopLocalTarget.DirPath
} else {
desktopPath = fleetDesktopLocalTarget.ExecPath
}
}
}
return osquerydPath, desktopPath, nil
}
func registerExtensionRunner(g *run.Group, extSockPath string, opts ...table.Opt) {
ext := table.NewRunner(extSockPath, opts...)
addSubsystem(g, "osqueryd extension runner", ext)
}
// desktopRunner runs the Fleet Desktop application.
type desktopRunner struct {
// desktopPath is the path to the desktop executable.
desktopPath string
updateRoot string
// fleetURL is the URL of the Fleet server.
fleetURL string
// trw is the Fleet Desktop token reader and writer (implements token rotation).
trw *token.ReadWriter
// fleetRootCA is the path to a certificate to use for server TLS verification.
fleetRootCA string
// insecure disables all TLS verification.
insecure bool
// fleetClientCrt is the raw TLS client certificate (in PEM format)
// to use for authenticating to the Fleet server.
fleetClientCrt []byte
// fleetClientKey is the raw TLS client private key (in PEM format)
// to use for authenticating to the Fleet server.
fleetClientKey []byte
// fleetAlternativeBrowserHost is an alternative host:port to use for
// the browser URLs in Fleet Desktop.
fleetAlternativeBrowserHost string
// interruptCh is closed when interrupt is triggered.
interruptCh chan struct{} //
// executeDoneCh is closed when execute returns.
executeDoneCh chan struct{}
}
func newDesktopRunner(
desktopPath, fleetURL, fleetRootCA string,
insecure bool,
trw *token.ReadWriter,
fleetClientCrt []byte, fleetClientKey []byte,
fleetAlternativeBrowserHost string,
updateRoot string,
) *desktopRunner {
return &desktopRunner{
desktopPath: desktopPath,
updateRoot: updateRoot,
fleetURL: fleetURL,
trw: trw,
fleetRootCA: fleetRootCA,
insecure: insecure,
fleetClientCrt: fleetClientCrt,
fleetClientKey: fleetClientKey,
fleetAlternativeBrowserHost: fleetAlternativeBrowserHost,
interruptCh: make(chan struct{}),
executeDoneCh: make(chan struct{}),
}
}
// execute makes sure the fleet-desktop application is running.
//
// We have to support the scenario where the user closes its sessions (log out).
// To support this, we add retries to execuser.Run. Basically retry execuser.Run until it succeeds,
// which will happen when the user logs in.
// Once fleet-desktop is started, the process is monitored (user processes get killed when the user
// closes all its sessions).
//
// NOTE(lucas): This logic could be improved to detect if there's a valid session or not first.
func (d *desktopRunner) Execute() error {
defer close(d.executeDoneCh)
log.Info().Msg("killing any pre-existing fleet-desktop instances")
if err := platform.SignalProcessBeforeTerminate(constant.DesktopAppExecName); err != nil &&
!errors.Is(err, platform.ErrProcessNotFound) &&
!errors.Is(err, platform.ErrComChannelNotFound) {
log.Error().Err(err).Msg("desktop early terminate")
}
log.Info().Str("path", d.desktopPath).Msg("opening")
url, err := url.Parse(d.fleetURL)
if err != nil {
return fmt.Errorf("invalid fleet-url: %w", err)
}
deviceURL, err := url.Parse(d.fleetURL)
if err != nil {
return fmt.Errorf("invalid fleet-url: %w", err)
}
deviceURL.Path = path.Join(url.Path, "device", d.trw.GetCached())
opts := []execuser.Option{
execuser.WithEnv("FLEET_DESKTOP_FLEET_URL", url.String()),
execuser.WithEnv("FLEET_DESKTOP_DEVICE_IDENTIFIER_PATH", d.trw.Path),
// TODO(roperzh): this env var is keept only for backwards compatibility,
// we should remove it once we think is safe
execuser.WithEnv("FLEET_DESKTOP_DEVICE_URL", deviceURL.String()),
execuser.WithEnv("FLEET_DESKTOP_FLEET_TLS_CLIENT_CERTIFICATE", string(d.fleetClientCrt)),
execuser.WithEnv("FLEET_DESKTOP_FLEET_TLS_CLIENT_KEY", string(d.fleetClientKey)),
execuser.WithEnv("FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST", d.fleetAlternativeBrowserHost),
execuser.WithEnv("FLEET_DESKTOP_TUF_UPDATE_ROOT", d.updateRoot),
}
if d.fleetRootCA != "" {
opts = append(opts, execuser.WithEnv("FLEET_DESKTOP_FLEET_ROOT_CA", d.fleetRootCA))
}
if d.insecure {
opts = append(opts, execuser.WithEnv("FLEET_DESKTOP_INSECURE", "1"))
}
for {
// First retry logic to start fleet-desktop.
if done := retry(30*time.Second, false, d.interruptCh, func() bool {
// On MacOS, if we attempt to run Fleet Desktop while the user is not logged in through
// the GUI, MacOS returns an error. See https://github.com/fleetdm/fleet/issues/14698
// for more details.
loggedInGui, err := user.IsUserLoggedInViaGui()
if err != nil {
log.Debug().Err(err).Msg("desktop.IsUserLoggedInGui")
return true
}
if !loggedInGui {
return true
}
// Orbit runs as root user on Unix and as SYSTEM (Windows Service) user on Windows.
// To be able to run the desktop application (mostly to register the icon in the system tray)
// we need to run the application as the login user.
// Package execuser provides multi-platform support for this.
if err := execuser.Run(d.desktopPath, opts...); err != nil {
log.Debug().Err(err).Msg("execuser.Run")
return true
}
return false
}); done {
return nil
}
// Second retry logic to monitor fleet-desktop.
// Call with waitFirst=true to give some time for the process to start.
if done := retry(30*time.Second, true, d.interruptCh, func() bool {
switch _, err := platform.GetProcessByName(constant.DesktopAppExecName); {
case err == nil:
return true // all good, process is running, retry.
case errors.Is(err, platform.ErrProcessNotFound):
log.Debug().Msgf("%s process not running", constant.DesktopAppExecName)
return false // process is not running, do not retry.
default:
log.Debug().Err(err).Msg("getProcessByName")
return true // failed to get process by name, retry.
}
}); done {
return nil
}
}
}
func retry(d time.Duration, waitFirst bool, done chan struct{}, fn func() bool) bool {
ticker := time.NewTicker(d)
defer ticker.Stop()
for {
if !waitFirst {
if retry := fn(); !retry {
return false
}
}
waitFirst = false
select {
case <-done:
return true
case <-ticker.C:
// OK
}
}
}
func (d *desktopRunner) Interrupt(err error) {
close(d.interruptCh) // Signal execute to return.
<-d.executeDoneCh // Wait for execute to return.
if err := platform.SignalProcessBeforeTerminate(constant.DesktopAppExecName); err != nil {
log.Error().Err(err).Msg("SignalProcessBeforeTerminate")
}
}
// osqueryHostInfo is used to parse osquery JSON output from system tables.
type osqueryHostInfo struct {
// HardwareUUID is the unique identifier for this device (extracted from `system_info` osquery table).
HardwareUUID string `json:"uuid"`
// HardwareSerial is the unique serial number for this device (extracted from `system_info` osquery table).
HardwareSerial string `json:"hardware_serial"`
// Hostname is the device's hostname (extracted from `system_info` osquery table).
Hostname string `json:"hostname"`
// Platform is the device's platform as defined by osquery (extracted from `os_version` osquery table).
Platform string `json:"platform"`
// InstanceID is the osquery's randomly generated instance ID
// (extracted from `osquery_info` osquery table).
InstanceID string `json:"instance_id"`
}
// getHostInfo retrieves system information about the host by shelling out to `osqueryd -S` and performing a `SELECT` query.
func getHostInfo(osqueryPath string, osqueryDBPath string) (*osqueryHostInfo, error) {
// Make sure parent directory exists (`osqueryd -S` doesn't create the parent directories).
if err := os.MkdirAll(filepath.Dir(osqueryDBPath), constant.DefaultDirMode); err != nil {
return nil, err
}
const systemQuery = "SELECT si.uuid, si.hardware_serial, si.hostname, os.platform, oi.instance_id FROM system_info si, os_version os, osquery_info oi"
args := []string{
"-S",
"--database_path", osqueryDBPath,
"--json", systemQuery,
}
log.Debug().Str("query", systemQuery).Msg("running single query")
cmd := exec.Command(osqueryPath, args...)
var (
osquerydStdout bytes.Buffer
osquerydStderr bytes.Buffer
)
cmd.Stdout = &osquerydStdout
cmd.Stderr = &osquerydStderr
var info []osqueryHostInfo
if err := cmd.Run(); err != nil {
// osquery may return correct data with an exit status 78, in which case we only log the error
// Related issue: https://github.com/osquery/osquery/issues/6566
unmarshalErr := json.Unmarshal(osquerydStdout.Bytes(), &info)
// Note: Unmarshal will fail on an empty buffer output.
if unmarshalErr != nil {
// Since the original command failed, we log the original error and the output for debugging purposes.
log.Error().Str(
"output", string(osquerydStdout.Bytes()),
).Str(
"stderr", string(osquerydStderr.Bytes()),
).Msg("getHostInfo via osquery")
return nil, err
}
log.Warn().Str("status", err.Error()).Msg("getHostInfo via osquery returned data, but with a non-zero exit status")
}
if len(info) == 0 {
if err := json.Unmarshal(osquerydStdout.Bytes(), &info); err != nil {
return nil, err
}
}
if len(info) != 1 {
return nil, fmt.Errorf("invalid number of rows from system info query: %d", len(info))
}
return &info[0], nil
}
var versionCommand = &cli.Command{
Name: "version",
Usage: "Get the orbit version",
Flags: []cli.Flag{},
Action: func(c *cli.Context) error {
fmt.Println("orbit " + build.Version)
fmt.Println("commit - " + build.Commit)
fmt.Println("date - " + build.Date)
return nil
},
}
// serviceChecker is a helper to gracefully shutdown the runners group when a
// system service stop request was received.
//
// This struct and its methods are designed to play nicely with `oklog.Group`.
type serviceChecker struct {
svcInterruptCh chan struct{} // closed when system service stop is requested
localInterruptCh chan struct{} // closed when serviceChecker interrupt is called
}
func newSystemChecker() *serviceChecker {
return &serviceChecker{
svcInterruptCh: make(chan struct{}),
localInterruptCh: make(chan struct{}),
}
}
// execute will just return when required locally or by the system service
func (s *serviceChecker) Execute() error {
for {
select {
case <-s.svcInterruptCh:
return errors.New("os service stop request")
case <-s.localInterruptCh:
return errors.New("internal service interrupt")
}
}
}
func (s *serviceChecker) Interrupt(err error) {
close(s.localInterruptCh) // Signal execute to return.
}
// capabilitiesChecker is a helper to restart Orbit as soon as certain capabilities
// are changed in the server.
//
// This struct and its methods are designed to play nicely with `oklog.Group`.
type capabilitiesChecker struct {
client *service.OrbitClient
interruptCh chan struct{} // closed when interrupt is triggered
executeDoneCh chan struct{} // closed when execute returns
}
func newCapabilitiesChecker(client *service.OrbitClient) *capabilitiesChecker {
return &capabilitiesChecker{
client: client,
interruptCh: make(chan struct{}),
executeDoneCh: make(chan struct{}),
}
}
// execute will poll the server for capabilities and emit a stop signal to restart
// Orbit if certain capabilities are enabled.
//
// You need to add an explicit check for each capability you want to watch for
func (f *capabilitiesChecker) Execute() error {
defer close(f.executeDoneCh)
capabilitiesCheckTicker := time.NewTicker(5 * time.Minute)
// Do an initial ping to store the initial capabilities if needed
if len(f.client.GetServerCapabilities()) == 0 {
if err := f.client.Ping(); err != nil {
logging.LogErrIfEnvNotSetDebug(constant.SilenceEnrollLogErrorEnvVar, err, "pinging the server")
}
}
for {
select {
case <-capabilitiesCheckTicker.C:
oldCapabilities := f.client.GetServerCapabilities()
// ping the server to get the latest capabilities
if err := f.client.Ping(); err != nil {
logging.LogErrIfEnvNotSetDebug(constant.SilenceEnrollLogErrorEnvVar, err, "pinging the server")
continue
}
newCapabilities := f.client.GetServerCapabilities()
if oldCapabilities.Has(fleet.CapabilityOrbitEndpoints) !=
newCapabilities.Has(fleet.CapabilityOrbitEndpoints) {
log.Info().Msgf("%s capability changed, restarting", fleet.CapabilityOrbitEndpoints)
return nil
}
if oldCapabilities.Has(fleet.CapabilityTokenRotation) !=
newCapabilities.Has(fleet.CapabilityTokenRotation) {
log.Info().Msgf("%s capability changed, restarting", fleet.CapabilityTokenRotation)
return nil
}
if oldCapabilities.Has(fleet.CapabilityEndUserEmail) !=
newCapabilities.Has(fleet.CapabilityEndUserEmail) {
log.Info().Msgf("%s capability changed, restarting", fleet.CapabilityEndUserEmail)
return nil
}
case <-f.interruptCh:
return nil
}
}
}
func (f *capabilitiesChecker) Interrupt(err error) {
close(f.interruptCh) // Signal execute to return.
<-f.executeDoneCh // Wait for execute to return.
}
// writeSecret writes the orbit enroll secret to the designated file. We do
// this at runtime for packages that are using --use-system-config, since they
// don't contain a secret file in their payload.
//
// This implementation is very similar to the one in orbit/pkg/packaging but
// intentionally kept separate to prevent issues since the writes happen at two
// completely different circumstances.
func writeSecret(enrollSecret string, orbitRoot string) error {
path := filepath.Join(orbitRoot, constant.OsqueryEnrollSecretFileName)
if err := secure.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil {
return fmt.Errorf("mkdir: %w", err)
}
if err := os.WriteFile(path, []byte(enrollSecret), constant.DefaultFileMode); err != nil {
return fmt.Errorf("write file: %w", err)
}
return nil
}
// serverOverridesRunner is a oklog.Group runner that polls for configuration overrides from Fleet.
type serverOverridesRunner struct {
rootDir string
fallbackCfg fallbackServerOverridesConfig
desktopEnabled bool
cancel chan struct{}
triggerOrbitRestart func(reason string)
}
// newServerOverridesReveiver creates a runner for updating server overrides configuration with values fetched from Fleet.
func newServerOverridesReceiver(
rootDir string,
fallbackCfg fallbackServerOverridesConfig,
desktopEnabled bool,
triggerOrbitRestart func(reason string),
) *serverOverridesRunner {
return &serverOverridesRunner{
rootDir: rootDir,
fallbackCfg: fallbackCfg,
desktopEnabled: desktopEnabled,
cancel: make(chan struct{}),
triggerOrbitRestart: triggerOrbitRestart,
}
}
func (r *serverOverridesRunner) Run(orbitCfg *fleet.OrbitConfig) error {
overrideCfg, err := loadServerOverrides(r.rootDir)
if err != nil {
return err
}
if orbitCfg.UpdateChannels == nil {
// Server is not setting or doesn't know of
// this feature (old server version), so nothing to do.
return nil
}
if cfgsDiffer(overrideCfg, orbitCfg, r.desktopEnabled) {
if err := r.updateServerOverrides(orbitCfg); err != nil {
return err
}
r.triggerOrbitRestart("server overrides updated")
return nil
}
return nil
}
// cfgsDiffer returns whether the local server overrides differ from the fetched remotely.
func cfgsDiffer(overrideCfg *serverOverridesConfig, orbitCfg *fleet.OrbitConfig, desktopEnabled bool) bool {
localUpdateChannelsCfg := &fleet.OrbitUpdateChannels{
Orbit: overrideCfg.OrbitChannel,
Osqueryd: overrideCfg.OsquerydChannel,
Desktop: overrideCfg.DesktopChannel,
}
remoteUpdateChannelsCfg := orbitCfg.UpdateChannels
setStableAsDefault := func(cfg *fleet.OrbitUpdateChannels) {
if cfg.Orbit == "" {
cfg.Orbit = "stable"
}
if cfg.Osqueryd == "" {
cfg.Osqueryd = "stable"
}
if cfg.Desktop == "" {
cfg.Desktop = "stable"
}
}
setStableAsDefault(localUpdateChannelsCfg)
setStableAsDefault(remoteUpdateChannelsCfg)
local := *localUpdateChannelsCfg
remote := *remoteUpdateChannelsCfg
if !desktopEnabled {
local.Desktop = ""
remote.Desktop = ""
}
return local != remote
}
// serverOverridesConfig holds the currently supported fields that can be
// overriden by server configuration.
type serverOverridesConfig struct {
// OrbitChannel defines the override for the orbit's channel.
OrbitChannel string `json:"orbit-channel"`
// OsquerydChannel defines the override for the osqueryd's channel.
OsquerydChannel string `json:"osqueryd-channel"`
// DesktopChannel defines the override for the Fleet Desktop's channel.
DesktopChannel string `json:"desktop-channel"`
fallbackServerOverridesConfig
}
// fallbackServerOverridesConfig contains fallback configuration in case the server
// settings are invalid (e.g. invalid update channels that don't exist).
// Whenever the user sets an invalid channel, then the fallback paths are used to get
// fleetd up and running with the last known good configuration.
//
// NOTE: We don't need orbit's path because the `orbit` component is a special case that uses
// a symlink to define the last known valid version.
type fallbackServerOverridesConfig struct {
// OsquerydPath contains the path of the osqueryd executable last known to be valid.
OsquerydPath string `json:"fallback-osqueryd-path"`
// DesktopPath contains the path of the Fleet Desktop executable last known to be valid.
DesktopPath string `json:"fallback-desktop-path"`
}
func (f fallbackServerOverridesConfig) empty() bool {
return f.OsquerydPath == "" && f.DesktopPath == ""
}
// updateServerOverrides updates the server override local file with the configuration fetched from Fleet.
func (r *serverOverridesRunner) updateServerOverrides(remoteCfg *fleet.OrbitConfig) error {
overrideCfg := serverOverridesConfig{
OrbitChannel: remoteCfg.UpdateChannels.Orbit,
OsquerydChannel: remoteCfg.UpdateChannels.Osqueryd,
DesktopChannel: remoteCfg.UpdateChannels.Desktop,
fallbackServerOverridesConfig: r.fallbackCfg,
}
data, err := json.MarshalIndent(overrideCfg, "", " ")
if err != nil {
return fmt.Errorf("marshal override config: %w", err)
}
serverOverridesPath := filepath.Join(r.rootDir, constant.ServerOverridesFileName)
if err := os.WriteFile(serverOverridesPath, data, constant.DefaultFileMode); err != nil {
return fmt.Errorf("write override config: %w", err)
}
return nil
}
// loadServerOverrides loads the server overrides from the local file.
func loadServerOverrides(rootDir string) (*serverOverridesConfig, error) {
serverOverridesPath := filepath.Join(rootDir, constant.ServerOverridesFileName)
data, err := os.ReadFile(serverOverridesPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return &serverOverridesConfig{}, nil
}
return nil, err
}
var cfg serverOverridesConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// subSystem is an interface that implements the methods needed for oklog/run.Group.
type subSystem interface {
// Execute partially implements the interface needed for oklog/run.Group.Add.
Execute() error
// Interrupt partially implements the interface needed for oklog/run.Group.Add.
Interrupt(err error)
}
// addSubsystem adds a new subsystem to the oklog/run.Group.
func addSubsystem(g *run.Group, name string, s subSystem) {
g.Add(
func() error {
log.Debug().Msgf("start %s", name)
return s.Execute()
}, func(err error) {
log.Info().Err(err).Msgf("interrupt %s", name)
s.Interrupt(err)
},
)
}
// wrapSubsystem wraps functions to implement the subSystem interface.
type wrapSubsystem struct {
execute func() error
interrupt func(err error)
}
// Execute partially implements subSystem.
func (w *wrapSubsystem) Execute() error {
return w.execute()
}
// Interrupt partially implements subSystem.
func (w *wrapSubsystem) Interrupt(err error) {
w.interrupt(err)
}