From e0d0e8031599844db9d25043143061f7b694e68b Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 6 Jan 2025 11:33:24 -0600 Subject: [PATCH] Cloudfront URL config changes (#25145) For #24868 (subtask) # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality --- changes/23823-cloudfront-cdn | 4 ++ cmd/fleet/main.go | 37 ++++++++--------- cmd/fleet/serve.go | 2 + server/config/config.go | 78 +++++++++++++++++++++++++++--------- server/config/config_test.go | 41 +++++++++++++++++++ 5 files changed, 124 insertions(+), 38 deletions(-) create mode 100644 changes/23823-cloudfront-cdn diff --git a/changes/23823-cloudfront-cdn b/changes/23823-cloudfront-cdn new file mode 100644 index 000000000000..21db000a6965 --- /dev/null +++ b/changes/23823-cloudfront-cdn @@ -0,0 +1,4 @@ +Allow delivery of bootstrap packages and software installers using signed URLs from CloudFront CDN. To enable, configure server settings: +- s3_software_installers_cloudfront_url +- s3_software_installers_cloudfront_url_signing_public_key_id +- s3_software_installers_cloudfront_url_signing_private_key diff --git a/cmd/fleet/main.go b/cmd/fleet/main.go index b39cbc2ee942..c60fc5a55ac0 100644 --- a/cmd/fleet/main.go +++ b/cmd/fleet/main.go @@ -104,24 +104,25 @@ func applyDevFlags(cfg *config.FleetConfig) { cfg.Prometheus.BasicAuth.Password = "insecure" } - cfg.S3 = config.S3Config{ - CarvesBucket: "carves-dev", - CarvesRegion: "minio", - CarvesPrefix: "dev-prefix", - CarvesEndpointURL: "localhost:9000", - CarvesAccessKeyID: "minio", - CarvesSecretAccessKey: "minio123!", - CarvesDisableSSL: true, - CarvesForceS3PathStyle: true, - - SoftwareInstallersBucket: "software-installers-dev", - SoftwareInstallersRegion: "minio", - SoftwareInstallersPrefix: "dev-prefix", - SoftwareInstallersEndpointURL: "localhost:9000", - SoftwareInstallersAccessKeyID: "minio", - SoftwareInstallersSecretAccessKey: "minio123!", - SoftwareInstallersDisableSSL: true, - SoftwareInstallersForceS3PathStyle: true, + cfg.S3.CarvesBucket = "carves-dev" + cfg.S3.CarvesRegion = "minio" + cfg.S3.CarvesPrefix = "dev-prefix" + cfg.S3.CarvesEndpointURL = "localhost:9000" + cfg.S3.CarvesAccessKeyID = "minio" + cfg.S3.CarvesSecretAccessKey = "minio123!" + cfg.S3.CarvesDisableSSL = true + cfg.S3.CarvesForceS3PathStyle = true + + // Allow the software installers bucket to be overridden in dev mode + if cfg.S3.SoftwareInstallersBucket == "" { + cfg.S3.SoftwareInstallersBucket = "software-installers-dev" + cfg.S3.SoftwareInstallersRegion = "minio" + cfg.S3.SoftwareInstallersPrefix = "dev-prefix" + cfg.S3.SoftwareInstallersEndpointURL = "localhost:9000" + cfg.S3.SoftwareInstallersAccessKeyID = "minio" + cfg.S3.SoftwareInstallersSecretAccessKey = "minio123!" + cfg.S3.SoftwareInstallersDisableSSL = true + cfg.S3.SoftwareInstallersForceS3PathStyle = true } cfg.Packaging.S3 = config.S3Config{ diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 2fdab8223f7e..9c227735a4b5 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -779,6 +779,8 @@ the way that the Fleet server works. } bootstrapPackageStore = bstore level.Info(logger).Log("msg", "using S3 bootstrap package store", "bucket", config.S3.SoftwareInstallersBucket) + + config.S3.ValidateCloudfrontURL(initFatal) } else { installerDir := os.TempDir() if dir := os.Getenv("FLEET_SOFTWARE_INSTALLER_STORE_DIR"); dir != "" { diff --git a/server/config/config.go b/server/config/config.go index bdfe0898dcf9..cdb932bd5fff 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -316,16 +316,48 @@ type S3Config struct { CarvesDisableSSL bool `yaml:"carves_disable_ssl"` CarvesForceS3PathStyle bool `yaml:"carves_force_s3_path_style"` - SoftwareInstallersBucket string `yaml:"software_installers_bucket"` - SoftwareInstallersPrefix string `yaml:"software_installers_prefix"` - SoftwareInstallersRegion string `yaml:"software_installers_region"` - SoftwareInstallersEndpointURL string `yaml:"software_installers_endpoint_url"` - SoftwareInstallersAccessKeyID string `yaml:"software_installers_access_key_id"` - SoftwareInstallersSecretAccessKey string `yaml:"software_installers_secret_access_key"` - SoftwareInstallersStsAssumeRoleArn string `yaml:"software_installers_sts_assume_role_arn"` - SoftwareInstallersStsExternalID string `yaml:"software_installers_sts_external_id"` - SoftwareInstallersDisableSSL bool `yaml:"software_installers_disable_ssl"` - SoftwareInstallersForceS3PathStyle bool `yaml:"software_installers_force_s3_path_style"` + SoftwareInstallersBucket string `yaml:"software_installers_bucket"` + SoftwareInstallersPrefix string `yaml:"software_installers_prefix"` + SoftwareInstallersRegion string `yaml:"software_installers_region"` + SoftwareInstallersEndpointURL string `yaml:"software_installers_endpoint_url"` + SoftwareInstallersAccessKeyID string `yaml:"software_installers_access_key_id"` + SoftwareInstallersSecretAccessKey string `yaml:"software_installers_secret_access_key"` + SoftwareInstallersStsAssumeRoleArn string `yaml:"software_installers_sts_assume_role_arn"` + SoftwareInstallersStsExternalID string `yaml:"software_installers_sts_external_id"` + SoftwareInstallersDisableSSL bool `yaml:"software_installers_disable_ssl"` + SoftwareInstallersForceS3PathStyle bool `yaml:"software_installers_force_s3_path_style"` + SoftwareInstallersCloudfrontURL string `yaml:"software_installers_cloudfront_url"` + SoftwareInstallersCloudfrontURLSigningPublicKeyID string `yaml:"software_installers_cloudfront_url_signing_public_key_id"` + SoftwareInstallersCloudfrontURLSigningPrivateKey string `yaml:"software_installers_cloudfront_url_signing_private_key"` +} + +func (s S3Config) ValidateCloudfrontURL(initFatal func(err error, msg string)) { + if s.SoftwareInstallersCloudfrontURL != "" { + cloudfrontURL, err := url.Parse(s.SoftwareInstallersCloudfrontURL) + if err != nil { + initFatal(err, "S3 software installers cloudfront URL") + return + } + if cloudfrontURL.Scheme != "https" { + initFatal(errors.New("cloudfront url scheme must be https"), "S3 software installers cloudfront URL") + return + } + if s.SoftwareInstallersCloudfrontURLSigningPrivateKey != "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID == "" || + s.SoftwareInstallersCloudfrontURLSigningPrivateKey == "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID != "" { + initFatal(errors.New("Couldn't configure. Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set for URL signing."), + "S3 software installers cloudfront URL") + return + } + if s.SoftwareInstallersCloudfrontURLSigningPrivateKey == "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID == "" { + initFatal(errors.New("Couldn't configure. Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set when CloudFront distribution URL is set."), + "S3 software installers cloudfront URL") + return + } + } else if s.SoftwareInstallersCloudfrontURLSigningPrivateKey != "" || s.SoftwareInstallersCloudfrontURLSigningPublicKeyID != "" { + initFatal(errors.New("Couldn't configure. `s3_software_installers_cloudfront_url` must be set to use `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key`."), + "S3 software installers cloudfront URL") + return + } } func (s S3Config) BucketsAndPrefixesMatch() bool { @@ -1197,6 +1229,9 @@ func (man Manager) addConfigs() { man.addConfigString("s3.software_installers_sts_external_id", "", "Optional unique identifier that can be used by the principal assuming the role to assert its identity.") man.addConfigBool("s3.software_installers_disable_ssl", false, "Disable SSL (typically for local testing)") man.addConfigBool("s3.software_installers_force_s3_path_style", false, "Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`") + man.addConfigString("s3.software_installers_cloudfront_url", "", "CloudFront URL for software installers") + man.addConfigString("s3.software_installers_cloudfront_url_signing_public_key_id", "", "CloudFront public key ID for URL signing") + man.addConfigString("s3.software_installers_cloudfront_url_signing_private_key", "", "CloudFront private key for URL signing") // PubSub man.addConfigString("pubsub.project", "", "Google Cloud Project to use") @@ -1622,16 +1657,19 @@ func (man Manager) loadS3Config() S3Config { DisableSSL: man.getConfigBool("s3.disable_ssl"), ForceS3PathStyle: man.getConfigBool("s3.force_s3_path_style"), - SoftwareInstallersBucket: man.getConfigString("s3.software_installers_bucket"), - SoftwareInstallersPrefix: man.getConfigString("s3.software_installers_prefix"), - SoftwareInstallersRegion: man.getConfigString("s3.software_installers_region"), - SoftwareInstallersEndpointURL: man.getConfigString("s3.software_installers_endpoint_url"), - SoftwareInstallersAccessKeyID: man.getConfigString("s3.software_installers_access_key_id"), - SoftwareInstallersSecretAccessKey: man.getConfigString("s3.software_installers_secret_access_key"), - SoftwareInstallersStsAssumeRoleArn: man.getConfigString("s3.software_installers_sts_assume_role_arn"), - SoftwareInstallersStsExternalID: man.getConfigString("s3.software_installers_sts_external_id"), - SoftwareInstallersDisableSSL: man.getConfigBool("s3.software_installers_disable_ssl"), - SoftwareInstallersForceS3PathStyle: man.getConfigBool("s3.software_installers_force_s3_path_style"), + SoftwareInstallersBucket: man.getConfigString("s3.software_installers_bucket"), + SoftwareInstallersPrefix: man.getConfigString("s3.software_installers_prefix"), + SoftwareInstallersRegion: man.getConfigString("s3.software_installers_region"), + SoftwareInstallersEndpointURL: man.getConfigString("s3.software_installers_endpoint_url"), + SoftwareInstallersAccessKeyID: man.getConfigString("s3.software_installers_access_key_id"), + SoftwareInstallersSecretAccessKey: man.getConfigString("s3.software_installers_secret_access_key"), + SoftwareInstallersStsAssumeRoleArn: man.getConfigString("s3.software_installers_sts_assume_role_arn"), + SoftwareInstallersStsExternalID: man.getConfigString("s3.software_installers_sts_external_id"), + SoftwareInstallersDisableSSL: man.getConfigBool("s3.software_installers_disable_ssl"), + SoftwareInstallersForceS3PathStyle: man.getConfigBool("s3.software_installers_force_s3_path_style"), + SoftwareInstallersCloudfrontURL: man.getConfigString("s3.software_installers_cloudfront_url"), + SoftwareInstallersCloudfrontURLSigningPublicKeyID: man.getConfigString("s3.software_installers_cloudfront_url_signing_public_key_id"), + SoftwareInstallersCloudfrontURLSigningPrivateKey: man.getConfigString("s3.software_installers_cloudfront_url_signing_private_key"), } } diff --git a/server/config/config_test.go b/server/config/config_test.go index c2df31584041..95aa2cd1ac0c 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -695,3 +695,44 @@ e+Z1cALnWREYhEPv4JrR5U0VvqeIdExDD6Ida61yvd7oc59pn0kpfKjozPJr6FsU // prevent static analysis tools from raising issues due to detection of private key // in code. func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } + +func TestValidateCloudfrontURL(t *testing.T) { + t.Parallel() + cases := []struct { + name string + url string + publicKey string + privateKey string + errMatches string + }{ + {"happy path", "https://example.com", "public", "private", ""}, + {"bad URL", "bozo!://example.com", "public", "private", "parse"}, + {"non-HTTPS URL", "http://example.com", "public", "private", "cloudfront url scheme must be https"}, + {"missing URL", "", "public", "private", "`s3_software_installers_cloudfront_url` must be set"}, + {"missing public key", "https://example.com", "", "private", + "Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set"}, + {"missing private key", "https://example.com", "public", "", + "Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set"}, + {"missing keys", "https://example.com", "", "", + "Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + s3 := S3Config{ + SoftwareInstallersCloudfrontURL: c.url, + SoftwareInstallersCloudfrontURLSigningPublicKeyID: c.publicKey, + SoftwareInstallersCloudfrontURLSigningPrivateKey: c.privateKey, + } + initFatal := func(err error, msg string) { + if c.errMatches != "" { + require.Error(t, err) + require.Regexp(t, c.errMatches, err.Error()) + } else { + t.Errorf("unexpected error: %v", err) + } + } + s3.ValidateCloudfrontURL(initFatal) + }) + } +}