Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 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.
- **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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 20 additions & 10 deletions pkg/providers/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,20 @@ 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)
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 == "") != (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
Expand Down Expand Up @@ -156,7 +159,15 @@ 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))
} 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
var err error
Expand Down Expand Up @@ -502,7 +513,6 @@ func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) {
assignWorker(cloudfrontProvider.GetResource)
}


go func() {
workersWaitGroup.Wait()
close(results)
Expand Down
55 changes: 55 additions & 0 deletions pkg/providers/aws/aws_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
Loading