Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: IMDS support for providing custom endpoint #2859

Merged
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
7 changes: 7 additions & 0 deletions .changes/nextrelease/feat-imds-custom-endpoint-support.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "feature",
"category": "Credentials",
"description": "Adds support for specifying custom IMDS endpoint when using the InstanceProfileProvider."
}
]
35 changes: 35 additions & 0 deletions src/Credentials/CredentialsUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Aws\Credentials;

final class CredentialsUtils
yenfryherrerafeliz marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* Determines whether a given host
* is a loopback address.
*
* @param $host
*
* @return bool
*/
public static function isLoopBackAddress($host): bool
yenfryherrerafeliz marked this conversation as resolved.
Show resolved Hide resolved
{
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);
}
}
135 changes: 132 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,24 @@ 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.
yenfryherrerafeliz marked this conversation as resolved.
Show resolved Hide resolved
* The value must contain a valid URI scheme. If the URI scheme is not https, it must
* resolve to a loopback address.
* - 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 +82,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] ?? null;
if (!empty($this->endpoint) && !$this->isValidEndpoint($this->endpoint)) {
throw new \InvalidArgumentException('The provided URI "' . $this->endpoint . '" is invalid, or contains an unsupported host');
}

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

/**
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,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 invalid, or contains an unsupported host');
}

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:
yenfryherrerafeliz marked this conversation as resolved.
Show resolved Hide resolved
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(gethostbyname($host))
|| in_array(
$uri,
[self::DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT, self::DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT]
);
}

return true;
}
}
2 changes: 1 addition & 1 deletion tests/Credentials/CredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use PHPUnit\Framework\TestCase;

/**
* @covers Aws\Credentials\Credentials
* @covers \Aws\Credentials\Credentials
*/
class CredentialsTest extends TestCase
{
Expand Down
58 changes: 58 additions & 0 deletions tests/Credentials/CredentialsUtilsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
namespace Aws\Test\Credentials;

use Aws\Credentials\CredentialsUtils;
use PHPUnit\Framework\TestCase;

/**
* @covers \Aws\Credentials\CredentialsUtils
*/
class CredentialsUtilsTest extends TestCase
{

/**
* @param string $host
* @param bool $expectedResult
*
* @dataProvider loopBackAddressCasesProvider
*/
public function testLoopBackAddressCases(string $host, bool $expectedResult)
{
$isLoopBack = CredentialsUtils::isLoopBackAddress($host);
$this->assertEquals($expectedResult, $isLoopBack);
}

/**
* @return string[]
*/
public function loopBackAddressCasesProvider(): array
{
return [
'IPv6_invalid_loopBack' =>
[
'host' => '::2',
'expected' => false
],
'IPv6_valid_loopBack' =>
[
'host' => '::1',
'expected' => true
],
'IPv4_invalid_loopBack' =>
[
'host' => '192.168.0.1',
'expected' => false
],
'IPv4_valid_loopBack' =>
[
'host' => '127.0.0.1',
'expected' => true
],
'IPv4_valid_loopBack_2' =>
[
'host' => '127.0.0.255',
'expected' => true
],
];
}
}
Loading