From ef682e21cdd9802d9aa7c79aa253a564ffaae627 Mon Sep 17 00:00:00 2001 From: Sagnik Haldar Date: Fri, 12 Jun 2026 11:13:57 +0530 Subject: [PATCH 1/2] feat(aws): support keyless authentication (IRSA / instance profile) Make aws_access_key/aws_secret_key optional. When omitted, fall back to the AWS SDK default credential chain so IRSA, EC2/ECS instance profiles, env vars and shared config work without injecting static creds into the provider config. --- PROVIDERS.md | 22 ++++++++++++++++++++-- README.md | 1 + pkg/providers/aws/aws.go | 25 +++++++++++++++++-------- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/PROVIDERS.md b/PROVIDERS.md index a617124..3f094e3 100644 --- a/PROVIDERS.md +++ b/PROVIDERS.md @@ -9,9 +9,9 @@ Amazon Web Services can be integrated by using the following configuration block provider: aws # id is the name defined by user for filtering (optional) id: staging - # aws_access_key is the access key for AWS account + # aws_access_key is the access key for AWS account (optional - see "Keyless Authentication" below) aws_access_key: $AWS_ACCESS_KEY - # aws_secret_key is the secret key for AWS account + # aws_secret_key is the secret key for AWS account (optional - see "Keyless Authentication" below) aws_secret_key: $AWS_SECRET_KEY # aws_session_token session token for temporary security credentials retrieved via STS (optional) aws_session_token: $AWS_SESSION_TOKEN @@ -31,6 +31,24 @@ Amazon Web Services can be integrated by using the following configuration block `aws_access_key` and `aws_secret_key` can be generated in the IAM console. We recommend creating a new IAM user with `Read Only` permissions and providing the access token for the user. +#### Keyless Authentication (IRSA / Instance Profile / Environment) + +`aws_access_key` and `aws_secret_key` are **optional**. When both are omitted, the provider falls back to the AWS SDK default credential chain, so no secrets need to be injected into the config file. This enables: + +- **IRSA (IAM Roles for Service Accounts)** on EKS — credentials are picked up automatically from the `AWS_WEB_IDENTITY_TOKEN_FILE` and `AWS_ROLE_ARN` environment variables injected by the pod identity webhook. +- **EC2 / ECS instance profiles** — credentials are resolved from the instance/task metadata endpoint. +- **Environment variables** — `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`. +- **Shared config / profile** — `~/.aws/credentials` and `~/.aws/config`. + +```yaml +# Zero-key config relying on IRSA / instance profile / environment +- provider: aws + id: keyless-discovery + # no aws_access_key / aws_secret_key — credentials resolved by the SDK default chain +``` + +> Note: if you provide one of `aws_access_key` / `aws_secret_key`, you must provide both. `assume_role_arn`, `assume_role_name` and `org_discovery_role_arn` all work on top of keyless base credentials too. + Scopes Required - 1. EC2 2. Route53 diff --git a/README.md b/README.md index a13cc05..2db8a3f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Cloudlist is a multi-cloud tool for getting Assets from Cloud Providers. This is - List Cloud assets with multiple configurations - Multiple Cloud providers support + - Keyless authentication support (AWS IRSA / instance profiles, GCP workload identity) - Multiple output format support - Multiple filters support - Highly extensible making adding new providers a breeze diff --git a/pkg/providers/aws/aws.go b/pkg/providers/aws/aws.go index 630e02a..764c463 100644 --- a/pkg/providers/aws/aws.go +++ b/pkg/providers/aws/aws.go @@ -49,13 +49,16 @@ type ProviderOptions struct { func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { p.Id, _ = block.GetMetadata("id") - accessKey, ok := block.GetMetadata(apiAccessKey) - if !ok { - return &schema.ErrNoSuchKey{Name: apiAccessKey} - } - accessToken, ok := block.GetMetadata(apiSecretKey) - if !ok { - return &schema.ErrNoSuchKey{Name: apiSecretKey} + // aws_access_key/aws_secret_key are optional. When both are omitted the + // provider falls back to the AWS SDK default credential chain, which + // supports keyless auth such as IRSA (IAM Roles for Service Accounts), + // EC2/ECS instance profiles and AWS_* environment variables. + accessKey, _ := block.GetMetadata(apiAccessKey) + accessToken, _ := block.GetMetadata(apiSecretKey) + // If one of the static-credential pair is set, both must be set; + // a half-configured pair is almost always a mistake. + if (accessKey == "") != (accessToken == "") { + return errors.Errorf("both %s and %s must be provided together", apiAccessKey, apiSecretKey) } p.Token, _ = block.GetMetadata(sessionToken) p.AccessKey = accessKey @@ -156,7 +159,13 @@ func New(block schema.OptionBlock) (*Provider, error) { provider := &Provider{options: options} config := aws.NewConfig() config.WithRegion("us-east-1") - config.WithCredentials(credentials.NewStaticCredentials(options.AccessKey, options.SecretKey, options.Token)) + // Only set static credentials when explicitly provided. Otherwise leave + // config.Credentials nil so session.NewSession resolves credentials via the + // SDK default chain (env vars, shared config, IRSA web identity, EC2/ECS + // instance profile), enabling keyless authentication. + if options.AccessKey != "" && options.SecretKey != "" { + config.WithCredentials(credentials.NewStaticCredentials(options.AccessKey, options.SecretKey, options.Token)) + } var sess *session.Session var err error From 3c530cdf49d4571cd38553ebdb5678d88c41ade8 Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Wed, 17 Jun 2026 08:32:39 +0200 Subject: [PATCH 2/2] review fixes --- PROVIDERS.md | 2 +- pkg/providers/aws/aws.go | 9 +++--- pkg/providers/aws/aws_test.go | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 pkg/providers/aws/aws_test.go diff --git a/PROVIDERS.md b/PROVIDERS.md index 3f094e3..d57ae3e 100644 --- a/PROVIDERS.md +++ b/PROVIDERS.md @@ -33,7 +33,7 @@ Amazon Web Services can be integrated by using the following configuration block #### Keyless Authentication (IRSA / Instance Profile / Environment) -`aws_access_key` and `aws_secret_key` are **optional**. When both are omitted, the provider falls back to the AWS SDK default credential chain, so no secrets need to be injected into the config file. This enables: +`aws_access_key` and `aws_secret_key` are **optional**. When both are omitted from the config, the provider falls back to the AWS SDK default credential chain, so no secrets need to be injected into the config file. To use keyless mode, leave the keys out entirely (an `aws_access_key: $UNSET_ENV_VAR` whose environment variable is unset is treated as a literal value, not as keyless). This enables: - **IRSA (IAM Roles for Service Accounts)** on EKS — credentials are picked up automatically from the `AWS_WEB_IDENTITY_TOKEN_FILE` and `AWS_ROLE_ARN` environment variables injected by the pod identity webhook. - **EC2 / ECS instance profiles** — credentials are resolved from the instance/task metadata endpoint. diff --git a/pkg/providers/aws/aws.go b/pkg/providers/aws/aws.go index 764c463..8d30630 100644 --- a/pkg/providers/aws/aws.go +++ b/pkg/providers/aws/aws.go @@ -54,15 +54,15 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { // supports keyless auth such as IRSA (IAM Roles for Service Accounts), // EC2/ECS instance profiles and AWS_* environment variables. accessKey, _ := block.GetMetadata(apiAccessKey) - accessToken, _ := block.GetMetadata(apiSecretKey) + secretKey, _ := block.GetMetadata(apiSecretKey) // If one of the static-credential pair is set, both must be set; // a half-configured pair is almost always a mistake. - if (accessKey == "") != (accessToken == "") { + if (accessKey == "") != (secretKey == "") { return errors.Errorf("both %s and %s must be provided together", apiAccessKey, apiSecretKey) } p.Token, _ = block.GetMetadata(sessionToken) p.AccessKey = accessKey - p.SecretKey = accessToken + p.SecretKey = secretKey if assumeRoleArn, ok := block.GetMetadata(assumeRoleArn); ok { p.AssumeRoleArn = assumeRoleArn @@ -165,6 +165,8 @@ func New(block schema.OptionBlock) (*Provider, error) { // instance profile), enabling keyless authentication. if options.AccessKey != "" && options.SecretKey != "" { config.WithCredentials(credentials.NewStaticCredentials(options.AccessKey, options.SecretKey, options.Token)) + } else { + gologger.Verbose().Msgf("[aws] No static credentials configured for %q; using AWS SDK default credential chain (IRSA / instance profile / environment / shared config)", options.Id) } var sess *session.Session @@ -511,7 +513,6 @@ func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) { assignWorker(cloudfrontProvider.GetResource) } - go func() { workersWaitGroup.Wait() close(results) diff --git a/pkg/providers/aws/aws_test.go b/pkg/providers/aws/aws_test.go new file mode 100644 index 0000000..8d4afe1 --- /dev/null +++ b/pkg/providers/aws/aws_test.go @@ -0,0 +1,55 @@ +package aws + +import ( + "testing" + + "github.com/projectdiscovery/cloudlist/pkg/schema" + "github.com/stretchr/testify/require" +) + +// TestParseOptionBlock covers the static-credential pair validation that backs +// keyless authentication: both keys present is valid, both omitted is valid +// (keyless), and a half-configured pair is rejected. +func TestParseOptionBlock(t *testing.T) { + tests := []struct { + name string + block schema.OptionBlock + wantErr bool + wantAccessKey string + wantSecretKey string + }{ + { + name: "both keys present", + block: schema.OptionBlock{"aws_access_key": "AKIAEXAMPLE", "aws_secret_key": "secret"}, + wantAccessKey: "AKIAEXAMPLE", + wantSecretKey: "secret", + }, + { + name: "both keys omitted is keyless", + block: schema.OptionBlock{}, + }, + { + name: "only access key is an error", + block: schema.OptionBlock{"aws_access_key": "AKIAEXAMPLE"}, + wantErr: true, + }, + { + name: "only secret key is an error", + block: schema.OptionBlock{"aws_secret_key": "secret"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var opts ProviderOptions + err := opts.ParseOptionBlock(tt.block) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.wantAccessKey, opts.AccessKey) + require.Equal(t, tt.wantSecretKey, opts.SecretKey) + }) + } +}