Skip to content

Commit 74b61ed

Browse files
authored
Add support for EKS Pod Identity (#1781)
1 parent ace3835 commit 74b61ed

File tree

9 files changed

+172
-32
lines changed

9 files changed

+172
-32
lines changed

couscous.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ menu:
211211
ecs-container:
212212
text: ECS container metadata
213213
url: /authentication/ecs-container.html
214+
pod-identity:
215+
text: ECS container metadata
216+
url: /authentication/pod-identity.html
214217
environment:
215218
text: Environment variables
216219
url: /authentication/environment.html
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
category: authentication
3+
---
4+
5+
# Using EKS Pod Identity
6+
7+
When you run code within an EKS cluster that has the Pod Identity Agent enabled, AsyncAws is able to fetch Credentials from the service account attached to
8+
your pod using the [Pod Identity Agent](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html).
9+
10+
When running an application on EKS, this is the simplest way to grant permissions to the application. You
11+
have nothing to configure on the application, you only grant permissions on the Role attached to the service account.

docs/configuration.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ See [IAM Roles for Tasks](https://docs.aws.amazon.com/AmazonECS/latest/developer
109109
Enable the endpoint discovery when the operation support it
110110
See [Endpoint discovery](https://docs.aws.amazon.com/sdkref/latest/guide/feature-endpoint-discovery.html) for more information.
111111

112+
### podIdentityCredentialsFullUri
113+
114+
Full Uri to the endpoint of the Pod Identity agent, which should already be injected by the Pod Identity agent when using the [PodIdentity Provider](/authentication/pod-identity.md)
115+
116+
### podIdentityAuthorizationTokenFile
117+
118+
Path to the file that contains the Pod Identity access token, which should already be injected by the Pod Identity agent when using the [PodIdentity Provider](/authentication/pod-identity.md)
119+
112120
## S3 specific Configuration reference
113121

114122
### pathStyleEndpoint

src/Core/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## NOT RELEASED
44

5+
### Added
6+
7+
- Added support for EKS Pod Identity
8+
59
## 1.22.1
610

711
### Changed

src/Core/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
},
3939
"extra": {
4040
"branch-alias": {
41-
"dev-master": "1.22-dev"
41+
"dev-master": "1.23-dev"
4242
}
4343
}
4444
}

src/Core/src/Configuration.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ final class Configuration
3131
public const OPTION_ROLE_SESSION_NAME = 'roleSessionName';
3232
public const OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI = 'containerCredentialsRelativeUri';
3333
public const OPTION_ENDPOINT_DISCOVERY_ENABLED = 'endpointDiscoveryEnabled';
34+
public const OPTION_POD_IDENTITY_CREDENTIALS_FULL_URI = 'podIdentityCredentialsFullUri';
35+
public const OPTION_POD_IDENTITY_AUTHORIZATION_TOKEN_FILE = 'podIdentityAuthorizationTokenFile';
3436

3537
// S3 specific option
3638
public const OPTION_PATH_STYLE_ENDPOINT = 'pathStyleEndpoint';
@@ -53,6 +55,8 @@ final class Configuration
5355
self::OPTION_ENDPOINT_DISCOVERY_ENABLED => true,
5456
self::OPTION_PATH_STYLE_ENDPOINT => true,
5557
self::OPTION_SEND_CHUNKED_BODY => true,
58+
self::OPTION_POD_IDENTITY_CREDENTIALS_FULL_URI => true,
59+
self::OPTION_POD_IDENTITY_AUTHORIZATION_TOKEN_FILE => true,
5660
];
5761

5862
// Put fallback options into groups to avoid mixing of provided config and environment variables
@@ -74,6 +78,8 @@ final class Configuration
7478
],
7579
[self::OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI => 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'],
7680
[self::OPTION_ENDPOINT_DISCOVERY_ENABLED => ['AWS_ENDPOINT_DISCOVERY_ENABLED', 'AWS_ENABLE_ENDPOINT_DISCOVERY']],
81+
[self::OPTION_POD_IDENTITY_CREDENTIALS_FULL_URI => 'AWS_CONTAINER_CREDENTIALS_FULL_URI'],
82+
[self::OPTION_POD_IDENTITY_AUTHORIZATION_TOKEN_FILE => 'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE'],
7783
];
7884

7985
private const DEFAULT_OPTIONS = [

src/Core/src/Credentials/ContainerProvider.php

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414
use Symfony\Contracts\HttpClient\HttpClientInterface;
1515

1616
/**
17-
* Provides Credentials from the running ECS.
17+
* Provides Credentials for containers running in EKS with Pod Identity or ECS.
1818
*
1919
* @see https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/index.html?com/amazonaws/auth/ContainerCredentialsProvider.html
20+
* @see https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html
2021
*/
2122
final class ContainerProvider implements CredentialProvider
2223
{
23-
private const ENDPOINT = 'http://169.254.170.2';
24+
use TokenFileLoader;
25+
26+
private const ECS_HOST = '169.254.170.2';
27+
private const EKS_HOST_IPV4 = '169.254.170.23';
28+
private const EKS_HOST_IPV6 = 'fd00:ec2::23';
2429

2530
/**
2631
* @var LoggerInterface
@@ -46,15 +51,33 @@ public function __construct(?HttpClientInterface $httpClient = null, ?LoggerInte
4651

4752
public function getCredentials(Configuration $configuration): ?Credentials
4853
{
49-
$relativeUri = $configuration->get(Configuration::OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI);
54+
$fullUri = $this->getFullUri($configuration);
55+
5056
// introduces an early exit if the env variable is not set.
51-
if (empty($relativeUri)) {
57+
if (empty($fullUri)) {
5258
return null;
5359
}
5460

61+
if (!$this->isUriValid($fullUri)) {
62+
$this->logger->warning('Invalid URI "{uri}" provided.', ['uri' => $fullUri]);
63+
64+
return null;
65+
}
66+
67+
$tokenFile = $configuration->get(Configuration::OPTION_POD_IDENTITY_AUTHORIZATION_TOKEN_FILE);
68+
if (!empty($tokenFile)) {
69+
try {
70+
$tokenFileContent = $this->getTokenFileContent($tokenFile);
71+
} catch (\Exception $e) {
72+
$this->logger->warning('"Error reading PodIdentityTokenFile "{tokenFile}.', ['tokenFile' => $tokenFile, 'exception' => $e]);
73+
74+
return null;
75+
}
76+
}
77+
5578
// fetch credentials from ecs endpoint
5679
try {
57-
$response = $this->httpClient->request('GET', self::ENDPOINT . $relativeUri, ['timeout' => $this->timeout]);
80+
$response = $this->httpClient->request('GET', $fullUri, ['headers' => $this->getHeaders($tokenFileContent ?? null), 'timeout' => $this->timeout]);
5881
$result = $response->toArray();
5982
} catch (DecodingExceptionInterface $e) {
6083
$this->logger->info('Failed to decode Credentials.', ['exception' => $e]);
@@ -77,4 +100,78 @@ public function getCredentials(Configuration $configuration): ?Credentials
77100
Credentials::adjustExpireDate(new \DateTimeImmutable($result['Expiration']), $date)
78101
);
79102
}
103+
104+
/**
105+
* Checks if the provided IP address is a loopback address.
106+
*
107+
* @param string $host the host address to check
108+
*
109+
* @return bool true if the IP is a loopback address, false otherwise
110+
*/
111+
private function isLoopBackAddress(string $host)
112+
{
113+
// Validate that the input is a valid IP address
114+
if (!filter_var($host, \FILTER_VALIDATE_IP)) {
115+
return false;
116+
}
117+
118+
// Convert the IP address to binary format
119+
$packedIp = inet_pton($host);
120+
121+
// Check if the IP is in the 127.0.0.0/8 range
122+
if (4 === \strlen($packedIp)) {
123+
return 127 === \ord($packedIp[0]);
124+
}
125+
126+
// Check if the IP is ::1
127+
if (16 === \strlen($packedIp)) {
128+
return $packedIp === inet_pton('::1');
129+
}
130+
131+
// Unknown IP format
132+
return false;
133+
}
134+
135+
private function getFullUri(Configuration $configuration): ?string
136+
{
137+
$relativeUri = $configuration->get(Configuration::OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI);
138+
139+
if (null !== $relativeUri) {
140+
return 'http://' . self::ECS_HOST . $relativeUri;
141+
}
142+
143+
return $configuration->get(Configuration::OPTION_POD_IDENTITY_CREDENTIALS_FULL_URI);
144+
}
145+
146+
private function getHeaders(?string $tokenFileContent): array
147+
{
148+
return $tokenFileContent ? ['Authorization' => $tokenFileContent] : [];
149+
}
150+
151+
private function isUriValid(string $uri): bool
152+
{
153+
$parsedUri = parse_url($uri);
154+
if (false === $parsedUri) {
155+
return false;
156+
}
157+
158+
if (!isset($parsedUri['scheme'])) {
159+
return false;
160+
}
161+
162+
if ('https' !== $parsedUri['scheme']) {
163+
$host = trim($parsedUri['host'] ?? '', '[]');
164+
if (self::EKS_HOST_IPV4 === $host || self::EKS_HOST_IPV6 === $host) {
165+
return true;
166+
}
167+
168+
if (self::ECS_HOST === $host) {
169+
return true;
170+
}
171+
172+
return $this->isLoopBackAddress($host);
173+
}
174+
175+
return true;
176+
}
80177
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AsyncAws\Core\Credentials;
6+
7+
use AsyncAws\Core\Exception\RuntimeException;
8+
9+
trait TokenFileLoader
10+
{
11+
/**
12+
* @see https://github.com/async-aws/aws/issues/900
13+
* @see https://github.com/aws/aws-sdk-php/issues/2014
14+
* @see https://github.com/aws/aws-sdk-php/pull/2043
15+
*/
16+
public function getTokenFileContent(string $tokenFile): string
17+
{
18+
$token = @file_get_contents($tokenFile);
19+
20+
if (false !== $token) {
21+
return $token;
22+
}
23+
24+
$tokenDir = \dirname($tokenFile);
25+
$tokenLink = readlink($tokenFile);
26+
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . $tokenLink);
27+
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . \dirname($tokenLink));
28+
clearstatcache(true, $tokenFile);
29+
30+
if (false === $token = file_get_contents($tokenFile)) {
31+
throw new RuntimeException('Failed to read data');
32+
}
33+
34+
return $token;
35+
}
36+
}

src/Core/src/Credentials/WebIdentityProvider.php

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
final class WebIdentityProvider implements CredentialProvider
2222
{
2323
use DateFromResult;
24+
use TokenFileLoader;
2425

2526
/**
2627
* @var IniFileLoader
@@ -129,30 +130,4 @@ private function getCredentialsFromRole(string $roleArn, string $tokenFile, ?str
129130
Credentials::adjustExpireDate($credentials->getExpiration(), $this->getDateFromResult($result))
130131
);
131132
}
132-
133-
/**
134-
* @see https://github.com/async-aws/aws/issues/900
135-
* @see https://github.com/aws/aws-sdk-php/issues/2014
136-
* @see https://github.com/aws/aws-sdk-php/pull/2043
137-
*/
138-
private function getTokenFileContent(string $tokenFile): string
139-
{
140-
$token = @file_get_contents($tokenFile);
141-
142-
if (false !== $token) {
143-
return $token;
144-
}
145-
146-
$tokenDir = \dirname($tokenFile);
147-
$tokenLink = readlink($tokenFile);
148-
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . $tokenLink);
149-
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . \dirname($tokenLink));
150-
clearstatcache(true, $tokenFile);
151-
152-
if (false === $token = file_get_contents($tokenFile)) {
153-
throw new RuntimeException('Failed to read data');
154-
}
155-
156-
return $token;
157-
}
158133
}

0 commit comments

Comments
 (0)