mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 16:39:01 +00:00
For #32571. Original PR from the community: https://github.com/fleetdm/fleet/pull/32573. Changes on this PR: - Only setting the checksum algorithm when using GCS as backend (to not break other S3 backends). - Changes for carves, bootstrap packages, and software icons which also use S3. ## Testing - [X] QA'd all new/changed functionality manually ```sh FLEET_S3_SOFTWARE_INSTALLERS_BUCKET=some-software-installers-bucket \ FLEET_S3_SOFTWARE_INSTALLERS_ACCESS_KEY_ID=... \ FLEET_S3_SOFTWARE_INSTALLERS_SECRET_ACCESS_KEY=... \ FLEET_S3_SOFTWARE_INSTALLERS_ENDPOINT_URL=https://storage.googleapis.com \ FLEET_S3_SOFTWARE_INSTALLERS_REGION=us \ FLEET_S3_SOFTWARE_INSTALLERS_FORCE_S3_PATH_STYLE=true \ FLEET_S3_CARVES_BUCKET=some-carves-bucket \ FLEET_S3_CARVES_ACCESS_KEY_ID=... \ FLEET_S3_CARVES_SECRET_ACCESS_KEY=... \ FLEET_S3_CARVES_ENDPOINT_URL=https://storage.googleapis.com \ FLEET_S3_CARVES_REGION=us \ FLEET_S3_CARVES_FORCE_S3_PATH_STYLE=true \ ./build/fleet serve --dev --dev_license --logging_debug 2>&1 | tee ~/fleet.txt ```
238 lines
8.2 KiB
Go
238 lines
8.2 KiB
Go
package s3
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
"github.com/fleetdm/fleet/v4/server/aws_common"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
|
aws_config "github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/aws/smithy-go/middleware"
|
|
smithyhttp "github.com/aws/smithy-go/transport/http"
|
|
)
|
|
|
|
const awsRegionHint = "us-east-1"
|
|
|
|
type s3store struct {
|
|
s3Client *s3.Client
|
|
bucket string
|
|
prefix string
|
|
cloudFrontConfig *config.S3CloudFrontConfig
|
|
}
|
|
|
|
type installerNotFoundError struct{}
|
|
|
|
var _ fleet.NotFoundError = (*installerNotFoundError)(nil)
|
|
|
|
func (p installerNotFoundError) Error() string {
|
|
return "installer not found"
|
|
}
|
|
|
|
func (p installerNotFoundError) IsNotFound() bool {
|
|
return true
|
|
}
|
|
|
|
// newS3Store initializes an S3 Datastore.
|
|
func newS3Store(cfg config.S3ConfigInternal) (*s3store, error) {
|
|
var opts []func(*aws_config.LoadOptions) error
|
|
|
|
// The service endpoint is deprecated, but we still set it
|
|
// in case users are using it.
|
|
// It is also used when testing with minio.
|
|
if cfg.EndpointURL != "" {
|
|
opts = append(opts, aws_config.WithEndpointResolver(aws.EndpointResolverFunc(
|
|
func(service, region string) (aws.Endpoint, error) {
|
|
return aws.Endpoint{
|
|
URL: cfg.EndpointURL,
|
|
}, nil
|
|
})),
|
|
)
|
|
}
|
|
|
|
// DisableSSL is only used for testing.
|
|
if cfg.DisableSSL {
|
|
// Ignoring "G402: TLS InsecureSkipVerify set true", this is only used for automated testing.
|
|
c := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{ //nolint:gosec
|
|
InsecureSkipVerify: false,
|
|
}))
|
|
opts = append(opts, aws_config.WithHTTPClient(c))
|
|
}
|
|
|
|
// Use default auth provider if no static credentials were provided.
|
|
if cfg.AccessKeyID != "" && cfg.SecretAccessKey != "" {
|
|
opts = append(opts, aws_config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
|
cfg.AccessKeyID,
|
|
cfg.SecretAccessKey,
|
|
"",
|
|
)))
|
|
}
|
|
|
|
if cfg.Region == "" {
|
|
// Attempt to deduce region from bucket.
|
|
conf, err := aws_config.LoadDefaultConfig(context.Background(),
|
|
append(opts, aws_config.WithRegion(awsRegionHint))...,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create default config to get bucket region: %w", err)
|
|
}
|
|
bucketRegion, err := manager.GetBucketRegion(context.Background(), s3.NewFromConfig(conf), cfg.Bucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get bucket region: %w", err)
|
|
}
|
|
cfg.Region = bucketRegion
|
|
}
|
|
|
|
opts = append(opts, aws_config.WithRegion(cfg.Region))
|
|
conf, err := aws_config.LoadDefaultConfig(context.Background(), opts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create default config: %w", err)
|
|
}
|
|
|
|
if cfg.StsAssumeRoleArn != "" {
|
|
conf, err = aws_common.ConfigureAssumeRoleProvider(conf, opts, cfg.StsAssumeRoleArn, cfg.StsExternalID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to configure assume role provider: %w", err)
|
|
}
|
|
}
|
|
|
|
s3Client := s3.NewFromConfig(conf, func(o *s3.Options) {
|
|
o.UsePathStyle = cfg.ForceS3PathStyle
|
|
|
|
// Apply workaround if using Google Cloud Storage (GCS) endpoint
|
|
// This fixes signature issues with AWS SDK v2 when using GCS
|
|
// See: https://github.com/aws/aws-sdk-go-v2/issues/1816#issuecomment-1927281540
|
|
if cfg.EndpointURL != "" && isGCS(cfg.EndpointURL) {
|
|
// GCS alters the Accept-Encoding header which breaks the request signature
|
|
ignoreSigningHeaders(o, []string{"Accept-Encoding"})
|
|
// GCS also has issues with trailing checksums in UploadPart and PutObject operations
|
|
disableTrailingChecksumForGCS(o)
|
|
}
|
|
})
|
|
|
|
return &s3store{
|
|
s3Client: s3Client,
|
|
bucket: cfg.Bucket,
|
|
prefix: cfg.Prefix,
|
|
cloudFrontConfig: cfg.CloudFrontConfig,
|
|
}, nil
|
|
}
|
|
|
|
// CreateTestBucket creates a bucket with the provided name and a default
|
|
// bucket config. Only recommended for local testing.
|
|
func (s *s3store) CreateTestBucket(ctx context.Context, name string) error {
|
|
_, err := s.s3Client.CreateBucket(ctx, &s3.CreateBucketInput{
|
|
Bucket: &name,
|
|
CreateBucketConfiguration: &types.CreateBucketConfiguration{},
|
|
})
|
|
|
|
// Don't error if the bucket already exists
|
|
var (
|
|
bucketAlreadyExists *types.BucketAlreadyExists
|
|
bucketAlreadyOwnedByYou *types.BucketAlreadyOwnedByYou
|
|
)
|
|
if errors.As(err, &bucketAlreadyExists) || errors.As(err, &bucketAlreadyOwnedByYou) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// GCS workaround middleware functions to fix signature issues
|
|
// See: https://github.com/aws/aws-sdk-go-v2/issues/1816#issuecomment-1927281540
|
|
|
|
type ignoredHeadersKey struct{}
|
|
|
|
// ignoreSigningHeaders excludes the listed headers from the request signature
|
|
// because some providers (like GCS) may alter them, causing signature mismatches.
|
|
func ignoreSigningHeaders(o *s3.Options, headers []string) {
|
|
o.APIOptions = append(o.APIOptions, func(stack *middleware.Stack) error {
|
|
if err := stack.Finalize.Insert(ignoreHeaders(headers), "Signing", middleware.Before); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := stack.Finalize.Insert(restoreIgnored(), "Signing", middleware.After); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func ignoreHeaders(headers []string) middleware.FinalizeMiddleware {
|
|
return middleware.FinalizeMiddlewareFunc(
|
|
"IgnoreHeaders",
|
|
func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (out middleware.FinalizeOutput, metadata middleware.Metadata, err error) {
|
|
req, ok := in.Request.(*smithyhttp.Request)
|
|
if !ok {
|
|
return out, metadata, &v4.SigningError{Err: fmt.Errorf("(ignoreHeaders) unexpected request middleware type %T", in.Request)}
|
|
}
|
|
|
|
ignored := make(map[string]string, len(headers))
|
|
for _, h := range headers {
|
|
ignored[h] = req.Header.Get(h)
|
|
req.Header.Del(h)
|
|
}
|
|
|
|
ctx = middleware.WithStackValue(ctx, ignoredHeadersKey{}, ignored)
|
|
|
|
return next.HandleFinalize(ctx, in)
|
|
},
|
|
)
|
|
}
|
|
|
|
func restoreIgnored() middleware.FinalizeMiddleware {
|
|
return middleware.FinalizeMiddlewareFunc(
|
|
"RestoreIgnored",
|
|
func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (out middleware.FinalizeOutput, metadata middleware.Metadata, err error) {
|
|
req, ok := in.Request.(*smithyhttp.Request)
|
|
if !ok {
|
|
return out, metadata, &v4.SigningError{Err: fmt.Errorf("(restoreIgnored) unexpected request middleware type %T", in.Request)}
|
|
}
|
|
|
|
ignored, _ := middleware.GetStackValue(ctx, ignoredHeadersKey{}).(map[string]string)
|
|
for k, v := range ignored {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
return next.HandleFinalize(ctx, in)
|
|
},
|
|
)
|
|
}
|
|
|
|
// disableTrailingChecksumForGCS disables trailing checksums for UploadPart and PutObject operations using reflection
|
|
// This is part of the GCS compatibility workaround as GCS doesn't support trailing checksums
|
|
func disableTrailingChecksumForGCS(o *s3.Options) {
|
|
o.APIOptions = append(o.APIOptions, func(stack *middleware.Stack) error {
|
|
return stack.Initialize.Add(middleware.InitializeMiddlewareFunc(
|
|
"DisableTrailingChecksum",
|
|
func(ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (out middleware.InitializeOutput, metadata middleware.Metadata, err error) {
|
|
// Check if this is an UploadPart or PutObject operation
|
|
if opName := middleware.GetOperationName(ctx); opName == "UploadPart" || opName == "PutObject" {
|
|
// Use reflection to disable trailing checksums in the checksum middleware
|
|
// This is a hack, but it's the only way to disable trailing checksums currently
|
|
if checksumMiddleware, ok := stack.Finalize.Get("AWSChecksum:ComputeInputPayloadChecksum"); ok {
|
|
if v := reflect.ValueOf(checksumMiddleware).Elem(); v.IsValid() {
|
|
if field := v.FieldByName("EnableTrailingChecksum"); field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {
|
|
field.SetBool(false)
|
|
}
|
|
}
|
|
}
|
|
// Remove the trailing checksum middleware entirely
|
|
_, _ = stack.Finalize.Remove("addInputChecksumTrailer")
|
|
}
|
|
return next.HandleInitialize(ctx, in)
|
|
},
|
|
), middleware.Before)
|
|
})
|
|
}
|