Skip to content

Commit

Permalink
Added signed URLs (#25197)
Browse files Browse the repository at this point in the history
For #24869 

This subtask contains code to sign the CloudFront software installer and
bootstrap package URL using AWS SDK URL signer.
It works with the current bootstrap package delivery. For software
installers, fleetd will need to be modified to take advantage of this
URL in a future subtask (which will also include updated API contributor
docs).

My article on signed URLs, for context:
https://victoronsoftware.com/posts/cloudfront-signed-urls/

# Checklist for submitter

- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
  • Loading branch information
getvictor authored Jan 9, 2025
1 parent 689e78a commit 68b7cf9
Show file tree
Hide file tree
Showing 20 changed files with 686 additions and 59 deletions.
8 changes: 5 additions & 3 deletions cmd/fleet/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ func newWorkerIntegrationsSchedule(
logger kitlog.Logger,
depStorage *mysql.NanoDEPStorage,
commander *apple_mdm.MDMAppleCommander,
bootstrapPackageStore fleet.MDMBootstrapPackageStore,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronWorkerIntegrations)
Expand Down Expand Up @@ -681,9 +682,10 @@ func newWorkerIntegrationsSchedule(
DEPClient: depCli,
}
appleMDM := &worker.AppleMDM{
Datastore: ds,
Log: logger,
Commander: commander,
Datastore: ds,
Log: logger,
Commander: commander,
BootstrapPackageStore: bootstrapPackageStore,
}
dbMigrate := &worker.DBMigration{
Datastore: ds,
Expand Down
22 changes: 20 additions & 2 deletions cmd/fleet/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"crypto"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
Expand Down Expand Up @@ -43,6 +44,7 @@ import (
"github.com/fleetdm/fleet/v4/server/logging"
"github.com/fleetdm/fleet/v4/server/mail"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/cryptoutil"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/buford"
Expand Down Expand Up @@ -766,6 +768,23 @@ the way that the Fleet server works.
if config.S3.BucketsAndPrefixesMatch() {
level.Warn(logger).Log("msg", "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")
Expand All @@ -780,7 +799,6 @@ 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 != "" {
Expand Down Expand Up @@ -914,7 +932,7 @@ the way that the Fleet server works.

if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService)
return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander)
return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander, bootstrapPackageStore)
}); err != nil {
initFatal(err, "failed to register worker integrations schedule")
}
Expand Down
68 changes: 61 additions & 7 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,10 +797,70 @@ func (svc *Service) DownloadSoftwareInstaller(ctx context.Context, skipAuthz boo
return svc.getSoftwareInstallerBinary(ctx, meta.StorageID, meta.Name)
}

func (svc *Service) GetSoftwareInstallDetails(ctx context.Context, installUUID string) (*fleet.SoftwareInstallDetails, error) {
// Call the base (non-premium) service to get the software install details
details, err := svc.Service.GetSoftwareInstallDetails(ctx, installUUID)
if err != nil {
return nil, err
}

// SoftwareInstallersCloudFrontSigner can only be set if license.IsPremium()
if svc.config.S3.SoftwareInstallersCloudFrontSigner != nil {
// Sign the URL for the installer
installerURL, err := svc.getSoftwareInstallURL(ctx, details.InstallerID)
if err != nil {
// We log the error but continue to return the details without the signed URL because orbit can still
// try to download the installer via Fleet server.
level.Error(svc.logger).Log("msg", "error getting software installer URL; check CloudFront configuration", "err", err)
} else {
details.SoftwareInstallerURL = installerURL
}
}

return details, nil
}

func (svc *Service) getSoftwareInstallURL(ctx context.Context, installerID uint) (*fleet.SoftwareInstallerURL, error) {
meta, err := svc.validateAndGetSoftwareInstallerMetadata(ctx, installerID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "validating software installer metadata for download")
}

// Note: we could check if the installer exists in the S3 store.
// However, if we fail and don't return a URL installer, the Orbit client will still try to download the installer via the Fleet server,
// and we will end up checking if the installer exists in the S3 store again.
// So, to reduce server load and speed up the "happy path" software install, we skip the check here and risk returning a URL that doesn't work.
// If CloudFront is misconfigured, the server and Orbit clients will experience a greater load since they'll be doing throw-away work.

// Get the signed URL
signedURL, err := svc.softwareInstallStore.Sign(ctx, meta.StorageID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "signing software installer URL")
}
return &fleet.SoftwareInstallerURL{
URL: signedURL,
Filename: meta.Name,
}, nil
}

func (svc *Service) OrbitDownloadSoftwareInstaller(ctx context.Context, installerID uint) (*fleet.DownloadSoftwareInstallerPayload, error) {
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)

meta, err := svc.validateAndGetSoftwareInstallerMetadata(ctx, installerID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "validating software installer metadata for download")
}

// Note that we do allow downloading an installer that is on a different team
// than the host's team, because the install request might have come while
// the host was on that team, and then the host got moved to a different team
// but the request is still pending execution.

return svc.getSoftwareInstallerBinary(ctx, meta.StorageID, meta.Name)
}

func (svc *Service) validateAndGetSoftwareInstallerMetadata(ctx context.Context, installerID uint) (*fleet.SoftwareInstaller, error) {
host, ok := hostctx.FromContext(ctx)
if !ok {
return nil, fleet.OrbitError{Message: "internal error: missing host from request context"}
Expand All @@ -820,13 +880,7 @@ func (svc *Service) OrbitDownloadSoftwareInstaller(ctx context.Context, installe
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software installer metadata")
}

// Note that we do allow downloading an installer that is on a different team
// than the host's team, because the install request might have come while
// the host was on that team, and then the host got moved to a different team
// but the request is still pending execution.

return svc.getSoftwareInstallerBinary(ctx, meta.StorageID, meta.Name)
return meta, nil
}

func (svc *Service) getSoftwareInstallerBinary(ctx context.Context, storageID string, filename string) (*fleet.DownloadSoftwareInstallerPayload, error) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ require (
github.com/apache/thrift v0.18.1 // indirect
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/cloudfront/sign v1.8.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/c-bata/go-prompt v0.2.3 // indirect
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.44.288 h1:Ln7fIao/nl0ACtelgR1I4AiEw/GLNkKcXfCaHupUW5Q=
github.com/aws/aws-sdk-go v1.44.288/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw=
github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/feature/cloudfront/sign v1.8.3 h1:/d7ZHq/2m+1Uzw4mnizCZbTAWB/dJ3CPy0N1qUpUpI0=
github.com/aws/aws-sdk-go-v2/feature/cloudfront/sign v1.8.3/go.mod h1:xWMYk6dLhV33jy2YrbOsv2l3fZTDMWE1yIIbvnD13gU=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU=
Expand Down
69 changes: 43 additions & 26 deletions server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/json"
Expand Down Expand Up @@ -316,24 +317,25 @@ 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"`
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)
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"`
SoftwareInstallersCloudFrontSigner crypto.Signer `yaml:"-"`
}

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
Expand All @@ -342,18 +344,18 @@ func (s S3Config) ValidateCloudfrontURL(initFatal func(err error, msg string)) {
initFatal(errors.New("cloudfront url scheme must be https"), "S3 software installers cloudfront URL")
return
}
if s.SoftwareInstallersCloudfrontURLSigningPrivateKey != "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID == "" ||
s.SoftwareInstallersCloudfrontURLSigningPrivateKey == "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID != "" {
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 == "" {
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 != "" {
} 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
Expand All @@ -375,7 +377,7 @@ func (s S3Config) BucketsAndPrefixesMatch() bool {
}

func (s S3Config) SoftwareInstallersToInternalCfg() S3ConfigInternal {
return S3ConfigInternal{
configInternal := S3ConfigInternal{
Bucket: s.SoftwareInstallersBucket,
Prefix: s.SoftwareInstallersPrefix,
Region: s.SoftwareInstallersRegion,
Expand All @@ -387,6 +389,14 @@ func (s S3Config) SoftwareInstallersToInternalCfg() S3ConfigInternal {
DisableSSL: s.SoftwareInstallersDisableSSL,
ForceS3PathStyle: s.SoftwareInstallersForceS3PathStyle,
}
if s.SoftwareInstallersCloudFrontSigner != nil {
configInternal.CloudFrontConfig = &S3CloudFrontConfig{
BaseURL: s.SoftwareInstallersCloudFrontURL,
SigningPublicKeyID: s.SoftwareInstallersCloudFrontURLSigningPublicKeyID,
Signer: s.SoftwareInstallersCloudFrontSigner,
}
}
return configInternal
}

// CarvesToInternalCfg creates an internal S3 config struct from the ingested S3 config. Note: we
Expand Down Expand Up @@ -450,6 +460,13 @@ type S3ConfigInternal struct {
StsExternalID string
DisableSSL bool
ForceS3PathStyle bool
CloudFrontConfig *S3CloudFrontConfig
}

type S3CloudFrontConfig struct {
BaseURL string
SigningPublicKeyID string
Signer crypto.Signer
}

// PubSubConfig defines configs the for Google PubSub logging plugin
Expand Down Expand Up @@ -1667,9 +1684,9 @@ func (man Manager) loadS3Config() S3Config {
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"),
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"),
}
}

Expand Down
8 changes: 4 additions & 4 deletions server/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,9 +720,9 @@ func TestValidateCloudfrontURL(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
s3 := S3Config{
SoftwareInstallersCloudfrontURL: c.url,
SoftwareInstallersCloudfrontURLSigningPublicKeyID: c.publicKey,
SoftwareInstallersCloudfrontURLSigningPrivateKey: c.privateKey,
SoftwareInstallersCloudFrontURL: c.url,
SoftwareInstallersCloudFrontURLSigningPublicKeyID: c.publicKey,
SoftwareInstallersCloudFrontURLSigningPrivateKey: c.privateKey,
}
initFatal := func(err error, msg string) {
if c.errMatches != "" {
Expand All @@ -732,7 +732,7 @@ func TestValidateCloudfrontURL(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
}
s3.ValidateCloudfrontURL(initFatal)
s3.ValidateCloudFrontURL(initFatal)
})
}
}
4 changes: 4 additions & 0 deletions server/datastore/filesystem/software_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ func (i *SoftwareInstallerStore) Cleanup(ctx context.Context, usedInstallerIDs [
return count, ctxerr.Wrap(ctx, errors.Join(errs...), "delete unused software installers")
}

func (i *SoftwareInstallerStore) Sign(ctx context.Context, _ string) (string, error) {
return "", ctxerr.New(ctx, "signing not supported for software installers in filesystem store")
}

// pathForInstaller builds local filesystem path to identify the software
// installer.
func (i *SoftwareInstallerStore) pathForInstaller(installerID string) string {
Expand Down
Loading

0 comments on commit 68b7cf9

Please sign in to comment.