Skip to content

Commit

Permalink
feat: IMDS support for providing custom endpoint
Browse files Browse the repository at this point in the history
When using IMDS for fetching credentials, customers should be able to provide their custom endpoint when desired, and that is what this change does. Basically, customer can provide a custom endpoint by doing one of the following options:
Please note that a valid URI value needs to be provided, otherwise a credential exception will be thrown.
- Providing a parameter called 'endpoint' to the constructor of the InstanceProfileProvider.
- By setting an environment variable called AWS_EC2_METADATA_SERVICE_ENDPOINT.
- By defining a key-value config in the config file ~/.aws/config
  • Loading branch information
yenfryherrerafeliz committed Jan 11, 2024
1 parent 63c7202 commit 5d6492f
Show file tree
Hide file tree
Showing 4 changed files with 443 additions and 34 deletions.
37 changes: 37 additions & 0 deletions src/Credentials/CredentialsUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Aws\Credentials;

final class CredentialsUtils
{
private function __construct() {}

/**
* Determines whether a given host
* is a loopback address.
*
* @param $host
*
* @return bool
*/
public static function isLoopBackAddress($host): bool
{
if (!filter_var($host, FILTER_VALIDATE_IP)) {
return false;
}

if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
if ($host === '::1') {
return true;
}

return false;
}

$loopbackStart = ip2long('127.0.0.0');
$loopbackEnd = ip2long('127.255.255.255');
$ipLong = ip2long($host);

return ($ipLong >= $loopbackStart && $ipLong <= $loopbackEnd);
}
}
33 changes: 2 additions & 31 deletions src/Credentials/EcsCredentialProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ private function getEcsUri()
if (!empty($credFullUri))
return $credFullUri;
}

return self::SERVER_URI . $credsUri;
}

Expand Down Expand Up @@ -192,41 +192,12 @@ private function isCompatibleUri($uri)
if ($host !== $ecsHost
&& $host !== $eksHost
&& $host !== self::EKS_SERVER_HOST_IPV6
&& !$this->isLoopbackAddress(gethostbyname($host))
&& !CredentialsUtils::isLoopBackAddress(gethostbyname($host))
) {
return false;
}
}

return true;
}

/**
* Determines whether or not a given host
* is a loopback address.
*
* @param $host
*
* @return bool
*/
private function isLoopbackAddress($host)
{
if (!filter_var($host, FILTER_VALIDATE_IP)) {
return false;
}

if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
if ($host === '::1') {
return true;
}

return false;
}

$loopbackStart = ip2long('127.0.0.0');
$loopbackEnd = ip2long('127.255.255.255');
$ipLong = ip2long($host);

return ($ipLong >= $loopbackStart && $ipLong <= $loopbackEnd);
}
}
133 changes: 130 additions & 3 deletions src/Credentials/InstanceProfileProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@
*/
class InstanceProfileProvider
{
const SERVER_URI = 'http://169.254.169.254/latest/';
const CRED_PATH = 'meta-data/iam/security-credentials/';
const TOKEN_PATH = 'api/token';
const ENV_DISABLE = 'AWS_EC2_METADATA_DISABLED';
const ENV_TIMEOUT = 'AWS_METADATA_SERVICE_TIMEOUT';
const ENV_RETRIES = 'AWS_METADATA_SERVICE_NUM_ATTEMPTS';
const CFG_EC2_METADATA_V1_DISABLED = 'ec2_metadata_v1_disabled';
const CFG_EC2_METADATA_SERVICE_ENDPOINT = 'ec2_metadata_service_endpoint';
const CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE = 'ec2_metadata_service_endpoint_mode';
const DEFAULT_TIMEOUT = 1.0;
const DEFAULT_RETRIES = 3;
const DEFAULT_TOKEN_TTL_SECONDS = 21600;
const DEFAULT_AWS_EC2_METADATA_V1_DISABLED = false;
const ENDPOINT_MODE_IPv4 = 'IPv4';
const ENDPOINT_MODE_IPv6 = 'IPv6';
private const DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT = 'http://169.254.169.254';
private const DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT = 'http://[fd00:ec2::254]';

/** @var string */
private $profile;
Expand All @@ -49,13 +54,22 @@ class InstanceProfileProvider
/** @var bool|null */
private $ec2MetadataV1Disabled;

/** @var string */
private $endpoint;

/** @var string */
private $endpointMode;

/**
* The constructor accepts the following options:
*
* - timeout: Connection timeout, in seconds.
* - profile: Optional EC2 profile name, if known.
* - retries: Optional number of retries to be attempted.
* - ec2_metadata_v1_disabled: Optional for disabling the fallback to IMDSv1.
* - endpoint: Optional for overriding the default endpoint to be used for fetching credentials.
* - endpoint_mode: Optional for overriding the default endpoint mode (IPv4|IPv6) to be used for
* resolving the default endpoint.
*
* @param array $config Configuration options.
*/
Expand All @@ -66,6 +80,12 @@ public function __construct(array $config = [])
$this->retries = (int) getenv(self::ENV_RETRIES) ?: ($config['retries'] ?? self::DEFAULT_RETRIES);
$this->client = $config['client'] ?? \Aws\default_http_handler();
$this->ec2MetadataV1Disabled = $config[self::CFG_EC2_METADATA_V1_DISABLED] ?? null;
$this->endpoint =$config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT] ?? $config['endpoint'] ?? null;
if (!empty($this->endpoint) && !$this->isValidEndpoint($this->endpoint)) {
throw new \InvalidArgumentException('The provided URI "' . $this->endpoint . '" is not a valid URI scheme');
}

$this->endpointMode = $config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE] ?? $config['endpoint_mode'] ?? null;
}

/**
Expand Down Expand Up @@ -227,7 +247,7 @@ private function request($url, $method = 'GET', $headers = [])
}

$fn = $this->client;
$request = new Request($method, self::SERVER_URI . $url);
$request = new Request($method, $this->resolveEndpoint() . $url);
$userAgent = 'aws-sdk-php/' . Sdk::VERSION;
if (defined('HHVM_VERSION')) {
$userAgent .= ' HHVM/' . HHVM_VERSION;
Expand Down Expand Up @@ -314,7 +334,7 @@ private function decodeResult($response)
*
* @return bool
*/
private function shouldFallbackToIMDSv1(): bool
private function shouldFallbackToIMDSv1(): bool
{
$isImdsV1Disabled = \Aws\boolean_value($this->ec2MetadataV1Disabled)
?? \Aws\boolean_value(
Expand All @@ -329,4 +349,111 @@ private function shouldFallbackToIMDSv1(): bool

return !$isImdsV1Disabled;
}

/**
* Resolves the metadata service endpoint. If the endpoint is not provided
* or configured then, the default endpoint, based on the endpoint mode resolved,
* will be used.
* Example: if endpoint_mode is resolved to be IPv4 and the endpoint is not provided
* then, the endpoint to be used will be http://169.254.169.254.
*
* @return string
*/
private function resolveEndpoint(): string
{
$endpoint = $this->endpoint;
if (is_null($endpoint)) {
$endpoint = ConfigurationResolver::resolve(
self::CFG_EC2_METADATA_SERVICE_ENDPOINT,
$this->getDefaultEndpoint(),
'string',
['use_aws_shared_config_files' => true]
);
}

if (!$this->isValidEndpoint($endpoint)) {
throw new CredentialsException('The provided URI "' . $endpoint . '" is not a valid URI scheme');
}

if (substr($endpoint, strlen($endpoint) - 1) !== '/') {
$endpoint = $endpoint . '/';
}

return $endpoint . 'latest/';
}

/**
* Resolves the default metadata service endpoint.
* If endpoint_mode is resolved as IPv4 then:
* - endpoint = http://169.254.169.254
* If endpoint_mode is resolved as IPv6 then:
* - endpoint = http://[fd00:ec2::254]
*
* @return string
*/
private function getDefaultEndpoint(): string
{
$endpointMode = $this->resolveEndpointMode();
switch ($endpointMode) {
case self::ENDPOINT_MODE_IPv4:
return self::DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT;
case self::ENDPOINT_MODE_IPv6:
return self::DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT;
}

throw new CredentialsException("Invalid endpoint mode '$endpointMode' resolved");
}

/**
* Resolves the endpoint mode to be considered when resolving the default
* metadata service endpoint.
*
* @return string
*/
private function resolveEndpointMode(): string
{
$endpointMode = $this->endpointMode;
if (is_null($endpointMode)) {
$endpointMode = ConfigurationResolver::resolve(
self::CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE,
self::ENDPOINT_MODE_IPv4,
'string',
['use_aws_shared_config_files' => true]
);
}

return $endpointMode;
}

/**
* This method checks for whether a provide URI is valid.
* @param string $uri this parameter is the uri to do the validation against to.
* if the value for $uri is null.
*
* @return string|null
*/
private function isValidEndpoint(
string $uri
): bool
{
// We make sure first the provided uri is a valid URL
$isValidURL = filter_var($uri, FILTER_VALIDATE_URL) !== false;
if (!$isValidURL) {
return false;
}

// We make sure that if is a no secure host then it must be a loop back address.
$parsedUri = parse_url($uri);
if ($parsedUri['scheme'] !== 'https') {
$host = trim($parsedUri['host'], '[]');

return CredentialsUtils::isLoopBackAddress($host)
|| in_array(
$uri,
[self::DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT, self::DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT]
);
}

return true;
}
}
Loading

0 comments on commit 5d6492f

Please sign in to comment.