Skip to content
Merged
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
3 changes: 3 additions & 0 deletions couscous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ menu:
ecs-container:
text: ECS container metadata
url: /authentication/ecs-container.html
pod-identity:
text: ECS container metadata
url: /authentication/pod-identity.html
environment:
text: Environment variables
url: /authentication/environment.html
Expand Down
11 changes: 11 additions & 0 deletions docs/authentication/pod-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
category: authentication
---

# Using EKS Pod Identity

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
your pod using the [Pod Identity Agent](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html).

When running an application on EKS, this is the simplest way to grant permissions to the application. You
have nothing to configure on the application, you only grant permissions on the Role attached to the service account.
8 changes: 8 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ See [IAM Roles for Tasks](https://docs.aws.amazon.com/AmazonECS/latest/developer
Enable the endpoint discovery when the operation support it
See [Endpoint discovery](https://docs.aws.amazon.com/sdkref/latest/guide/feature-endpoint-discovery.html) for more information.

### podIdentityCredentialsFullUri

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)

### podIdentityAuthorizationTokenFile

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)

## S3 specific Configuration reference

### pathStyleEndpoint
Expand Down
4 changes: 4 additions & 0 deletions src/Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## NOT RELEASED

### Added

- Added support for EKS Pod Identity

## 1.22.1

### Changed
Expand Down
2 changes: 1 addition & 1 deletion src/Core/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.22-dev"
"dev-master": "1.23-dev"
}
}
}
6 changes: 6 additions & 0 deletions src/Core/src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ final class Configuration
public const OPTION_ROLE_SESSION_NAME = 'roleSessionName';
public const OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI = 'containerCredentialsRelativeUri';
public const OPTION_ENDPOINT_DISCOVERY_ENABLED = 'endpointDiscoveryEnabled';
public const OPTION_POD_IDENTITY_CREDENTIALS_FULL_URI = 'podIdentityCredentialsFullUri';
public const OPTION_POD_IDENTITY_AUTHORIZATION_TOKEN_FILE = 'podIdentityAuthorizationTokenFile';

// S3 specific option
public const OPTION_PATH_STYLE_ENDPOINT = 'pathStyleEndpoint';
Expand All @@ -53,6 +55,8 @@ final class Configuration
self::OPTION_ENDPOINT_DISCOVERY_ENABLED => true,
self::OPTION_PATH_STYLE_ENDPOINT => true,
self::OPTION_SEND_CHUNKED_BODY => true,
self::OPTION_POD_IDENTITY_CREDENTIALS_FULL_URI => true,
self::OPTION_POD_IDENTITY_AUTHORIZATION_TOKEN_FILE => true,
];

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

private const DEFAULT_OPTIONS = [
Expand Down
107 changes: 102 additions & 5 deletions src/Core/src/Credentials/ContainerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* Provides Credentials from the running ECS.
* Provides Credentials for containers running in EKS with Pod Identity or ECS.
*
* @see https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/index.html?com/amazonaws/auth/ContainerCredentialsProvider.html
* @see https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html
*/
final class ContainerProvider implements CredentialProvider
{
private const ENDPOINT = 'http://169.254.170.2';
use TokenFileLoader;

private const ECS_HOST = '169.254.170.2';
private const EKS_HOST_IPV4 = '169.254.170.23';
private const EKS_HOST_IPV6 = 'fd00:ec2::23';

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

public function getCredentials(Configuration $configuration): ?Credentials
{
$relativeUri = $configuration->get(Configuration::OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI);
$fullUri = $this->getFullUri($configuration);

// introduces an early exit if the env variable is not set.
if (empty($relativeUri)) {
if (empty($fullUri)) {
return null;
}

if (!$this->isUriValid($fullUri)) {
$this->logger->warning('Invalid URI "{uri}" provided.', ['uri' => $fullUri]);

return null;
}

$tokenFile = $configuration->get(Configuration::OPTION_POD_IDENTITY_AUTHORIZATION_TOKEN_FILE);
if (!empty($tokenFile)) {
try {
$tokenFileContent = $this->getTokenFileContent($tokenFile);
} catch (\Exception $e) {
$this->logger->warning('"Error reading PodIdentityTokenFile "{tokenFile}.', ['tokenFile' => $tokenFile, 'exception' => $e]);

return null;
}
}

// fetch credentials from ecs endpoint
try {
$response = $this->httpClient->request('GET', self::ENDPOINT . $relativeUri, ['timeout' => $this->timeout]);
$response = $this->httpClient->request('GET', $fullUri, ['headers' => $this->getHeaders($tokenFileContent ?? null), 'timeout' => $this->timeout]);
$result = $response->toArray();
} catch (DecodingExceptionInterface $e) {
$this->logger->info('Failed to decode Credentials.', ['exception' => $e]);
Expand All @@ -77,4 +100,78 @@ public function getCredentials(Configuration $configuration): ?Credentials
Credentials::adjustExpireDate(new \DateTimeImmutable($result['Expiration']), $date)
);
}

/**
* Checks if the provided IP address is a loopback address.
*
* @param string $host the host address to check
*
* @return bool true if the IP is a loopback address, false otherwise
*/
private function isLoopBackAddress(string $host)
{
// Validate that the input is a valid IP address
if (!filter_var($host, \FILTER_VALIDATE_IP)) {
return false;
}

// Convert the IP address to binary format
$packedIp = inet_pton($host);

// Check if the IP is in the 127.0.0.0/8 range
if (4 === \strlen($packedIp)) {
return 127 === \ord($packedIp[0]);
}

// Check if the IP is ::1
if (16 === \strlen($packedIp)) {
return $packedIp === inet_pton('::1');
}

// Unknown IP format
return false;
}

private function getFullUri(Configuration $configuration): ?string
{
$relativeUri = $configuration->get(Configuration::OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI);

if (null !== $relativeUri) {
return 'http://' . self::ECS_HOST . $relativeUri;
}

return $configuration->get(Configuration::OPTION_POD_IDENTITY_CREDENTIALS_FULL_URI);
}

private function getHeaders(?string $tokenFileContent): array
{
return $tokenFileContent ? ['Authorization' => $tokenFileContent] : [];
}

private function isUriValid(string $uri): bool
{
$parsedUri = parse_url($uri);
if (false === $parsedUri) {
return false;
}

if (!isset($parsedUri['scheme'])) {
return false;
}

if ('https' !== $parsedUri['scheme']) {
$host = trim($parsedUri['host'] ?? '', '[]');
if (self::EKS_HOST_IPV4 === $host || self::EKS_HOST_IPV6 === $host) {
return true;
}

if (self::ECS_HOST === $host) {
return true;
}

return $this->isLoopBackAddress($host);
}

return true;
}
}
36 changes: 36 additions & 0 deletions src/Core/src/Credentials/TokenFileLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace AsyncAws\Core\Credentials;

use AsyncAws\Core\Exception\RuntimeException;

trait TokenFileLoader
{
/**
* @see https://github.com/async-aws/aws/issues/900
* @see https://github.com/aws/aws-sdk-php/issues/2014
* @see https://github.com/aws/aws-sdk-php/pull/2043
*/
public function getTokenFileContent(string $tokenFile): string
{
$token = @file_get_contents($tokenFile);

if (false !== $token) {
return $token;
}

$tokenDir = \dirname($tokenFile);
$tokenLink = readlink($tokenFile);
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . $tokenLink);
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . \dirname($tokenLink));
clearstatcache(true, $tokenFile);

if (false === $token = file_get_contents($tokenFile)) {
throw new RuntimeException('Failed to read data');
}

return $token;
}
}
27 changes: 1 addition & 26 deletions src/Core/src/Credentials/WebIdentityProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
final class WebIdentityProvider implements CredentialProvider
{
use DateFromResult;
use TokenFileLoader;

/**
* @var IniFileLoader
Expand Down Expand Up @@ -129,30 +130,4 @@ private function getCredentialsFromRole(string $roleArn, string $tokenFile, ?str
Credentials::adjustExpireDate($credentials->getExpiration(), $this->getDateFromResult($result))
);
}

/**
* @see https://github.com/async-aws/aws/issues/900
* @see https://github.com/aws/aws-sdk-php/issues/2014
* @see https://github.com/aws/aws-sdk-php/pull/2043
*/
private function getTokenFileContent(string $tokenFile): string
{
$token = @file_get_contents($tokenFile);

if (false !== $token) {
return $token;
}

$tokenDir = \dirname($tokenFile);
$tokenLink = readlink($tokenFile);
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . $tokenLink);
clearstatcache(true, $tokenDir . \DIRECTORY_SEPARATOR . \dirname($tokenLink));
clearstatcache(true, $tokenFile);

if (false === $token = file_get_contents($tokenFile)) {
throw new RuntimeException('Failed to read data');
}

return $token;
}
}
Loading