Skip to content

Commit 9365a08

Browse files
authored
Merge pull request #1169 from mspiewak/add-eks-pod-identity-support
Add eks pod identity credential provider
2 parents c57fcd8 + 8aea136 commit 9365a08

File tree

5 files changed

+190
-7
lines changed

5 files changed

+190
-7
lines changed

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ the equivalent of:
5555

5656
```elixir
5757
config :ex_aws,
58-
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
59-
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role]
58+
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :pod_identity, :instance_role],
59+
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :pod_identity, :instance_role]
6060
```
6161

6262
This means it will try to resolve credentials in order:
6363

6464
* Look for the AWS standard `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables
65+
* Try to use [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) if running on EKS with Pod Identity configured
6566
* Resolve credentials with IAM
6667
* If running inside ECS and a [task role](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) has been assigned it will use it
6768
* Otherwise it will fall back to the [instance role](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
@@ -80,10 +81,19 @@ variable, you can use that with `{:awscli, :system, timeout}`
8081

8182
```elixir
8283
config :ex_aws,
83-
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, {:awscli, "default", 30}, :instance_role],
84-
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, {:awscli, "default", 30}, :instance_role]
84+
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, {:awscli, "default", 30}, :pod_identity, :instance_role],
85+
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, {:awscli, "default", 30}, :pod_identity, :instance_role]
8586
```
8687

88+
### EKS Pod Identity configuration
89+
90+
For applications running on Amazon EKS, ExAws supports [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) for credential resolution. Pod Identity automatically injects the required environment variables into your pods when properly configured:
91+
92+
* `AWS_CONTAINER_CREDENTIALS_FULL_URI` - The endpoint URL for credential retrieval
93+
* `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` - Path to the JWT token file
94+
95+
No additional configuration is required in ExAws - it will automatically detect and use Pod Identity credentials when these environment variables are present. Pod Identity provides improved security and isolation compared to instance roles by providing pod-level credential scoping.
96+
8797
For role based authentication via `role_arn` and `source_profile` an additional
8898
dependency is required:
8999

lib/ex_aws/config.ex

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ defmodule ExAws.Config do
5050
:external_id
5151
]
5252

53+
@pod_identity_config [
54+
:access_key_id,
55+
:secret_access_key,
56+
:security_token
57+
]
58+
5359
@type t :: %{} | Keyword.t()
5460

5561
@doc """
@@ -107,6 +113,7 @@ defmodule ExAws.Config do
107113
Enum.reduce(refreshable, overrides, fn
108114
:awscli, overrides -> Map.drop(overrides, @awscli_config)
109115
:instance_role, overrides -> Map.drop(overrides, @instance_role_config)
116+
:pod_identity, overrides -> Map.drop(overrides, @pod_identity_config)
110117
end)
111118
else
112119
overrides
@@ -115,7 +122,7 @@ defmodule ExAws.Config do
115122
Map.merge(config, overrides)
116123
end
117124

118-
# :awscli and :instance_role both read creds from ExAws.Config.AuthCache which
125+
# :awscli, :instance_role, and :pod_identity all read creds from ExAws.Config.AuthCache which
119126
# is "refreshable". This is useful for long running streams where the creds can
120127
# change while the stream is still running.
121128
defp add_refreshable_metadata(config, %{refreshable: true}) do
@@ -124,6 +131,7 @@ defmodule ExAws.Config do
124131
|> Enum.reduce([], fn
125132
{:awscli, _, _}, acc -> [:awscli | acc]
126133
:instance_role, acc -> [:instance_role | acc]
134+
:pod_identity, acc -> [:pod_identity | acc]
127135
_, acc -> acc
128136
end)
129137
|> Enum.uniq()
@@ -181,6 +189,12 @@ defmodule ExAws.Config do
181189
|> valid_map_or_nil
182190
end
183191

192+
def retrieve_runtime_value(:pod_identity, config) do
193+
ExAws.Config.AuthCache.get(:pod_identity, config)
194+
|> Map.take(@pod_identity_config)
195+
|> valid_map_or_nil
196+
end
197+
184198
def retrieve_runtime_value({:awscli, profile, expiration}, _) do
185199
ExAws.Config.AuthCache.get(profile, expiration * 1000)
186200
|> Map.take(@awscli_config)

lib/ex_aws/config/auth_cache.ex

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule ExAws.Config.AuthCache do
77

88
@refresh_lead_time 300_000
99
@instance_auth_key :aws_instance_auth
10+
@pod_identity_auth_key :aws_pod_identity_auth
1011

1112
defmodule AuthConfigAdapter do
1213
@moduledoc false
@@ -24,6 +25,11 @@ defmodule ExAws.Config.AuthCache do
2425
|> refresh_auth_if_required(config)
2526
end
2627

28+
def get(:pod_identity, config) do
29+
:ets.lookup(__MODULE__, @pod_identity_auth_key)
30+
|> refresh_pod_identity_if_required(config)
31+
end
32+
2733
def get(profile, expiration) do
2834
case :ets.lookup(__MODULE__, {:awscli, profile}) do
2935
[{{:awscli, ^profile}, auth_config}] ->
@@ -46,6 +52,11 @@ defmodule ExAws.Config.AuthCache do
4652
{:reply, auth, ets}
4753
end
4854

55+
def handle_call({:refresh_pod_identity_auth, config}, _from, ets) do
56+
auth = refresh_pod_identity_auth(config, ets)
57+
{:reply, auth, ets}
58+
end
59+
4960
def handle_call({:refresh_awscli_config, profile, expiration}, _from, ets) do
5061
auth = refresh_awscli_config(profile, expiration, ets)
5162
{:reply, auth, ets}
@@ -56,6 +67,11 @@ defmodule ExAws.Config.AuthCache do
5667
{:noreply, ets}
5768
end
5869

70+
def handle_info({:refresh_pod_identity_auth, config}, ets) do
71+
refresh_pod_identity_auth(config, ets)
72+
{:noreply, ets}
73+
end
74+
5975
def handle_info({:refresh_awscli_config, profile, expiration}, ets) do
6076
refresh_awscli_config(profile, expiration, ets)
6177
{:noreply, ets}
@@ -135,6 +151,50 @@ defmodule ExAws.Config.AuthCache do
135151
auth
136152
end
137153

154+
defp refresh_pod_identity_if_required([], config) do
155+
GenServer.call(__MODULE__, {:refresh_pod_identity_auth, config}, 30_000)
156+
end
157+
158+
defp refresh_pod_identity_if_required([{_key, cached_auth}], config) do
159+
if next_refresh_in(cached_auth) > 0 do
160+
cached_auth
161+
else
162+
GenServer.call(__MODULE__, {:refresh_pod_identity_auth, config}, 30_000)
163+
end
164+
end
165+
166+
defp refresh_pod_identity_auth(config, ets) do
167+
:ets.lookup(__MODULE__, @pod_identity_auth_key)
168+
|> refresh_pod_identity_if_stale(config, ets)
169+
end
170+
171+
defp refresh_pod_identity_if_stale([], config, ets) do
172+
refresh_pod_identity_now(config, ets)
173+
end
174+
175+
defp refresh_pod_identity_if_stale([{_key, cached_auth}], config, ets) do
176+
if next_refresh_in(cached_auth) > @refresh_lead_time do
177+
cached_auth
178+
else
179+
refresh_pod_identity_now(config, ets)
180+
end
181+
end
182+
183+
defp refresh_pod_identity_if_stale(_, config, ets), do: refresh_pod_identity_now(config, ets)
184+
185+
defp refresh_pod_identity_now(config, ets) do
186+
# Only attempt pod identity if the environment is properly configured
187+
if ExAws.PodIdentity.available?() do
188+
auth = ExAws.PodIdentity.security_credentials(config)
189+
:ets.insert(ets, {@pod_identity_auth_key, auth})
190+
Process.send_after(__MODULE__, {:refresh_pod_identity_auth, config}, next_refresh_in(auth))
191+
auth
192+
else
193+
# Return empty map if pod identity is not available
194+
%{}
195+
end
196+
end
197+
138198
defp next_refresh_in(%{expiration: expiration}) do
139199
try do
140200
expires_in_ms =

lib/ex_aws/config/defaults.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ defmodule ExAws.Config.Defaults do
44
"""
55

66
@common %{
7-
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
8-
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role],
7+
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :pod_identity, :instance_role],
8+
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :pod_identity, :instance_role],
99
http_client: ExAws.Request.Hackney,
1010
json_codec: Jason,
1111
retries: [

lib/ex_aws/pod_identity.ex

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
defmodule ExAws.PodIdentity do
2+
@moduledoc false
3+
4+
# Provides access to AWS credentials via EKS Pod Identity
5+
# https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html
6+
7+
# Environment variables used by EKS Pod Identity
8+
@credentials_uri_env "AWS_CONTAINER_CREDENTIALS_FULL_URI"
9+
@authorization_token_file_env "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"
10+
11+
def available? do
12+
case {System.get_env(@credentials_uri_env), System.get_env(@authorization_token_file_env)} do
13+
{uri, token_file} when is_binary(uri) and is_binary(token_file) -> true
14+
_ -> false
15+
end
16+
end
17+
18+
def security_credentials(config) do
19+
with {:ok, token} <- read_authorization_token(),
20+
{:ok, credentials} <- fetch_credentials(config, token) do
21+
parse_credentials(credentials)
22+
else
23+
{:error, reason} ->
24+
raise """
25+
Pod Identity Error: #{inspect(reason)}
26+
27+
You tried to access AWS credentials via EKS Pod Identity, but it failed.
28+
This happens when the pod is not properly configured with Pod Identity
29+
or the required environment variables are not set.
30+
31+
Required environment variables:
32+
- #{@credentials_uri_env}
33+
- #{@authorization_token_file_env}
34+
35+
Please check your EKS Pod Identity configuration.
36+
"""
37+
end
38+
end
39+
40+
defp read_authorization_token do
41+
case System.get_env(@authorization_token_file_env) do
42+
nil ->
43+
{:error, "#{@authorization_token_file_env} environment variable not set"}
44+
45+
token_file_path ->
46+
case File.read(token_file_path) do
47+
{:ok, token} ->
48+
{:ok, String.trim(token)}
49+
50+
{:error, reason} ->
51+
{:error, "Failed to read authorization token from #{token_file_path}: #{reason}"}
52+
end
53+
end
54+
end
55+
56+
defp fetch_credentials(config, token) do
57+
case System.get_env(@credentials_uri_env) do
58+
nil ->
59+
{:error, "#{@credentials_uri_env} environment variable not set"}
60+
61+
credentials_uri ->
62+
headers = [{"Authorization", token}]
63+
64+
case config.http_client.request(:get, credentials_uri, "", headers, http_opts())
65+
|> ExAws.Request.maybe_transform_response() do
66+
{:ok, %{status_code: 200, body: body}} ->
67+
case config.json_codec.decode(body) do
68+
{:ok, credentials} -> {:ok, credentials}
69+
{:error, reason} -> {:error, "Failed to decode credentials JSON: #{reason}"}
70+
end
71+
72+
{:ok, %{status_code: status_code, body: body}} ->
73+
{:error, "HTTP #{status_code}: #{body}"}
74+
75+
{:error, reason} ->
76+
{:error, "HTTP request failed: #{reason}"}
77+
end
78+
end
79+
end
80+
81+
defp parse_credentials(credentials) do
82+
%{
83+
access_key_id: credentials["AccessKeyId"],
84+
secret_access_key: credentials["SecretAccessKey"],
85+
security_token: credentials["Token"],
86+
expiration: credentials["Expiration"]
87+
}
88+
end
89+
90+
defp http_opts do
91+
defaults = [follow_redirect: false, recv_timeout: 5_000]
92+
93+
overrides =
94+
Application.get_env(:ex_aws, :pod_identity, [])
95+
|> Keyword.get(:http_opts, [])
96+
97+
Keyword.merge(defaults, overrides)
98+
end
99+
end

0 commit comments

Comments
 (0)