mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +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>
100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
package aws_common
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
)
|
|
|
|
const (
|
|
// Token validity is 15 minutes, but we refresh after 10 minutes plus jitter
|
|
tokenRefreshTime = 10 * time.Minute
|
|
maxJitter = 30 * time.Second
|
|
)
|
|
|
|
// IAMTokenCache holds a cached token and its generation time
|
|
type IAMTokenCache struct {
|
|
token string
|
|
generated time.Time
|
|
}
|
|
|
|
// TokenGenerator is a function that generates a new IAM authentication token
|
|
type TokenGenerator func(ctx context.Context) (string, error)
|
|
|
|
// IAMAuthTokenManager manages AWS IAM authentication tokens with caching
|
|
type IAMAuthTokenManager struct {
|
|
// Token generator function specific to the service (RDS, ElastiCache, etc.)
|
|
generateToken TokenGenerator
|
|
|
|
// Token cache with RW mutex
|
|
cacheMu sync.RWMutex
|
|
cache *IAMTokenCache
|
|
}
|
|
|
|
// NewIAMAuthTokenManager creates a new IAM authentication token manager
|
|
func NewIAMAuthTokenManager(tokenGen TokenGenerator) *IAMAuthTokenManager {
|
|
return &IAMAuthTokenManager{
|
|
generateToken: tokenGen,
|
|
}
|
|
}
|
|
|
|
// GetToken retrieves a valid IAM authentication token, using cache when possible
|
|
func (m *IAMAuthTokenManager) GetToken(ctx context.Context) (string, error) {
|
|
// Calculate expiry time with jitter
|
|
jitter := time.Duration(rand.Int63n(int64(maxJitter))) //nolint:gosec // jitter doesn't need cryptographic randomness
|
|
expiryTime := tokenRefreshTime + jitter
|
|
|
|
// Check if we have a valid cached token
|
|
m.cacheMu.RLock()
|
|
if m.cache != nil && time.Since(m.cache.generated) < expiryTime {
|
|
token := m.cache.token
|
|
m.cacheMu.RUnlock()
|
|
return token, nil
|
|
}
|
|
m.cacheMu.RUnlock()
|
|
|
|
// Need to generate a new token
|
|
m.cacheMu.Lock()
|
|
defer m.cacheMu.Unlock()
|
|
|
|
// Double-check in case another goroutine generated a token while we were waiting
|
|
if m.cache != nil && time.Since(m.cache.generated) < expiryTime {
|
|
return m.cache.token, nil
|
|
}
|
|
|
|
token, err := m.generateToken(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
m.cache = &IAMTokenCache{
|
|
token: token,
|
|
generated: time.Now(),
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// LoadAWSConfig loads AWS configuration with optional assume role support
|
|
func LoadAWSConfig(ctx context.Context, region, assumeRoleArn, stsExternalID string) (aws.Config, error) {
|
|
opts := []func(*config.LoadOptions) error{config.WithRegion(region)}
|
|
cfg, err := config.LoadDefaultConfig(ctx, opts...)
|
|
if err != nil {
|
|
return aws.Config{}, fmt.Errorf("failed to load AWS config: %w", err)
|
|
}
|
|
|
|
// If assume role ARN is provided, configure it
|
|
if assumeRoleArn != "" {
|
|
cfg, err = ConfigureAssumeRoleProvider(cfg, opts, assumeRoleArn, stsExternalID)
|
|
if err != nil {
|
|
return aws.Config{}, fmt.Errorf("failed to configure assume role provider: %w", err)
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|