|
17 | 17 | use Symfony\Contracts\HttpClient\ResponseInterface; |
18 | 18 |
|
19 | 19 | /** |
20 | | - * Provides Credentials from the running EC2 metadata server using the IMDS version 1. |
| 20 | + * Provides Credentials from the running EC2 metadata server using the IMDSv1 and IMDSv2. |
21 | 21 | * |
22 | 22 | * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html |
23 | 23 | * |
24 | 24 | * @author Jérémy Derussé <jeremy@derusse.com> |
25 | 25 | */ |
26 | 26 | final class InstanceProvider implements CredentialProvider |
27 | 27 | { |
28 | | - private const ENDPOINT = 'http://169.254.169.254/latest/meta-data/iam/security-credentials'; |
| 28 | + private const TOKEN_ENDPOINT = 'http://169.254.169.254/latest/api/token'; |
| 29 | + private const METADATA_ENDPOINT = 'http://169.254.169.254/latest/meta-data/iam/security-credentials'; |
29 | 30 |
|
30 | 31 | private $logger; |
31 | 32 |
|
32 | 33 | private $httpClient; |
33 | 34 |
|
34 | 35 | private $timeout; |
35 | 36 |
|
36 | | - public function __construct(?HttpClientInterface $httpClient = null, ?LoggerInterface $logger = null, float $timeout = 1.0) |
| 37 | + private $tokenTtl; |
| 38 | + |
| 39 | + public function __construct(?HttpClientInterface $httpClient = null, ?LoggerInterface $logger = null, float $timeout = 1.0, int $tokenTtl = 21600) |
37 | 40 | { |
38 | 41 | $this->logger = $logger ?? new NullLogger(); |
39 | 42 | $this->httpClient = $httpClient ?? HttpClient::create(); |
40 | 43 | $this->timeout = $timeout; |
| 44 | + $this->tokenTtl = $tokenTtl; |
41 | 45 | } |
42 | 46 |
|
43 | 47 | public function getCredentials(Configuration $configuration): ?Credentials |
44 | 48 | { |
| 49 | + $token = $this->getToken(); |
| 50 | + $headers = []; |
| 51 | + |
| 52 | + if (null !== $token) { |
| 53 | + $headers = ['X-aws-ec2-metadata-token' => $token]; |
| 54 | + } |
| 55 | + |
45 | 56 | try { |
46 | 57 | // Fetch current Profile |
47 | | - $response = $this->httpClient->request('GET', self::ENDPOINT, ['timeout' => $this->timeout]); |
| 58 | + $response = $this->httpClient->request('GET', self::METADATA_ENDPOINT, [ |
| 59 | + 'timeout' => $this->timeout, |
| 60 | + 'headers' => $headers, |
| 61 | + ]); |
48 | 62 | $profile = $response->getContent(); |
49 | 63 |
|
50 | 64 | // Fetch credentials from profile |
51 | | - $response = $this->httpClient->request('GET', self::ENDPOINT . '/' . $profile, ['timeout' => $this->timeout]); |
| 65 | + $response = $this->httpClient->request('GET', self::METADATA_ENDPOINT . '/' . $profile, [ |
| 66 | + 'timeout' => $this->timeout, |
| 67 | + 'headers' => $headers, |
| 68 | + ]); |
52 | 69 | $result = $this->toArray($response); |
53 | 70 |
|
54 | 71 | if ('Success' !== $result['Code']) { |
@@ -106,4 +123,22 @@ private function toArray(ResponseInterface $response): array |
106 | 123 |
|
107 | 124 | return $content; |
108 | 125 | } |
| 126 | + |
| 127 | + private function getToken(): ?string |
| 128 | + { |
| 129 | + try { |
| 130 | + $response = $this->httpClient->request('PUT', self::TOKEN_ENDPOINT, |
| 131 | + [ |
| 132 | + 'timeout' => $this->timeout, |
| 133 | + 'headers' => ['X-aws-ec2-metadata-token-ttl-seconds' => $this->tokenTtl], |
| 134 | + ] |
| 135 | + ); |
| 136 | + |
| 137 | + return $response->getContent(); |
| 138 | + } catch (TransportExceptionInterface|HttpExceptionInterface $e) { |
| 139 | + $this->logger->info('Failed to fetch metadata token for IMDSv2, fallback to IMDSv1.', ['exception' => $e]); |
| 140 | + |
| 141 | + return null; |
| 142 | + } |
| 143 | + } |
109 | 144 | } |
0 commit comments