Skip to content

Commit 9a4322a

Browse files
bors[bot]omus
andauthored
Merge #621
621: Update credential precedence to match AWS CLI r=omus a=omus I noticed there were some credential precedence ordering differences between AWS.jl and AWS CLI. I ended up doing some experimentation with pairing different AWS CLI settings to determine the precedence ordering used by AWS CLI. Here are the results of those tests: - aws `--profile` used over env `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` - aws `--profile` used over env `AWS_PROFILE` - env `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` used over env `AWS_PROFILE` - env `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` used over config file `sso_*` - config file `sso_*` used over `~/.aws/credentials` (if exists) - `~/.aws/credentials` (if exists) used over config file `credential_process` - config file `credential_process` used over config file `aws_access_key_id`/`aws_secret_access_key` - config file `aws_access_key_id`/`aws_secret_access_key` used over EC2 instance metadata - config file `aws_access_key_id`/`aws_secret_access_key` used over `AWS_CONTAINER_CREDENTIALS_FULL_URI` Using `aws-cli/2.11.13 Python/3.11.3 Darwin/22.4.0 source/arm64 prompt/off` Notes: - Defining `sso_account_id` or `sso_role_name` in a profile without other `sso_*` keys results in an error about missing required configuration. Defining `sso_start_url` and `sso_region` by themselves doesn't produce this error. - Specifying the AWS credential file with `AWS_SHARED_CREDENTIALS_FILE` just replaces `~/.aws/credentials` - Tested this by specifying bad credentials in one source and valid ones in the other. As I didn't have an SSO setup to test against I could only force these to fail. - Some additional testing was done to verify that the credential preference ordering is linear. I didn't find any examples of non-linear ordering. Co-authored-by: Curtis Vogt <[email protected]>
2 parents 2e48bf7 + 7b1a425 commit 9a4322a

File tree

4 files changed

+426
-79
lines changed

4 files changed

+426
-79
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "AWS"
22
uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc"
33
license = "MIT"
4-
version = "1.85.0"
4+
version = "1.86.0"
55

66
[deps]
77
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

src/AWSCredentials.jl

+95-31
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export AWSCredentials,
2323
external_process_credentials,
2424
localhost_is_ec2,
2525
localhost_is_lambda,
26-
localhost_maybe_ec2
26+
localhost_maybe_ec2,
27+
sso_credentials
2728

2829
function localhost_maybe_ec2()
2930
return localhost_is_ec2() || isfile("/sys/devices/virtual/dmi/id/product_uuid")
@@ -41,20 +42,22 @@ The fields `access_key_id` and `secret_key` hold the access keys used to authent
4142
[Temporary Security Credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) require the extra session `token` field.
4243
The `user_arn` and `account_number` fields are used to cache the result of the [`aws_user_arn`](@ref) and [`aws_account_number`](@ref) functions.
4344
44-
AWS.jl searches for credentials in a series of possible locations and stops as soon as it finds credentials.
45-
The order of precedence for this search is as follows:
45+
AWS.jl searches for credentials in multiple locations and stops once any credentials are found.
46+
The credential preference order mostly [mirrors the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-authentication.html#cli-chap-authentication-precedence)
47+
and is as follows:
4648
47-
1. Passing credentials directly to the `AWSCredentials` constructor
49+
1. Credentials or a profile passed directly to the `AWSCredentials`
4850
2. [Environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html)
49-
3. Shared credential file [(~/.aws/credentials)](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html)
50-
4. AWS config file [(~/.aws/config)](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html).
51-
This includes [Single Sign-On (SSO)](http://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) credentials.
52-
SSO users should follow the configuration instructions at the above link, and use `aws sso login` to log in.
53-
5. Assume Role provider via the aws config file
54-
6. Instance metadata service on an Amazon EC2 instance that has an IAM role configured
51+
3. [Web Identity](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc)
52+
4. [AWS Single Sign-On (SSO)](http://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) provided via the AWS configuration file
53+
5. [AWS credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) (e.g. "~/.aws/credentials")
54+
6. [External process](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html) set via `credential_process` in the AWS configuration file
55+
7. [AWS configuration file](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html) set via `aws_access_key_id` in the AWS configuration file
56+
8. [Amazon ECS container credentials](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html)
57+
9. [Amazon EC2 instance metadata](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
5558
5659
Once the credentials are found, the method by which they were accessed is stored in the `renew` field
57-
and the DateTime at which they will expire is stored in the `expiry` field.
60+
and the `DateTime` at which they will expire is stored in the `expiry` field.
5861
This allows the credentials to be refreshed as needed using [`check_credentials`](@ref).
5962
If `renew` is set to `nothing`, no attempt will be made to refresh the credentials.
6063
Any renewal function is expected to return `nothing` on failure or a populated `AWSCredentials` object on success.
@@ -110,15 +113,21 @@ Checks credential locations in the order:
110113
function AWSCredentials(; profile=nothing, throw_cred_error=true)
111114
creds = nothing
112115
credential_function = () -> nothing
116+
explicit_profile = !isnothing(profile)
113117
profile = @something profile _aws_get_profile()
114118

115-
# Define our search options, expected to be callable with no arguments.
116-
# Throw NoCredentials if none are found
119+
# Define the credential preference order:
120+
# https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-authentication.html#cli-chap-authentication-precedence
121+
#
122+
# Note that the AWS CLI documentation states that EC2 instance credentials are preferred
123+
# over ECS container credentials. However, in practice when `AWS_CONTAINER_*`
124+
# environmental variables are set the ECS container credentials are prefered instead.
117125
functions = [
118-
env_var_credentials,
126+
() -> env_var_credentials(explicit_profile),
127+
credentials_from_webtoken,
128+
() -> sso_credentials(profile),
119129
() -> dot_aws_credentials(profile),
120130
() -> dot_aws_config(profile),
121-
credentials_from_webtoken,
122131
ecs_instance_credentials,
123132
() -> ec2_instance_credentials(profile),
124133
]
@@ -314,13 +323,14 @@ function ec2_instance_credentials(profile::AbstractString)
314323
end
315324

316325
"""
317-
ecs_instance_credentials() -> Union{AWSCredential, Nothing}
326+
ecs_instance_credentials() -> Union{AWSCredentials, Nothing}
318327
319-
Retrieve credentials from the local endpoint. Return `nothing` if not running on an ECS
320-
instance.
328+
Retrieve credentials from the ECS credential endpoint. If the ECS credential endpoint is
329+
unavailable then `nothing` will be returned.
321330
322331
More information can be found at:
323-
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
332+
- https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
333+
- https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html
324334
325335
# Returns
326336
- `AWSCredentials`: AWSCredentials from `ECS` credentials URI, `nothing` if the Env Var is
@@ -331,13 +341,23 @@ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
331341
- `ParsingError`: Invalid HTTP request target
332342
"""
333343
function ecs_instance_credentials()
334-
if !haskey(ENV, "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
344+
# The Amazon ECS agent will automatically populate the environmental variable
345+
# `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` when running inside of an ECS task. We're
346+
# interpreting this to mean than ECS credential provider should only be used if the
347+
# `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` variable is set.
348+
# – https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
349+
if haskey(ENV, "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
350+
endpoint = "http://169.254.170.2" * ENV["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]
351+
else
335352
return nothing
336353
end
337354

338-
uri = ENV["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]
339-
340-
response = @mock HTTP.request("GET", "http://169.254.170.2$uri")
355+
response = try
356+
@mock HTTP.request("GET", endpoint; retry=false, connect_timeout=5)
357+
catch e
358+
e isa HTTP.Exceptions.ConnectError && return nothing
359+
rethrow()
360+
end
341361
new_creds = String(response.body)
342362
new_creds = JSON.parse(new_creds)
343363

@@ -355,12 +375,15 @@ function ecs_instance_credentials()
355375
end
356376

357377
"""
358-
env_var_credentials() -> Union{AWSCredential, Nothing}
378+
env_var_credentials(explicit_profile::Bool=false) -> Union{AWSCredentials, Nothing}
359379
360380
Use AWS environmental variables (e.g. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.)
361381
to create AWSCredentials.
362382
"""
363-
function env_var_credentials()
383+
function env_var_credentials(explicit_profile::Bool=false)
384+
# Skip using environmental variables when a profile has been explicitly set
385+
explicit_profile && return nothing
386+
364387
if haskey(ENV, "AWS_ACCESS_KEY_ID") && haskey(ENV, "AWS_SECRET_ACCESS_KEY")
365388
return AWSCredentials(
366389
ENV["AWS_ACCESS_KEY_ID"],
@@ -375,9 +398,11 @@ function env_var_credentials()
375398
end
376399

377400
"""
378-
dot_aws_credentials(profile=nothing) -> Union{AWSCredential, Nothing}
401+
dot_aws_credentials(profile=nothing) -> Union{AWSCredentials, Nothing}
379402
380-
Retrieve AWSCredentials from the `~/.aws/credentials` file
403+
Retrieve `AWSCredentials` from the AWS CLI credentials file. The credential file defaults to
404+
"~/.aws/credentials" but can be specified using the env variable
405+
`AWS_SHARED_CREDENTIALS_FILE`.
381406
382407
# Arguments
383408
- `profile`: Specific profile used to get AWSCredentials, default is `nothing`
@@ -405,11 +430,45 @@ function dot_aws_credentials_file()
405430
end
406431

407432
"""
408-
dot_aws_config(profile=nothing) -> Union{AWSCredential, Nothing}
433+
sso_credentials(profile=nothing) -> Union{AWSCredentials, Nothing}
434+
435+
Retrieve credentials via AWS single sign-on (SSO) settings defined in the `profile` within
436+
the AWS configuration file. If no SSO settings are found for the `profile` `nothing` is
437+
returned.
438+
439+
# Arguments
440+
- `profile`: Specific profile used to get `AWSCredentials`, default is `nothing`
441+
"""
442+
function sso_credentials(profile=nothing)
443+
config_file = @mock dot_aws_config_file()
444+
445+
if isfile(config_file)
446+
ini = read(Inifile(), config_file)
447+
p = @something profile _aws_get_profile()
448+
449+
# get all the fields for that profile
450+
settings = _aws_profile_config(ini, p)
451+
isempty(settings) && return nothing
452+
453+
sso_start_url = get(settings, "sso_start_url", nothing)
454+
455+
if !isnothing(sso_start_url)
456+
access_key, secret_key, token, expiry = _aws_get_sso_credential_details(p, ini)
457+
return AWSCredentials(access_key, secret_key, token; expiry=expiry)
458+
end
459+
end
460+
461+
return nothing
462+
end
463+
464+
"""
465+
dot_aws_config(profile=nothing) -> Union{AWSCredentials, Nothing}
409466
410-
Retrieve AWSCredentials for the default or specified profile from the `~/.aws/config` file.
411-
Single sign-on profiles are also valid. If this fails, try to retrieve credentials from
412-
`_aws_get_role()`, otherwise return `nothing`
467+
Retrieve `AWSCredentials` from the AWS CLI configuration file. The configuration file
468+
defaults to "~/.aws/config" but can be specified using the env variable `AWS_CONFIG_FILE`.
469+
When no credentials are found for the given `profile` then the associated `source_profile`
470+
will be used to recursively look up credentials of source profiles. If still no credentials
471+
can be found then `nothing` will be returned.
413472
414473
# Arguments
415474
- `profile`: Specific profile used to get AWSCredentials, default is `nothing`
@@ -436,6 +495,11 @@ function dot_aws_config(profile=nothing)
436495
access_key, secret_key, token = _aws_get_credential_details(p, ini)
437496
return AWSCredentials(access_key, secret_key, token)
438497
elseif !isnothing(sso_start_url)
498+
# Deprecation should only appear if `dot_aws_config` is called directly
499+
Base.depwarn(
500+
"SSO support in `dot_aws_config` is deprecated, use `sso_credentials` instead.",
501+
:dot_aws_config,
502+
)
439503
access_key, secret_key, token, expiry = _aws_get_sso_credential_details(p, ini)
440504
return AWSCredentials(access_key, secret_key, token; expiry=expiry)
441505
else

src/utilities/credentials.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function _aws_get_role(role::AbstractString, ini::Inifile)
7171
duration_seconds = get(settings, "duration_seconds", nothing)
7272

7373
credentials = nothing
74-
for f in (dot_aws_credentials, dot_aws_config)
74+
for f in (sso_credentials, dot_aws_credentials, dot_aws_config)
7575
credentials = f(source_profile)
7676
credentials === nothing || break
7777
end

0 commit comments

Comments
 (0)