diff --git a/pkg/clients/v1/factory.go b/pkg/clients/v1/factory.go index e5bdcb71..8c4800a5 100644 --- a/pkg/clients/v1/factory.go +++ b/pkg/clients/v1/factory.go @@ -225,6 +225,9 @@ func createCloudWatchClient(logger logging.Logger, s *session.Session, region *s } func createTaggingClient(logger logging.Logger, session *session.Session, region *string, role config.Role, fips bool) tagging.Client { + // The createSession function for a service which does not support FIPS does not take a fips parameter + // This currently applies to createTagSession(Resource Groups Tagging), ASG (EC2 autoscaling), and Prometheus (Amazon Managed Prometheus) + // AWS FIPS Reference: https://aws.amazon.com/compliance/fips/ return tagging_v1.NewClient( logger, createTagSession(session, region, role, logger.IsDebugEnabled()), @@ -233,7 +236,7 @@ func createTaggingClient(logger logging.Logger, session *session.Session, region createAPIGatewayV2Session(session, region, role, fips, logger.IsDebugEnabled()), createEC2Session(session, region, role, fips, logger.IsDebugEnabled()), createDMSSession(session, region, role, fips, logger.IsDebugEnabled()), - createPrometheusSession(session, region, role, fips, logger.IsDebugEnabled()), + createPrometheusSession(session, region, role, logger.IsDebugEnabled()), createStorageGatewaySession(session, region, role, fips, logger.IsDebugEnabled()), createShieldSession(session, region, role, fips, logger.IsDebugEnabled()), ) @@ -415,12 +418,9 @@ func createEC2Session(sess *session.Session, region *string, role config.Role, f return ec2.New(sess, setSTSCreds(sess, config, role)) } -func createPrometheusSession(sess *session.Session, region *string, role config.Role, fips bool, isDebugEnabled bool) prometheusserviceiface.PrometheusServiceAPI { +func createPrometheusSession(sess *session.Session, region *string, role config.Role, isDebugEnabled bool) prometheusserviceiface.PrometheusServiceAPI { maxPrometheusAPIRetries := 10 config := &aws.Config{Region: region, MaxRetries: &maxPrometheusAPIRetries} - if fips { - config.UseFIPSEndpoint = endpoints.FIPSEndpointStateEnabled - } if isDebugEnabled { config.LogLevel = aws.LogLevel(aws.LogDebugWithHTTPBody) diff --git a/pkg/clients/v1/factory_test.go b/pkg/clients/v1/factory_test.go index a05ede24..37997ecc 100644 --- a/pkg/clients/v1/factory_test.go +++ b/pkg/clients/v1/factory_test.go @@ -1030,7 +1030,7 @@ func TestCreatePrometheusSession(t *testing.T) { t, "Prometheus", func(t *testing.T, s *session.Session, region *string, role config.Role, fips bool) { - iface := createPrometheusSession(s, region, role, fips, false) + iface := createPrometheusSession(s, region, role, false) if iface == nil { t.Fail() } diff --git a/pkg/clients/v2/factory.go b/pkg/clients/v2/factory.go index c3deca7e..921940ef 100644 --- a/pkg/clients/v2/factory.go +++ b/pkg/clients/v2/factory.go @@ -38,12 +38,13 @@ import ( type awsRegion = string type CachingFactory struct { - logger logging.Logger - stsRegion string - clients map[config.Role]map[awsRegion]*cachedClients - mu sync.Mutex - refreshed bool - cleared bool + logger logging.Logger + stsRegion string + clients map[config.Role]map[awsRegion]*cachedClients + mu sync.Mutex + refreshed bool + cleared bool + fipsEnabled bool } type cachedClients struct { @@ -86,10 +87,6 @@ func NewFactory(cfg config.ScrapeConf, fips bool, logger logging.Logger) (*Cachi }))) } - if fips { - options = append(options, aws_config.WithUseFIPSEndpoint(aws.FIPSEndpointStateEnabled)) - } - options = append(options, aws_config.WithRetryMaxAttempts(5)) c, err := aws_config.LoadDefaultConfig(context.TODO(), options...) @@ -150,9 +147,10 @@ func NewFactory(cfg config.ScrapeConf, fips bool, logger logging.Logger) (*Cachi } return &CachingFactory{ - logger: logger, - clients: cache, - stsRegion: cfg.StsRegion, + logger: logger, + clients: cache, + stsRegion: cfg.StsRegion, + fipsEnabled: fips, }, nil } @@ -280,6 +278,10 @@ func (c *CachingFactory) createCloudwatchClient(regionConfig *aws.Config) *cloud options.MaxAttempts = 5 options.MaxBackoff = 3 * time.Second }) + + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } @@ -287,6 +289,10 @@ func (c *CachingFactory) createTaggingClient(regionConfig *aws.Config) *resource return resourcegroupstaggingapi.NewFromConfig(*regionConfig, func(options *resourcegroupstaggingapi.Options) { if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody + + // The FIPS setting is ignored because FIPS is not available for resource groups tagging apis + // If enabled the SDK will try to use non-existent FIPS URLs, https://github.com/aws/aws-sdk-go-v2/issues/2138#issuecomment-1570791988 + // AWS FIPS Reference: https://aws.amazon.com/compliance/fips/ } }) } @@ -295,6 +301,12 @@ func (c *CachingFactory) createAutoScalingClient(assumedConfig *aws.Config) *aut return autoscaling.NewFromConfig(*assumedConfig, func(options *autoscaling.Options) { if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody + + // The FIPS setting is ignored because FIPS is not available for EC2 autoscaling apis + // If enabled the SDK will try to use non-existent FIPS URLs, https://github.com/aws/aws-sdk-go-v2/issues/2138#issuecomment-1570791988 + // AWS FIPS Reference: https://aws.amazon.com/compliance/fips/ + // EC2 autoscaling has FIPS compliant URLs for govcloud, but they do not use any FIPS prefixing. + // Tests ensure that this configuration will produce the correct URLs for the govcloud regions } }) } @@ -304,6 +316,9 @@ func (c *CachingFactory) createEC2Client(assumedConfig *aws.Config) *ec2.Client if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody } + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } @@ -312,6 +327,9 @@ func (c *CachingFactory) createDMSClient(assumedConfig *aws.Config) *databasemig if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody } + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } @@ -320,6 +338,9 @@ func (c *CachingFactory) createAPIGatewayClient(assumedConfig *aws.Config) *apig if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody } + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } @@ -328,6 +349,9 @@ func (c *CachingFactory) createAPIGatewayV2Client(assumedConfig *aws.Config) *ap if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody } + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } @@ -336,6 +360,9 @@ func (c *CachingFactory) createStorageGatewayClient(assumedConfig *aws.Config) * if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody } + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } @@ -344,6 +371,10 @@ func (c *CachingFactory) createPrometheusClient(assumedConfig *aws.Config) *amp. if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody } + + // The FIPS setting is ignored because FIPS is not available for amp apis + // If enabled the SDK will try to use non-existent FIPS URLs, https://github.com/aws/aws-sdk-go-v2/issues/2138#issuecomment-1570791988 + // AWS FIPS Reference: https://aws.amazon.com/compliance/fips/ }) } @@ -352,6 +383,9 @@ func (c *CachingFactory) createStsClient(awsConfig *aws.Config) *sts.Client { if c.stsRegion != "" { options.Region = c.stsRegion } + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } @@ -360,6 +394,9 @@ func (c *CachingFactory) createShieldClient(awsConfig *aws.Config) *shield.Clien if c.logger.IsDebugEnabled() { options.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody } + if c.fipsEnabled { + options.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled + } }) } diff --git a/pkg/clients/v2/factory_test.go b/pkg/clients/v2/factory_test.go index 6472d2bf..09bf6c2a 100644 --- a/pkg/clients/v2/factory_test.go +++ b/pkg/clients/v2/factory_test.go @@ -3,10 +3,18 @@ package v2 import ( "context" "os" + "reflect" "testing" + "unsafe" "github.com/aws/aws-sdk-go-v2/aws" - aws_config "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/amp" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigatewayv2" + "github.com/aws/aws-sdk-go-v2/service/autoscaling" + "github.com/aws/aws-sdk-go-v2/service/databasemigrationservice" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +24,17 @@ import ( "github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/model" ) +var configWithDefaultRoleAndRegion1 = config.ScrapeConf{ + Discovery: config.Discovery{ + Jobs: []*config.Job{ + { + Roles: []config.Role{{}}, + Regions: []string{"region1"}, + }, + }, + }, +} + func TestNewClientCache_initializes_clients(t *testing.T) { role1 := config.Role{ RoleArn: "role1", @@ -129,53 +148,14 @@ func TestNewClientCache_initializes_clients(t *testing.T) { } } -func TestNewClientCache_sets_fips(t *testing.T) { - config := config.ScrapeConf{ - Discovery: config.Discovery{ - ExportedTagsOnMetrics: nil, - Jobs: []*config.Job{ - { - Roles: []config.Role{{}}, - Regions: []string{"region1"}, - }, - }, - }, - } - output, err := NewFactory(config, true, logging.NewNopLogger()) - require.NoError(t, err) - - clients := output.clients[defaultRole]["region1"] - assert.NotNil(t, clients) - - foundLoadOptions := false - for _, sources := range clients.awsConfig.ConfigSources { - options, ok := sources.(aws_config.LoadOptions) - if !ok { - continue - } - foundLoadOptions = true - assert.Equal(t, aws.FIPSEndpointStateEnabled, options.UseFIPSEndpoint) - } - assert.True(t, foundLoadOptions) -} - func TestNewClientCache_sets_endpoint_override(t *testing.T) { - config := config.ScrapeConf{ - Discovery: config.Discovery{ - ExportedTagsOnMetrics: nil, - Jobs: []*config.Job{ - { - Roles: []config.Role{{}}, - Regions: []string{"region1"}, - }, - }, - }, - } - err := os.Setenv("AWS_ENDPOINT_URL", "https://totallynotaws.com") require.NoError(t, err) - output, err := NewFactory(config, false, logging.NewNopLogger()) + output, err := NewFactory(configWithDefaultRoleAndRegion1, false, logging.NewNopLogger()) + require.NoError(t, err) + + err = os.Unsetenv("AWS_ENDPOINT_URL") require.NoError(t, err) clients := output.clients[defaultRole]["region1"] @@ -213,19 +193,7 @@ func TestClientCache_Clear(t *testing.T) { func TestClientCache_Refresh(t *testing.T) { t.Run("creates all clients when config contains only discovery jobs", func(t *testing.T) { - config := config.ScrapeConf{ - Discovery: config.Discovery{ - ExportedTagsOnMetrics: nil, - Jobs: []*config.Job{ - { - Roles: []config.Role{{}}, - Regions: []string{"region1"}, - }, - }, - }, - } - - output, err := NewFactory(config, false, logging.NewNopLogger()) + output, err := NewFactory(configWithDefaultRoleAndRegion1, false, logging.NewNopLogger()) require.NoError(t, err) output.Refresh() @@ -415,6 +383,131 @@ func TestClientCache_GetTaggingClient(t *testing.T) { }) } +func TestClientCache_createTaggingClient_DoesNotEnableFIPS(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createTaggingClient(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[resourcegroupstaggingapi.Client, resourcegroupstaggingapi.Options](client) + require.NotNil(t, options) + + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateUnset) +} + +func TestClientCache_createAutoScalingClient(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createAutoScalingClient(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[autoscaling.Client, autoscaling.Options](client) + require.NotNil(t, options) + + t.Run("Does not enable FIPS", func(t *testing.T) { + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateUnset) + }) + + t.Run("Can resolve govcloud urls", func(t *testing.T) { + endpoint, err := options.EndpointResolver.ResolveEndpoint("us-gov-east-1", options.EndpointOptions) + assert.NoError(t, err) + assert.Equal(t, "https://autoscaling.us-gov-east-1.amazonaws.com", endpoint.URL) + + endpoint, err = options.EndpointResolver.ResolveEndpoint("us-gov-west-1", options.EndpointOptions) + assert.NoError(t, err) + assert.Equal(t, "https://autoscaling.us-gov-west-1.amazonaws.com", endpoint.URL) + }) +} + +func TestClientCache_createEC2Client_EnablesFIPS(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createEC2Client(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[ec2.Client, ec2.Options](client) + require.NotNil(t, options) + + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateEnabled) +} + +func TestClientCache_createDMSClient_EnablesFIPS(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createDMSClient(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[databasemigrationservice.Client, databasemigrationservice.Options](client) + require.NotNil(t, options) + + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateEnabled) +} + +func TestClientCache_createAPIGatewayClient_EnablesFIPS(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createAPIGatewayClient(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[apigateway.Client, apigateway.Options](client) + require.NotNil(t, options) + + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateEnabled) +} + +func TestClientCache_createAPIGatewayV2Client_EnablesFIPS(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createAPIGatewayV2Client(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[apigatewayv2.Client, apigatewayv2.Options](client) + require.NotNil(t, options) + + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateEnabled) +} + +func TestClientCache_createStorageGatewayClient_EnablesFIPS(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createAPIGatewayV2Client(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[apigatewayv2.Client, apigatewayv2.Options](client) + require.NotNil(t, options) + + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateEnabled) +} + +func TestClientCache_createPrometheusClient_DoesNotEnableFIPS(t *testing.T) { + factory, err := NewFactory(configWithDefaultRoleAndRegion1, true, logging.NewNopLogger()) + require.NoError(t, err) + + client := factory.createPrometheusClient(factory.clients[defaultRole]["region1"].awsConfig) + require.NotNil(t, client) + + options := getOptions[amp.Client, amp.Options](client) + require.NotNil(t, options) + + assert.Equal(t, options.EndpointOptions.UseFIPSEndpoint, aws.FIPSEndpointStateUnset) +} + +// getOptions uses reflection to pull the unexported options field off of any AWS Client +// the options of the client carries around a lot of info about how the client will behave and is helpful for +// testing lower level sdk configuration +func getOptions[T any, V any](awsClient *T) V { + field := reflect.ValueOf(awsClient).Elem().FieldByName("options") + options := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface().(V) + return options +} + type testClient struct{} func (t testClient) GetResources(_ context.Context, _ *config.Job, _ string) ([]*model.TaggedResource, error) {