Skip to content
Open
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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,37 @@ config :ex_aws,
awscli_auth_adapter: ExAws.STS.AuthCache.AssumeRoleWebIdentityAdapter
```

### Using Pod Identity tokens from ENV vars

For AWS EKS Pod Identity authentication, you can use the Pod Identity adapter that retrieves credentials directly from the container credentials endpoint. It uses the following environment variables:

`AWS_CONTAINER_CREDENTIALS_FULL_URI`: full URI of the container credentials endpoint
`AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE`: path to the file containing the authorization token

**Note**: If `AWS_CONTAINER_CREDENTIALS_FULL_URI` is not set or is `nil`, the adapter will return an empty map, allowing the authentication to fall back to other methods.

```elixir
config :ex_aws,
secret_access_key: [{:awscli, "profile_name", 30}],
access_key_id: [{:awscli, "profile_name", 30}],
awscli_auth_adapter: ExAws.STS.AuthCache.AssumeRolePodIdentityAdapter
```

You can also inject these values directly via configuration:

```elixir
config :ex_aws,
access_key_id: [{:awscli, "default", 30}],
secret_access_key: [{:awscli, "default", 30}],
awscli_auth_adapter: ExAws.STS.AuthCache.AssumeRolePodIdentityAdapter,
awscli_credentials: %{
"default" => %{
container_credentials_uri: "http://169.254.170.2/v2/credentials/12345678-1234-1234-1234-123456789012",
container_authorization_token_file: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
}
}
```

## License

The MIT License (MIT)
Expand Down
4 changes: 2 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# and its dependencies with the aid of the Config module.
import Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
Expand Down
52 changes: 52 additions & 0 deletions lib/ex_aws/sts/auth_cache/assume_role_pod_identity_adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule ExAws.STS.AuthCache.AssumeRolePodIdentityAdapter do
@moduledoc """
Provides a custom Adapter that intercepts ExAWS configuration
which uses Pod Identity Tokens for authentication.
"""

@behaviour ExAws.Config.AuthCache.AuthConfigAdapter

@impl true
def adapt_auth_config(config, _profile, _expiration) do
case container_credentials_full_uri(config) do
nil ->
%{}

uri ->
http_client = config[:http_client] || ExAws.Request.Hackney
json_codec = config[:json_codec] || Jason

case http_client.request(:get, uri, "", [
{"Authorization", container_authorization_token(config)}
], []) do
{:ok, %{body: body}} ->
values = json_codec.decode!(body)

%{
access_key_id: values["AccessKeyId"],
secret_access_key: values["SecretAccessKey"],
security_token: values["Token"],
expiration: values["Expiration"]
}

{:error, reason} ->
{:error, reason}
end
end
end

defp container_credentials_full_uri(config) do
config[:container_credentials_uri] || System.get_env("AWS_CONTAINER_CREDENTIALS_FULL_URI")
end

defp container_authorization_token(config) do
config
|> container_authorization_token_file()
|> File.read!()
end

defp container_authorization_token_file(config) do
config[:container_authorization_token_file] ||
System.get_env("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")
end
end
1 change: 0 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ defmodule ExAws.STS.Mixfile do
defp deps do
[
{:mox, ">= 0.0.3", only: :test},
{:briefly, ">= 0.0.3", only: :test},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:hackney, ">= 0.0.0", only: [:dev, :test]},
{:sweet_xml, ">= 0.0.0", only: [:dev, :test]},
Expand Down
13 changes: 6 additions & 7 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
%{
"briefly": {:hex, :briefly, "0.3.0", "16e6b76d2070ebc9cbd025fa85cf5dbaf52368c4bd896fb482b5a6b95a540c2f", [:mix], [], "hexpm", "c6ebf8fc3dcd4950dd10c03e953fb4f553a8bcf0ff4c8c40d71542434cd7e046"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm", "e3be2bc3ae67781db529b80aa7e7c49904a988596e2dbff897425b48b3581161"},
"earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
"ex_aws": {:hex, :ex_aws, "2.2.3", "f9bb1b04aaed3270f08c99d0268ff2c6596edb3a4a8792d18ffd37a74dd4810d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ddf1da94624e5f92afe36d4146086f72d85bb15998cec137c1fd854a3f04bc4"},
"ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"},
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
"mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
}
165 changes: 165 additions & 0 deletions test/lib/auth_cache/assume_role_pod_identity_adapter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
defmodule ExAws.STS.AuthCache.AssumeRolePodIdentityAdapterTest do
use ExUnit.Case, async: true
alias ExAws.STS.AuthCache.AssumeRolePodIdentityAdapter

import Mox

@container_credentials_uri "http://169.254.170.2/v2/credentials/12345678-1234-1234-1234-123456789012"
@authorization_token "Bearer token123"
@expiration 30_000

setup do
token_path = Path.join(System.tmp_dir!(), "test_token_#{System.unique_integer()}")
File.write!(token_path, @authorization_token)

System.put_env("AWS_CONTAINER_CREDENTIALS_FULL_URI", @container_credentials_uri)
System.put_env("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", token_path)

on_exit(fn ->
File.rm_rf(token_path)
System.delete_env("AWS_CONTAINER_CREDENTIALS_FULL_URI")
System.delete_env("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")
end)
end

describe "when the config values are injected" do
test "#adapt_auth_config" do
config = %{
container_credentials_uri: "http://custom.endpoint/credentials",
container_authorization_token_file: create_token_file("custom_token"),
http_client: ExAws.Request.HttpMock,
json_codec: Jason
}

response_body = %{
"AccessKeyId" => "AKIAIOSFODNN7EXAMPLE",
"SecretAccessKey" => "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token" => "session_token_example",
"Expiration" => "2023-12-31T23:59:59Z"
}

ExAws.Request.HttpMock
|> expect(:request, fn :get,
"http://custom.endpoint/credentials",
"",
[{"Authorization", "custom_token"}],
_opts ->
{:ok, %{body: Jason.encode!(response_body)}}
end)

expected = %{
access_key_id: "AKIAIOSFODNN7EXAMPLE",
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
security_token: "session_token_example",
expiration: "2023-12-31T23:59:59Z"
}

assert expected == AssumeRolePodIdentityAdapter.adapt_auth_config(config, nil, @expiration)
end
end

describe "when using environment variables" do
test "#adapt_auth_config" do
config = %{
http_client: ExAws.Request.HttpMock,
json_codec: Jason
}

response_body = %{
"AccessKeyId" => "AKIAIOSFODNN7EXAMPLE",
"SecretAccessKey" => "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token" => "session_token_example",
"Expiration" => "2023-12-31T23:59:59Z"
}

ExAws.Request.HttpMock
|> expect(:request, fn :get,
@container_credentials_uri,
"",
[{"Authorization", @authorization_token}],
_opts ->
{:ok, %{body: Jason.encode!(response_body)}}
end)

expected = %{
access_key_id: "AKIAIOSFODNN7EXAMPLE",
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
security_token: "session_token_example",
expiration: "2023-12-31T23:59:59Z"
}

assert expected == AssumeRolePodIdentityAdapter.adapt_auth_config(config, nil, @expiration)
end
end

describe "when the HTTP request fails" do
test "#adapt_auth_config returns error" do
config = %{
http_client: ExAws.Request.HttpMock,
json_codec: Jason
}

ExAws.Request.HttpMock
|> expect(:request, fn :get,
@container_credentials_uri,
"",
[{"Authorization", @authorization_token}],
_opts ->
{:error, :timeout}
end)

assert {:error, :timeout} == AssumeRolePodIdentityAdapter.adapt_auth_config(config, nil, @expiration)
end
end

describe "when the authorization token file does not exist" do
test "#adapt_auth_config raises File.Error" do
System.put_env("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", "./does_not_exist")

config = %{
http_client: ExAws.Request.HttpMock,
json_codec: Jason
}

assert_raise File.Error, fn ->
AssumeRolePodIdentityAdapter.adapt_auth_config(config, nil, @expiration)
end
end
end

describe "when the container credentials URI is not provided" do
test "#adapt_auth_config returns empty map when env var is not set" do
System.delete_env("AWS_CONTAINER_CREDENTIALS_FULL_URI")

config = %{
http_client: ExAws.Request.HttpMock,
json_codec: Jason
}

expected = %{}

assert expected == AssumeRolePodIdentityAdapter.adapt_auth_config(config, nil, @expiration)
end

test "#adapt_auth_config returns empty map when config URI is nil" do
# Clear environment variable to ensure we're only testing config
System.delete_env("AWS_CONTAINER_CREDENTIALS_FULL_URI")

config = %{
container_credentials_uri: nil,
http_client: ExAws.Request.HttpMock,
json_codec: Jason
}

expected = %{}

assert expected == AssumeRolePodIdentityAdapter.adapt_auth_config(config, nil, @expiration)
end
end

defp create_token_file(content) do
path = Path.join(System.tmp_dir!(), "test_token_#{System.unique_integer()}")
File.write!(path, content)
path
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ defmodule ExAws.STS.AuthCache.AssumeRoleWebIdentityAdapterTest do
@expiration 30_000

setup do
{:ok, path} = Briefly.create()
path = Path.join(System.tmp_dir!(), "web_identity_token_#{System.unique_integer()}")
File.write!(path, @web_identity_token)

System.put_env("AWS_WEB_IDENTITY_TOKEN_FILE", path)
System.put_env("AWS_ROLE_ARN", @role_arn)

on_exit(fn ->
File.rm_rf(path)
System.delete_env("AWS_WEB_IDENTITY_TOKEN_FILE")
System.delete_env("AWS_ROLE_ARN")
end)
Expand Down