mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 08:28:52 +00:00
for #1817 # Details This PR gives Fleet servers the ability to connect to RDS MySQL and Elasticache Redis via AWS [Identity and Access Management (IAM)](https://aws.amazon.com/iam/). It is based almost entirely on the work of @titanous, branched from his [original pull request](https://github.com/fleetdm/fleet/pull/31075). The main differences between his branch and this are: 1. Removal of auto-detection of AWS region (and cache name for Elasticache) in favor of specifying these values in configuration. The auto-detection is admittedly handy but parsing AWS host URLs is not considered a best practice. 2. Relying on the existence of these new configs to determine whether or not to connect via IAM. This sidesteps a thorny issue of whether to try an IAM-based Elasticache connection when a password is not supplied, since this is technically a valid setup. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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/guides/committing-changes.md#changes-files) for more information. ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually - besides using @titanous's excellent test tool, I verified the following end-to-end: - [X] regular (non RDS) MySQL connection - [X] RDS MySQL connection using username/password - [X] RDS MySQL connection using IAM (no role) - [X] RDS MySQL connection using IAM (assuming role) - [X] regular (non Elasticache) Redis connection - [X] Elasticache Redis connection using username/password - [X] Elasticache Redis connection using NO password (without IAM) - [X] Elasticache Redis connection using IAM (no role) - [X] Elasticache Redis connection using IAM (assuming role) --------- Co-authored-by: Jonathan Rudenberg <jonathan@titanous.com> Co-authored-by: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
92 lines
2.7 KiB
Go
92 lines
2.7 KiB
Go
package redis
|
|
|
|
//go:generate go run gen_aws_region_map.go
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
|
"github.com/fleetdm/fleet/v4/server/aws_common"
|
|
)
|
|
|
|
const (
|
|
// emptySHA256 is the SHA256 hash of an empty payload (for GET requests)
|
|
emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
|
|
elastiCacheServiceName = "elasticache"
|
|
)
|
|
|
|
// awsIAMAuthTokenGenerator generates AWS IAM authentication tokens for ElastiCache
|
|
type awsIAMAuthTokenGenerator struct {
|
|
credentials aws.CredentialsProvider
|
|
signer *v4.Signer
|
|
region string
|
|
clusterName string
|
|
userName string
|
|
tokenManager *aws_common.IAMAuthTokenManager
|
|
}
|
|
|
|
// newAWSIAMAuthTokenGenerator creates a new AWS IAM authentication token generator
|
|
func newAWSIAMAuthTokenGenerator(clusterName, userName, region, assumeRoleArn, stsExternalID string) (*awsIAMAuthTokenGenerator, error) {
|
|
// Load AWS configuration
|
|
cfg, err := aws_common.LoadAWSConfig(context.Background(), region, assumeRoleArn, stsExternalID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
g := &awsIAMAuthTokenGenerator{
|
|
credentials: cfg.Credentials,
|
|
signer: v4.NewSigner(),
|
|
region: region,
|
|
clusterName: clusterName,
|
|
userName: userName,
|
|
}
|
|
|
|
// Create token manager with the generator's token generation function
|
|
g.tokenManager = aws_common.NewIAMAuthTokenManager(g.generateNewToken)
|
|
|
|
return g, nil
|
|
}
|
|
|
|
// generateAuthToken generates an IAM authentication token for ElastiCache
|
|
// It uses a cache to avoid generating new tokens for every connection
|
|
func (g *awsIAMAuthTokenGenerator) generateAuthToken(ctx context.Context) (string, error) {
|
|
return g.tokenManager.GetToken(ctx)
|
|
}
|
|
|
|
// generateNewToken creates a new IAM authentication token
|
|
func (g *awsIAMAuthTokenGenerator) generateNewToken(ctx context.Context) (string, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/", g.clusterName), nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
values := req.URL.Query()
|
|
values.Set("Action", "connect")
|
|
values.Set("User", g.userName)
|
|
req.URL.RawQuery = values.Encode()
|
|
|
|
creds, err := g.credentials.Retrieve(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to retrieve AWS credentials: %w", err)
|
|
}
|
|
|
|
// Set expiry time (15 minutes)
|
|
query := req.URL.Query()
|
|
query.Set("X-Amz-Expires", "900")
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
presignedURL, _, err := g.signer.PresignHTTP(ctx, creds, req, emptySHA256, elastiCacheServiceName, g.region, time.Now().UTC())
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to presign request: %w", err)
|
|
}
|
|
|
|
authToken := strings.TrimPrefix(presignedURL, "https://")
|
|
|
|
return authToken, nil
|
|
}
|