From 8d07181c6f34e2126560f3852165aa8f60b61f7d Mon Sep 17 00:00:00 2001 From: Kevin Rudde Date: Tue, 15 Oct 2024 12:26:19 +0200 Subject: [PATCH] Add support for EKS Pod Identity --- couscous.yml | 3 + docs/authentication/pod-identity.md | 11 ++ docs/configuration.md | 8 ++ src/Core/CHANGELOG.md | 4 + src/Core/composer.json | 2 +- src/Core/src/Configuration.php | 6 + .../src/Credentials/ContainerProvider.php | 107 +++++++++++++++++- src/Core/src/Credentials/TokenFileLoader.php | 36 ++++++ .../src/Credentials/WebIdentityProvider.php | 27 +---- 9 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 docs/authentication/pod-identity.md create mode 100644 src/Core/src/Credentials/TokenFileLoader.php diff --git a/couscous.yml b/couscous.yml index 16e128c17..f0278f243 100644 --- a/couscous.yml +++ b/couscous.yml @@ -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 diff --git a/docs/authentication/pod-identity.md b/docs/authentication/pod-identity.md new file mode 100644 index 000000000..373b60fdc --- /dev/null +++ b/docs/authentication/pod-identity.md @@ -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. diff --git a/docs/configuration.md b/docs/configuration.md index 0407407e4..a07184bae 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 diff --git a/src/Core/CHANGELOG.md b/src/Core/CHANGELOG.md index a004e6dd9..884801448 100644 --- a/src/Core/CHANGELOG.md +++ b/src/Core/CHANGELOG.md @@ -2,6 +2,10 @@ ## NOT RELEASED +### Added + +- Added support for EKS Pod Identity + ## 1.22.1 ### Changed diff --git a/src/Core/composer.json b/src/Core/composer.json index 1d23f07f2..3d7ce96d5 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -38,7 +38,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "1.23-dev" } } } diff --git a/src/Core/src/Configuration.php b/src/Core/src/Configuration.php index c48283a40..c7ddf3de3 100644 --- a/src/Core/src/Configuration.php +++ b/src/Core/src/Configuration.php @@ -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'; @@ -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 @@ -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 = [ diff --git a/src/Core/src/Credentials/ContainerProvider.php b/src/Core/src/Credentials/ContainerProvider.php index 6b31e545a..a2ab026ea 100644 --- a/src/Core/src/Credentials/ContainerProvider.php +++ b/src/Core/src/Credentials/ContainerProvider.php @@ -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 @@ -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]); @@ -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; + } } diff --git a/src/Core/src/Credentials/TokenFileLoader.php b/src/Core/src/Credentials/TokenFileLoader.php new file mode 100644 index 000000000..e5f6029a2 --- /dev/null +++ b/src/Core/src/Credentials/TokenFileLoader.php @@ -0,0 +1,36 @@ +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; - } }