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 'ec2_metadata_service_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 Dec 22, 2023
1 parent 63c7202 commit 24dd452
Show file tree
Hide file tree
Showing 2 changed files with 414 additions and 3 deletions.
156 changes: 153 additions & 3 deletions src/Credentials/InstanceProfileProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace Aws\Credentials;

use Aws\Configuration\ConfigurationResolver;
use Aws\Credentials\Utils\Validator;
use Aws\Exception\CredentialsException;
use Aws\Exception\InvalidJsonException;
use Aws\Sdk;
Expand All @@ -16,17 +17,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,6 +55,12 @@ class InstanceProfileProvider
/** @var bool|null */
private $ec2MetadataV1Disabled;

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

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

/**
* The constructor accepts the following options:
*
Expand All @@ -66,6 +78,16 @@ 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 = $this->validateURI(
$config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT] ?? $config['endpoint'] ?? null,
true,
'Aws\Exception\CredentialsException'
);
$this->endpointMode = $this->validateEndpointMode(
$config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE] ?? $config['endpoint_mode'] ?? null,
true,
'Aws\Exception\CredentialsException'
);
}

/**
Expand Down Expand Up @@ -227,7 +249,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 +336,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 +351,132 @@ 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->defaultEndpoint(),
'string',
['use_aws_shared_config_files' => true]
);
}

if (str_ends_with('/', $endpoint) === false) {
$endpoint = $endpoint . '/';
}

$endpoint = $endpoint . 'latest/';

return $this->validateURI($endpoint, false, 'Aws\Exception\CredentialsException');
}

/**
* 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 defaultEndpoint(): 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 $this->validateEndpointMode($endpointMode, false, 'Aws\Exception\CredentialsException');
}

/**
* This method checks for whether a provide URI is valid.
* @param string $uri this parameter is the uri to do the validation against to.
* @param bool $ignoreNulls when this parameter is false then, an exception will be thrown
* if the value for $uri is null.
* @param string $exceptionClass the exception class to be used in case of errors.
*
* @return string|null
*/
private function validateURI(
$uri,
$ignoreNulls=true,
$exceptionClass='\InvalidArgumentException'
): ?string
{
if (is_null($uri) && $ignoreNulls) {
return $uri;
}

if (filter_var($uri, FILTER_VALIDATE_URL) === false) {
throw new $exceptionClass(str_replace('$uri', $uri, 'The provided URI "$uri" is not a valid URI scheme'));
}

return $uri;
}

/**
* This method makes sure that the endpoint mode is either IPv4 or IPv6.
* @param string $endpointMode the endpoint mode to the validation against to.
* @param bool $ignoreNulls when this parameter is false then, an exception will be thrown
* if the value for $endpointMode is null.
* @param $exceptionClass
*
* @return string|null
*/
private function validateEndpointMode(
$endpointMode,
$ignoreNulls=true,
$exceptionClass='\InvalidArgumentException'
): ?string
{
$options = [self::ENDPOINT_MODE_IPv4, self::ENDPOINT_MODE_IPv6];
if (is_null($endpointMode) && $ignoreNulls) {
return $endpointMode;
}

if (!in_array($endpointMode, $options)) {
$errorMessage = str_replace('$value', $endpointMode, 'The provided value $value is not valid option of [$options]');
$errorMessage = str_replace('$options', implode(',', $options), $errorMessage);
throw new $exceptionClass($errorMessage);
}

return $endpointMode;
}
}
Loading

0 comments on commit 24dd452

Please sign in to comment.