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
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM gcr.io/distroless/static-debian12:nonroot

COPY baton-aws /baton-aws

ENTRYPOINT ["/baton-aws"]
14 changes: 9 additions & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,18 @@ func ValidateExternalId(input string) error {
// validateConfig is run after the configuration is loaded, and should return an error if it isn't valid.
func ValidateConfig(ctx context.Context, awsc *Aws) error {
if awsc.GetBool(UseAssumeField.FieldName) {
err := ValidateExternalId(awsc.GetString(ExternalIdField.FieldName))
err := connector.IsValidRoleARN(awsc.GetString(RoleArnField.FieldName))
if err != nil {
return err
}
err = connector.IsValidRoleARN(awsc.GetString(RoleArnField.FieldName))
if err != nil {
return err
// Only validate external-id for two-hop mode (when global-role-arn is set)
// Single-hop mode (IRSA → target role) doesn't require external-id
globalRoleArn := awsc.GetString(GlobalRoleArnField.FieldName)
if globalRoleArn != "" {
err = ValidateExternalId(awsc.GetString(ExternalIdField.FieldName))
if err != nil {
return err
}
}
}
return nil
Expand Down Expand Up @@ -153,7 +158,6 @@ var Config = field.NewConfiguration(
UseAssumeField,
},
[]field.SchemaField{
ExternalIdField,
RoleArnField,
},
)),
Expand Down
30 changes: 29 additions & 1 deletion pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,35 @@ func (o *AWS) getCallingConfig(ctx context.Context, region string) (awsSdk.Confi
return o.baseConfig, nil
}
l := ctxzap.Extract(ctx)
// ok, if we are an instance, we do the assumeRole twice, first time from our Instance role, INTO the binding account

// Single-hop mode: when globalRoleARN is empty, assume directly into roleARN
// This supports self-hosted deployments (e.g., EKS with IRSA) that don't need
// an intermediate binding account.
if o.globalRoleARN == "" && o.roleARN != "" {
l.Debug("aws-connector: using single-hop assume role mode",
zap.String("role_arn", o.roleARN),
)
stsSvc := sts.NewFromConfig(o.baseConfig)
callingCreds := awsSdk.NewCredentialsCache(stscreds.NewAssumeRoleProvider(stsSvc, o.roleARN, func(aro *stscreds.AssumeRoleOptions) {
if o.externalID != "" {
aro.ExternalID = awsSdk.String(o.externalID)
}
}))

_, err := callingCreds.Retrieve(ctx)
if err != nil {
return awsSdk.Config{}, fmt.Errorf("aws-connector: failed to assume role into '%s': %w", o.roleARN, err)
}

return awsSdk.Config{
HTTPClient: o.baseClient,
Region: region,
DefaultsMode: awsSdk.DefaultsModeInRegion,
Credentials: callingCreds,
}, nil
}

// Two-hop mode: if we are an instance, we do the assumeRole twice, first time from our Instance role, INTO the binding account
// and from there, into the customer account.
stsSvc := sts.NewFromConfig(o.baseConfig)
bindingCreds := awsSdk.NewCredentialsCache(stscreds.NewAssumeRoleProvider(stsSvc, o.globalRoleARN, func(aro *stscreds.AssumeRoleOptions) {
Expand Down
Loading