Skip to content

Commit 952ca94

Browse files
authored
Merge pull request #751 from hsagnik/feat/aws-keyless-irsa
feat(aws): support keyless authentication (IRSA / instance profile)
2 parents c3dc404 + 3c530cd commit 952ca94

4 files changed

Lines changed: 96 additions & 12 deletions

File tree

PROVIDERS.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ Amazon Web Services can be integrated by using the following configuration block
99
provider: aws
1010
# id is the name defined by user for filtering (optional)
1111
id: staging
12-
# aws_access_key is the access key for AWS account
12+
# aws_access_key is the access key for AWS account (optional - see "Keyless Authentication" below)
1313
aws_access_key: $AWS_ACCESS_KEY
14-
# aws_secret_key is the secret key for AWS account
14+
# aws_secret_key is the secret key for AWS account (optional - see "Keyless Authentication" below)
1515
aws_secret_key: $AWS_SECRET_KEY
1616
# aws_session_token session token for temporary security credentials retrieved via STS (optional)
1717
aws_session_token: $AWS_SESSION_TOKEN
@@ -31,6 +31,24 @@ Amazon Web Services can be integrated by using the following configuration block
3131
3232
`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.
3333

34+
#### Keyless Authentication (IRSA / Instance Profile / Environment)
35+
36+
`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:
37+
38+
- **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.
39+
- **EC2 / ECS instance profiles** — credentials are resolved from the instance/task metadata endpoint.
40+
- **Environment variables** — `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`.
41+
- **Shared config / profile** — `~/.aws/credentials` and `~/.aws/config`.
42+
43+
```yaml
44+
# Zero-key config relying on IRSA / instance profile / environment
45+
- provider: aws
46+
id: keyless-discovery
47+
# no aws_access_key / aws_secret_key — credentials resolved by the SDK default chain
48+
```
49+
50+
> 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.
51+
3452
Scopes Required -
3553
1. EC2
3654
2. Route53

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Cloudlist is a multi-cloud tool for getting Assets from Cloud Providers. This is
3838

3939
- List Cloud assets with multiple configurations
4040
- Multiple Cloud providers support
41+
- Keyless authentication support (AWS IRSA / instance profiles, GCP workload identity)
4142
- Multiple output format support
4243
- Multiple filters support
4344
- Highly extensible making adding new providers a breeze

pkg/providers/aws/aws.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,20 @@ type ProviderOptions struct {
4949

5050
func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error {
5151
p.Id, _ = block.GetMetadata("id")
52-
accessKey, ok := block.GetMetadata(apiAccessKey)
53-
if !ok {
54-
return &schema.ErrNoSuchKey{Name: apiAccessKey}
55-
}
56-
accessToken, ok := block.GetMetadata(apiSecretKey)
57-
if !ok {
58-
return &schema.ErrNoSuchKey{Name: apiSecretKey}
52+
// aws_access_key/aws_secret_key are optional. When both are omitted the
53+
// provider falls back to the AWS SDK default credential chain, which
54+
// supports keyless auth such as IRSA (IAM Roles for Service Accounts),
55+
// EC2/ECS instance profiles and AWS_* environment variables.
56+
accessKey, _ := block.GetMetadata(apiAccessKey)
57+
secretKey, _ := block.GetMetadata(apiSecretKey)
58+
// If one of the static-credential pair is set, both must be set;
59+
// a half-configured pair is almost always a mistake.
60+
if (accessKey == "") != (secretKey == "") {
61+
return errors.Errorf("both %s and %s must be provided together", apiAccessKey, apiSecretKey)
5962
}
6063
p.Token, _ = block.GetMetadata(sessionToken)
6164
p.AccessKey = accessKey
62-
p.SecretKey = accessToken
65+
p.SecretKey = secretKey
6366

6467
if assumeRoleArn, ok := block.GetMetadata(assumeRoleArn); ok {
6568
p.AssumeRoleArn = assumeRoleArn
@@ -156,7 +159,15 @@ func New(block schema.OptionBlock) (*Provider, error) {
156159
provider := &Provider{options: options}
157160
config := aws.NewConfig()
158161
config.WithRegion("us-east-1")
159-
config.WithCredentials(credentials.NewStaticCredentials(options.AccessKey, options.SecretKey, options.Token))
162+
// Only set static credentials when explicitly provided. Otherwise leave
163+
// config.Credentials nil so session.NewSession resolves credentials via the
164+
// SDK default chain (env vars, shared config, IRSA web identity, EC2/ECS
165+
// instance profile), enabling keyless authentication.
166+
if options.AccessKey != "" && options.SecretKey != "" {
167+
config.WithCredentials(credentials.NewStaticCredentials(options.AccessKey, options.SecretKey, options.Token))
168+
} else {
169+
gologger.Verbose().Msgf("[aws] No static credentials configured for %q; using AWS SDK default credential chain (IRSA / instance profile / environment / shared config)", options.Id)
170+
}
160171

161172
var sess *session.Session
162173
var err error
@@ -502,7 +513,6 @@ func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) {
502513
assignWorker(cloudfrontProvider.GetResource)
503514
}
504515

505-
506516
go func() {
507517
workersWaitGroup.Wait()
508518
close(results)

pkg/providers/aws/aws_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package aws
2+
3+
import (
4+
"testing"
5+
6+
"github.com/projectdiscovery/cloudlist/pkg/schema"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// TestParseOptionBlock covers the static-credential pair validation that backs
11+
// keyless authentication: both keys present is valid, both omitted is valid
12+
// (keyless), and a half-configured pair is rejected.
13+
func TestParseOptionBlock(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
block schema.OptionBlock
17+
wantErr bool
18+
wantAccessKey string
19+
wantSecretKey string
20+
}{
21+
{
22+
name: "both keys present",
23+
block: schema.OptionBlock{"aws_access_key": "AKIAEXAMPLE", "aws_secret_key": "secret"},
24+
wantAccessKey: "AKIAEXAMPLE",
25+
wantSecretKey: "secret",
26+
},
27+
{
28+
name: "both keys omitted is keyless",
29+
block: schema.OptionBlock{},
30+
},
31+
{
32+
name: "only access key is an error",
33+
block: schema.OptionBlock{"aws_access_key": "AKIAEXAMPLE"},
34+
wantErr: true,
35+
},
36+
{
37+
name: "only secret key is an error",
38+
block: schema.OptionBlock{"aws_secret_key": "secret"},
39+
wantErr: true,
40+
},
41+
}
42+
for _, tt := range tests {
43+
t.Run(tt.name, func(t *testing.T) {
44+
var opts ProviderOptions
45+
err := opts.ParseOptionBlock(tt.block)
46+
if tt.wantErr {
47+
require.Error(t, err)
48+
return
49+
}
50+
require.NoError(t, err)
51+
require.Equal(t, tt.wantAccessKey, opts.AccessKey)
52+
require.Equal(t, tt.wantSecretKey, opts.SecretKey)
53+
})
54+
}
55+
}

0 commit comments

Comments
 (0)