Skip to content

Fall back to default AWS credential chain when specified profile is not found#1098

Merged
yxxhero merged 5 commits into
mainfrom
copilot/fix-aws-credential-fallback
Apr 7, 2026
Merged

Fall back to default AWS credential chain when specified profile is not found#1098
yxxhero merged 5 commits into
mainfrom
copilot/fix-aws-credential-fallback

Conversation

Copilot AI commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

When an AWS profile is specified (e.g. ref+awssecrets://mysecret?profile=dev) but doesn't exist in ~/.aws/config, vals panics with failed to get shared config profile, dev. This breaks EC2 users who rely on instance IAM roles and have no local AWS config.

Changes

  • pkg/awsclicompat/session.go: In newConfig, catch config.SharedConfigProfileNotExistError and retry LoadDefaultConfig without the profile option, allowing the SDK's default credential chain (env vars, EC2 instance metadata, etc.) to take over. Extracted buildBaseOpts to share region/endpoint/log-level options between the initial load and the fallback.
  • pkg/awsclicompat/session_test.go: Added TestNewConfigProfileNotFoundFallback verifying that a non-existent profile silently falls back to default credentials.

Behavior

# Works for both profile-configured users AND EC2 instance profile users
mysecret: ref+awssecrets://mysecret?profile=dev
  • Profile exists → uses profile credentials (unchanged behavior)
  • Profile not found → falls back to default credential chain (EC2 instance profile, env vars, etc.) instead of panicking

Copilot AI linked an issue Apr 7, 2026 that may be closed by this pull request
When an AWS profile is specified (e.g. via `profile=dev` query parameter)
but does not exist in ~/.aws/config, instead of panicking with
"failed to get shared config profile, dev", the provider now falls back
to the default AWS credential chain (EC2 instance profile, environment
variables, etc.).

- Modified newConfig in pkg/awsclicompat/session.go to catch
  config.SharedConfigProfileNotExistError and retry loading config
  without the profile option
- Extracted buildBaseOpts helper to avoid duplicating region/endpoint/
  log-level option building
- Added TestNewConfigProfileNotFoundFallback test

Agent-Logs-Url: https://github.com/helmfile/vals/sessions/5d5f9e0c-bca5-4adf-a30c-0960ae95e7fe

Co-authored-by: yxxhero <[email protected]>
Copilot AI changed the title [WIP] Fix AWS credential fallback for EC2 instance profile Fall back to default AWS credential chain when specified profile is not found Apr 7, 2026
Copilot AI requested a review from yxxhero April 7, 2026 22:46
@yxxhero yxxhero requested a review from Copilot April 7, 2026 22:47
@yxxhero yxxhero marked this pull request as ready for review April 7, 2026 22:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the AWS SDK config-loading logic used by vals’ AWS-backed providers so that specifying a non-existent AWS profile no longer causes a panic, and instead falls back to the AWS SDK’s default credential chain (env/IMDS/etc.), improving compatibility for EC2/IAM-role environments.

Changes:

  • Add a profile-not-found fallback path in newConfig when LoadDefaultConfig fails due to a missing shared config profile.
  • Refactor shared AWS config options (region/endpoint/log-level) into buildBaseOpts for reuse across the initial load and fallback load.
  • Add a unit test intended to verify the fallback behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
pkg/awsclicompat/session.go Implements the missing-profile fallback and extracts shared LoadDefaultConfig options into buildBaseOpts.
pkg/awsclicompat/session_test.go Adds a test for the missing-profile fallback behavior in newConfig.

Comment thread pkg/awsclicompat/session.go Outdated
Comment on lines +145 to +161
switch {
case profile != "":
opts = append(opts, config.WithSharedConfigProfile(profile))
opts = append(baseOpts, config.WithSharedConfigProfile(profile))
case os.Getenv("FORCE_AWS_PROFILE") == "true":
if awsProfile := os.Getenv("AWS_PROFILE"); awsProfile != "" {
opts = append(opts, config.WithSharedConfigProfile(awsProfile))
opts = append(baseOpts, config.WithSharedConfigProfile(awsProfile))
}
}

cfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
// If the specified profile doesn't exist, fall back to default credentials
// (e.g. EC2 instance profile, environment variables, etc.)
var profileNotExist config.SharedConfigProfileNotExistError
if profile != "" && errors.As(err, &profileNotExist) {
return config.LoadDefaultConfig(ctx, baseOpts...)
}

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The profile-not-found fallback only triggers when the profile parameter is non-empty. If the profile was selected via FORCE_AWS_PROFILE=true + AWS_PROFILE, LoadDefaultConfig can still return SharedConfigProfileNotExistError and NewSession will panic. Consider tracking the effective selected profile name (param or env) and applying the same fallback (or update the comment to clarify fallback only applies to the explicit profile argument).

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +195
func TestNewConfigProfileNotFoundFallback(t *testing.T) {
// Use a profile name that is guaranteed not to exist on this machine.
const nonExistentProfile = "vals-test-profile-does-not-exist-12345"

ctx := context.Background()
cfg, err := newConfig(ctx, "", nonExistentProfile, "")
if err != nil {
t.Fatalf("newConfig with non-existent profile should fall back to default credentials, got error: %v", err)
}

// The returned config must have a non-nil credentials provider, which indicates
// the fallback to the default credential chain succeeded.
if cfg.Credentials == nil {
t.Error("expected cfg.Credentials to be non-nil after profile fallback")
}
}

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is not hermetic: it relies on a "guaranteed not to exist" profile name and on the machine/CI environment having a resolvable region and no conflicting AWS env vars. To make it deterministic, set AWS_CONFIG_FILE/AWS_SHARED_CREDENTIALS_FILE to temp empty files (or temp files without the profile), clear AWS_PROFILE/FORCE_AWS_PROFILE/AWS_SDK_LOAD_CONFIG, and pass an explicit region (e.g. us-east-1) so LoadDefaultConfig doesn’t depend on external config/IMDS.

Copilot uses AI. Check for mistakes.
@yxxhero

yxxhero commented Apr 7, 2026

Copy link
Copy Markdown
Member

@copilot fix comments and optimize code.

…ation

- Make TestNewConfigProfileNotFoundFallback hermetic: point to empty
  temp AWS config/credentials files and clear conflicting env vars
  (AWS_PROFILE, FORCE_AWS_PROFILE, AWS_ACCESS_KEY_ID, etc.)
- Track effectiveProfile explicitly to consolidate profile selection
  from both the explicit param and FORCE_AWS_PROFILE paths
- Restrict fallback to explicit `profile` param only (not FORCE_AWS_PROFILE),
  since the AWS SDK reads AWS_PROFILE from env even in the fallback load;
  document this in the function comment
- Remove TestNewConfigForceProfileNotFoundFallback (behaviour unchanged)

Agent-Logs-Url: https://github.com/helmfile/vals/sessions/55a70500-5118-449e-8a12-4142cc9e80f2

Co-authored-by: yxxhero <[email protected]>

Copilot AI commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix comments and optimize code.

Done in c0db520:

  • Hermetic test: TestNewConfigProfileNotFoundFallback now points to empty temp AWS config/credentials files and clears all conflicting env vars (AWS_PROFILE, FORCE_AWS_PROFILE, AWS_ACCESS_KEY_ID, etc.) plus supplies an explicit region, so it no longer depends on the machine's ~/.aws setup.
  • FORCE_AWS_PROFILE clarification: The newConfig function now tracks effectiveProfile (combining explicit param and FORCE_AWS_PROFILE env path into one place). The fallback is intentionally restricted to the explicit profile param — when FORCE_AWS_PROFILE=true+AWS_PROFILE selects the profile, the AWS SDK still reads AWS_PROFILE from the environment during the fallback LoadDefaultConfig call, which would cause the same error. This limitation is documented in the function comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread pkg/awsclicompat/session.go Outdated
// Build full options including profile selection
opts := baseOpts
if effectiveProfile != "" {
opts = append(baseOpts, config.WithSharedConfigProfile(effectiveProfile))

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In newConfig, the slice append uses append(baseOpts, ...) even though opts is the working slice. While it behaves the same today, it’s easy to misread and can be error-prone if opts is modified later. Prefer appending to opts directly for clarity (and to avoid subtle slice-aliasing surprises).

Suggested change
opts = append(baseOpts, config.WithSharedConfigProfile(effectiveProfile))
opts = append(opts, config.WithSharedConfigProfile(effectiveProfile))

Copilot uses AI. Check for mistakes.
Comment thread pkg/awsclicompat/session_test.go Outdated
Comment on lines +216 to +220
// The returned config must have a non-nil credentials provider, which indicates
// the fallback to the default credential chain succeeded.
if cfg.Credentials == nil {
t.Error("expected cfg.Credentials to be non-nil after profile fallback")
}

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test’s assertion (cfg.Credentials != nil) doesn’t actually prove the profile-not-found fallback was taken—config.LoadDefaultConfig typically populates a credentials provider even when no credentials can be resolved yet, so this can pass even if the fallback logic regresses. Consider first asserting that a direct config.LoadDefaultConfig with config.WithSharedConfigProfile(nonExistentProfile) returns SharedConfigProfileNotExistError, then asserting newConfig returns nil error under the same env setup.

Copilot uses AI. Check for mistakes.
@yxxhero

yxxhero commented Apr 7, 2026

Copy link
Copy Markdown
Member

@copilot fix comments and optimize code.

…ertion

- Use append(opts, ...) instead of append(baseOpts, ...) to avoid
  subtle slice-aliasing surprises if opts is modified later
- Replace weak cfg.Credentials != nil assertion with a two-step check:
  first confirm direct LoadDefaultConfig returns SharedConfigProfileNotExistError
  (proving the fallback code path is actually needed), then assert
  newConfig returns nil error (proving the fallback is taken)
- Rename second err to fallbackErr for clarity
- Add errors and awsconfig imports to session_test.go

Agent-Logs-Url: https://github.com/helmfile/vals/sessions/5aba2e69-9351-436c-bd3d-7ea90b97f4bd

Co-authored-by: yxxhero <[email protected]>

Copilot AI commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix comments and optimize code.

Done in 46cce94:

  • Slice aliasing fix (session.go): Changed append(baseOpts, ...) to append(opts, ...) so the working slice is always appended to.
  • Stronger test assertion (session_test.go): The test now first confirms that a direct LoadDefaultConfig with the non-existent profile returns SharedConfigProfileNotExistError (proving the fallback code path is actually needed), then asserts newConfig returns nil error (proving the fallback is taken). The weak cfg.Credentials != nil check is removed. The error variable is also renamed to fallbackErr for clarity.

@yxxhero yxxhero requested a review from Copilot April 7, 2026 23:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +157 to +166
cfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
// If the explicit profile parameter doesn't exist, fall back to the default
// credential chain (e.g. EC2 instance profile, environment variables, etc.).
// The fallback is not applied for FORCE_AWS_PROFILE/AWS_PROFILE env vars because
// the AWS SDK would still read AWS_PROFILE from the environment in the fallback load.
var profileNotExist config.SharedConfigProfileNotExistError
if profile != "" && errors.As(err, &profileNotExist) {
return config.LoadDefaultConfig(ctx, baseOpts...)
}

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the profile-not-found fallback you call config.LoadDefaultConfig(ctx, baseOpts...) without explicitly overriding the profile. If AWS_PROFILE is set in the environment (even when FORCE_AWS_PROFILE is not), the SDK will still honor it during the fallback load, which can cause the same SharedConfigProfileNotExistError and defeat the intended EC2/instance-role fallback. Consider explicitly overriding the profile for the fallback load (or otherwise ensuring the fallback ignores AWS_PROFILE), and add a regression test that sets AWS_PROFILE to the missing profile while passing the explicit profile parameter.

Copilot uses AI. Check for mistakes.
@yxxhero

yxxhero commented Apr 7, 2026

Copy link
Copy Markdown
Member

@copilot fix comments and optimize code.

In the profile-not-found fallback, use config.WithSharedConfigProfile("default")
to explicitly override AWS_PROFILE from the environment. Without this override,
the SDK would still honor AWS_PROFILE during the fallback load — if AWS_PROFILE
points to the same missing profile, the fallback would fail with the same
SharedConfigProfileNotExistError and defeat the intended EC2/instance-role fallback.

Also adds TestNewConfigProfileNotFoundFallbackWithAWSProfileEnv, a regression test
that sets AWS_PROFILE to the missing profile while passing the same explicit profile
parameter, proving the fallback now succeeds in that scenario.

Both fallback tests use a config file with a [default] section, reflecting the
realistic setup where a user has ~/.aws/config with a default profile but not the
one referenced in vals.

Agent-Logs-Url: https://github.com/helmfile/vals/sessions/4ccccc13-bfff-49b3-9807-bdbc717ac5c3

Co-authored-by: yxxhero <[email protected]>

Copilot AI commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix comments and optimize code.

Done in 402c52f:

  • session.go: The fallback now appends config.WithSharedConfigProfile("default") to baseOpts, explicitly overriding any AWS_PROFILE from the environment. Previously the bare baseOpts fallback load would still pick up AWS_PROFILE and fail with the same SharedConfigProfileNotExistError if it pointed to a missing profile.
  • session_test.go: Added TestNewConfigProfileNotFoundFallbackWithAWSProfileEnv — sets AWS_PROFILE=nonExistentProfile while passing the same profile as the explicit profile param, then asserts newConfig succeeds. Both fallback tests now use a config file with a [default] section (the realistic scenario: user has ~/.aws/config with a default profile but not the one referenced in vals).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +158 to +170
cfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
// If the explicit profile parameter doesn't exist, fall back to the default
// credential chain (e.g. EC2 instance profile, environment variables, etc.).
// Explicitly set profile to "default" so that AWS_PROFILE from the environment
// is not honored during the fallback (it might be set to the same missing profile
// and cause the same error). Using "default" falls through to the standard credential
// chain (env vars, EC2 IMDS, etc.) when no credentials are configured in the profile.
var profileNotExist config.SharedConfigProfileNotExistError
if profile != "" && errors.As(err, &profileNotExist) {
return config.LoadDefaultConfig(ctx, append(baseOpts, config.WithSharedConfigProfile("default"))...)
}
return aws.Config{}, err

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The profile-not-found path silently falls back to the default credential chain. This can mask typos/misconfiguration of an explicitly requested profile and may result in vals using unintended credentials/accounts. Consider emitting a warning when the fallback triggers (or making the fallback opt-in) so users can detect when their requested profile wasn’t actually used.

Copilot uses AI. Check for mistakes.
@yxxhero yxxhero merged commit 414b117 into main Apr 7, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AWS Credential Fallback to EC2 Instance Profile

3 participants