diff --git a/changes/refactor-named-functions-nil-checks b/changes/refactor-named-functions-nil-checks new file mode 100644 index 0000000000..ff0160f5c7 --- /dev/null +++ b/changes/refactor-named-functions-nil-checks @@ -0,0 +1 @@ +* Refactored large anonymous functions into named functions to improve nil-safety static analysis coverage. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 1ac08456e3..e14fc69227 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -141,1682 +141,9 @@ together all static assets and dependent libraries into a statically linked go binary (which you're executing right now). Use the options below to customize the way that the Fleet server works. `, + // runServeCmd is a named function so that NilAway can analyze it for nil-safety. Run: func(cmd *cobra.Command, args []string) { - config := configManager.LoadConfig() - - if dev_mode.IsEnabled { - applyDevFlags(&config) - } - - license, err := initLicense(&config, devLicense, devExpiredLicense) - if err != nil { - initFatal( - err, - "failed to load license - for help use https://fleetdm.com/contact", - ) - } - - if license != nil && license.IsPremium() && license.IsExpired() { - fleet.WriteExpiredLicenseBanner(os.Stderr) - } - - // Validate OTEL server options - if config.Logging.OtelLogsEnabled && !config.Logging.TracingEnabled { - initFatal( - errors.New("logging.otel_logs_enabled requires logging.tracing_enabled to be true"), - "OTEL logs require tracing for trace correlation", - ) - } - - // Init OTEL providers (traces, metrics, logs) - var loggerProvider *otelsdklog.LoggerProvider - var tracerProvider *sdktrace.TracerProvider - var meterProvider *sdkmetric.MeterProvider - if config.OTELEnabled() { - // Create shared resource with service identification attributes. - // OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES env vars can override - // the defaults below. - res, err := resource.New(context.Background(), - resource.WithSchemaURL(semconv.SchemaURL), - resource.WithAttributes( - semconv.ServiceName("fleet"), - semconv.ServiceVersion(version.Version().Version), - ), - resource.WithFromEnv(), - resource.WithTelemetrySDK(), - ) - if err != nil { - initFatal(err, "Failed to create OTEL resource") - } - - // Initialize OTEL traces - otlpTraceExporter, err := otlptrace.New(context.Background(), otlptracegrpc.NewClient( - otlptracegrpc.WithCompressor("gzip"), - )) - if err != nil { - initFatal(err, "Failed to initialize OTEL trace exporter") - } - // Configure batch span processor with smaller batch size to avoid exceeding message size limits (4MB default limit) - batchSpanProcessor := sdktrace.NewBatchSpanProcessor(otlpTraceExporter, - sdktrace.WithMaxExportBatchSize(256), // Reduce from default 512 to 256 - ) - tracerProvider = sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithSpanProcessor(batchSpanProcessor), - ) - otel.SetTracerProvider(tracerProvider) - - // Initialize OTEL metrics - metricExporter, err := otlpmetricgrpc.New(context.Background(), - otlpmetricgrpc.WithCompressor("gzip"), - ) - if err != nil { - initFatal(err, "Failed to initialize OTEL metrics exporter") - } - meterProvider = sdkmetric.NewMeterProvider( - sdkmetric.WithResource(res), - sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter)), - ) - otel.SetMeterProvider(meterProvider) - - // Initialize OTEL logs - if config.Logging.OtelLogsEnabled { - logExporter, err := otlploggrpc.New(context.Background(), - otlploggrpc.WithCompressor("gzip"), - ) - if err != nil { - initFatal(err, "Failed to initialize OTEL log exporter") - } - loggerProvider = otelsdklog.NewLoggerProvider( - otelsdklog.WithResource(res), - otelsdklog.WithProcessor(otelsdklog.NewBatchProcessor(logExporter)), - ) - } - } - - logger := initLogger(config, loggerProvider) - - // If you want to disable any logs by default, this is where to do it. - // - // For example: - // platform_logging.DisableTopic("deprecated-api-keys") - platform_logging.DisableTopic(platform_logging.DeprecatedFieldTopic) - - // Apply log topic overrides from config. Enables run first, then - // disables, so disable wins on conflict. - // Note that any topic not included in these lists will be considered - // enabled if it's encountered in a log. - for _, topic := range str.SplitAndTrim(config.Logging.EnableLogTopics, ",", true) { - platform_logging.EnableTopic(topic) - } - for _, topic := range str.SplitAndTrim(config.Logging.DisableLogTopics, ",", true) { - platform_logging.DisableTopic(topic) - } - - if dev_mode.IsEnabled { - createTestBuckets(cmd.Context(), &config, logger) - } - - allowedHostIdentifiers := map[string]bool{ - "provided": true, - "instance": true, - "uuid": true, - "hostname": true, - } - if !allowedHostIdentifiers[config.Osquery.HostIdentifier] { - initFatal(fmt.Errorf("%s is not a valid value for osquery_host_identifier", config.Osquery.HostIdentifier), "set host identifier") - } - - config.ConditionalAccess.Validate(initFatal) - - if len(config.Server.URLPrefix) > 0 { - // Massage provided prefix to match expected format - config.Server.URLPrefix = strings.TrimSuffix(config.Server.URLPrefix, "/") - if len(config.Server.URLPrefix) > 0 && !strings.HasPrefix(config.Server.URLPrefix, "/") { - config.Server.URLPrefix = "/" + config.Server.URLPrefix - } - - if !allowedURLPrefixRegexp.MatchString(config.Server.URLPrefix) { - initFatal( - fmt.Errorf("prefix must match regexp \"%s\"", allowedURLPrefixRegexp.String()), - "setting server URL prefix", - ) - } - } - - // Handle server private key configuration - either direct or via AWS Secrets Manager - if config.Server.PrivateKey != "" && config.Server.PrivateKeySecretArn != "" { - initFatal(errors.New("cannot specify both private_key and private_key_secret_arn"), "validate private key configuration") - } - - // Retrieve private key from AWS Secrets Manager if specified - if config.Server.PrivateKeySecretArn != "" { - privateKey, err := configpkg.RetrieveSecretsManagerSecret( - context.Background(), - config.Server.PrivateKeySecretArn, - config.Server.PrivateKeySecretRegion, - config.Server.PrivateKeySecretSTSAssumeRoleArn, - config.Server.PrivateKeySecretSTSExternalID, - ) - if err != nil { - initFatal(err, "retrieve private key from secrets manager") - } - config.Server.PrivateKey = privateKey - } - - if len(config.Server.PrivateKey) > 0 { - if len(config.Server.PrivateKey) < 32 { - initFatal(errors.New("private key must be at least 32 bytes long"), "validate private key") - } - - // We truncate to 32 bytes because AES-256 requires a 32 byte (256 bit) PK, but some - // infra setups generate keys that are longer than 32 bytes. - config.Server.PrivateKey = config.Server.PrivateKey[:32] - } - - var ds fleet.Datastore - var carveStore fleet.CarveStore - - opts := []mysql.DBOption{mysql.Logger(logger), mysql.WithFleetConfig(&config)} - if config.MysqlReadReplica.Address != "" { - opts = append(opts, mysql.Replica(&config.MysqlReadReplica)) - } - // NOTE this will disable OTEL/APM interceptor - if dev_mode.Env("FLEET_DEV_ENABLE_SQL_INTERCEPTOR") != "" { - opts = append(opts, mysql.WithInterceptor(&devSQLInterceptor{ - logger: logger.With("component", "sql-interceptor"), - })) - } - - if config.Logging.TracingEnabled { - opts = append(opts, mysql.TracingEnabled(&config.Logging)) - } - - // Configure default max request body size based on config - platform_http.MaxRequestBodySize = config.Server.DefaultMaxRequestBodySize - - // Create database connections that can be shared across datastores - dbConns, err := mysql.NewDBConnections(config.Mysql, opts...) - if err != nil { - initFatal(err, "initializing database connections") - } - - mds, err := mysql.NewDatastore(dbConns, config.Mysql, clock.C) - if err != nil { - initFatal(err, "initializing datastore") - } - ds = mds - - if config.S3.CarvesBucket != "" || config.S3.Bucket != "" { - carveStore, err = s3.NewCarveStore(config.S3, ds) - if err != nil { - initFatal(err, "initializing S3 carvestore") - } - } else { - carveStore = ds - } - - migrationStatus, err := ds.MigrationStatus(cmd.Context()) - if err != nil { - initFatal(err, "retrieving migration status") - } - - switch migrationStatus.StatusCode { - case fleet.AllMigrationsCompleted: - // OK - case fleet.UnknownMigrations: - printUnknownMigrationsMessage(migrationStatus.UnknownTable, migrationStatus.UnknownData) - if dev_mode.IsEnabled { - os.Exit(1) - } - case fleet.NeedsFleetv4732Fix: - printFleetv4732FixNeededMessage() - if !config.Upgrades.AllowMissingMigrations { - os.Exit(1) - } - case fleet.UnknownFleetv4732State: - printFleetv4732UnknownStateMessage(migrationStatus.StatusCode) - if !config.Upgrades.AllowMissingMigrations { - os.Exit(1) - } - case fleet.SomeMigrationsCompleted: - tables, data := migrationStatus.MissingTable, migrationStatus.MissingData - printMissingMigrationsWarning(tables, data) - if !config.Upgrades.AllowMissingMigrations { - os.Exit(1) - } - case fleet.NoMigrationsCompleted: - printDatabaseNotInitializedError() - os.Exit(1) - } - - if initializingDS, ok := ds.(initializer); ok { - if err := initializingDS.Initialize(); err != nil { - initFatal(err, "loading built in data") - } - } - - // Strip the Redis URI scheme if it's present. Scheme docs are at: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml - // This allows us to use Render's Redis service in render.yaml, including the free tier. - // In the future, we could support the full Redis URI if needed (including username, password, database, etc.) - redisAddress := strings.TrimPrefix(config.Redis.Address, "redis://") - redisPool, err := redis.NewPool(redis.PoolConfig{ - Server: redisAddress, - Username: config.Redis.Username, - Password: config.Redis.Password, - Database: config.Redis.Database, - UseTLS: config.Redis.UseTLS, - Region: config.Redis.Region, - CacheName: config.Redis.CacheName, - StsAssumeRoleArn: config.Redis.StsAssumeRoleArn, - StsExternalID: config.Redis.StsExternalID, - ConnTimeout: config.Redis.ConnectTimeout, - KeepAlive: config.Redis.KeepAlive, - ConnectRetryAttempts: config.Redis.ConnectRetryAttempts, - ClusterFollowRedirections: config.Redis.ClusterFollowRedirections, - ClusterReadFromReplica: config.Redis.ClusterReadFromReplica, - TLSCert: config.Redis.TLSCert, - TLSKey: config.Redis.TLSKey, - TLSCA: config.Redis.TLSCA, - TLSServerName: config.Redis.TLSServerName, - TLSHandshakeTimeout: config.Redis.TLSHandshakeTimeout, - MaxIdleConns: config.Redis.MaxIdleConns, - MaxOpenConns: config.Redis.MaxOpenConns, - ConnMaxLifetime: config.Redis.ConnMaxLifetime, - IdleTimeout: config.Redis.IdleTimeout, - ConnWaitTimeout: config.Redis.ConnWaitTimeout, - WriteTimeout: config.Redis.WriteTimeout, - ReadTimeout: config.Redis.ReadTimeout, - }) - if err != nil { - initFatal(err, "initialize Redis") - } - logger.InfoContext(cmd.Context(), "redis initialized", "component", "redis", "mode", redisPool.Mode()) - - ds = cached_mysql.New(ds) - var dsOpts []mysqlredis.Option - if license.DeviceCount > 0 && config.License.EnforceHostLimit { - dsOpts = append(dsOpts, mysqlredis.WithEnforcedHostLimit(license.DeviceCount)) - } - redisWrapperDS := mysqlredis.New(ds, redisPool, dsOpts...) - ds = redisWrapperDS - - resultStore := pubsub.NewRedisQueryResults(redisPool, config.Redis.DuplicateResults, - logger.With("component", "query-results"), - ) - liveQueryStore := live_query.NewRedisLiveQuery(redisPool, logger, liveQueryMemCacheDuration) - ssoSessionStore := sso.NewSessionStore(redisPool) - - // Set common configuration for all logging. - loggingConfig := logging.Config{ - Filesystem: logging.FilesystemConfig{ - EnableLogRotation: config.Filesystem.EnableLogRotation, - EnableLogCompression: config.Filesystem.EnableLogCompression, - MaxSize: config.Filesystem.MaxSize, - MaxAge: config.Filesystem.MaxAge, - MaxBackups: config.Filesystem.MaxBackups, - }, - Webhook: logging.WebhookConfig{}, - Firehose: logging.FirehoseConfig{ - Region: config.Firehose.Region, - EndpointURL: config.Firehose.EndpointURL, - AccessKeyID: config.Firehose.AccessKeyID, - SecretAccessKey: config.Firehose.SecretAccessKey, - StsAssumeRoleArn: config.Firehose.StsAssumeRoleArn, - StsExternalID: config.Firehose.StsExternalID, - }, - Kinesis: logging.KinesisConfig{ - Region: config.Kinesis.Region, - EndpointURL: config.Kinesis.EndpointURL, - AccessKeyID: config.Kinesis.AccessKeyID, - SecretAccessKey: config.Kinesis.SecretAccessKey, - StsAssumeRoleArn: config.Kinesis.StsAssumeRoleArn, - StsExternalID: config.Kinesis.StsExternalID, - }, - Lambda: logging.LambdaConfig{ - Region: config.Lambda.Region, - AccessKeyID: config.Lambda.AccessKeyID, - SecretAccessKey: config.Lambda.SecretAccessKey, - StsAssumeRoleArn: config.Lambda.StsAssumeRoleArn, - StsExternalID: config.Lambda.StsExternalID, - }, - PubSub: logging.PubSubConfig{ - Project: config.PubSub.Project, - }, - KafkaREST: logging.KafkaRESTConfig{ - ProxyHost: config.KafkaREST.ProxyHost, - ContentTypeValue: config.KafkaREST.ContentTypeValue, - Timeout: config.KafkaREST.Timeout, - }, - Nats: logging.NatsConfig{ - Server: config.Nats.Server, - CredFile: config.Nats.CredFile, - NKeyFile: config.Nats.NKeyFile, - TLSClientCertFile: config.Nats.TLSClientCrtFile, - TLSClientKeyFile: config.Nats.TLSClientKeyFile, - CACertFile: config.Nats.CACrtFile, - Compression: config.Nats.Compression, - JetStream: config.Nats.JetStream, - Timeout: config.Nats.Timeout, - }, - } - - // Set specific configuration to osqueryd status logs. - loggingConfig.Plugin = config.Osquery.StatusLogPlugin - loggingConfig.Filesystem.LogFile = config.Filesystem.StatusLogFile - loggingConfig.Webhook.URL = config.Webhook.StatusURL - loggingConfig.Firehose.StreamName = config.Firehose.StatusStream - loggingConfig.Kinesis.StreamName = config.Kinesis.StatusStream - loggingConfig.Lambda.Function = config.Lambda.StatusFunction - loggingConfig.PubSub.Topic = config.PubSub.StatusTopic - loggingConfig.PubSub.AddAttributes = false // only used by result logs - loggingConfig.KafkaREST.Topic = config.KafkaREST.StatusTopic - loggingConfig.Nats.Subject = config.Nats.StatusSubject - - osquerydStatusLogger, err := logging.NewJSONLogger(cmd.Context(), "status", loggingConfig, logger) - if err != nil { - initFatal(err, "initializing osqueryd status logging") - } - - // Set specific configuration to osqueryd result logs. - loggingConfig.Plugin = config.Osquery.ResultLogPlugin - loggingConfig.Filesystem.LogFile = config.Filesystem.ResultLogFile - loggingConfig.Webhook.URL = config.Webhook.ResultURL - loggingConfig.Firehose.StreamName = config.Firehose.ResultStream - loggingConfig.Kinesis.StreamName = config.Kinesis.ResultStream - loggingConfig.Lambda.Function = config.Lambda.ResultFunction - loggingConfig.PubSub.Topic = config.PubSub.ResultTopic - loggingConfig.PubSub.AddAttributes = config.PubSub.AddAttributes - loggingConfig.KafkaREST.Topic = config.KafkaREST.ResultTopic - loggingConfig.Nats.Subject = config.Nats.ResultSubject - - osquerydResultLogger, err := logging.NewJSONLogger(cmd.Context(), "result", loggingConfig, logger) - if err != nil { - initFatal(err, "initializing osqueryd result logging") - } - - var auditLogger fleet.JSONLogger - if license.IsPremium() && config.Activity.EnableAuditLog { - // Set specific configuration to audit logs. - loggingConfig.Plugin = config.Activity.AuditLogPlugin - loggingConfig.Filesystem.LogFile = config.Filesystem.AuditLogFile - loggingConfig.Firehose.StreamName = config.Firehose.AuditStream - loggingConfig.Kinesis.StreamName = config.Kinesis.AuditStream - loggingConfig.Lambda.Function = config.Lambda.AuditFunction - loggingConfig.PubSub.Topic = config.PubSub.AuditTopic - loggingConfig.PubSub.AddAttributes = false // only used by result logs - loggingConfig.KafkaREST.Topic = config.KafkaREST.AuditTopic - loggingConfig.Nats.Subject = config.Nats.AuditSubject - - auditLogger, err = logging.NewJSONLogger(cmd.Context(), "audit", loggingConfig, logger) - if err != nil { - initFatal(err, "initializing audit logging") - } - } - - failingPolicySet := redis_policy_set.NewFailing(redisPool) - - task := async.NewTask(ds, redisPool, clock.C, &config) - - if config.Sentry.Dsn != "" { - v := version.Version() - err = sentry.Init(sentry.ClientOptions{ - Dsn: config.Sentry.Dsn, - Release: fmt.Sprintf("%s_%s_%s", v.Version, v.Branch, v.Revision), - }) - if err != nil { - initFatal(err, "initializing sentry") - } - logger.InfoContext(cmd.Context(), "sentry initialized", "dsn", config.Sentry.Dsn) - - defer sentry.Recover() - defer sentry.Flush(2 * time.Second) - } - - var geoIP fleet.GeoIP - geoIP = &fleet.NoOpGeoIP{} - if config.GeoIP.DatabasePath != "" { - maxmind, err := fleet.NewMaxMindGeoIP(logger, config.GeoIP.DatabasePath) - if err != nil { - logger.ErrorContext(cmd.Context(), "failed to initialize maxmind geoip, check database path", "database_path", - config.GeoIP.DatabasePath, "error", err) - } else { - geoIP = maxmind - } - } - - if config.MDM.EnableCustomOSUpdatesAndFileVault && !license.IsPremium() { - config.MDM.EnableCustomOSUpdatesAndFileVault = false - logger.WarnContext(cmd.Context(), "Disabling custom OS updates and FileVault management because Fleet Premium license is not present") - } - - mdmStorage, err := mds.NewMDMAppleMDMStorage() - if err != nil { - initFatal(err, "initialize mdm apple MySQL storage") - } - - depStorage, err := mds.NewMDMAppleDEPStorage() - if err != nil { - initFatal(err, "initialize Apple BM DEP storage") - } - - scepStorage, err := mds.NewSCEPDepot() - if err != nil { - initFatal(err, "initialize mdm apple scep storage") - } - - var mdmPushService push.Pusher - nanoMDMLogger := service.NewNanoMDMLogger(logger.With("component", "apple-mdm-push")) - pushProviderFactory := buford.NewPushProviderFactory(buford.WithNewClient(func(cert *tls.Certificate) (*http.Client, error) { - return fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{ - Certificates: []tls.Certificate{*cert}, - })), nil - })) - if dev_mode.Env("FLEET_DEV_MDM_APPLE_DISABLE_PUSH") == "1" { - mdmPushService = nopPusher{} - } else { - mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger) - } - mds.WithPusher(mdmPushService) - - checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) { - _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names, nil) - if err != nil { - if fleet.IsNotFound(err) || errors.Is(err, mysql.ErrPartialResult) { - return false, nil - } - return false, err - } - return true, nil - } - - // reconcile Apple Business Manager configuration environment variables with the database - if config.MDM.IsAppleAPNsSet() || config.MDM.IsAppleSCEPSet() { - if len(config.Server.PrivateKey) == 0 { - initFatal(errors.New("inserting MDM APNs and SCEP assets"), - "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - // first we'll check if the APNs and SCEP assets are already in the database and - // only insert config values if they're not already present in the database - toInsert := make(map[fleet.MDMAssetName]struct{}, 4) - - // check DB for APNs assets - found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetAPNSCert, fleet.MDMAssetAPNSKey}) - switch { - case err != nil: - initFatal(err, "reading APNs assets from database") - case !found: - toInsert[fleet.MDMAssetAPNSCert] = struct{}{} - toInsert[fleet.MDMAssetAPNSKey] = struct{}{} - default: - logger.WarnContext(cmd.Context(), - "Your server already has stored APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.") - } - - // check DB for SCEP assets - found, err = checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) - switch { - case err != nil: - initFatal(err, "reading SCEP assets from database") - case !found: - toInsert[fleet.MDMAssetCACert] = struct{}{} - toInsert[fleet.MDMAssetCAKey] = struct{}{} - default: - logger.WarnContext(cmd.Context(), - "Your server already has stored SCEP certificates. Fleet will ignore any certificates provided via environment variables when this happens.") - } - - if len(toInsert) > 0 { - if !config.MDM.IsAppleAPNsSet() { - initFatal(errors.New("Apple APNs MDM configuration must be provided when Apple SCEP is provided"), - "validate Apple MDM") - } else if !config.MDM.IsAppleSCEPSet() { - initFatal(errors.New("Apple SCEP MDM configuration must be provided when Apple APNs is provided"), - "validate Apple MDM") - } - - // parse the APNs and SCEP assets from the config - _, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs() - if err != nil { - initFatal(err, "parse Apple APNs certificate and key from config") - } - _, appleSCEPCertPEM, appleSCEPKeyPEM, err := config.MDM.AppleSCEP() - if err != nil { - initFatal(err, "load Apple SCEP certificate and key from config") - } - - var args []fleet.MDMConfigAsset - for name := range toInsert { - switch name { - case fleet.MDMAssetAPNSCert: - args = append(args, fleet.MDMConfigAsset{Name: name, Value: apnsCertPEM}) - case fleet.MDMAssetAPNSKey: - args = append(args, fleet.MDMConfigAsset{Name: name, Value: apnsKeyPEM}) - case fleet.MDMAssetCACert: - args = append(args, fleet.MDMConfigAsset{Name: name, Value: appleSCEPCertPEM}) - case fleet.MDMAssetCAKey: - args = append(args, fleet.MDMConfigAsset{Name: name, Value: appleSCEPKeyPEM}) - } - } - - if err := ds.InsertMDMConfigAssets(context.Background(), args, nil); err != nil { - if mysql.IsDuplicate(err) { - // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case - logger.DebugContext(cmd.Context(), "unexpected duplicate key error inserting MDM APNs and SCEP assets") - } else { - initFatal(err, "inserting MDM APNs and SCEP assets") - } - } - } - } - - // reconcile Apple Business Manager configuration environment variables with the database - if config.MDM.IsAppleBMSet() { - if len(config.Server.PrivateKey) == 0 { - initFatal(errors.New("inserting MDM ABM assets"), - "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - appleBM, err := config.MDM.AppleBM() - if err != nil { - initFatal(err, "parse Apple BM token, certificate and key from config") - } - - toInsert := make([]fleet.MDMConfigAsset, 0, 2) - - found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetABMKey, fleet.MDMAssetABMCert}) - switch { - case err != nil: - initFatal(err, "reading ABM assets from database") - case !found: - toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM}, - fleet.MDMConfigAsset{Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM}) - default: - logger.WarnContext(cmd.Context(), - "Your server already has stored ABM certificates and token. Fleet will ignore any certificates provided via environment variables when this happens.") - } - - if len(toInsert) > 0 { - err := ds.InsertMDMConfigAssets(context.Background(), toInsert, nil) - switch { - case err != nil && mysql.IsDuplicate(err): - // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case - logger.DebugContext(cmd.Context(), "unexpected duplicate key error inserting ABM assets") - case err != nil: - initFatal(err, "inserting ABM assets") - default: - // insert the ABM token without any metdata; it'll be picked by the - // apple_mdm_dep_profile_assigner cron and backfilled - if _, err := ds.InsertABMToken(context.Background(), &fleet.ABMToken{ - EncryptedToken: appleBM.EncryptedToken, - RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, - time.UTC), // 2000-01-01 is our "zero value" for time - }); err != nil { - initFatal(err, "save ABM token") - } - } - } - } - - appCfg, err := ds.AppConfig(context.Background()) - if err != nil { - initFatal(err, "loading app config") - } - - appCfg.MDM.EnabledAndConfigured = false - appCfg.MDM.AppleBMEnabledAndConfigured = false - if len(config.Server.PrivateKey) > 0 { - appCfg.MDM.EnabledAndConfigured, err = checkMDMAssets([]fleet.MDMAssetName{ - fleet.MDMAssetCACert, - fleet.MDMAssetCAKey, - fleet.MDMAssetAPNSKey, - fleet.MDMAssetAPNSCert, - }) - if err != nil { - initFatal(err, "loading MDM assets from database") - } - - var appleBMCerts bool - appleBMCerts, err = checkMDMAssets([]fleet.MDMAssetName{ - fleet.MDMAssetABMCert, - fleet.MDMAssetABMKey, - }) - if err != nil { - initFatal(err, "loading MDM ABM assets from database") - } - if appleBMCerts { - // the ABM certs are there, check if a token exists and if so, apple - // BM is enabled and configured. - count, err := ds.GetABMTokenCount(context.Background()) - if err != nil { - initFatal(err, "loading MDM ABM token from database") - } - appCfg.MDM.AppleBMEnabledAndConfigured = count > 0 - } - } - if appCfg.MDM.EnabledAndConfigured { - logger.InfoContext(cmd.Context(), "Apple MDM enabled") - } - if appCfg.MDM.AppleBMEnabledAndConfigured { - logger.InfoContext(cmd.Context(), "Apple Business Manager enabled") - } - - // register the Microsoft MDM services - var ( - wstepCertManager microsoft_mdm.CertManager - ) - - // Configuring WSTEP certs - if config.MDM.IsMicrosoftWSTEPSet() { - _, crtPEM, keyPEM, err := config.MDM.MicrosoftWSTEP() - if err != nil { - initFatal(err, "validate Microsoft WSTEP certificate and key") - } - wstepCertManager, err = microsoft_mdm.NewCertManager(ds, crtPEM, keyPEM) - if err != nil { - initFatal(err, "initialize mdm microsoft wstep depot") - } - } - - // save the app config with the updated MDM.Enabled value - if err := ds.SaveAppConfig(context.Background(), appCfg); err != nil { - initFatal(err, "saving app config") - } - - // setup mail service - if appCfg.SMTPSettings != nil && appCfg.SMTPSettings.SMTPEnabled { - // if SMTP is already enabled then default the backend to empty string, which fill force load the SMTP implementation - if config.Email.EmailBackend != "" { - config.Email.EmailBackend = "" - logger.WarnContext(cmd.Context(), "SMTP is already enabled, first disable SMTP to utilize a different email backend") - } - } - mailService, err := mail.NewService(config) - if err != nil { - logger.ErrorContext(cmd.Context(), "failed to configure mailing service", "err", err) - } - - cronSchedules := fleet.NewCronSchedules() - - baseCtx := licensectx.NewContext(context.Background(), license) - ctx, cancelFunc := context.WithCancel(baseCtx) - defer cancelFunc() - - // Channel used to trigger graceful shutdown on fatal DB errors (e.g. Aurora failover). - dbFatalCh := make(chan error, 1) - common_mysql.SetFatalErrorHandler(func(ctx context.Context, err error) { - logger.ErrorContext(ctx, "fatal database error detected, initiating graceful shutdown", "err", err) - select { - case dbFatalCh <- err: - default: - } - }) - - var conditionalAccessMicrosoftProxy *conditional_access_microsoft_proxy.Proxy - if config.MicrosoftCompliancePartner.IsSet() { - var err error - conditionalAccessMicrosoftProxy, err = conditional_access_microsoft_proxy.New( - config.MicrosoftCompliancePartner.ProxyURI, - config.MicrosoftCompliancePartner.ProxyAPIKey, - func() (string, error) { - appCfg, err := ds.AppConfig(ctx) - if err != nil { - return "", fmt.Errorf("failed to load appconfig: %w", err) - } - return appCfg.ServerSettings.ServerURL, nil - }, - ) - if err != nil { - initFatal(err, "new microsoft compliance proxy") - } - } - - eh := errorstore.NewHandler(ctx, redisPool, logger, config.Logging.ErrorRetentionPeriod) - scepConfigMgr := scep.NewSCEPConfigService(logger, nil) - digiCertService := digicert.NewService(digicert.WithLogger(logger)) - ctx = ctxerr.NewContext(ctx, eh) - - // Declare svc early so the closure below can capture it. - var svc fleet.Service - config.MDM.AndroidAgent.Validate(initFatal) - androidSvc, err := android_service.NewService( - ctx, - logger, - ds, - config.License.Key, - config.Server.PrivateKey, - ds, - func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { - return svc.NewActivity(ctx, user, activity) - }, - config.MDM.AndroidAgent, - ) - if err != nil { - initFatal(err, "initializing android service") - } - - svc, err = service.NewService( - ctx, - ds, - task, - resultStore, - logger, - &service.OsqueryLogger{ - Status: osquerydStatusLogger, - Result: osquerydResultLogger, - }, - config, - mailService, - clock.C, - ssoSessionStore, - liveQueryStore, - carveStore, - failingPolicySet, - geoIP, - redisWrapperDS, - depStorage, - mdmStorage, - mdmPushService, - cronSchedules, - wstepCertManager, - scepConfigMgr, - digiCertService, - conditionalAccessMicrosoftProxy, - redis_key_value.New(redisPool), - androidSvc, - ) - if err != nil { - initFatal(err, "initializing service") - } - - var softwareInstallStore fleet.SoftwareInstallerStore - var bootstrapPackageStore fleet.MDMBootstrapPackageStore - var softwareTitleIconStore fleet.SoftwareTitleIconStore - var distributedLock fleet.Lock - if license.IsPremium() { - hydrantService := est.NewService(est.WithLogger(logger)) - profileMatcher := apple_mdm.NewProfileMatcher(redisPool) - if config.S3.SoftwareInstallersBucket != "" { - if config.S3.BucketsAndPrefixesMatch() { - logger.WarnContext(ctx, - "the S3 buckets and prefixes for carves and software installers appear to be identical, this can cause issues") - } - // Extract the CloudFront URL signer before creating the S3 stores. - config.S3.ValidateCloudFrontURL(initFatal) - if config.S3.SoftwareInstallersCloudFrontURLSigningPrivateKey != "" { - // Strip newlines from private key - signingPrivateKey := strings.ReplaceAll(config.S3.SoftwareInstallersCloudFrontURLSigningPrivateKey, "\\n", "\n") - privateKey, err := cryptoutil.ParsePrivateKey([]byte(signingPrivateKey), - "CloudFront URL signing private key") - if err != nil { - initFatal(err, "parsing CloudFront URL signing private key") - } - var ok bool - config.S3.SoftwareInstallersCloudFrontSigner, ok = privateKey.(crypto.Signer) - if !ok { - initFatal(errors.New("CloudFront URL signing private key is not a crypto.Signer"), - "parsing CloudFront URL signing private key") - } - } - store, err := s3.NewSoftwareInstallerStore(config.S3) - if err != nil { - initFatal(err, "initializing S3 software installer store") - } - softwareInstallStore = store - logger.InfoContext(ctx, "using S3 software installer store", "bucket", config.S3.SoftwareInstallersBucket) - - bstore, err := s3.NewBootstrapPackageStore(config.S3) - if err != nil { - initFatal(err, "initializing S3 bootstrap package store") - } - bootstrapPackageStore = bstore - logger.InfoContext(ctx, "using S3 bootstrap package store", "bucket", config.S3.SoftwareInstallersBucket) - - softwareTitleIconStore, err = s3.NewSoftwareTitleIconStore(config.S3) - if err != nil { - initFatal(err, "initializing S3 software title icon store") - } - logger.InfoContext(ctx, "using S3 software title icon store", "bucket", config.S3.SoftwareInstallersBucket) - } else { - installerDir := os.TempDir() - if dir := os.Getenv("FLEET_SOFTWARE_INSTALLER_STORE_DIR"); dir != "" { - installerDir = dir - } - store, err := filesystem.NewSoftwareInstallerStore(installerDir) - if err != nil { - logger.ErrorContext(ctx, "failed to configure local filesystem software installer store", "err", err) - softwareInstallStore = failing.NewFailingSoftwareInstallerStore() - } else { - softwareInstallStore = store - logger.InfoContext(ctx, - "using local filesystem software installer store, this is not suitable for production use", "directory", - installerDir) - } - - iconDir := os.TempDir() - if dir := os.Getenv("FLEET_SOFTWARE_TITLE_ICON_STORE_DIR"); dir != "" { - iconDir = dir - } - iconStore, err := filesystem.NewSoftwareTitleIconStore(iconDir) - if err != nil { - logger.ErrorContext(ctx, "failed to configure local filesystem software title icon store", "err", err) - softwareTitleIconStore = failing.NewFailingSoftwareTitleIconStore() - } else { - softwareTitleIconStore = iconStore - logger.WarnContext(ctx, - "using local filesystem software title icon store, this is not suitable for production use", "directory", - iconDir) - } - } - - distributedLock = redis_lock.NewLock(redisPool) - svc, err = eeservice.NewService( - svc, - ds, - logger, - config, - mailService, - clock.C, - depStorage, - apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), - ssoSessionStore, - profileMatcher, - softwareInstallStore, - bootstrapPackageStore, - softwareTitleIconStore, - distributedLock, - redis_key_value.New(redisPool), - scepConfigMgr, - digiCertService, - androidSvc, - hydrantService, - ) - if err != nil { - initFatal(err, "initial Fleet Premium service") - } - } - - instanceID, err := server.GenerateRandomText(64) - if err != nil { - initFatal(errors.New("Error generating random instance identifier"), "") - } - logger.InfoContext(ctx, "instance info", "instanceID", instanceID) - - // Bootstrap activity bounded context (needed for cron schedules and HTTP routes) - activitySvc, activityRoutes := createActivityBoundedContext(svc, dbConns, logger) - // Inject the activity bounded context into the main service - svc.SetActivityService(activitySvc) - - // Perform a cleanup of cron_stats outside of the cronSchedules because the - // schedule package uses cron_stats entries to decide whether a schedule will - // run or not (see https://github.com/fleetdm/fleet/issues/9486). - go func() { - cleanupCronStats := func() { - logger.DebugContext(ctx, "cleaning up cron_stats") - // Datastore.CleanupCronStats should be safe to run by multiple fleet - // instances at the same time and it should not be an expensive operation. - if err := ds.CleanupCronStats(ctx); err != nil { - logger.InfoContext(ctx, "failed to clean up cron_stats", "err", err) - } - } - - cleanupCronStats() - - cleanUpCronStatsTick := time.NewTicker(1 * time.Hour) - defer cleanUpCronStatsTick.Stop() - for { - select { - case <-ctx.Done(): - return - case <-cleanUpCronStatsTick.C: - cleanupCronStats() - } - } - }() - - if softwareInstallStore != nil { - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - return cronUninstallSoftwareMigration(ctx, instanceID, ds, softwareInstallStore, logger) - }, - ); err != nil { - initFatal(err, fmt.Sprintf("failed to register %s", fleet.CronUninstallSoftwareMigration)) - } - - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - return cronUpgradeCodeSoftwareMigration(ctx, instanceID, ds, softwareInstallStore, logger) - }, - ); err != nil { - initFatal(err, fmt.Sprintf("failed to register %s", fleet.CronUpgradeCodeSoftwareMigration)) - } - } - - if config.Server.FrequentCleanupsEnabled { - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - return newFrequentCleanupsSchedule(ctx, instanceID, ds, liveQueryStore, logger) - }, - ); err != nil { - initFatal(err, "failed to register frequent_cleanups schedule") - } - } - - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - return newCleanupsAndAggregationSchedule( - ctx, instanceID, ds, svc, logger, redisWrapperDS, &config, commander, softwareInstallStore, bootstrapPackageStore, softwareTitleIconStore, androidSvc, activitySvc, - ) - }, - ); err != nil { - initFatal(err, "failed to register cleanups_then_aggregations schedule") - } - - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - return newQueryResultsCleanupSchedule(ctx, instanceID, ds, liveQueryStore, logger) - }, - ); err != nil { - initFatal(err, "failed to register query_results_cleanup schedule") - } - - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - return newUpcomingActivitiesSchedule(ctx, instanceID, ds, logger) - }, - ); err != nil { - initFatal(err, "failed to register upcoming_activities_maintenance schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newUsageStatisticsSchedule(ctx, instanceID, ds, config, logger) - }); err != nil { - initFatal(err, "failed to register stats schedule") - } - - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - return newBatchActivitiesSchedule(ctx, instanceID, ds, logger) - }); err != nil { - initFatal(err, "failed to register batch activities schedule") - } - - vulnerabilityScheduleDisabled := false - if config.Vulnerabilities.DisableSchedule { - vulnerabilityScheduleDisabled = true - logger.InfoContext(ctx, "vulnerabilities schedule disabled via vulnerabilities.disable_schedule") - } - if config.Vulnerabilities.CurrentInstanceChecks == "no" || config.Vulnerabilities.CurrentInstanceChecks == "0" { - logger.InfoContext(ctx, "vulnerabilities schedule disabled via vulnerabilities.current_instance_checks") - vulnerabilityScheduleDisabled = true - } - if !vulnerabilityScheduleDisabled { - // vuln processing by default is run by internal cron mechanism - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newVulnerabilitiesSchedule(ctx, instanceID, ds, logger, &config.Vulnerabilities) - }); err != nil { - initFatal(err, "failed to register vulnerabilities schedule") - } - } else { - // Register a remote trigger proxy so triggering still works - // when the vulnerability schedule runs on a separate server. - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return schedule.NewRemoteTriggerSchedule(string(fleet.CronVulnerabilities), ds), nil - }); err != nil { - initFatal(err, "failed to register remote vulnerability trigger") - } - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newAutomationsSchedule(ctx, instanceID, ds, logger, 5*time.Minute, failingPolicySet) - }); err != nil { - initFatal(err, "failed to register automations schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander, androidSvc) - }); err != nil { - initFatal(err, "failed to register worker integrations schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - vppInstaller := svc.(fleet.AppleMDMVPPInstaller) - return newAppleMDMWorkerSchedule(ctx, instanceID, ds, logger, commander, bootstrapPackageStore, vppInstaller) - }); err != nil { - initFatal(err, "failed to register apple_mdm_worker schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newAppleMDMDEPProfileAssigner(ctx, instanceID, config.MDM.AppleDEPSyncPeriodicity, ds, depStorage, logger) - }); err != nil { - initFatal(err, "failed to register apple_mdm_dep_profile_assigner schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newMDMAppleServiceDiscoverySchedule(ctx, instanceID, ds, depStorage, logger, config.Server.URLPrefix) - }); err != nil { - initFatal(err, "failed to register mdm_apple_service_discovery schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newAppleMDMProfileManagerSchedule( - ctx, - instanceID, - ds, - apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), - logger, - ) - }); err != nil { - initFatal(err, "failed to register mdm_apple_profile_manager schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newWindowsMDMProfileManagerSchedule( - ctx, - instanceID, - ds, - logger, - ) - }); err != nil { - initFatal(err, "failed to register mdm_windows_profile_manager schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newAndroidMDMProfileManagerSchedule( - ctx, - instanceID, - ds, - logger, - config.License.Key, // NOTE: this requires the license key, not the parsed *LicenseInfo available in the ctx - config.MDM.AndroidAgent, - ) - }); err != nil { - initFatal(err, "failed to register mdm_android_profile_manager schedule") - } - - // Register Android MDM Device Reconciler schedule (same interval as Android profile manager) - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newAndroidMDMDeviceReconcilerSchedule( - ctx, - instanceID, - ds, - logger, - config.License.Key, - svc.NewActivity, - ) - }); err != nil { - initFatal(err, "failed to register mdm_android_device_reconciler schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return cronEnableAndroidAppReportsOnDefaultPolicy(ctx, instanceID, ds, logger, androidSvc) - }); err != nil { - initFatal(err, "failed to register enable_android_app_reports_on_default_policy cron") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return cronMigrateToPerHostPolicy(ctx, instanceID, ds, logger, androidSvc) - }); err != nil { - initFatal(err, "failed to register migrate_to_per_host_policy cron") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newMDMAPNsPusher( - ctx, - instanceID, - ds, - apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), - logger, - ) - }); err != nil { - initFatal(err, "failed to register APNs pusher schedule") - } - - if license.IsPremium() { - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - return newIPhoneIPadRefetcher(ctx, instanceID, 10*time.Minute, ds, commander, logger, svc.NewActivity) - }); err != nil { - initFatal(err, "failed to register apple_mdm_iphone_ipad_refetcher schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - return newIPhoneIPadReviver(ctx, instanceID, ds, commander, logger) - }); err != nil { - initFatal(err, "failed to register apple_mdm_iphone_ipad_reviver schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newMaintainedAppSchedule(ctx, instanceID, ds, logger) - }); err != nil { - initFatal(err, "failed to register maintained apps schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newRefreshVPPAppVersionsSchedule(ctx, instanceID, ds, logger, apple_apps.Configure(ctx, ds, config.License.Key, config.MDM.AppleConnectJWT)) - }); err != nil { - initFatal(err, "failed to register refresh vpp app versions schedule") - } - - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - return newRecoveryLockPasswordSchedule(ctx, instanceID, ds, commander, logger) - }); err != nil { - initFatal(err, "failed to register recovery lock password schedule") - } - } - - if license.IsPremium() && config.Activity.EnableAuditLog { - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newActivitiesStreamingSchedule(ctx, instanceID, activitySvc, ds, logger, auditLogger) - }); err != nil { - initFatal(err, "failed to register activities streaming schedule") - } - } - - if license.IsPremium() { - if err := cronSchedules.StartCronSchedule( - func() (fleet.CronSchedule, error) { - if config.Calendar.Periodicity > 0 { - config.Calendar.SetAlwaysReloadEvent(true) - } else { - config.Calendar.Periodicity = 5 * time.Minute - } - return cron.NewCalendarSchedule(ctx, instanceID, ds, distributedLock, config.Calendar, logger) - }, - ); err != nil { - initFatal(err, "failed to register calendar schedule") - } - } - - // Start the service that calculates and updates host vitals label membership. - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newHostVitalsLabelMembershipSchedule(ctx, instanceID, ds, logger) - }); err != nil { - initFatal(err, "failed to register host vitals label membership schedule") - } - - // Start the service that marks activities as completed. - if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newBatchActivityCompletionCheckerSchedule(ctx, instanceID, ds, logger) - }); err != nil { - initFatal(err, "failed to register batch activity completion checker schedule") - } - - logger.InfoContext(ctx, fmt.Sprintf("started cron schedules: %s", strings.Join(cronSchedules.ScheduleNames(), ", "))) - - // StartCollectors starts a goroutine per collector, using ctx to cancel. - task.StartCollectors(ctx, logger.With("cron", "async_task")) - - // Flush seen hosts every second - hostsAsyncCfg := config.Osquery.AsyncConfigForTask(configpkg.AsyncTaskHostLastSeen) - if !hostsAsyncCfg.Enabled { - go func() { - for range time.Tick(time.Duration(rand.Intn(10)+1) * time.Second) { - if err := task.FlushHostsLastSeen(baseCtx, clock.C.Now()); err != nil { - logger.InfoContext(ctx, "failed to update host seen times", "err", err) - } - } - }() - } - - fieldKeys := []string{"method", "error"} - requestCount := kitprometheus.NewCounterFrom(prometheus.CounterOpts{ - Namespace: "api", - Subsystem: "service", - Name: "request_count", - Help: "Number of requests received.", - }, fieldKeys) - requestLatency := kitprometheus.NewSummaryFrom(prometheus.SummaryOpts{ - Namespace: "api", - Subsystem: "service", - Name: "request_latency_microseconds", - Help: "Total duration of requests in microseconds.", - }, fieldKeys) - - svc = service.NewMetricsService(svc, requestCount, requestLatency) - - httpLogger := logger.With("component", "http") - - limiterStore := &redis.ThrottledStore{ - Pool: redisPool, - KeyPrefix: "ratelimit::", - } - - var httpSigVerifier func(http.Handler) http.Handler - if license.IsPremium() { - httpSigVerifier, err = httpsig.Middleware(ds, config.Auth.RequireHTTPMessageSignature, logger.With("component", "http-sig-verifier")) - if err != nil { - initFatal(err, "initializing HTTP signature verifier") - } - } - - // This is off by default for testing and development uses only. - cspEV := os.Getenv("FLEET_SERVER_ENABLE_CSP") - serveCSP := cspEV == "1" || cspEV == "true" - - var apiHandler, frontendHandler, endUserEnrollOTAHandler http.Handler - { - frontendHandler = service.PrometheusMetricsHandler( - "get_frontend", - service.ServeFrontend(config.Server.URLPrefix, config.Server.SandboxEnabled, httpLogger, serveCSP), - ) - - frontendHandler = service.WithMDMEnrollmentMiddleware(svc, httpLogger, frontendHandler) - - var extra []service.ExtraHandlerOption - if config.MDM.SSORateLimitPerMinute > 0 { - extra = append(extra, service.WithMdmSsoRateLimit(throttled.PerMin(config.MDM.SSORateLimitPerMinute))) - } - extra = append(extra, service.WithHTTPSigVerifier(httpSigVerifier)) - - apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore, redisPool, carveStore, - []endpointer.HandlerRoutesFunc{android_service.GetRoutes(svc, androidSvc), activityRoutes}, extra...) - - if serveCSP { - // Only injecting this if CSP is turned on since the default security headers add some overhead to each request - apiHandler = endpointer.BrowserSecurityHeadersHandler(serveCSP, apiHandler) - } - - setupRequired, err := svc.SetupRequired(baseCtx) - if err != nil { - initFatal(err, "fetching setup requirement") - } - // WithSetup will check if first time setup is required - // By performing the same check inside main, we can make server startups - // more efficient after the first startup. - if setupRequired { - apiHandler = service.WithSetup(svc, logger, apiHandler) - frontendHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler, config.Server.URLPrefix) - } else { - frontendHandler = service.RedirectSetupToLogin(svc, logger, frontendHandler, config.Server.URLPrefix) - } - - endUserEnrollOTAHandler = service.ServeEndUserEnrollOTA( - svc, - config.Server.URLPrefix, - ds, - logger, - serveCSP, - ) - } - - healthCheckers := make(map[string]health.Checker) - { - // a list of dependencies which could affect the status of the app if unavailable. - deps := map[string]interface{}{ - "mysql": ds, - "redis": resultStore, - } - - // convert all dependencies to health.Checker if they implement the healthz methods. - for name, dep := range deps { - if hc, ok := dep.(health.Checker); ok { - healthCheckers[name] = hc - } else { - initFatal(errors.New(name+" should be a health.Checker"), "initializing health checks") - } - } - - } - - // Instantiate a gRPC service to handle launcher requests. - launcher := launcher.New(svc, logger, grpc.NewServer( - grpc.ChainUnaryInterceptor( - grpc_recovery.UnaryServerInterceptor(), - ), - grpc.ChainStreamInterceptor( - grpc_recovery.StreamServerInterceptor(), - ), - ), healthCheckers) - - rootMux := http.NewServeMux() - rootMux.Handle("/healthz", service.PrometheusMetricsHandler("healthz", otelmw.WrapHandler(health.Handler(httpLogger, healthCheckers), "/healthz", config))) - rootMux.Handle("/version", service.PrometheusMetricsHandler("version", otelmw.WrapHandler(version.Handler(), "/version", config))) - rootMux.Handle("/assets/", service.PrometheusMetricsHandler("static_assets", otelmw.WrapHandlerDynamic(service.ServeStaticAssets("/assets/", serveCSP), config))) - - if len(config.Server.PrivateKey) > 0 { - commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - ddmService := service.NewMDMAppleDDMService(ds, logger) - vppInstaller := svc.(fleet.AppleMDMVPPInstaller) - mdmCheckinAndCommandService := service.NewMDMAppleCheckinAndCommandService( - ds, - commander, - vppInstaller, - license.IsPremium(), - logger, - redis_key_value.New(redisPool), - svc.NewActivity, - ) - - mdmCheckinAndCommandService.RegisterResultsHandler("InstalledApplicationList", service.NewInstalledApplicationListResultsHandler(ds, commander, logger, config.Server.VPPVerifyTimeout, config.Server.VPPVerifyRequestDelay, svc.NewActivity)) - mdmCheckinAndCommandService.RegisterResultsHandler(fleet.DeviceLocationCmdName, service.NewDeviceLocationResultsHandler(ds, commander, logger)) - mdmCheckinAndCommandService.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, service.NewSetRecoveryLockResultsHandler(ds, logger, svc.NewActivity)) - - hasSCEPChallenge, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge}) - if err != nil { - initFatal(err, "checking SCEP challenge in database") - } - if !hasSCEPChallenge { - scepChallenge := config.MDM.AppleSCEPChallenge - if scepChallenge == "" { - scepChallenge = uuid.NewString() - } - - err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ - {Name: fleet.MDMAssetSCEPChallenge, Value: []byte(scepChallenge)}, - }, nil) - if err != nil { - // duplicate key errors mean that we already - // have a value for those keys in the - // database, fail to initalize on other - // cases. - if !mysql.IsDuplicate(err) { - initFatal(err, "inserting SCEP challenge") - } - - logger.WarnContext(ctx, - "Your server already has stored a SCEP challenge. Fleet will ignore this value provided via environment variables when this happens.") - } - } - if err := service.RegisterAppleMDMProtocolServices( - rootMux, - config.MDM, - mdmStorage, - scepStorage, - logger, - mdmCheckinAndCommandService, - ddmService, - commander, - appCfg.ServerSettings.ServerURL, - config, - ); err != nil { - initFatal(err, "setup mdm apple services") - } - } - - if license.IsPremium() { - // SCEP proxy (for NDES, etc.) - if err = service.RegisterSCEPProxy(rootMux, ds, logger, nil, &config); err != nil { - initFatal(err, "setup SCEP proxy") - } - if err = scim.RegisterSCIM(rootMux, ds, svc, logger, &config); err != nil { - initFatal(err, "setup SCIM") - } - // Host identify and conditional access SCEP feature only works if a private key has been set up - if len(config.Server.PrivateKey) > 0 { - hostIdentitySCEPDepot, err := mds.NewHostIdentitySCEPDepot(logger.With("component", "host-id-scep-depot"), &config) - if err != nil { - initFatal(err, "setup host identity SCEP depot") - } - if err = hostidentity.RegisterSCEP(rootMux, hostIdentitySCEPDepot, ds, logger, &config); err != nil { - initFatal(err, "setup host identity SCEP") - } - - // Conditional Access SCEP - condAccessSCEPDepot, err := mds.NewConditionalAccessSCEPDepot(logger.With("component", "conditional-access-scep-depot"), &config) - if err != nil { - initFatal(err, "setup conditional access SCEP depot") - } - if err = condaccess.RegisterSCEP(ctx, rootMux, condAccessSCEPDepot, ds, logger, &config); err != nil { - initFatal(err, "setup conditional access SCEP") - } - - // Conditional Access IdP (Okta) - if err = condaccess.RegisterIdP(rootMux, ds, logger, &config, limiterStore); err != nil { - initFatal(err, "setup conditional access IdP") - } - } else { - logger.WarnContext(ctx, - "Host identity and conditional access SCEP is not available because no server private key has been set up.") - } - } - - if config.Prometheus.BasicAuth.Username != "" && config.Prometheus.BasicAuth.Password != "" { - rootMux.Handle("/metrics", basicAuthHandler( - config.Prometheus.BasicAuth.Username, - config.Prometheus.BasicAuth.Password, - service.PrometheusMetricsHandler("metrics", otelmw.WrapHandler(promhttp.Handler(), "/metrics", config)), - )) - } else { - if config.Prometheus.BasicAuth.Disable { - logger.InfoContext(ctx, "metrics endpoint enabled with http basic auth disabled") - rootMux.Handle("/metrics", service.PrometheusMetricsHandler("metrics", otelmw.WrapHandler(promhttp.Handler(), "/metrics", config))) - } else { - logger.InfoContext(ctx, "metrics endpoint disabled (http basic auth credentials not set)") - } - } - - // We must wrap the Handler here to set special per-endpoint Read/Write - // timeouts, so that we have access to the raw http.ResponseWriter. - // Otherwise, the handler is wrapped by the promhttp response delegator, - // which does not support the Unwrap call needed to work with - // ResponseController. - // - // See https://pkg.go.dev/net/http#NewResponseController which explains - // the Unwrap method that the prometheus wrapper of http.ResponseWriter - // does not implement. - rootMux.HandleFunc("/api/", func(rw http.ResponseWriter, req *http.Request) { - if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/scripts/run/sync") { - // when running a script synchronously, we wait a while for a script - // execution result, so the write timeout (to write the response) - // must be extended. - rc := http.NewResponseController(rw) - // add an additional 30 seconds to prevent race conditions where the - // request is terminated early. - if err := rc.SetWriteDeadline(time.Now().Add(scripts.MaxServerWaitTime + (30 * time.Second))); err != nil { - logger.ErrorContext(req.Context(), - "http middleware failed to override endpoint write timeout for script sync run", - "response_writer_type", fmt.Sprintf("%T", rw), - "response_writer", fmt.Sprintf("%+v", rw), - "err", err, - ) - } - } - - if (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/software/package")) || - (req.Method == http.MethodPatch && strings.HasSuffix(req.URL.Path, "/package") && strings.Contains(req.URL.Path, - "/fleet/software/titles/")) || - (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/bootstrap")) || - (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet_maintained_apps")) || - (req.Method == http.MethodGet && strings.Contains(req.URL.Path, "/package/token")) || - (req.Method == http.MethodPost && strings.Contains(req.URL.Path, "orbit/software_install/package")) { - var zeroTime time.Time - rc := http.NewResponseController(rw) - // For large software installers and bootstrap packages, the server time needs time to read the full - // request body so we use the zero value to remove the deadline and override the - // default read timeout. - // TODO: Is this really how we want to handle this? Or would an arbitrarily long - // timeout be better? - if err := rc.SetReadDeadline(zeroTime); err != nil { - logger.ErrorContext(req.Context(), - "http middleware failed to override endpoint read timeout for software package upload", - "response_writer_type", fmt.Sprintf("%T", rw), - "response_writer", fmt.Sprintf("%+v", rw), - "err", err, - ) - } - // For large software installers, the server time needs time to store the - // installer to S3 (or the configured storage location) and write the response - // body so we use the zero value to remove the deadline and override the - // default write timeout. - // TODO: Is this really how we want to handle this? Or would an arbitrarily long - // timeout be better? - if err := rc.SetWriteDeadline(zeroTime); err != nil { - logger.ErrorContext(req.Context(), - "http middleware failed to override endpoint write timeout for software package upload", - "response_writer_type", fmt.Sprintf("%T", rw), - "response_writer", fmt.Sprintf("%+v", rw), - "err", err, - ) - } - - // We need to add the context value here because we need the installer max size when doing request - // parsing, which happens somewhere where we're only passed the request (and not the service object) - req.Body = http.MaxBytesReader(rw, req.Body, config.Server.MaxInstallerSizeBytes) - req = req.WithContext(installersize.NewContext(req.Context(), config.Server.MaxInstallerSizeBytes)) - } - - if req.Method == http.MethodGet && strings.HasSuffix(req.URL.Path, "/fleet/android_enterprise/signup_sse") { - // When enabling Android MDM, frontend UI will wait for the admin to finish the setup in Google. - rc := http.NewResponseController(rw) - if err := rc.SetWriteDeadline(time.Now().Add(30 * time.Minute)); err != nil { - logger.ErrorContext(req.Context(), - "http middleware failed to override endpoint write timeout for android enterpriset setup", - "response_writer_type", fmt.Sprintf("%T", rw), - "response_writer", fmt.Sprintf("%+v", rw), - "err", err, - ) - } - } - - if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/mdm/profiles/batch") || - (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/configuration_profiles/batch")) { - // For customers using large profiles and/or large numbers of profiles, the - // server needs time to completely read the request body and also to process - // all the side effects of a potentially large number of profiles being changed - // across a large number of hosts, so set the timeouts a bit higher than default - rc := http.NewResponseController(rw) - if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Minute)); err != nil { - logger.ErrorContext(req.Context(), - "http middleware failed to override endpoint write timeout for MDM profiles batch endpoint", - "response_writer_type", fmt.Sprintf("%T", rw), - "response_writer", fmt.Sprintf("%+v", rw), - "err", err, - ) - } - if err := rc.SetReadDeadline(time.Now().Add(5 * time.Minute)); err != nil { - logger.ErrorContext(req.Context(), - "http middleware failed to override endpoint read timeout for MDM profiles batch endpoint", - "response_writer_type", fmt.Sprintf("%T", rw), - "response_writer", fmt.Sprintf("%+v", rw), - "err", err, - ) - } - } - - apiHandler.ServeHTTP(rw, req) - }) - // The `/api/{version}/fleet/scim` base path is used by SCIM handler. In order to route the `details` route to the apiHandler, - // we have to explicitly handle that path at the root. The Go router takes precedence for a more specific path. The v1/latest are used in the path for it to be more specific. - // The Fleet API was designed this way for end-user simplicity. - rootMux.Handle("/api/v1/fleet/scim/details", apiHandler) - rootMux.Handle("/api/latest/fleet/scim/details", apiHandler) - - rootMux.Handle("/enroll", otelmw.WrapHandler(endUserEnrollOTAHandler, "/enroll", config)) - rootMux.Handle("/", otelmw.WrapHandler(frontendHandler, "/", config)) - - debugHandler := &debugMux{ - fleetAuthenticatedHandler: service.MakeDebugHandler(svc, config, logger, eh, ds), - } - rootMux.Handle("/debug/", otelmw.WrapHandlerDynamic(debugHandler, config)) - - if debug { - // Add debug endpoints with a random - // authorization token - debugToken, err := server.GenerateRandomText(24) - if err != nil { - initFatal(err, "generating debug token") - } - debugHandler.tokenAuthenticatedHandler = http.StripPrefix("/debug/", netbug.AuthHandler(debugToken)) - fmt.Printf("*** Debug mode enabled ***\nAccess the debug endpoints at /debug/?token=%s\n", url.QueryEscape(debugToken)) - } - - if len(config.Server.URLPrefix) > 0 { - prefixMux := http.NewServeMux() - prefixMux.Handle(config.Server.URLPrefix+"/", http.StripPrefix(config.Server.URLPrefix, rootMux)) - rootMux = prefixMux - } - - // NOTE(lucas): It seems we missed updating this value from 90s (see #1798) to 25s after we - // decided to make the synchronous live query API to take up to 25 seconds. - // Not changing this to not break any long running requests (like when uploading software - // packages via GitOps). - liveQueryRestPeriod := 90 * time.Second - if v := os.Getenv("FLEET_LIVE_QUERY_REST_PERIOD"); v != "" { - duration, err := time.ParseDuration(v) - if err != nil { - logger.ErrorContext(ctx, "failed to parse live query rest period", "err", err) - } else { - liveQueryRestPeriod = duration - } - } - - // The "GET /api/latest/fleet/queries/run" API requires - // WriteTimeout to be higher than the live query rest period - // (otherwise the response is not sent back to the client). - // - // We add 10s to the live query rest period to allow the writing - // of the response. - liveQueryRestPeriod += 10 * time.Second - - // Create the handler based on whether tracing should be there - var handler http.Handler - if config.Logging.TracingEnabled && config.Logging.TracingType == "elasticapm" { - handler = launcher.Handler(apmhttp.Wrap(rootMux)) - } else { - handler = launcher.Handler(rootMux) - } - - srv := config.Server.DefaultHTTPServer(ctx, handler) - if liveQueryRestPeriod > srv.WriteTimeout { - srv.WriteTimeout = liveQueryRestPeriod - } - srv.SetKeepAlivesEnabled(config.Server.Keepalive) - errs := make(chan error, 2) - go func() { - if !config.Server.TLS { - logger.InfoContext(ctx, "listening", "transport", "http", "address", config.Server.Address) - errs <- srv.ListenAndServe() - } else { - logger.InfoContext(ctx, "listening", "transport", "https", "address", config.Server.Address) - srv.TLSConfig = getTLSConfig(config.Server.TLSProfile) - errs <- srv.ListenAndServeTLS( - config.Server.Cert, - config.Server.Key, - ) - } - }() - go func() { - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - select { - case <-sig: - case <-dbFatalCh: - } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - errs <- func() error { - cancelFunc() - cleanupCronStatsOnShutdown(ctx, ds, logger, instanceID) - launcher.GracefulStop() - // Flush any pending OTEL data before shutting down - if tracerProvider != nil { - if err := tracerProvider.Shutdown(ctx); err != nil { - logger.ErrorContext(ctx, "failed to shutdown OTEL tracer provider", "err", err) - } - } - if meterProvider != nil { - if err := meterProvider.Shutdown(ctx); err != nil { - logger.ErrorContext(ctx, "failed to shutdown OTEL meter provider", "err", err) - } - } - if loggerProvider != nil { - if err := loggerProvider.Shutdown(ctx); err != nil { - logger.ErrorContext(ctx, "failed to shutdown OTEL logger provider", "err", err) - } - } - return srv.Shutdown(ctx) - }() - }() - - // block on errs signal - logger.InfoContext(ctx, "terminated", "err", <-errs) + runServeCmd(cmd, configManager, debug, devLicense, devExpiredLicense) }, } @@ -1828,6 +155,1685 @@ the way that the Fleet server works. return serveCmd } +// runServeCmd is a named function so that NilAway can analyze it for nil-safety. +func runServeCmd(cmd *cobra.Command, configManager configpkg.Manager, debug, devLicense, devExpiredLicense bool) { + config := configManager.LoadConfig() + + if dev_mode.IsEnabled { + applyDevFlags(&config) + } + + license, err := initLicense(&config, devLicense, devExpiredLicense) + if err != nil { + initFatal( + err, + "failed to load license - for help use https://fleetdm.com/contact", + ) + } + + if license != nil && license.IsPremium() && license.IsExpired() { + fleet.WriteExpiredLicenseBanner(os.Stderr) + } + + // Validate OTEL server options + if config.Logging.OtelLogsEnabled && !config.Logging.TracingEnabled { + initFatal( + errors.New("logging.otel_logs_enabled requires logging.tracing_enabled to be true"), + "OTEL logs require tracing for trace correlation", + ) + } + + // Init OTEL providers (traces, metrics, logs) + var loggerProvider *otelsdklog.LoggerProvider + var tracerProvider *sdktrace.TracerProvider + var meterProvider *sdkmetric.MeterProvider + if config.OTELEnabled() { + // Create shared resource with service identification attributes. + // OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES env vars can override + // the defaults below. + res, err := resource.New(context.Background(), + resource.WithSchemaURL(semconv.SchemaURL), + resource.WithAttributes( + semconv.ServiceName("fleet"), + semconv.ServiceVersion(version.Version().Version), + ), + resource.WithFromEnv(), + resource.WithTelemetrySDK(), + ) + if err != nil { + initFatal(err, "Failed to create OTEL resource") + } + + // Initialize OTEL traces + otlpTraceExporter, err := otlptrace.New(context.Background(), otlptracegrpc.NewClient( + otlptracegrpc.WithCompressor("gzip"), + )) + if err != nil { + initFatal(err, "Failed to initialize OTEL trace exporter") + } + // Configure batch span processor with smaller batch size to avoid exceeding message size limits (4MB default limit) + batchSpanProcessor := sdktrace.NewBatchSpanProcessor(otlpTraceExporter, + sdktrace.WithMaxExportBatchSize(256), // Reduce from default 512 to 256 + ) + tracerProvider = sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithSpanProcessor(batchSpanProcessor), + ) + otel.SetTracerProvider(tracerProvider) + + // Initialize OTEL metrics + metricExporter, err := otlpmetricgrpc.New(context.Background(), + otlpmetricgrpc.WithCompressor("gzip"), + ) + if err != nil { + initFatal(err, "Failed to initialize OTEL metrics exporter") + } + meterProvider = sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter)), + ) + otel.SetMeterProvider(meterProvider) + + // Initialize OTEL logs + if config.Logging.OtelLogsEnabled { + logExporter, err := otlploggrpc.New(context.Background(), + otlploggrpc.WithCompressor("gzip"), + ) + if err != nil { + initFatal(err, "Failed to initialize OTEL log exporter") + } + loggerProvider = otelsdklog.NewLoggerProvider( + otelsdklog.WithResource(res), + otelsdklog.WithProcessor(otelsdklog.NewBatchProcessor(logExporter)), + ) + } + } + + logger := initLogger(config, loggerProvider) + + // If you want to disable any logs by default, this is where to do it. + // + // For example: + // platform_logging.DisableTopic("deprecated-api-keys") + platform_logging.DisableTopic(platform_logging.DeprecatedFieldTopic) + + // Apply log topic overrides from config. Enables run first, then + // disables, so disable wins on conflict. + // Note that any topic not included in these lists will be considered + // enabled if it's encountered in a log. + for _, topic := range str.SplitAndTrim(config.Logging.EnableLogTopics, ",", true) { + platform_logging.EnableTopic(topic) + } + for _, topic := range str.SplitAndTrim(config.Logging.DisableLogTopics, ",", true) { + platform_logging.DisableTopic(topic) + } + + if dev_mode.IsEnabled { + createTestBuckets(cmd.Context(), &config, logger) + } + + allowedHostIdentifiers := map[string]bool{ + "provided": true, + "instance": true, + "uuid": true, + "hostname": true, + } + if !allowedHostIdentifiers[config.Osquery.HostIdentifier] { + initFatal(fmt.Errorf("%s is not a valid value for osquery_host_identifier", config.Osquery.HostIdentifier), "set host identifier") + } + + config.ConditionalAccess.Validate(initFatal) + + if len(config.Server.URLPrefix) > 0 { + // Massage provided prefix to match expected format + config.Server.URLPrefix = strings.TrimSuffix(config.Server.URLPrefix, "/") + if len(config.Server.URLPrefix) > 0 && !strings.HasPrefix(config.Server.URLPrefix, "/") { + config.Server.URLPrefix = "/" + config.Server.URLPrefix + } + + if !allowedURLPrefixRegexp.MatchString(config.Server.URLPrefix) { + initFatal( + fmt.Errorf("prefix must match regexp \"%s\"", allowedURLPrefixRegexp.String()), + "setting server URL prefix", + ) + } + } + + // Handle server private key configuration - either direct or via AWS Secrets Manager + if config.Server.PrivateKey != "" && config.Server.PrivateKeySecretArn != "" { + initFatal(errors.New("cannot specify both private_key and private_key_secret_arn"), "validate private key configuration") + } + + // Retrieve private key from AWS Secrets Manager if specified + if config.Server.PrivateKeySecretArn != "" { + privateKey, err := configpkg.RetrieveSecretsManagerSecret( + context.Background(), + config.Server.PrivateKeySecretArn, + config.Server.PrivateKeySecretRegion, + config.Server.PrivateKeySecretSTSAssumeRoleArn, + config.Server.PrivateKeySecretSTSExternalID, + ) + if err != nil { + initFatal(err, "retrieve private key from secrets manager") + } + config.Server.PrivateKey = privateKey + } + + if len(config.Server.PrivateKey) > 0 { + if len(config.Server.PrivateKey) < 32 { + initFatal(errors.New("private key must be at least 32 bytes long"), "validate private key") + } + + // We truncate to 32 bytes because AES-256 requires a 32 byte (256 bit) PK, but some + // infra setups generate keys that are longer than 32 bytes. + config.Server.PrivateKey = config.Server.PrivateKey[:32] + } + + var ds fleet.Datastore + var carveStore fleet.CarveStore + + opts := []mysql.DBOption{mysql.Logger(logger), mysql.WithFleetConfig(&config)} + if config.MysqlReadReplica.Address != "" { + opts = append(opts, mysql.Replica(&config.MysqlReadReplica)) + } + // NOTE this will disable OTEL/APM interceptor + if dev_mode.Env("FLEET_DEV_ENABLE_SQL_INTERCEPTOR") != "" { + opts = append(opts, mysql.WithInterceptor(&devSQLInterceptor{ + logger: logger.With("component", "sql-interceptor"), + })) + } + + if config.Logging.TracingEnabled { + opts = append(opts, mysql.TracingEnabled(&config.Logging)) + } + + // Configure default max request body size based on config + platform_http.MaxRequestBodySize = config.Server.DefaultMaxRequestBodySize + + // Create database connections that can be shared across datastores + dbConns, err := mysql.NewDBConnections(config.Mysql, opts...) + if err != nil { + initFatal(err, "initializing database connections") + } + + mds, err := mysql.NewDatastore(dbConns, config.Mysql, clock.C) + if err != nil { + initFatal(err, "initializing datastore") + } + ds = mds + + if config.S3.CarvesBucket != "" || config.S3.Bucket != "" { + carveStore, err = s3.NewCarveStore(config.S3, ds) + if err != nil { + initFatal(err, "initializing S3 carvestore") + } + } else { + carveStore = ds + } + + migrationStatus, err := ds.MigrationStatus(cmd.Context()) + if err != nil { + initFatal(err, "retrieving migration status") + } + + switch migrationStatus.StatusCode { + case fleet.AllMigrationsCompleted: + // OK + case fleet.UnknownMigrations: + printUnknownMigrationsMessage(migrationStatus.UnknownTable, migrationStatus.UnknownData) + if dev_mode.IsEnabled { + os.Exit(1) + } + case fleet.NeedsFleetv4732Fix: + printFleetv4732FixNeededMessage() + if !config.Upgrades.AllowMissingMigrations { + os.Exit(1) + } + case fleet.UnknownFleetv4732State: + printFleetv4732UnknownStateMessage(migrationStatus.StatusCode) + if !config.Upgrades.AllowMissingMigrations { + os.Exit(1) + } + case fleet.SomeMigrationsCompleted: + tables, data := migrationStatus.MissingTable, migrationStatus.MissingData + printMissingMigrationsWarning(tables, data) + if !config.Upgrades.AllowMissingMigrations { + os.Exit(1) + } + case fleet.NoMigrationsCompleted: + printDatabaseNotInitializedError() + os.Exit(1) + } + + if initializingDS, ok := ds.(initializer); ok { + if err := initializingDS.Initialize(); err != nil { + initFatal(err, "loading built in data") + } + } + + // Strip the Redis URI scheme if it's present. Scheme docs are at: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml + // This allows us to use Render's Redis service in render.yaml, including the free tier. + // In the future, we could support the full Redis URI if needed (including username, password, database, etc.) + redisAddress := strings.TrimPrefix(config.Redis.Address, "redis://") + redisPool, err := redis.NewPool(redis.PoolConfig{ + Server: redisAddress, + Username: config.Redis.Username, + Password: config.Redis.Password, + Database: config.Redis.Database, + UseTLS: config.Redis.UseTLS, + Region: config.Redis.Region, + CacheName: config.Redis.CacheName, + StsAssumeRoleArn: config.Redis.StsAssumeRoleArn, + StsExternalID: config.Redis.StsExternalID, + ConnTimeout: config.Redis.ConnectTimeout, + KeepAlive: config.Redis.KeepAlive, + ConnectRetryAttempts: config.Redis.ConnectRetryAttempts, + ClusterFollowRedirections: config.Redis.ClusterFollowRedirections, + ClusterReadFromReplica: config.Redis.ClusterReadFromReplica, + TLSCert: config.Redis.TLSCert, + TLSKey: config.Redis.TLSKey, + TLSCA: config.Redis.TLSCA, + TLSServerName: config.Redis.TLSServerName, + TLSHandshakeTimeout: config.Redis.TLSHandshakeTimeout, + MaxIdleConns: config.Redis.MaxIdleConns, + MaxOpenConns: config.Redis.MaxOpenConns, + ConnMaxLifetime: config.Redis.ConnMaxLifetime, + IdleTimeout: config.Redis.IdleTimeout, + ConnWaitTimeout: config.Redis.ConnWaitTimeout, + WriteTimeout: config.Redis.WriteTimeout, + ReadTimeout: config.Redis.ReadTimeout, + }) + if err != nil { + initFatal(err, "initialize Redis") + } + logger.InfoContext(cmd.Context(), "redis initialized", "component", "redis", "mode", redisPool.Mode()) + + ds = cached_mysql.New(ds) + var dsOpts []mysqlredis.Option + if license.DeviceCount > 0 && config.License.EnforceHostLimit { + dsOpts = append(dsOpts, mysqlredis.WithEnforcedHostLimit(license.DeviceCount)) + } + redisWrapperDS := mysqlredis.New(ds, redisPool, dsOpts...) + ds = redisWrapperDS + + resultStore := pubsub.NewRedisQueryResults(redisPool, config.Redis.DuplicateResults, + logger.With("component", "query-results"), + ) + liveQueryStore := live_query.NewRedisLiveQuery(redisPool, logger, liveQueryMemCacheDuration) + ssoSessionStore := sso.NewSessionStore(redisPool) + + // Set common configuration for all logging. + loggingConfig := logging.Config{ + Filesystem: logging.FilesystemConfig{ + EnableLogRotation: config.Filesystem.EnableLogRotation, + EnableLogCompression: config.Filesystem.EnableLogCompression, + MaxSize: config.Filesystem.MaxSize, + MaxAge: config.Filesystem.MaxAge, + MaxBackups: config.Filesystem.MaxBackups, + }, + Webhook: logging.WebhookConfig{}, + Firehose: logging.FirehoseConfig{ + Region: config.Firehose.Region, + EndpointURL: config.Firehose.EndpointURL, + AccessKeyID: config.Firehose.AccessKeyID, + SecretAccessKey: config.Firehose.SecretAccessKey, + StsAssumeRoleArn: config.Firehose.StsAssumeRoleArn, + StsExternalID: config.Firehose.StsExternalID, + }, + Kinesis: logging.KinesisConfig{ + Region: config.Kinesis.Region, + EndpointURL: config.Kinesis.EndpointURL, + AccessKeyID: config.Kinesis.AccessKeyID, + SecretAccessKey: config.Kinesis.SecretAccessKey, + StsAssumeRoleArn: config.Kinesis.StsAssumeRoleArn, + StsExternalID: config.Kinesis.StsExternalID, + }, + Lambda: logging.LambdaConfig{ + Region: config.Lambda.Region, + AccessKeyID: config.Lambda.AccessKeyID, + SecretAccessKey: config.Lambda.SecretAccessKey, + StsAssumeRoleArn: config.Lambda.StsAssumeRoleArn, + StsExternalID: config.Lambda.StsExternalID, + }, + PubSub: logging.PubSubConfig{ + Project: config.PubSub.Project, + }, + KafkaREST: logging.KafkaRESTConfig{ + ProxyHost: config.KafkaREST.ProxyHost, + ContentTypeValue: config.KafkaREST.ContentTypeValue, + Timeout: config.KafkaREST.Timeout, + }, + Nats: logging.NatsConfig{ + Server: config.Nats.Server, + CredFile: config.Nats.CredFile, + NKeyFile: config.Nats.NKeyFile, + TLSClientCertFile: config.Nats.TLSClientCrtFile, + TLSClientKeyFile: config.Nats.TLSClientKeyFile, + CACertFile: config.Nats.CACrtFile, + Compression: config.Nats.Compression, + JetStream: config.Nats.JetStream, + Timeout: config.Nats.Timeout, + }, + } + + // Set specific configuration to osqueryd status logs. + loggingConfig.Plugin = config.Osquery.StatusLogPlugin + loggingConfig.Filesystem.LogFile = config.Filesystem.StatusLogFile + loggingConfig.Webhook.URL = config.Webhook.StatusURL + loggingConfig.Firehose.StreamName = config.Firehose.StatusStream + loggingConfig.Kinesis.StreamName = config.Kinesis.StatusStream + loggingConfig.Lambda.Function = config.Lambda.StatusFunction + loggingConfig.PubSub.Topic = config.PubSub.StatusTopic + loggingConfig.PubSub.AddAttributes = false // only used by result logs + loggingConfig.KafkaREST.Topic = config.KafkaREST.StatusTopic + loggingConfig.Nats.Subject = config.Nats.StatusSubject + + osquerydStatusLogger, err := logging.NewJSONLogger(cmd.Context(), "status", loggingConfig, logger) + if err != nil { + initFatal(err, "initializing osqueryd status logging") + } + + // Set specific configuration to osqueryd result logs. + loggingConfig.Plugin = config.Osquery.ResultLogPlugin + loggingConfig.Filesystem.LogFile = config.Filesystem.ResultLogFile + loggingConfig.Webhook.URL = config.Webhook.ResultURL + loggingConfig.Firehose.StreamName = config.Firehose.ResultStream + loggingConfig.Kinesis.StreamName = config.Kinesis.ResultStream + loggingConfig.Lambda.Function = config.Lambda.ResultFunction + loggingConfig.PubSub.Topic = config.PubSub.ResultTopic + loggingConfig.PubSub.AddAttributes = config.PubSub.AddAttributes + loggingConfig.KafkaREST.Topic = config.KafkaREST.ResultTopic + loggingConfig.Nats.Subject = config.Nats.ResultSubject + + osquerydResultLogger, err := logging.NewJSONLogger(cmd.Context(), "result", loggingConfig, logger) + if err != nil { + initFatal(err, "initializing osqueryd result logging") + } + + var auditLogger fleet.JSONLogger + if license.IsPremium() && config.Activity.EnableAuditLog { + // Set specific configuration to audit logs. + loggingConfig.Plugin = config.Activity.AuditLogPlugin + loggingConfig.Filesystem.LogFile = config.Filesystem.AuditLogFile + loggingConfig.Firehose.StreamName = config.Firehose.AuditStream + loggingConfig.Kinesis.StreamName = config.Kinesis.AuditStream + loggingConfig.Lambda.Function = config.Lambda.AuditFunction + loggingConfig.PubSub.Topic = config.PubSub.AuditTopic + loggingConfig.PubSub.AddAttributes = false // only used by result logs + loggingConfig.KafkaREST.Topic = config.KafkaREST.AuditTopic + loggingConfig.Nats.Subject = config.Nats.AuditSubject + + auditLogger, err = logging.NewJSONLogger(cmd.Context(), "audit", loggingConfig, logger) + if err != nil { + initFatal(err, "initializing audit logging") + } + } + + failingPolicySet := redis_policy_set.NewFailing(redisPool) + + task := async.NewTask(ds, redisPool, clock.C, &config) + + if config.Sentry.Dsn != "" { + v := version.Version() + err = sentry.Init(sentry.ClientOptions{ + Dsn: config.Sentry.Dsn, + Release: fmt.Sprintf("%s_%s_%s", v.Version, v.Branch, v.Revision), + }) + if err != nil { + initFatal(err, "initializing sentry") + } + logger.InfoContext(cmd.Context(), "sentry initialized", "dsn", config.Sentry.Dsn) + + defer sentry.Recover() + defer sentry.Flush(2 * time.Second) + } + + var geoIP fleet.GeoIP + geoIP = &fleet.NoOpGeoIP{} + if config.GeoIP.DatabasePath != "" { + maxmind, err := fleet.NewMaxMindGeoIP(logger, config.GeoIP.DatabasePath) + if err != nil { + logger.ErrorContext(cmd.Context(), "failed to initialize maxmind geoip, check database path", "database_path", + config.GeoIP.DatabasePath, "error", err) + } else { + geoIP = maxmind + } + } + + if config.MDM.EnableCustomOSUpdatesAndFileVault && !license.IsPremium() { + config.MDM.EnableCustomOSUpdatesAndFileVault = false + logger.WarnContext(cmd.Context(), "Disabling custom OS updates and FileVault management because Fleet Premium license is not present") + } + + mdmStorage, err := mds.NewMDMAppleMDMStorage() + if err != nil { + initFatal(err, "initialize mdm apple MySQL storage") + } + + depStorage, err := mds.NewMDMAppleDEPStorage() + if err != nil { + initFatal(err, "initialize Apple BM DEP storage") + } + + scepStorage, err := mds.NewSCEPDepot() + if err != nil { + initFatal(err, "initialize mdm apple scep storage") + } + + var mdmPushService push.Pusher + nanoMDMLogger := service.NewNanoMDMLogger(logger.With("component", "apple-mdm-push")) + pushProviderFactory := buford.NewPushProviderFactory(buford.WithNewClient(func(cert *tls.Certificate) (*http.Client, error) { + return fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{ + Certificates: []tls.Certificate{*cert}, + })), nil + })) + if dev_mode.Env("FLEET_DEV_MDM_APPLE_DISABLE_PUSH") == "1" { + mdmPushService = nopPusher{} + } else { + mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger) + } + mds.WithPusher(mdmPushService) + + checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) { + _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names, nil) + if err != nil { + if fleet.IsNotFound(err) || errors.Is(err, mysql.ErrPartialResult) { + return false, nil + } + return false, err + } + return true, nil + } + + // reconcile Apple Business Manager configuration environment variables with the database + if config.MDM.IsAppleAPNsSet() || config.MDM.IsAppleSCEPSet() { + if len(config.Server.PrivateKey) == 0 { + initFatal(errors.New("inserting MDM APNs and SCEP assets"), + "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + // first we'll check if the APNs and SCEP assets are already in the database and + // only insert config values if they're not already present in the database + toInsert := make(map[fleet.MDMAssetName]struct{}, 4) + + // check DB for APNs assets + found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetAPNSCert, fleet.MDMAssetAPNSKey}) + switch { + case err != nil: + initFatal(err, "reading APNs assets from database") + case !found: + toInsert[fleet.MDMAssetAPNSCert] = struct{}{} + toInsert[fleet.MDMAssetAPNSKey] = struct{}{} + default: + logger.WarnContext(cmd.Context(), + "Your server already has stored APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.") + } + + // check DB for SCEP assets + found, err = checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) + switch { + case err != nil: + initFatal(err, "reading SCEP assets from database") + case !found: + toInsert[fleet.MDMAssetCACert] = struct{}{} + toInsert[fleet.MDMAssetCAKey] = struct{}{} + default: + logger.WarnContext(cmd.Context(), + "Your server already has stored SCEP certificates. Fleet will ignore any certificates provided via environment variables when this happens.") + } + + if len(toInsert) > 0 { + if !config.MDM.IsAppleAPNsSet() { + initFatal(errors.New("Apple APNs MDM configuration must be provided when Apple SCEP is provided"), + "validate Apple MDM") + } else if !config.MDM.IsAppleSCEPSet() { + initFatal(errors.New("Apple SCEP MDM configuration must be provided when Apple APNs is provided"), + "validate Apple MDM") + } + + // parse the APNs and SCEP assets from the config + _, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs() + if err != nil { + initFatal(err, "parse Apple APNs certificate and key from config") + } + _, appleSCEPCertPEM, appleSCEPKeyPEM, err := config.MDM.AppleSCEP() + if err != nil { + initFatal(err, "load Apple SCEP certificate and key from config") + } + + var args []fleet.MDMConfigAsset + for name := range toInsert { + switch name { + case fleet.MDMAssetAPNSCert: + args = append(args, fleet.MDMConfigAsset{Name: name, Value: apnsCertPEM}) + case fleet.MDMAssetAPNSKey: + args = append(args, fleet.MDMConfigAsset{Name: name, Value: apnsKeyPEM}) + case fleet.MDMAssetCACert: + args = append(args, fleet.MDMConfigAsset{Name: name, Value: appleSCEPCertPEM}) + case fleet.MDMAssetCAKey: + args = append(args, fleet.MDMConfigAsset{Name: name, Value: appleSCEPKeyPEM}) + } + } + + if err := ds.InsertMDMConfigAssets(context.Background(), args, nil); err != nil { + if mysql.IsDuplicate(err) { + // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case + logger.DebugContext(cmd.Context(), "unexpected duplicate key error inserting MDM APNs and SCEP assets") + } else { + initFatal(err, "inserting MDM APNs and SCEP assets") + } + } + } + } + + // reconcile Apple Business Manager configuration environment variables with the database + if config.MDM.IsAppleBMSet() { + if len(config.Server.PrivateKey) == 0 { + initFatal(errors.New("inserting MDM ABM assets"), + "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + appleBM, err := config.MDM.AppleBM() + if err != nil { + initFatal(err, "parse Apple BM token, certificate and key from config") + } + + toInsert := make([]fleet.MDMConfigAsset, 0, 2) + + found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetABMKey, fleet.MDMAssetABMCert}) + switch { + case err != nil: + initFatal(err, "reading ABM assets from database") + case !found: + toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM}, + fleet.MDMConfigAsset{Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM}) + default: + logger.WarnContext(cmd.Context(), + "Your server already has stored ABM certificates and token. Fleet will ignore any certificates provided via environment variables when this happens.") + } + + if len(toInsert) > 0 { + err := ds.InsertMDMConfigAssets(context.Background(), toInsert, nil) + switch { + case err != nil && mysql.IsDuplicate(err): + // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case + logger.DebugContext(cmd.Context(), "unexpected duplicate key error inserting ABM assets") + case err != nil: + initFatal(err, "inserting ABM assets") + default: + // insert the ABM token without any metdata; it'll be picked by the + // apple_mdm_dep_profile_assigner cron and backfilled + if _, err := ds.InsertABMToken(context.Background(), &fleet.ABMToken{ + EncryptedToken: appleBM.EncryptedToken, + RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, + time.UTC), // 2000-01-01 is our "zero value" for time + }); err != nil { + initFatal(err, "save ABM token") + } + } + } + } + + appCfg, err := ds.AppConfig(context.Background()) + if err != nil { + initFatal(err, "loading app config") + } + + appCfg.MDM.EnabledAndConfigured = false + appCfg.MDM.AppleBMEnabledAndConfigured = false + if len(config.Server.PrivateKey) > 0 { + appCfg.MDM.EnabledAndConfigured, err = checkMDMAssets([]fleet.MDMAssetName{ + fleet.MDMAssetCACert, + fleet.MDMAssetCAKey, + fleet.MDMAssetAPNSKey, + fleet.MDMAssetAPNSCert, + }) + if err != nil { + initFatal(err, "loading MDM assets from database") + } + + var appleBMCerts bool + appleBMCerts, err = checkMDMAssets([]fleet.MDMAssetName{ + fleet.MDMAssetABMCert, + fleet.MDMAssetABMKey, + }) + if err != nil { + initFatal(err, "loading MDM ABM assets from database") + } + if appleBMCerts { + // the ABM certs are there, check if a token exists and if so, apple + // BM is enabled and configured. + count, err := ds.GetABMTokenCount(context.Background()) + if err != nil { + initFatal(err, "loading MDM ABM token from database") + } + appCfg.MDM.AppleBMEnabledAndConfigured = count > 0 + } + } + if appCfg.MDM.EnabledAndConfigured { + logger.InfoContext(cmd.Context(), "Apple MDM enabled") + } + if appCfg.MDM.AppleBMEnabledAndConfigured { + logger.InfoContext(cmd.Context(), "Apple Business Manager enabled") + } + + // register the Microsoft MDM services + var ( + wstepCertManager microsoft_mdm.CertManager + ) + + // Configuring WSTEP certs + if config.MDM.IsMicrosoftWSTEPSet() { + _, crtPEM, keyPEM, err := config.MDM.MicrosoftWSTEP() + if err != nil { + initFatal(err, "validate Microsoft WSTEP certificate and key") + } + wstepCertManager, err = microsoft_mdm.NewCertManager(ds, crtPEM, keyPEM) + if err != nil { + initFatal(err, "initialize mdm microsoft wstep depot") + } + } + + // save the app config with the updated MDM.Enabled value + if err := ds.SaveAppConfig(context.Background(), appCfg); err != nil { + initFatal(err, "saving app config") + } + + // setup mail service + if appCfg.SMTPSettings != nil && appCfg.SMTPSettings.SMTPEnabled { + // if SMTP is already enabled then default the backend to empty string, which fill force load the SMTP implementation + if config.Email.EmailBackend != "" { + config.Email.EmailBackend = "" + logger.WarnContext(cmd.Context(), "SMTP is already enabled, first disable SMTP to utilize a different email backend") + } + } + mailService, err := mail.NewService(config) + if err != nil { + logger.ErrorContext(cmd.Context(), "failed to configure mailing service", "err", err) + } + + cronSchedules := fleet.NewCronSchedules() + + baseCtx := licensectx.NewContext(context.Background(), license) + ctx, cancelFunc := context.WithCancel(baseCtx) + defer cancelFunc() + + // Channel used to trigger graceful shutdown on fatal DB errors (e.g. Aurora failover). + dbFatalCh := make(chan error, 1) + common_mysql.SetFatalErrorHandler(func(ctx context.Context, err error) { + logger.ErrorContext(ctx, "fatal database error detected, initiating graceful shutdown", "err", err) + select { + case dbFatalCh <- err: + default: + } + }) + + var conditionalAccessMicrosoftProxy *conditional_access_microsoft_proxy.Proxy + if config.MicrosoftCompliancePartner.IsSet() { + var err error + conditionalAccessMicrosoftProxy, err = conditional_access_microsoft_proxy.New( + config.MicrosoftCompliancePartner.ProxyURI, + config.MicrosoftCompliancePartner.ProxyAPIKey, + func() (string, error) { + appCfg, err := ds.AppConfig(ctx) + if err != nil { + return "", fmt.Errorf("failed to load appconfig: %w", err) + } + return appCfg.ServerSettings.ServerURL, nil + }, + ) + if err != nil { + initFatal(err, "new microsoft compliance proxy") + } + } + + eh := errorstore.NewHandler(ctx, redisPool, logger, config.Logging.ErrorRetentionPeriod) + scepConfigMgr := scep.NewSCEPConfigService(logger, nil) + digiCertService := digicert.NewService(digicert.WithLogger(logger)) + ctx = ctxerr.NewContext(ctx, eh) + + // Declare svc early so the closure below can capture it. + var svc fleet.Service + config.MDM.AndroidAgent.Validate(initFatal) + androidSvc, err := android_service.NewService( + ctx, + logger, + ds, + config.License.Key, + config.Server.PrivateKey, + ds, + func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + return svc.NewActivity(ctx, user, activity) + }, + config.MDM.AndroidAgent, + ) + if err != nil { + initFatal(err, "initializing android service") + } + + svc, err = service.NewService( + ctx, + ds, + task, + resultStore, + logger, + &service.OsqueryLogger{ + Status: osquerydStatusLogger, + Result: osquerydResultLogger, + }, + config, + mailService, + clock.C, + ssoSessionStore, + liveQueryStore, + carveStore, + failingPolicySet, + geoIP, + redisWrapperDS, + depStorage, + mdmStorage, + mdmPushService, + cronSchedules, + wstepCertManager, + scepConfigMgr, + digiCertService, + conditionalAccessMicrosoftProxy, + redis_key_value.New(redisPool), + androidSvc, + ) + if err != nil { + initFatal(err, "initializing service") + } + + var softwareInstallStore fleet.SoftwareInstallerStore + var bootstrapPackageStore fleet.MDMBootstrapPackageStore + var softwareTitleIconStore fleet.SoftwareTitleIconStore + var distributedLock fleet.Lock + if license.IsPremium() { + hydrantService := est.NewService(est.WithLogger(logger)) + profileMatcher := apple_mdm.NewProfileMatcher(redisPool) + if config.S3.SoftwareInstallersBucket != "" { + if config.S3.BucketsAndPrefixesMatch() { + logger.WarnContext(ctx, + "the S3 buckets and prefixes for carves and software installers appear to be identical, this can cause issues") + } + // Extract the CloudFront URL signer before creating the S3 stores. + config.S3.ValidateCloudFrontURL(initFatal) + if config.S3.SoftwareInstallersCloudFrontURLSigningPrivateKey != "" { + // Strip newlines from private key + signingPrivateKey := strings.ReplaceAll(config.S3.SoftwareInstallersCloudFrontURLSigningPrivateKey, "\\n", "\n") + privateKey, err := cryptoutil.ParsePrivateKey([]byte(signingPrivateKey), + "CloudFront URL signing private key") + if err != nil { + initFatal(err, "parsing CloudFront URL signing private key") + } + var ok bool + config.S3.SoftwareInstallersCloudFrontSigner, ok = privateKey.(crypto.Signer) + if !ok { + initFatal(errors.New("CloudFront URL signing private key is not a crypto.Signer"), + "parsing CloudFront URL signing private key") + } + } + store, err := s3.NewSoftwareInstallerStore(config.S3) + if err != nil { + initFatal(err, "initializing S3 software installer store") + } + softwareInstallStore = store + logger.InfoContext(ctx, "using S3 software installer store", "bucket", config.S3.SoftwareInstallersBucket) + + bstore, err := s3.NewBootstrapPackageStore(config.S3) + if err != nil { + initFatal(err, "initializing S3 bootstrap package store") + } + bootstrapPackageStore = bstore + logger.InfoContext(ctx, "using S3 bootstrap package store", "bucket", config.S3.SoftwareInstallersBucket) + + softwareTitleIconStore, err = s3.NewSoftwareTitleIconStore(config.S3) + if err != nil { + initFatal(err, "initializing S3 software title icon store") + } + logger.InfoContext(ctx, "using S3 software title icon store", "bucket", config.S3.SoftwareInstallersBucket) + } else { + installerDir := os.TempDir() + if dir := os.Getenv("FLEET_SOFTWARE_INSTALLER_STORE_DIR"); dir != "" { + installerDir = dir + } + store, err := filesystem.NewSoftwareInstallerStore(installerDir) + if err != nil { + logger.ErrorContext(ctx, "failed to configure local filesystem software installer store", "err", err) + softwareInstallStore = failing.NewFailingSoftwareInstallerStore() + } else { + softwareInstallStore = store + logger.InfoContext(ctx, + "using local filesystem software installer store, this is not suitable for production use", "directory", + installerDir) + } + + iconDir := os.TempDir() + if dir := os.Getenv("FLEET_SOFTWARE_TITLE_ICON_STORE_DIR"); dir != "" { + iconDir = dir + } + iconStore, err := filesystem.NewSoftwareTitleIconStore(iconDir) + if err != nil { + logger.ErrorContext(ctx, "failed to configure local filesystem software title icon store", "err", err) + softwareTitleIconStore = failing.NewFailingSoftwareTitleIconStore() + } else { + softwareTitleIconStore = iconStore + logger.WarnContext(ctx, + "using local filesystem software title icon store, this is not suitable for production use", "directory", + iconDir) + } + } + + distributedLock = redis_lock.NewLock(redisPool) + svc, err = eeservice.NewService( + svc, + ds, + logger, + config, + mailService, + clock.C, + depStorage, + apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), + ssoSessionStore, + profileMatcher, + softwareInstallStore, + bootstrapPackageStore, + softwareTitleIconStore, + distributedLock, + redis_key_value.New(redisPool), + scepConfigMgr, + digiCertService, + androidSvc, + hydrantService, + ) + if err != nil { + initFatal(err, "initial Fleet Premium service") + } + } + + instanceID, err := server.GenerateRandomText(64) + if err != nil { + initFatal(errors.New("Error generating random instance identifier"), "") + } + logger.InfoContext(ctx, "instance info", "instanceID", instanceID) + + // Bootstrap activity bounded context (needed for cron schedules and HTTP routes) + activitySvc, activityRoutes := createActivityBoundedContext(svc, dbConns, logger) + // Inject the activity bounded context into the main service + svc.SetActivityService(activitySvc) + + // Perform a cleanup of cron_stats outside of the cronSchedules because the + // schedule package uses cron_stats entries to decide whether a schedule will + // run or not (see https://github.com/fleetdm/fleet/issues/9486). + go func() { + cleanupCronStats := func() { + logger.DebugContext(ctx, "cleaning up cron_stats") + // Datastore.CleanupCronStats should be safe to run by multiple fleet + // instances at the same time and it should not be an expensive operation. + if err := ds.CleanupCronStats(ctx); err != nil { + logger.InfoContext(ctx, "failed to clean up cron_stats", "err", err) + } + } + + cleanupCronStats() + + cleanUpCronStatsTick := time.NewTicker(1 * time.Hour) + defer cleanUpCronStatsTick.Stop() + for { + select { + case <-ctx.Done(): + return + case <-cleanUpCronStatsTick.C: + cleanupCronStats() + } + } + }() + + if softwareInstallStore != nil { + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + return cronUninstallSoftwareMigration(ctx, instanceID, ds, softwareInstallStore, logger) + }, + ); err != nil { + initFatal(err, fmt.Sprintf("failed to register %s", fleet.CronUninstallSoftwareMigration)) + } + + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + return cronUpgradeCodeSoftwareMigration(ctx, instanceID, ds, softwareInstallStore, logger) + }, + ); err != nil { + initFatal(err, fmt.Sprintf("failed to register %s", fleet.CronUpgradeCodeSoftwareMigration)) + } + } + + if config.Server.FrequentCleanupsEnabled { + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + return newFrequentCleanupsSchedule(ctx, instanceID, ds, liveQueryStore, logger) + }, + ); err != nil { + initFatal(err, "failed to register frequent_cleanups schedule") + } + } + + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + return newCleanupsAndAggregationSchedule( + ctx, instanceID, ds, svc, logger, redisWrapperDS, &config, commander, softwareInstallStore, bootstrapPackageStore, softwareTitleIconStore, androidSvc, activitySvc, + ) + }, + ); err != nil { + initFatal(err, "failed to register cleanups_then_aggregations schedule") + } + + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + return newQueryResultsCleanupSchedule(ctx, instanceID, ds, liveQueryStore, logger) + }, + ); err != nil { + initFatal(err, "failed to register query_results_cleanup schedule") + } + + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + return newUpcomingActivitiesSchedule(ctx, instanceID, ds, logger) + }, + ); err != nil { + initFatal(err, "failed to register upcoming_activities_maintenance schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newUsageStatisticsSchedule(ctx, instanceID, ds, config, logger) + }); err != nil { + initFatal(err, "failed to register stats schedule") + } + + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + return newBatchActivitiesSchedule(ctx, instanceID, ds, logger) + }); err != nil { + initFatal(err, "failed to register batch activities schedule") + } + + vulnerabilityScheduleDisabled := false + if config.Vulnerabilities.DisableSchedule { + vulnerabilityScheduleDisabled = true + logger.InfoContext(ctx, "vulnerabilities schedule disabled via vulnerabilities.disable_schedule") + } + if config.Vulnerabilities.CurrentInstanceChecks == "no" || config.Vulnerabilities.CurrentInstanceChecks == "0" { + logger.InfoContext(ctx, "vulnerabilities schedule disabled via vulnerabilities.current_instance_checks") + vulnerabilityScheduleDisabled = true + } + if !vulnerabilityScheduleDisabled { + // vuln processing by default is run by internal cron mechanism + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newVulnerabilitiesSchedule(ctx, instanceID, ds, logger, &config.Vulnerabilities) + }); err != nil { + initFatal(err, "failed to register vulnerabilities schedule") + } + } else { + // Register a remote trigger proxy so triggering still works + // when the vulnerability schedule runs on a separate server. + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return schedule.NewRemoteTriggerSchedule(string(fleet.CronVulnerabilities), ds), nil + }); err != nil { + initFatal(err, "failed to register remote vulnerability trigger") + } + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newAutomationsSchedule(ctx, instanceID, ds, logger, 5*time.Minute, failingPolicySet) + }); err != nil { + initFatal(err, "failed to register automations schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander, androidSvc) + }); err != nil { + initFatal(err, "failed to register worker integrations schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + vppInstaller := svc.(fleet.AppleMDMVPPInstaller) + return newAppleMDMWorkerSchedule(ctx, instanceID, ds, logger, commander, bootstrapPackageStore, vppInstaller) + }); err != nil { + initFatal(err, "failed to register apple_mdm_worker schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newAppleMDMDEPProfileAssigner(ctx, instanceID, config.MDM.AppleDEPSyncPeriodicity, ds, depStorage, logger) + }); err != nil { + initFatal(err, "failed to register apple_mdm_dep_profile_assigner schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newMDMAppleServiceDiscoverySchedule(ctx, instanceID, ds, depStorage, logger, config.Server.URLPrefix) + }); err != nil { + initFatal(err, "failed to register mdm_apple_service_discovery schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newAppleMDMProfileManagerSchedule( + ctx, + instanceID, + ds, + apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), + logger, + ) + }); err != nil { + initFatal(err, "failed to register mdm_apple_profile_manager schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newWindowsMDMProfileManagerSchedule( + ctx, + instanceID, + ds, + logger, + ) + }); err != nil { + initFatal(err, "failed to register mdm_windows_profile_manager schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newAndroidMDMProfileManagerSchedule( + ctx, + instanceID, + ds, + logger, + config.License.Key, // NOTE: this requires the license key, not the parsed *LicenseInfo available in the ctx + config.MDM.AndroidAgent, + ) + }); err != nil { + initFatal(err, "failed to register mdm_android_profile_manager schedule") + } + + // Register Android MDM Device Reconciler schedule (same interval as Android profile manager) + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newAndroidMDMDeviceReconcilerSchedule( + ctx, + instanceID, + ds, + logger, + config.License.Key, + svc.NewActivity, + ) + }); err != nil { + initFatal(err, "failed to register mdm_android_device_reconciler schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return cronEnableAndroidAppReportsOnDefaultPolicy(ctx, instanceID, ds, logger, androidSvc) + }); err != nil { + initFatal(err, "failed to register enable_android_app_reports_on_default_policy cron") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return cronMigrateToPerHostPolicy(ctx, instanceID, ds, logger, androidSvc) + }); err != nil { + initFatal(err, "failed to register migrate_to_per_host_policy cron") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newMDMAPNsPusher( + ctx, + instanceID, + ds, + apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), + logger, + ) + }); err != nil { + initFatal(err, "failed to register APNs pusher schedule") + } + + if license.IsPremium() { + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + return newIPhoneIPadRefetcher(ctx, instanceID, 10*time.Minute, ds, commander, logger, svc.NewActivity) + }); err != nil { + initFatal(err, "failed to register apple_mdm_iphone_ipad_refetcher schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + return newIPhoneIPadReviver(ctx, instanceID, ds, commander, logger) + }); err != nil { + initFatal(err, "failed to register apple_mdm_iphone_ipad_reviver schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newMaintainedAppSchedule(ctx, instanceID, ds, logger) + }); err != nil { + initFatal(err, "failed to register maintained apps schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newRefreshVPPAppVersionsSchedule(ctx, instanceID, ds, logger, apple_apps.Configure(ctx, ds, config.License.Key, config.MDM.AppleConnectJWT)) + }); err != nil { + initFatal(err, "failed to register refresh vpp app versions schedule") + } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + return newRecoveryLockPasswordSchedule(ctx, instanceID, ds, commander, logger) + }); err != nil { + initFatal(err, "failed to register recovery lock password schedule") + } + } + + if license.IsPremium() && config.Activity.EnableAuditLog { + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newActivitiesStreamingSchedule(ctx, instanceID, activitySvc, ds, logger, auditLogger) + }); err != nil { + initFatal(err, "failed to register activities streaming schedule") + } + } + + if license.IsPremium() { + if err := cronSchedules.StartCronSchedule( + func() (fleet.CronSchedule, error) { + if config.Calendar.Periodicity > 0 { + config.Calendar.SetAlwaysReloadEvent(true) + } else { + config.Calendar.Periodicity = 5 * time.Minute + } + return cron.NewCalendarSchedule(ctx, instanceID, ds, distributedLock, config.Calendar, logger) + }, + ); err != nil { + initFatal(err, "failed to register calendar schedule") + } + } + + // Start the service that calculates and updates host vitals label membership. + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newHostVitalsLabelMembershipSchedule(ctx, instanceID, ds, logger) + }); err != nil { + initFatal(err, "failed to register host vitals label membership schedule") + } + + // Start the service that marks activities as completed. + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newBatchActivityCompletionCheckerSchedule(ctx, instanceID, ds, logger) + }); err != nil { + initFatal(err, "failed to register batch activity completion checker schedule") + } + + logger.InfoContext(ctx, fmt.Sprintf("started cron schedules: %s", strings.Join(cronSchedules.ScheduleNames(), ", "))) + + // StartCollectors starts a goroutine per collector, using ctx to cancel. + task.StartCollectors(ctx, logger.With("cron", "async_task")) + + // Flush seen hosts every second + hostsAsyncCfg := config.Osquery.AsyncConfigForTask(configpkg.AsyncTaskHostLastSeen) + if !hostsAsyncCfg.Enabled { + go func() { + for range time.Tick(time.Duration(rand.Intn(10)+1) * time.Second) { + if err := task.FlushHostsLastSeen(baseCtx, clock.C.Now()); err != nil { + logger.InfoContext(ctx, "failed to update host seen times", "err", err) + } + } + }() + } + + fieldKeys := []string{"method", "error"} + requestCount := kitprometheus.NewCounterFrom(prometheus.CounterOpts{ + Namespace: "api", + Subsystem: "service", + Name: "request_count", + Help: "Number of requests received.", + }, fieldKeys) + requestLatency := kitprometheus.NewSummaryFrom(prometheus.SummaryOpts{ + Namespace: "api", + Subsystem: "service", + Name: "request_latency_microseconds", + Help: "Total duration of requests in microseconds.", + }, fieldKeys) + + svc = service.NewMetricsService(svc, requestCount, requestLatency) + + httpLogger := logger.With("component", "http") + + limiterStore := &redis.ThrottledStore{ + Pool: redisPool, + KeyPrefix: "ratelimit::", + } + + var httpSigVerifier func(http.Handler) http.Handler + if license.IsPremium() { + httpSigVerifier, err = httpsig.Middleware(ds, config.Auth.RequireHTTPMessageSignature, logger.With("component", "http-sig-verifier")) + if err != nil { + initFatal(err, "initializing HTTP signature verifier") + } + } + + // This is off by default for testing and development uses only. + cspEV := os.Getenv("FLEET_SERVER_ENABLE_CSP") + serveCSP := cspEV == "1" || cspEV == "true" + + var apiHandler, frontendHandler, endUserEnrollOTAHandler http.Handler + { + frontendHandler = service.PrometheusMetricsHandler( + "get_frontend", + service.ServeFrontend(config.Server.URLPrefix, config.Server.SandboxEnabled, httpLogger, serveCSP), + ) + + frontendHandler = service.WithMDMEnrollmentMiddleware(svc, httpLogger, frontendHandler) + + var extra []service.ExtraHandlerOption + if config.MDM.SSORateLimitPerMinute > 0 { + extra = append(extra, service.WithMdmSsoRateLimit(throttled.PerMin(config.MDM.SSORateLimitPerMinute))) + } + extra = append(extra, service.WithHTTPSigVerifier(httpSigVerifier)) + + apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore, redisPool, carveStore, + []endpointer.HandlerRoutesFunc{android_service.GetRoutes(svc, androidSvc), activityRoutes}, extra...) + + if serveCSP { + // Only injecting this if CSP is turned on since the default security headers add some overhead to each request + apiHandler = endpointer.BrowserSecurityHeadersHandler(serveCSP, apiHandler) + } + + setupRequired, err := svc.SetupRequired(baseCtx) + if err != nil { + initFatal(err, "fetching setup requirement") + } + // WithSetup will check if first time setup is required + // By performing the same check inside main, we can make server startups + // more efficient after the first startup. + if setupRequired { + apiHandler = service.WithSetup(svc, logger, apiHandler) + frontendHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler, config.Server.URLPrefix) + } else { + frontendHandler = service.RedirectSetupToLogin(svc, logger, frontendHandler, config.Server.URLPrefix) + } + + endUserEnrollOTAHandler = service.ServeEndUserEnrollOTA( + svc, + config.Server.URLPrefix, + ds, + logger, + serveCSP, + ) + } + + healthCheckers := make(map[string]health.Checker) + { + // a list of dependencies which could affect the status of the app if unavailable. + deps := map[string]any{ + "mysql": ds, + "redis": resultStore, + } + + // convert all dependencies to health.Checker if they implement the healthz methods. + for name, dep := range deps { + if hc, ok := dep.(health.Checker); ok { + healthCheckers[name] = hc + } else { + initFatal(errors.New(name+" should be a health.Checker"), "initializing health checks") + } + } + + } + + // Instantiate a gRPC service to handle launcher requests. + launcher := launcher.New(svc, logger, grpc.NewServer( + grpc.ChainUnaryInterceptor( + grpc_recovery.UnaryServerInterceptor(), + ), + grpc.ChainStreamInterceptor( + grpc_recovery.StreamServerInterceptor(), + ), + ), healthCheckers) + + rootMux := http.NewServeMux() + rootMux.Handle("/healthz", service.PrometheusMetricsHandler("healthz", otelmw.WrapHandler(health.Handler(httpLogger, healthCheckers), "/healthz", config))) + rootMux.Handle("/version", service.PrometheusMetricsHandler("version", otelmw.WrapHandler(version.Handler(), "/version", config))) + rootMux.Handle("/assets/", service.PrometheusMetricsHandler("static_assets", otelmw.WrapHandlerDynamic(service.ServeStaticAssets("/assets/", serveCSP), config))) + + if len(config.Server.PrivateKey) > 0 { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + ddmService := service.NewMDMAppleDDMService(ds, logger) + vppInstaller := svc.(fleet.AppleMDMVPPInstaller) + mdmCheckinAndCommandService := service.NewMDMAppleCheckinAndCommandService( + ds, + commander, + vppInstaller, + license.IsPremium(), + logger, + redis_key_value.New(redisPool), + svc.NewActivity, + ) + + mdmCheckinAndCommandService.RegisterResultsHandler("InstalledApplicationList", service.NewInstalledApplicationListResultsHandler(ds, commander, logger, config.Server.VPPVerifyTimeout, config.Server.VPPVerifyRequestDelay, svc.NewActivity)) + mdmCheckinAndCommandService.RegisterResultsHandler(fleet.DeviceLocationCmdName, service.NewDeviceLocationResultsHandler(ds, commander, logger)) + mdmCheckinAndCommandService.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, service.NewSetRecoveryLockResultsHandler(ds, logger, svc.NewActivity)) + + hasSCEPChallenge, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge}) + if err != nil { + initFatal(err, "checking SCEP challenge in database") + } + if !hasSCEPChallenge { + scepChallenge := config.MDM.AppleSCEPChallenge + if scepChallenge == "" { + scepChallenge = uuid.NewString() + } + + err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ + {Name: fleet.MDMAssetSCEPChallenge, Value: []byte(scepChallenge)}, + }, nil) + if err != nil { + // duplicate key errors mean that we already + // have a value for those keys in the + // database, fail to initalize on other + // cases. + if !mysql.IsDuplicate(err) { + initFatal(err, "inserting SCEP challenge") + } + + logger.WarnContext(ctx, + "Your server already has stored a SCEP challenge. Fleet will ignore this value provided via environment variables when this happens.") + } + } + if err := service.RegisterAppleMDMProtocolServices( + rootMux, + config.MDM, + mdmStorage, + scepStorage, + logger, + mdmCheckinAndCommandService, + ddmService, + commander, + appCfg.ServerSettings.ServerURL, + config, + ); err != nil { + initFatal(err, "setup mdm apple services") + } + } + + if license.IsPremium() { + // SCEP proxy (for NDES, etc.) + if err = service.RegisterSCEPProxy(rootMux, ds, logger, nil, &config); err != nil { + initFatal(err, "setup SCEP proxy") + } + if err = scim.RegisterSCIM(rootMux, ds, svc, logger, &config); err != nil { + initFatal(err, "setup SCIM") + } + // Host identify and conditional access SCEP feature only works if a private key has been set up + if len(config.Server.PrivateKey) > 0 { + hostIdentitySCEPDepot, err := mds.NewHostIdentitySCEPDepot(logger.With("component", "host-id-scep-depot"), &config) + if err != nil { + initFatal(err, "setup host identity SCEP depot") + } + if err = hostidentity.RegisterSCEP(rootMux, hostIdentitySCEPDepot, ds, logger, &config); err != nil { + initFatal(err, "setup host identity SCEP") + } + + // Conditional Access SCEP + condAccessSCEPDepot, err := mds.NewConditionalAccessSCEPDepot(logger.With("component", "conditional-access-scep-depot"), &config) + if err != nil { + initFatal(err, "setup conditional access SCEP depot") + } + if err = condaccess.RegisterSCEP(ctx, rootMux, condAccessSCEPDepot, ds, logger, &config); err != nil { + initFatal(err, "setup conditional access SCEP") + } + + // Conditional Access IdP (Okta) + if err = condaccess.RegisterIdP(rootMux, ds, logger, &config, limiterStore); err != nil { + initFatal(err, "setup conditional access IdP") + } + } else { + logger.WarnContext(ctx, + "Host identity and conditional access SCEP is not available because no server private key has been set up.") + } + } + + if config.Prometheus.BasicAuth.Username != "" && config.Prometheus.BasicAuth.Password != "" { + rootMux.Handle("/metrics", basicAuthHandler( + config.Prometheus.BasicAuth.Username, + config.Prometheus.BasicAuth.Password, + service.PrometheusMetricsHandler("metrics", otelmw.WrapHandler(promhttp.Handler(), "/metrics", config)), + )) + } else { + if config.Prometheus.BasicAuth.Disable { + logger.InfoContext(ctx, "metrics endpoint enabled with http basic auth disabled") + rootMux.Handle("/metrics", service.PrometheusMetricsHandler("metrics", otelmw.WrapHandler(promhttp.Handler(), "/metrics", config))) + } else { + logger.InfoContext(ctx, "metrics endpoint disabled (http basic auth credentials not set)") + } + } + + // We must wrap the Handler here to set special per-endpoint Read/Write + // timeouts, so that we have access to the raw http.ResponseWriter. + // Otherwise, the handler is wrapped by the promhttp response delegator, + // which does not support the Unwrap call needed to work with + // ResponseController. + // + // See https://pkg.go.dev/net/http#NewResponseController which explains + // the Unwrap method that the prometheus wrapper of http.ResponseWriter + // does not implement. + rootMux.HandleFunc("/api/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/scripts/run/sync") { + // when running a script synchronously, we wait a while for a script + // execution result, so the write timeout (to write the response) + // must be extended. + rc := http.NewResponseController(rw) + // add an additional 30 seconds to prevent race conditions where the + // request is terminated early. + if err := rc.SetWriteDeadline(time.Now().Add(scripts.MaxServerWaitTime + (30 * time.Second))); err != nil { + logger.ErrorContext(req.Context(), + "http middleware failed to override endpoint write timeout for script sync run", + "response_writer_type", fmt.Sprintf("%T", rw), + "response_writer", fmt.Sprintf("%+v", rw), + "err", err, + ) + } + } + + if (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/software/package")) || + (req.Method == http.MethodPatch && strings.HasSuffix(req.URL.Path, "/package") && strings.Contains(req.URL.Path, + "/fleet/software/titles/")) || + (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/bootstrap")) || + (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet_maintained_apps")) || + (req.Method == http.MethodGet && strings.Contains(req.URL.Path, "/package/token")) || + (req.Method == http.MethodPost && strings.Contains(req.URL.Path, "orbit/software_install/package")) { + var zeroTime time.Time + rc := http.NewResponseController(rw) + // For large software installers and bootstrap packages, the server time needs time to read the full + // request body so we use the zero value to remove the deadline and override the + // default read timeout. + // TODO: Is this really how we want to handle this? Or would an arbitrarily long + // timeout be better? + if err := rc.SetReadDeadline(zeroTime); err != nil { + logger.ErrorContext(req.Context(), + "http middleware failed to override endpoint read timeout for software package upload", + "response_writer_type", fmt.Sprintf("%T", rw), + "response_writer", fmt.Sprintf("%+v", rw), + "err", err, + ) + } + // For large software installers, the server time needs time to store the + // installer to S3 (or the configured storage location) and write the response + // body so we use the zero value to remove the deadline and override the + // default write timeout. + // TODO: Is this really how we want to handle this? Or would an arbitrarily long + // timeout be better? + if err := rc.SetWriteDeadline(zeroTime); err != nil { + logger.ErrorContext(req.Context(), + "http middleware failed to override endpoint write timeout for software package upload", + "response_writer_type", fmt.Sprintf("%T", rw), + "response_writer", fmt.Sprintf("%+v", rw), + "err", err, + ) + } + + // We need to add the context value here because we need the installer max size when doing request + // parsing, which happens somewhere where we're only passed the request (and not the service object) + req.Body = http.MaxBytesReader(rw, req.Body, config.Server.MaxInstallerSizeBytes) + req = req.WithContext(installersize.NewContext(req.Context(), config.Server.MaxInstallerSizeBytes)) + } + + if req.Method == http.MethodGet && strings.HasSuffix(req.URL.Path, "/fleet/android_enterprise/signup_sse") { + // When enabling Android MDM, frontend UI will wait for the admin to finish the setup in Google. + rc := http.NewResponseController(rw) + if err := rc.SetWriteDeadline(time.Now().Add(30 * time.Minute)); err != nil { + logger.ErrorContext(req.Context(), + "http middleware failed to override endpoint write timeout for android enterpriset setup", + "response_writer_type", fmt.Sprintf("%T", rw), + "response_writer", fmt.Sprintf("%+v", rw), + "err", err, + ) + } + } + + if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/mdm/profiles/batch") || + (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/configuration_profiles/batch")) { + // For customers using large profiles and/or large numbers of profiles, the + // server needs time to completely read the request body and also to process + // all the side effects of a potentially large number of profiles being changed + // across a large number of hosts, so set the timeouts a bit higher than default + rc := http.NewResponseController(rw) + if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Minute)); err != nil { + logger.ErrorContext(req.Context(), + "http middleware failed to override endpoint write timeout for MDM profiles batch endpoint", + "response_writer_type", fmt.Sprintf("%T", rw), + "response_writer", fmt.Sprintf("%+v", rw), + "err", err, + ) + } + if err := rc.SetReadDeadline(time.Now().Add(5 * time.Minute)); err != nil { + logger.ErrorContext(req.Context(), + "http middleware failed to override endpoint read timeout for MDM profiles batch endpoint", + "response_writer_type", fmt.Sprintf("%T", rw), + "response_writer", fmt.Sprintf("%+v", rw), + "err", err, + ) + } + } + + apiHandler.ServeHTTP(rw, req) + }) + // The `/api/{version}/fleet/scim` base path is used by SCIM handler. In order to route the `details` route to the apiHandler, + // we have to explicitly handle that path at the root. The Go router takes precedence for a more specific path. The v1/latest are used in the path for it to be more specific. + // The Fleet API was designed this way for end-user simplicity. + rootMux.Handle("/api/v1/fleet/scim/details", apiHandler) + rootMux.Handle("/api/latest/fleet/scim/details", apiHandler) + + rootMux.Handle("/enroll", otelmw.WrapHandler(endUserEnrollOTAHandler, "/enroll", config)) + rootMux.Handle("/", otelmw.WrapHandler(frontendHandler, "/", config)) + + debugHandler := &debugMux{ + fleetAuthenticatedHandler: service.MakeDebugHandler(svc, config, logger, eh, ds), + } + rootMux.Handle("/debug/", otelmw.WrapHandlerDynamic(debugHandler, config)) + + if debug { + // Add debug endpoints with a random + // authorization token + debugToken, err := server.GenerateRandomText(24) + if err != nil { + initFatal(err, "generating debug token") + } + debugHandler.tokenAuthenticatedHandler = http.StripPrefix("/debug/", netbug.AuthHandler(debugToken)) + fmt.Printf("*** Debug mode enabled ***\nAccess the debug endpoints at /debug/?token=%s\n", url.QueryEscape(debugToken)) + } + + if len(config.Server.URLPrefix) > 0 { + prefixMux := http.NewServeMux() + prefixMux.Handle(config.Server.URLPrefix+"/", http.StripPrefix(config.Server.URLPrefix, rootMux)) + rootMux = prefixMux + } + + // NOTE(lucas): It seems we missed updating this value from 90s (see #1798) to 25s after we + // decided to make the synchronous live query API to take up to 25 seconds. + // Not changing this to not break any long running requests (like when uploading software + // packages via GitOps). + liveQueryRestPeriod := 90 * time.Second + if v := os.Getenv("FLEET_LIVE_QUERY_REST_PERIOD"); v != "" { + duration, err := time.ParseDuration(v) + if err != nil { + logger.ErrorContext(ctx, "failed to parse live query rest period", "err", err) + } else { + liveQueryRestPeriod = duration + } + } + + // The "GET /api/latest/fleet/queries/run" API requires + // WriteTimeout to be higher than the live query rest period + // (otherwise the response is not sent back to the client). + // + // We add 10s to the live query rest period to allow the writing + // of the response. + liveQueryRestPeriod += 10 * time.Second + + // Create the handler based on whether tracing should be there + var handler http.Handler + if config.Logging.TracingEnabled && config.Logging.TracingType == "elasticapm" { + handler = launcher.Handler(apmhttp.Wrap(rootMux)) + } else { + handler = launcher.Handler(rootMux) + } + + srv := config.Server.DefaultHTTPServer(ctx, handler) + if liveQueryRestPeriod > srv.WriteTimeout { + srv.WriteTimeout = liveQueryRestPeriod + } + srv.SetKeepAlivesEnabled(config.Server.Keepalive) + errs := make(chan error, 2) + go func() { + if !config.Server.TLS { + logger.InfoContext(ctx, "listening", "transport", "http", "address", config.Server.Address) + errs <- srv.ListenAndServe() + } else { + logger.InfoContext(ctx, "listening", "transport", "https", "address", config.Server.Address) + srv.TLSConfig = getTLSConfig(config.Server.TLSProfile) + errs <- srv.ListenAndServeTLS( + config.Server.Cert, + config.Server.Key, + ) + } + }() + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + select { + case <-sig: + case <-dbFatalCh: + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + errs <- func() error { + cancelFunc() + cleanupCronStatsOnShutdown(ctx, ds, logger, instanceID) + launcher.GracefulStop() + // Flush any pending OTEL data before shutting down + if tracerProvider != nil { + if err := tracerProvider.Shutdown(ctx); err != nil { + logger.ErrorContext(ctx, "failed to shutdown OTEL tracer provider", "err", err) + } + } + if meterProvider != nil { + if err := meterProvider.Shutdown(ctx); err != nil { + logger.ErrorContext(ctx, "failed to shutdown OTEL meter provider", "err", err) + } + } + if loggerProvider != nil { + if err := loggerProvider.Shutdown(ctx); err != nil { + logger.ErrorContext(ctx, "failed to shutdown OTEL logger provider", "err", err) + } + } + return srv.Shutdown(ctx) + }() + }() + + // block on errs signal + logger.InfoContext(ctx, "terminated", "err", <-errs) +} + func createActivityBoundedContext(svc fleet.Service, dbConns *common_mysql.DBConnections, logger *slog.Logger) (activity_api.Service, endpointer.HandlerRoutesFunc) { legacyAuthorizer, err := authz.NewAuthorizer() if err != nil { diff --git a/orbit/changes/refactor-named-functions-nil-checks b/orbit/changes/refactor-named-functions-nil-checks new file mode 100644 index 0000000000..aefb66cea3 --- /dev/null +++ b/orbit/changes/refactor-named-functions-nil-checks @@ -0,0 +1 @@ +* Refactored large anonymous function into a named function to improve nil-safety static analysis coverage. diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index b7dfde4825..a05b6f7449 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -267,660 +267,701 @@ func main() { } return nil } - app.Action = func(c *cli.Context) error { - if c.Bool("version") { - fmt.Println("orbit " + build.Version) - return nil - } - startTime := time.Now() + // orbitAction is a named function so that NilAway can analyze it for nil-safety. + app.Action = orbitAction - 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, - )) + 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") + } +} + +// orbitAction is a named function so that NilAway can analyze it for nil-safety. +func orbitAction(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) + zerolog.SetGlobalLevel(zerolog.InfoLevel) - if c.Bool("debug") { - zerolog.SetGlobalLevel(zerolog.DebugLevel) - } + 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) - } + // 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("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 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) - } + if odb := c.String("osquery-db"); odb != "" && !filepath.IsAbs(odb) { + return fmt.Errorf("the osquery database must be an absolute path: %q", odb) + } - readEnrollSecretFromFile := func(enrollSecretPath string) error { - // 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 { //nolint:gocritic // ignore ifElseChain - 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 { //nolint:gocritic // ignore ifElseChain - 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 { //nolint:gocritic // ignore ifElseChain - 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) - } - } - } - return nil - } - 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") - } - if err := readEnrollSecretFromFile(enrollSecretPath); err != nil { - return err - } - } - tryReadEnrollSecretFromKeystore := func() error { - if c.String("enroll-secret") == "" && keystore.Supported() && !c.Bool("disable-keystore") { - 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) - } - } - return nil - } - if !(runtime.GOOS == "darwin" && c.Bool("use-system-configuration")) { - if err := tryReadEnrollSecretFromKeystore(); err != nil { - return 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 err := writeFleetURL(config.FleetURL, c.String("root-dir")); err != nil { - return fmt.Errorf("write fleet URL: %w", err) - } - } - - if c.String("fleet-url") != "" && c.String("enroll-secret") != "" { - log.Info().Msg("found configuration values in system profile") - break - } - - // If we didn't find the configuration values, try to read them from the stored files. - // First, get Fleet URL - b, err := os.ReadFile(path.Join(c.String("root-dir"), constant.FleetURLFileName)) - if err != nil { - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("read fleet URL file: %w", err) - } - } else { - fleetURL := strings.TrimSpace(string(b)) - if err = c.Set("fleet-url", fleetURL); err != nil { - return fmt.Errorf("set fleet URL from file: %w", err) - } - } - // Now, get enroll secret - if err := readEnrollSecretFromFile(path.Join(c.String("root-dir"), constant.OsqueryEnrollSecretFileName)); err != nil { - return err - } - // Since the normal enroll secret flow supports keychain, we can use it here as well. - // The story to remove the enroll secret from macOS MDM profile is: https://github.com/fleetdm/fleet/issues/16118 - if err := tryReadEnrollSecretFromKeystore(); err != nil { - // Log the error but don't return it, as we want to keep trying to read the configuration - // from the system profile. - log.Error().Err(err).Msg("failed to read enroll secret from keystore") - } - if c.String("fleet-url") != "" && c.String("enroll-secret") != "" { - log.Info().Msg("found configuration values in local files") - break - } - - log.Info().Msg("didn't find configuration values in system profile, trying again in 30 seconds") - time.Sleep(30 * time.Second) - } - } - - if updateURL := c.String("update-url"); updateURL != update.OldFleetTUFURL && updateURL != update.DefaultURL { - // Migrate agents running with a custom TUF to use the new metadata file. - // We'll keep the old metadata file to support downgrades. - newMetadataFilePath := filepath.Join(c.String("root-dir"), update.MetadataFileName) - ok, err := file.Exists(newMetadataFilePath) - if err != nil { - // If we cannot stat this file then we cannot do other operations on it thus we fail with fatal error. - log.Fatal().Err(err).Msg("failed to check for new metadata file path") - } - if !ok { - oldMetadataFilePath := filepath.Join(c.String("root-dir"), update.OldMetadataFileName) - err := file.Copy(oldMetadataFilePath, newMetadataFilePath, constant.DefaultFileMode) - if err != nil { - // If we cannot write to this file then we cannot do other operations on it thus we fail with fatal error. - log.Fatal().Err(err).Msg("failed to copy new metadata file path") - } - } - } - - localStore, err := filestore.New(filepath.Join(c.String("root-dir"), update.MetadataFileName)) + readEnrollSecretFromFile := func(enrollSecretPath string) error { + // 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 { - log.Fatal().Err(err).Msg("create local metadata store") - } - - opt := update.DefaultOptions - - if c.Bool("fleet-desktop") { - switch runtime.GOOS { - case "darwin": - opt.Targets[constant.DesktopTUFTargetName] = update.DesktopMacOSTarget - case "windows": - if runtime.GOARCH == "arm64" { - opt.Targets[constant.DesktopTUFTargetName] = update.DesktopWindowsArm64Target - } else { - opt.Targets[constant.DesktopTUFTargetName] = update.DesktopWindowsTarget - } - case "linux": - if runtime.GOARCH == "arm64" { - opt.Targets[constant.DesktopTUFTargetName] = update.DesktopLinuxArm64Target - } else { - opt.Targets[constant.DesktopTUFTargetName] = 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(constant.DesktopTUFTargetName, c.String("desktop-channel")) - } - - // Override default channels with the provided values. - opt.Targets.SetTargetChannel(constant.OrbitTUFTargetName, c.String("orbit-channel")) - opt.Targets.SetTargetChannel(constant.OsqueryTUFTargetName, c.String("osqueryd-channel")) - - opt.RootDirectory = c.String("root-dir") - opt.ServerURL = c.String("update-url") - if opt.ServerURL == update.OldFleetTUFURL { - // - // This only gets executed on orbit 1.38.0+ - // when it is configured to connect to the old TUF server - // (fleetd instances packaged before the migration, - // built by fleetctl previous to v4.63.0). - // - opt.ServerURL = update.DefaultURL - } - 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 - var osqueryVersion string - 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") - } - - signaturesExpiredAtStartup := updater.SignaturesExpired() - if signaturesExpiredAtStartup { - log.Info().Err(err).Msg("detected signatures expired at startup") - } - - targets := []string{constant.OrbitTUFTargetName, constant.OsqueryTUFTargetName} - - if c.Bool("fleet-desktop") { - targets = append(targets, constant.DesktopTUFTargetName) - } - 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, - SignaturesExpiredAtStartup: signaturesExpiredAtStartup, - }) - if err != nil { - return err - } - - // Get current version of osquery - log.Info().Msgf("orbit version: %s", build.Version) - osquerydPath, err = updater.ExecutableLocalPath(constant.OsqueryTUFTargetName) - 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 - 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) + if !errors.Is(err, os.ErrNotExist) || !keystore.Supported() || c.Bool("disable-keystore") { + return fmt.Errorf("read enroll secret file: %w", err) } } else { - log.Info().Msg("running with auto updates disabled") - updater = update.NewDisabled(opt) - osquerydPath, err = updater.ExecutableLocalPath(constant.OsqueryTUFTargetName) - if err != nil { - log.Fatal().Err(err).Msgf("locate %s", constant.OsqueryTUFTargetName) + 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 v, err := update.GetVersion(osquerydPath); err == nil && v != "" { - log.Info().Msgf("Found osquery version: %s", v) - osqueryVersion = v - } - if c.Bool("fleet-desktop") { - if runtime.GOOS == "darwin" { - desktopPath, err = updater.DirLocalPath(constant.DesktopTUFTargetName) - if err != nil { - return fmt.Errorf("get %s target: %w", constant.DesktopTUFTargetName, err) + if keystore.Supported() && !c.Bool("disable-keystore") { + // Check if secret is already in the keystore. + secretFromKeystore, err := keystore.GetSecret() + if err != nil { //nolint:gocritic // ignore ifElseChain + 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 { //nolint:gocritic // ignore ifElseChain + 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 { //nolint:gocritic // ignore ifElseChain + 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 { - desktopPath, err = updater.ExecutableLocalPath(constant.DesktopTUFTargetName) - if err != nil { - return fmt.Errorf("get %s target: %w", constant.DesktopTUFTargetName, err) - } + // Keystore secret found, and it matches the secret from the file. + deleteSecretPathIfExists(enrollSecretPath) } } } + return nil + } + 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") + } + if err := readEnrollSecretFromFile(enrollSecretPath); err != nil { + return err + } + } + tryReadEnrollSecretFromKeystore := func() error { + if c.String("enroll-secret") == "" && keystore.Supported() && !c.Bool("disable-keystore") { + 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) + } + } + return nil + } + if !(runtime.GOOS == "darwin" && c.Bool("use-system-configuration")) { + if err := tryReadEnrollSecretFromKeystore(); err != nil { + return 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 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 err := writeFleetURL(config.FleetURL, c.String("root-dir")); err != nil { + return fmt.Errorf("write fleet URL: %w", err) + } } - if err := os.RemoveAll(path); err != nil { - log.Info().Err(err).Msg("remove .old") - return nil + if c.String("fleet-url") != "" && c.String("enroll-secret") != "" { + log.Info().Msg("found configuration values in system profile") + break } - 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, - PlatformLike: osqueryHostInfo.PlatformLike, - ComputerName: osqueryHostInfo.ComputerName, - HardwareModel: osqueryHostInfo.HardwareModel, - } - - 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 and - // https://github.com/fleetdm/fleet/issues/31934 for more details. - - tmpDBPath := filepath.Join(os.TempDir(), strings.Join([]string{uuid.NewString(), "tmp-db"}, "-")) - oi, err := getHostInfo(osquerydPath, tmpDBPath) + // If we didn't find the configuration values, try to read them from the stored files. + // First, get Fleet URL + b, err := os.ReadFile(path.Join(c.String("root-dir"), constant.FleetURLFileName)) 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") - } - - // Read the stored hardware UUID from file (if it exists) - hardwareUUIDFile := filepath.Join(c.String("root-dir"), constant.HardwareUUIDFileName) - var storedUUID string - - fileContent, err := os.ReadFile(hardwareUUIDFile) - if err != nil { - if !os.IsNotExist(err) { - // If there's an error other than file not existing, log it - log.Warn().Err(err).Msg("failed to read hardware UUID file") + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("read fleet URL file: %w", err) } - // If file doesn't exist or can't be read, create it with the current UUID - if err := os.WriteFile(hardwareUUIDFile, []byte(oi.HardwareUUID), constant.DefaultFileMode); err != nil { - log.Error().Err(err).Msg("failed to write hardware UUID file") - } else { - log.Debug().Str("uuid", oi.HardwareUUID).Msg("created hardware UUID file") - } - storedUUID = oi.HardwareUUID } else { - storedUUID = strings.TrimSpace(string(fileContent)) + fleetURL := strings.TrimSpace(string(b)) + if err = c.Set("fleet-url", fleetURL); err != nil { + return fmt.Errorf("set fleet URL from file: %w", err) + } + } + // Now, get enroll secret + if err := readEnrollSecretFromFile(path.Join(c.String("root-dir"), constant.OsqueryEnrollSecretFileName)); err != nil { + return err + } + // Since the normal enroll secret flow supports keychain, we can use it here as well. + // The story to remove the enroll secret from macOS MDM profile is: https://github.com/fleetdm/fleet/issues/16118 + if err := tryReadEnrollSecretFromKeystore(); err != nil { + // Log the error but don't return it, as we want to keep trying to read the configuration + // from the system profile. + log.Error().Err(err).Msg("failed to read enroll secret from keystore") + } + if c.String("fleet-url") != "" && c.String("enroll-secret") != "" { + log.Info().Msg("found configuration values in local files") + break } - if !strings.EqualFold(oi.HardwareUUID, storedUUID) { - // Then we have moved to a new physical machine, so we should restart! - log.Info().Str("stored_uuid", storedUUID).Str("current_uuid", oi.HardwareUUID).Msg("detected hardware migration") + log.Info().Msg("didn't find configuration values in system profile, trying again in 30 seconds") + time.Sleep(30 * time.Second) + } + } - // Remove the hardware UUID file so it gets recreated with the new UUID on restart - if err := os.RemoveAll(hardwareUUIDFile); err != nil { - return fmt.Errorf("removing old hardware UUID file: %w", err) - } + if updateURL := c.String("update-url"); updateURL != update.OldFleetTUFURL && updateURL != update.DefaultURL { + // Migrate agents running with a custom TUF to use the new metadata file. + // We'll keep the old metadata file to support downgrades. + newMetadataFilePath := filepath.Join(c.String("root-dir"), update.MetadataFileName) + ok, err := file.Exists(newMetadataFilePath) + if err != nil { + // If we cannot stat this file then we cannot do other operations on it thus we fail with fatal error. + log.Fatal().Err(err).Msg("failed to check for new metadata file path") + } + if !ok { + oldMetadataFilePath := filepath.Join(c.String("root-dir"), update.OldMetadataFileName) + err := file.Copy(oldMetadataFilePath, newMetadataFilePath, constant.DefaultFileMode) + if err != nil { + // If we cannot write to this file then we cannot do other operations on it thus we fail with fatal error. + log.Fatal().Err(err).Msg("failed to copy new metadata file path") + } + } + } - // 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) - } + localStore, err := filestore.New(filepath.Join(c.String("root-dir"), update.MetadataFileName)) + if err != nil { + log.Fatal().Err(err).Msg("create local metadata store") + } - // We can remove these because we want them 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) - } - if err := os.RemoveAll(filepath.Join(c.String("root-dir"), constant.DesktopTokenFileName)); err != nil { - return fmt.Errorf("removing old Fleet Desktop identifier file: %w", err) - } + opt := update.DefaultOptions - return errors.New("found a new hardware uuid, restarting") + if c.Bool("fleet-desktop") { + switch runtime.GOOS { + case "darwin": + opt.Targets[constant.DesktopTUFTargetName] = update.DesktopMacOSTarget + case "windows": + if runtime.GOARCH == "arm64" { + opt.Targets[constant.DesktopTUFTargetName] = update.DesktopWindowsArm64Target + } else { + opt.Targets[constant.DesktopTUFTargetName] = update.DesktopWindowsTarget + } + case "linux": + if runtime.GOARCH == "arm64" { + opt.Targets[constant.DesktopTUFTargetName] = update.DesktopLinuxArm64Target + } else { + opt.Targets[constant.DesktopTUFTargetName] = 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(constant.DesktopTUFTargetName, c.String("desktop-channel")) + } + + // Override default channels with the provided values. + opt.Targets.SetTargetChannel(constant.OrbitTUFTargetName, c.String("orbit-channel")) + opt.Targets.SetTargetChannel(constant.OsqueryTUFTargetName, c.String("osqueryd-channel")) + + opt.RootDirectory = c.String("root-dir") + opt.ServerURL = c.String("update-url") + if opt.ServerURL == update.OldFleetTUFURL { + // + // This only gets executed on orbit 1.38.0+ + // when it is configured to connect to the old TUF server + // (fleetd instances packaged before the migration, + // built by fleetctl previous to v4.63.0). + // + opt.ServerURL = update.DefaultURL + } + 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 + var osqueryVersion string + 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") + } + + signaturesExpiredAtStartup := updater.SignaturesExpired() + if signaturesExpiredAtStartup { + log.Info().Err(err).Msg("detected signatures expired at startup") + } + + targets := []string{constant.OrbitTUFTargetName, constant.OsqueryTUFTargetName} + + if c.Bool("fleet-desktop") { + targets = append(targets, constant.DesktopTUFTargetName) + } + 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, + SignaturesExpiredAtStartup: signaturesExpiredAtStartup, + }) + if err != nil { + return err + } + + // Get current version of osquery + log.Info().Msgf("orbit version: %s", build.Version) + osquerydPath, err = updater.ExecutableLocalPath(constant.OsqueryTUFTargetName) + 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 + osqueryVersion = version } } - // 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 + // 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 } - 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 + 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), ) - 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)) + 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) } - - fleetURL := c.String("fleet-url") - if !strings.HasPrefix(fleetURL, "http") { - fleetURL = "https://" + fleetURL + } else { + log.Info().Msg("running with auto updates disabled") + updater = update.NewDisabled(opt) + osquerydPath, err = updater.ExecutableLocalPath(constant.OsqueryTUFTargetName) + if err != nil { + log.Fatal().Err(err).Msgf("locate %s", constant.OsqueryTUFTargetName) } - - 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}), - ) + if v, err := update.GetVersion(osquerydPath); err == nil && v != "" { + log.Info().Msgf("Found osquery version: %s", v) + osqueryVersion = v } - - if runtime.GOOS == "windows" { - if systemDrive, ok := os.LookupEnv("SystemDrive"); ok { - options = append(options, osquery.WithEnv([]string{fmt.Sprintf("SystemDrive=%s", systemDrive)})) + if c.Bool("fleet-desktop") { + if runtime.GOOS == "darwin" { + desktopPath, err = updater.DirLocalPath(constant.DesktopTUFTargetName) + if err != nil { + return fmt.Errorf("get %s target: %w", constant.DesktopTUFTargetName, err) + } + } else { + desktopPath, err = updater.ExecutableLocalPath(constant.DesktopTUFTargetName) + if err != nil { + return fmt.Errorf("get %s target: %w", constant.DesktopTUFTargetName, err) + } } } + } - var certPath string + // 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 + } - // Both options --fleet-managed-host-identity-certificate and --insecure make use of a local HTTPS proxy. - // If the user sets both --fleet-managed-host-identity-certificate and --insecure then only the proxy - // for the fleet managed client certificate will be executed. - if fleetURL != "https://" && c.Bool("insecure") && !c.Bool("fleet-managed-host-identity-certificate") { - proxy, err := insecure.NewTLSProxy(fleetURL) - if err != nil { - return fmt.Errorf("create TLS proxy: %w", err) + 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, + PlatformLike: osqueryHostInfo.PlatformLike, + ComputerName: osqueryHostInfo.ComputerName, + HardwareModel: osqueryHostInfo.HardwareModel, + } + + 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 and + // https://github.com/fleetdm/fleet/issues/31934 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") + } + + // Read the stored hardware UUID from file (if it exists) + hardwareUUIDFile := filepath.Join(c.String("root-dir"), constant.HardwareUUIDFileName) + var storedUUID string + + fileContent, err := os.ReadFile(hardwareUUIDFile) + if err != nil { + if !os.IsNotExist(err) { + // If there's an error other than file not existing, log it + log.Warn().Err(err).Msg("failed to read hardware UUID file") + } + // If file doesn't exist or can't be read, create it with the current UUID + if err := os.WriteFile(hardwareUUIDFile, []byte(oi.HardwareUUID), constant.DefaultFileMode); err != nil { + log.Error().Err(err).Msg("failed to write hardware UUID file") + } else { + log.Debug().Str("uuid", oi.HardwareUUID).Msg("created hardware UUID file") + } + storedUUID = oi.HardwareUUID + } else { + storedUUID = strings.TrimSpace(string(fileContent)) + } + + if !strings.EqualFold(oi.HardwareUUID, storedUUID) { + // Then we have moved to a new physical machine, so we should restart! + log.Info().Str("stored_uuid", storedUUID).Str("current_uuid", oi.HardwareUUID).Msg("detected hardware migration") + + // Remove the hardware UUID file so it gets recreated with the new UUID on restart + if err := os.RemoveAll(hardwareUUIDFile); err != nil { + return fmt.Errorf("removing old hardware UUID file: %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) + // 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) } - certPath = filepath.Join(proxyDirectory, "fleet.crt") - - // Write cert that proxy uses - err = os.WriteFile(certPath, []byte(insecure.ServerCert), os.FileMode(0o644)) - if err != nil { - return fmt.Errorf("write server cert: %w", err) + // We can remove these because we want them 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) + } + if err := os.RemoveAll(filepath.Join(c.String("root-dir"), constant.DesktopTokenFileName)); err != nil { + return fmt.Errorf("removing old Fleet Desktop identifier file: %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), - } + 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 + } + + 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}), + ) + } + + if runtime.GOOS == "windows" { + if systemDrive, ok := os.LookupEnv("SystemDrive"); ok { + options = append(options, osquery.WithEnv([]string{fmt.Sprintf("SystemDrive=%s", systemDrive)})) + } + } + + var certPath string + + // Both options --fleet-managed-host-identity-certificate and --insecure make use of a local HTTPS proxy. + // If the user sets both --fleet-managed-host-identity-certificate and --insecure then only the proxy + // for the fleet managed client certificate will be executed. + if fleetURL != "https://" && c.Bool("insecure") && !c.Bool("fleet-managed-host-identity-certificate") { + 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.FileMode(0o644)) + 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(osqueryVersion, 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(osqueryVersion, 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 { @@ -931,644 +972,607 @@ func main() { } options = append(options, - osquery.WithFlags(osquery.FleetFlags(osqueryVersion, 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(osqueryVersion, parsedURL)), - ) - - if certPath = c.String("fleet-certificate"); certPath != "" { - // Check and log if there are any errors with TLS connection. - pool, err := certificate.LoadPEM(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 certificate: %w", err) + return fmt.Errorf("load certs.pem: %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}), - ) + 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.") - } + 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) + 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) + } + + if c.Bool("fleet-managed-host-identity-certificate") { + if runtime.GOOS != "linux" { + return errors.New("fleet-managed-host-identity-certificate is only supported on Linux") } - - if c.Bool("fleet-managed-host-identity-certificate") { - if runtime.GOOS != "linux" { - return errors.New("fleet-managed-host-identity-certificate is only supported on Linux") - } - if fleetClientCrt != nil { - return errors.New("fleet-managed-host-identity-certificate for HTTP signing, and TLS client certificates may not be specified together") - } - } - - 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, - })) + return errors.New("fleet-managed-host-identity-certificate for HTTP signing, and TLS client certificates may not be specified together") } + } - var ( - signerWrapper func(*http.Client) *http.Client - hostIdentityCertificatePath string - orbitClient *service.OrbitClient - ) - if c.Bool("fleet-managed-host-identity-certificate") { - commonName := osqueryHostInfo.HardwareUUID - if c.String("host-identifier") == "instance" { - commonName = osqueryHostInfo.InstanceID - } - hostIdentityCredentials, err := hostidentity.Setup( - c.Context, - c.String("root-dir"), - fleetURL+"/api/fleet/orbit/host_identity/scep", - c.String("enroll-secret"), - commonName, - c.String("fleet-certificate"), - c.Bool("insecure"), - log.Logger, - func(reason string) { - if orbitClient != nil { - orbitClient.TriggerOrbitRestart(reason) - } - }, - ) - if err != nil { - if c.Bool("fleet-desktop") { - // Generic error for when the TPM-backed certificate could not be generated. - // (e.g. invalid enroll secret, server down). - errorMessage := "🔒🚫 Missing Fleet certificate.\nPlease contact your IT admin." - if errors.As(err, &securehw.ErrSecureHWUnavailable{}) { - errorMessage = "🔒🚫 TPM 2.0 device unavailable.\nPlease contact your IT admin." - } - if err := executeFleetDesktopWithPermanentError(desktopPath, errorMessage); err != nil { - log.Error().Err(err).Msg("failed to launch Fleet Desktop with permanent error") - } - } - return fmt.Errorf("failed to create or load client certificate: %w", err) - } - defer hostIdentityCredentials.Close() + 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, + })) + } - log.Info().Str( - "commonName", hostIdentityCredentials.Certificate.Subject.CommonName, - ).Msg("certificate issued successfully") - - cryptoSigner, err := hostIdentityCredentials.SecureHWKey.HTTPSigner() - if err != nil { - return fmt.Errorf("error getting secure HW backed signer: %w", err) - } - - // Get serial number as hex string - certSN := strings.ToUpper(hostIdentityCredentials.Certificate.SerialNumber.Text(16)) - - // Get ECC algorithm for signing. - var signingAlgorithm httpsig.Algorithm - switch v := cryptoSigner.ECCAlgorithm(); v { - case securehw.ECCAlgorithmP256: - signingAlgorithm = httpsig.Algo_ECDSA_P256_SHA256 - case securehw.ECCAlgorithmP384: - signingAlgorithm = httpsig.Algo_ECDSA_P384_SHA384 - default: - return fmt.Errorf("invalid ECC algorithm: %v", v) - } - - httpSigner, err := fleethttpsig.Signer(certSN, cryptoSigner, signingAlgorithm) - if err != nil { - return fmt.Errorf("failed to create HTTP signer: %w", err) - } - - proxyDirectory := filepath.Join(c.String("root-dir"), "proxy") - proxy, err := httpsigproxy.NewProxy(proxyDirectory, fleetURL, c.String("fleet-certificate"), c.Bool("insecure"), httpSigner) - if err != nil { - return fmt.Errorf("create TLS proxy: %w", err) - } - - addSubsystem(&g, "httpsig localhost proxy", &wrapSubsystem{ - execute: func() error { - log.Info(). - Str("addr", proxy.ParsedURL.String()). - Str("target", fleetURL). - Msg("httpsig localhost proxy") - return proxy.Serve() - }, - interrupt: func(_ error) { - if err := proxy.Close(); err != nil { - log.Error().Err(err).Msg("close httpsig proxy") - } - }, - }) - - signerWrapper = func(client *http.Client) *http.Client { - return httpsig.NewHTTPClient(client, httpSigner, nil) - } - hostIdentityCertificatePath = hostIdentityCredentials.CertificatePath - - options = append(options, - osquery.WithFlags(osquery.FleetFlags(osqueryVersion, proxy.ParsedURL)), - - // This is overriding the previous set of --tls_server_certs in osquery.FleetFlags above. - osquery.WithFlags([]string{"--tls_server_certs", proxy.CertificatePath}), - ) + var ( + signerWrapper func(*http.Client) *http.Client + hostIdentityCertificatePath string + orbitClient *service.OrbitClient + ) + if c.Bool("fleet-managed-host-identity-certificate") { + commonName := osqueryHostInfo.HardwareUUID + if c.String("host-identifier") == "instance" { + commonName = osqueryHostInfo.InstanceID } - - orbitClient, err = service.NewOrbitClient( + hostIdentityCredentials, err := hostidentity.Setup( + c.Context, c.String("root-dir"), - fleetURL, + fleetURL+"/api/fleet/orbit/host_identity/scep", + c.String("enroll-secret"), + commonName, 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") - }, + log.Logger, + func(reason string) { + if orbitClient != nil { + orbitClient.TriggerOrbitRestart(reason) + } }, - signerWrapper, - hostIdentityCertificatePath, ) if err != nil { - return fmt.Errorf("error new orbit client: %w", err) + if c.Bool("fleet-desktop") { + // Generic error for when the TPM-backed certificate could not be generated. + // (e.g. invalid enroll secret, server down). + errorMessage := "🔒🚫 Missing Fleet certificate.\nPlease contact your IT admin." + if errors.As(err, &securehw.ErrSecureHWUnavailable{}) { + errorMessage = "🔒🚫 TPM 2.0 device unavailable.\nPlease contact your IT admin." + } + if err := executeFleetDesktopWithPermanentError(desktopPath, errorMessage); err != nil { + log.Error().Err(err).Msg("failed to launch Fleet Desktop with permanent error") + } + } + return fmt.Errorf("failed to create or load client certificate: %w", err) + } + defer hostIdentityCredentials.Close() + + log.Info().Str( + "commonName", hostIdentityCredentials.Certificate.Subject.CommonName, + ).Msg("certificate issued successfully") + + cryptoSigner, err := hostIdentityCredentials.SecureHWKey.HTTPSigner() + if err != nil { + return fmt.Errorf("error getting secure HW backed signer: %w", err) } - // Set the function that will be called to open the SSO window if an enroll - // request returns an "end user authentication required" error. - orbitClient.SetOpenSSOWindowFunc(func() error { - err = openBrowserWindow(fleetURL + "/mdm/sso?initiator=setup_experience&host_uuid=" + orbitHostInfo.HardwareUUID) - if err != nil { - return fmt.Errorf("opening browser: %w", err) - } - return nil + // Get serial number as hex string + certSN := strings.ToUpper(hostIdentityCredentials.Certificate.SerialNumber.Text(16)) + + // Get ECC algorithm for signing. + var signingAlgorithm httpsig.Algorithm + switch v := cryptoSigner.ECCAlgorithm(); v { + case securehw.ECCAlgorithmP256: + signingAlgorithm = httpsig.Algo_ECDSA_P256_SHA256 + case securehw.ECCAlgorithmP384: + signingAlgorithm = httpsig.Algo_ECDSA_P384_SHA384 + default: + return fmt.Errorf("invalid ECC algorithm: %v", v) + } + + httpSigner, err := fleethttpsig.Signer(certSN, cryptoSigner, signingAlgorithm) + if err != nil { + return fmt.Errorf("failed to create HTTP signer: %w", err) + } + + proxyDirectory := filepath.Join(c.String("root-dir"), "proxy") + proxy, err := httpsigproxy.NewProxy(proxyDirectory, fleetURL, c.String("fleet-certificate"), c.Bool("insecure"), httpSigner) + if err != nil { + return fmt.Errorf("create TLS proxy: %w", err) + } + + addSubsystem(&g, "httpsig localhost proxy", &wrapSubsystem{ + execute: func() error { + log.Info(). + Str("addr", proxy.ParsedURL.String()). + Str("target", fleetURL). + Msg("httpsig localhost proxy") + return proxy.Serve() + }, + interrupt: func(_ error) { + if err := proxy.Close(); err != nil { + log.Error().Err(err).Msg("close httpsig proxy") + } + }, }) - // 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 - - // 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, c.String("root-dir"), - ) - orbitClient.RegisterConfigReceiver(scriptConfigReceiver) - - var trw *token.ReadWriter - var deviceClient *service.DeviceClient - // 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) + signerWrapper = func(client *http.Client) *http.Client { + return httpsig.NewHTTPClient(client, httpSigner, nil) } + hostIdentityCertificatePath = hostIdentityCredentials.CertificatePath - // Create a new token read/writer that will store the token on disk. - // This token will be used to identify this desktop to the Fleet server. - trw = token.NewReadWriter(filepath.Join(c.String("root-dir"), constant.DesktopTokenFileName), deviceClient.CheckToken) - if err := trw.LoadOrGenerate(); err != nil { - return fmt.Errorf("initializing token read writer: %w", err) - } + options = append(options, + osquery.WithFlags(osquery.FleetFlags(osqueryVersion, proxy.ParsedURL)), - // we enable remote updates only if the server supports them by setting - // this function. - trw.SetRemoteUpdateFunc( - func(token string) error { - return orbitClient.SetOrUpdateDeviceToken(token) + // This is overriding the previous set of --tls_server_certs in osquery.FleetFlags above. + osquery.WithFlags([]string{"--tls_server_certs", proxy.CertificatePath}), + ) + } + + 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") + }, + }, + signerWrapper, + hostIdentityCertificatePath, + ) + if err != nil { + return fmt.Errorf("error new orbit client: %w", err) + } + + // Set the function that will be called to open the SSO window if an enroll + // request returns an "end user authentication required" error. + orbitClient.SetOpenSSOWindowFunc(func() error { + err = openBrowserWindow(fleetURL + "/mdm/sso?initiator=setup_experience&host_uuid=" + orbitHostInfo.HardwareUUID) + if err != nil { + return fmt.Errorf("opening browser: %w", err) + } + return nil + }) + + // 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 + + // 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, c.String("root-dir"), + ) + orbitClient.RegisterConfigReceiver(scriptConfigReceiver) + + var trw *token.ReadWriter + var deviceClient *service.DeviceClient + // 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) + } + + // Create a new token read/writer that will store the token on disk. + // This token will be used to identify this desktop to the Fleet server. + trw = token.NewReadWriter(filepath.Join(c.String("root-dir"), constant.DesktopTokenFileName), deviceClient.CheckToken) + if err := trw.LoadOrGenerate(); err != nil { + return fmt.Errorf("initializing token read writer: %w", err) + } + + // we enable remote updates only if the server supports them by setting + // this function. + trw.SetRemoteUpdateFunc( + func(token string) error { + return orbitClient.SetOrUpdateDeviceToken(token) + }, + ) + + // Check if the token is not expired and still good. + // If not, rotate the token iff the server is reachable. + if serverIsReachable { + expired, _ := trw.HasExpired() + if expired || deviceClient.CheckToken(trw.GetCached()) != nil { + if err := trw.Rotate(); err != nil { + return fmt.Errorf("rotating token: %w", err) + } + } + } + + if c.Bool("fleet-desktop") { + // Ensure that the token rotation checker is started, + // so that we have a valid token to launch the + // My Device page. + stopRotation := trw.StartRotation() + defer stopRotation() + } + + 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, + })) + setupExperiencer := setupexperience.NewSetupExperiencer(orbitClient, deviceClient, c.String("root-dir"), trw) + // Use the legacy UI if the server indicates so via capabilities. + setupExperiencer.UseLegacyUI = !orbitClient.GetServerCapabilities().Has(fleet.CapabilityMacOSWebSetupExperience) + orbitClient.RegisterConfigReceiver(setupExperiencer) + orbitClient.RegisterConfigReceiver(update.ApplySwiftDialogDownloaderMiddleware(updateRunner)) + + case "windows": + orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMEnrollmentFetcherMiddleware(windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient)) + comWorker, err := bitlocker.NewCOMWorker() + if err != nil { + return fmt.Errorf("create BitLocker COM worker: %w", err) + } + defer comWorker.Close() + orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMBitlockerFetcherMiddleware( + windowsMDMBitlockerCommandFrequency, orbitClient, comWorker)) + case "linux": + orbitClient.RegisterConfigReceiver(luks.New(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, ) - // Check if the token is not expired and still good. - // If not, rotate the token iff the server is reachable. - if serverIsReachable { - expired, _ := trw.HasExpired() - if expired || deviceClient.CheckToken(trw.GetCached()) != nil { - if err := trw.Rotate(); err != nil { - return fmt.Errorf("rotating token: %w", err) - } - } - } + orbitClient.RegisterConfigReceiver(serverOverridesReceiver) + } - if c.Bool("fleet-desktop") { - // Ensure that the token rotation checker is started, - // so that we have a valid token to launch the - // My Device page. - stopRotation := trw.StartRotation() - defer stopRotation() - } - - 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, - })) - setupExperiencer := setupexperience.NewSetupExperiencer(orbitClient, deviceClient, c.String("root-dir"), trw) - // Use the legacy UI if the server indicates so via capabilities. - setupExperiencer.UseLegacyUI = !orbitClient.GetServerCapabilities().Has(fleet.CapabilityMacOSWebSetupExperience) - orbitClient.RegisterConfigReceiver(setupExperiencer) - orbitClient.RegisterConfigReceiver(update.ApplySwiftDialogDownloaderMiddleware(updateRunner)) - - case "windows": - orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMEnrollmentFetcherMiddleware(windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient)) - comWorker, err := bitlocker.NewCOMWorker() - if err != nil { - return fmt.Errorf("create BitLocker COM worker: %w", err) - } - defer comWorker.Close() - orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMBitlockerFetcherMiddleware( - windowsMDMBitlockerCommandFrequency, orbitClient, comWorker)) - case "linux": - orbitClient.RegisterConfigReceiver(luks.New(orbitClient)) - } - - flagUpdateReceiver := update.NewFlagReceiver(orbitClient.TriggerOrbitRestart, update.FlagUpdateOptions{ + // 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"), - }) - orbitClient.RegisterConfigReceiver(flagUpdateReceiver) + }, updateRunner, orbitClient.TriggerOrbitRestart) - 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 an early check of fleetd configuration (iff server can be reached) - // 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 serverIsReachable { - 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, - }) - - // 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")) + // call UpdateAction on the updateRunner after we have fetched extensions from Fleet + _, err := updateRunner.UpdateAction() 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})) + // OK, initial call may fail, ok to continue + logging.LogErrIfEnvNotSet(constant.SilenceEnrollLogErrorEnvVar, err, "initial extensions update action failed") } - // --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"}), - ) + 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) } - // 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})) + orbitClient.RegisterConfigReceiver(extRunner) + } + + // Run an early check of fleetd configuration (iff server can be reached) + // 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 serverIsReachable { + 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 } + } - // 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})) - options = append(options, optionsAfterFlagfile...) + addSubsystem(&g, "config receivers", &wrapSubsystem{ + execute: orbitClient.ExecuteConfigReceivers, + interrupt: orbitClient.InterruptConfigReceivers, + }) - // 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())) + // 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})) + } - // 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) + // --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})) + options = append(options, optionsAfterFlagfile...) + + // 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) + + 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") + }, + }, + nil, + "", + ) + 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) } - addSubsystem(&g, "osqueryd runner", r) + desktopVersion, err = update.GetVersion(runPath) + if err == nil && desktopVersion != "" { + log.Info().Msgf("Found fleet-desktop version: %s", desktopVersion) + } else { + desktopVersion = "unknown" + } + } - checkerClient, err := service.NewOrbitClient( - c.String("root-dir"), + 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, + opt.ServerURL, + )), + ) + + 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"), - 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") - }, - }, - nil, - "", + trw, + rawClientCrt, + rawClientKey, + c.String("fleet-desktop-alternative-browser-host"), + opt.RootDirectory, ) - 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, - opt.ServerURL, - )), - ) - - 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, - ) - go func() { - for { - msg := <-desktopRunner.errorNotifyCh - log.Error().Err(errors.New(msg)).Msg("fleet-desktop runner error") - // Vital errors are always sent to Fleet, regardless of the error reporting setting FLEET_ENABLE_POST_CLIENT_DEBUG_ERRORS. - fleetdErr := fleet.FleetdError{ - Vital: true, - ErrorSource: "fleet-desktop", - ErrorSourceVersion: desktopVersion, - ErrorTimestamp: time.Now(), - ErrorMessage: msg, - ErrorAdditionalInfo: map[string]interface{}{ - "orbit_version": build.Version, - "osquery_version": osqueryHostInfo.OsqueryVersion, - "os_platform": osqueryHostInfo.Platform, - "os_platform_like": osqueryHostInfo.PlatformLike, - "os_version": osqueryHostInfo.OSVersion, - }, - } - if err = deviceClient.ReportError(trw.GetCached(), fleetdErr); err != nil { - log.Error().Err(err).Msg(fmt.Sprintf("failed to send error report to Fleet: %s", msg)) - } + go func() { + for { + msg := <-desktopRunner.errorNotifyCh + log.Error().Err(errors.New(msg)).Msg("fleet-desktop runner error") + // Vital errors are always sent to Fleet, regardless of the error reporting setting FLEET_ENABLE_POST_CLIENT_DEBUG_ERRORS. + fleetdErr := fleet.FleetdError{ + Vital: true, + ErrorSource: "fleet-desktop", + ErrorSourceVersion: desktopVersion, + ErrorTimestamp: time.Now(), + ErrorMessage: msg, + ErrorAdditionalInfo: map[string]any{ + "orbit_version": build.Version, + "osquery_version": osqueryHostInfo.OsqueryVersion, + "os_platform": osqueryHostInfo.Platform, + "os_platform_like": osqueryHostInfo.PlatformLike, + "os_version": osqueryHostInfo.OSVersion, + }, } - }() - 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 err = deviceClient.ReportError(trw.GetCached(), fleetdErr); err != nil { + log.Error().Err(err).Msg(fmt.Sprintf("failed to send error report to Fleet: %s", msg)) } } + }() + addSubsystem(&g, "desktop runner", desktopRunner) + } - 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, c.String("root-dir")) - 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")) - - setupExperienceOS := runtime.GOOS == "linux" || runtime.GOOS == "windows" - setupExperienceNotDisabled := !c.Bool("disable-setup-experience") - runSetupExperience := setupExperienceOS && setupExperienceNotDisabled - log.Debug(). - Bool("setupExperienceOS", setupExperienceOS). - Bool("notDisabled", setupExperienceNotDisabled). - Msg("checking setup experience preflight values") - - openMyDevicePage := func() error { - if !c.Bool("fleet-desktop") { - log.Debug().Msg("fleet desktop disabled, not launching my device page") - return nil - } - - 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) - return openBrowserWindow(browserURL) - } - - if runSetupExperience { - log.Debug().Msg("web setup experience enabled") - if err := processSetupExperience(orbitClient, c.String("root-dir"), openMyDevicePage); err != nil { - log.Error().Err(err).Msg("initiating setup experience") + // --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.Debug().Msg("not running setup experience") + 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 err := g.Run(); err != nil { - log.Error().Err(err).Msg("unexpected exit") + 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, c.String("root-dir")) + 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")) + + setupExperienceOS := runtime.GOOS == "linux" || runtime.GOOS == "windows" + setupExperienceNotDisabled := !c.Bool("disable-setup-experience") + runSetupExperience := setupExperienceOS && setupExperienceNotDisabled + log.Debug(). + Bool("setupExperienceOS", setupExperienceOS). + Bool("notDisabled", setupExperienceNotDisabled). + Msg("checking setup experience preflight values") + + openMyDevicePage := func() error { + if !c.Bool("fleet-desktop") { + log.Debug().Msg("fleet desktop disabled, not launching my device page") + return nil } - close(appDoneCh) // Signal to indicate runners have just ended - return nil + 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) + return openBrowserWindow(browserURL) } - if len(os.Args) == 2 && os.Args[1] == "--help" { - platform.PreUpdateQuirks() + if runSetupExperience { + log.Debug().Msg("web setup experience enabled") + if err := processSetupExperience(orbitClient, c.String("root-dir"), openMyDevicePage); err != nil { + log.Error().Err(err).Msg("initiating setup experience") + } + } else { + log.Debug().Msg("not running setup experience") } - if err := app.Run(os.Args); err != nil { - log.Error().Err(err).Msg("run orbit failed") + if err := g.Run(); err != nil { + log.Error().Err(err).Msg("unexpected exit") } + + close(appDoneCh) // Signal to indicate runners have just ended + return nil } func processSetupExperience(orbitClient *service.OrbitClient, rootDir string, openMyDevicePage func() error) error {