From 0fa619ed533f18a1208cb813da3afb7a7e8e2630 Mon Sep 17 00:00:00 2001 From: Jake Goldsborough Date: Tue, 7 Oct 2025 13:37:10 -0700 Subject: [PATCH] Fix credential chain precedence bug with credential_source = Environment When using a profile with `credential_source = Environment` and `role_arn`, environment credentials were returned directly instead of being used as source credentials for role assumption. The issue occurred because `env_credentials` (position 7 in the credential provider chain) would return environment variables before `assume_role_credentials` (position 10) could use them as source credentials for role assumption. This only affected scenarios where no explicit profile was set on the client, causing the default profile to be used. Changes: - Modified `env_credentials` to check if the active profile is configured with `credential_source = Environment` and return nil if so, allowing the chain to continue to role assumption logic - Added `profile_uses_env_as_credential_source?` helper method to SharedConfig to detect this configuration - Added test case that reproduces the bug and verifies the fix The fix ensures environment credentials are properly used as source credentials for role assumption when configured, rather than being returned directly by the credential chain. Fixes #3301 --- .../aws-sdk-core/credential_provider_chain.rb | 13 ++++- .../lib/aws-sdk-core/shared_config.rb | 20 +++++++ .../aws/credential_resolution_chain_spec.rb | 53 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb index 25163f33e1c..73d86580f52 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb @@ -99,11 +99,22 @@ def static_profile_process_credentials(options) nil end - def env_credentials(_options) + def env_credentials(options) key = %w[AWS_ACCESS_KEY_ID AMAZON_ACCESS_KEY_ID AWS_ACCESS_KEY] secret = %w[AWS_SECRET_ACCESS_KEY AMAZON_SECRET_ACCESS_KEY AWS_SECRET_KEY] token = %w[AWS_SESSION_TOKEN AMAZON_SESSION_TOKEN] account_id = %w[AWS_ACCOUNT_ID] + + # Don't return env creds directly if they're meant to be used as source credentials + # for assume role with credential_source = Environment + if Aws.shared_config.config_enabled? + profile_name = options[:config] ? options[:config].profile : nil + profile_name ||= determine_profile_name(options) + if Aws.shared_config.profile_uses_env_as_credential_source?(profile_name) + return nil + end + end + creds = Credentials.new( envar(key), envar(secret), diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb b/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb index b270b6ba12c..0756a52754d 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb @@ -236,6 +236,26 @@ def self.config_reader(*attrs) :ignore_configured_endpoint_urls ) + # Check if a profile is configured for assume role with credential_source = Environment + # This is used to determine if environment credentials should be used as source credentials + # for role assumption rather than returned directly. + # @param profile [String] the profile name to check + # @return [Boolean] true if the profile has both role_arn and credential_source = Environment + def profile_uses_env_as_credential_source?(profile) + return false unless @config_enabled && profile + + # Check both credentials and config files - credentials takes precedence + prof_cfg = @parsed_credentials.fetch(profile, {}) if @parsed_credentials + # Only check config if credentials didn't have the keys we need + if prof_cfg.nil? || (!prof_cfg['role_arn'] && @parsed_config) + prof_cfg = @parsed_config.fetch(profile, {}) + end + + prof_cfg && + prof_cfg['role_arn'] && + prof_cfg['credential_source'] == 'Environment' + end + private # Get a config value from from shared credential/config files. diff --git a/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb b/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb index 80a57339351..9b18af0a371 100644 --- a/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb +++ b/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb @@ -1009,6 +1009,59 @@ module Aws region: 'us-east-1' ) end + + it 'assumes role when default profile has credential_source=Environment, not return env vars directly' do + stub_const( + 'ENV', + 'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB', + 'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB' + ) + + # Create empty credentials file (no default profile) + temp_credentials = Tempfile.new(['aws_credentials', '.ini']) + temp_credentials.write("") + temp_credentials.close + + # Create config file with default profile using credential_source = Environment + temp_config = Tempfile.new(['aws_config', '.ini']) + temp_config.write(<<~CONFIG) + [default] + region = us-east-1 + role_arn = arn:aws:iam::123456789012:role/foo + credential_source = Environment + CONFIG + temp_config.close + + # Reload config with temp files + Aws.shared_config.fresh( + config_enabled: true, + credentials_path: temp_credentials.path, + config_path: temp_config.path + ) + + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'AKID_ENV_STUB', # Source creds + 'AR_AKID', # Assumed role creds + 'AR_SECRET', + 'AR_TOKEN' + ) + + # Don't specify profile - should use default profile + client = ApiHelper.sample_rest_xml::Client.new( + region: 'us-east-1' + ) + creds = client.config.credentials.credentials + + # Should use assumed role credentials + expect(creds.access_key_id).to eq('AR_AKID') + # Should NOT return env credentials directly + expect(creds.access_key_id).not_to eq('AKID_ENV_STUB') + + # Cleanup + temp_config.unlink + temp_credentials.unlink + end end describe 'AWS_SDK_CONFIG_OPT_OUT set' do