Skip to content

Commit 61d9bfd

Browse files
HimtanayaHimtanaya Bhadada
and
Himtanaya Bhadada
authored
Migrate from IMDSv1 to IMDSv2 (#78)
IMDSv2 uses session authentication to retrieve EC2 instance metadata. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html Co-authored-by: Himtanaya Bhadada <himmy@amazon.com>
1 parent 24cc7e5 commit 61d9bfd

File tree

2 files changed

+43
-9
lines changed

2 files changed

+43
-9
lines changed

aws_embedded_metrics/environment/ec2_environment.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,34 @@
2424
log = logging.getLogger(__name__)
2525
Config = get_config()
2626

27+
# Documentation for configuring instance metadata can be found here:
28+
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
29+
TOKEN_ENDPOINT = "http://169.254.169.254/latest/api/token"
30+
TOKEN_REQUEST_HEADER_KEY = "X-aws-ec2-metadata-token-ttl-seconds"
31+
TOKEN_REQUEST_HEADER_VALUE = "21600"
2732
DEFAULT_EC2_METADATA_ENDPOINT = (
2833
"http://169.254.169.254/latest/dynamic/instance-identity/document"
2934
)
35+
METADATA_REQUEST_HEADER_KEY = "X-aws-ec2-metadata-token"
3036

3137

32-
async def fetch( # type: ignore
33-
session: aiohttp.ClientSession, url: str
38+
async def fetchJSON(
39+
session: aiohttp.ClientSession, method: str, url: str, headers: Dict[str, str],
3440
) -> Dict[str, Any]:
35-
async with session.get(url, timeout=2) as response:
41+
async with session.request(method, url, timeout=2, headers=headers) as response:
3642
# content_type=None prevents validation of the HTTP Content-Type header
3743
# The EC2 metadata endpoint uses text/plain instead of application/json
3844
# https://github.com/aio-libs/aiohttp/blob/7f777333a4ec0043ddf2e8d67146a626089773d9/aiohttp/web_request.py#L582-L585
3945
return cast(Dict[str, Any], await response.json(content_type=None))
4046

4147

48+
async def fetchString(
49+
session: aiohttp.ClientSession, method: str, url: str, headers: Dict[str, str]
50+
) -> str:
51+
async with session.request(method, url, timeout=2, headers=headers) as response:
52+
return await response.text()
53+
54+
4255
class EC2Environment(Environment):
4356
def __init__(self) -> None:
4457
self.sink: Optional[AgentSink] = None
@@ -48,10 +61,22 @@ async def probe(self) -> bool:
4861
metadata_endpoint = (
4962
Config.ec2_metadata_endpoint or DEFAULT_EC2_METADATA_ENDPOINT
5063
)
64+
token_header = {TOKEN_REQUEST_HEADER_KEY: TOKEN_REQUEST_HEADER_VALUE}
65+
log.info("Fetching token for EC2 metadata request from: %s", TOKEN_ENDPOINT)
66+
try:
67+
token = await fetchString(session, "PUT", TOKEN_ENDPOINT, token_header)
68+
log.debug("Received token for request to EC2 metadata endpoint.")
69+
except Exception:
70+
log.info(
71+
"Failed to fetch token for EC2 metadata request from %s", TOKEN_ENDPOINT
72+
)
73+
return False
74+
5175
log.info("Fetching EC2 metadata from: %s", metadata_endpoint)
5276
try:
53-
response_json = await fetch(session, metadata_endpoint)
54-
log.debug("Received response from EC2 metdata endpoint.")
77+
metadata_request_header = {METADATA_REQUEST_HEADER_KEY: token}
78+
response_json = await fetchJSON(session, "GET", metadata_endpoint, metadata_request_header)
79+
log.debug("Received response from EC2 metadata endpoint.")
5580
self.metadata = response_json
5681
return True
5782
except Exception:

tests/environment/test_ec2_environment.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
@pytest.mark.asyncio
1717
async def test_probe_returns_true_if_fetch_succeeds(aresponses):
1818
# arrange
19-
configure_response(aresponses, "{}")
19+
configure_response(aresponses, fake.pystr(), "{}")
2020
env = EC2Environment()
2121

2222
# act
@@ -55,7 +55,7 @@ def test_get_name_returns_configured_name():
5555
async def test_get_type_returns_ec2_instance(aresponses):
5656
# arrange
5757
expected = "AWS::EC2::Instance"
58-
configure_response(aresponses, "{}")
58+
configure_response(aresponses, fake.pystr(), "{}")
5959
env = EC2Environment()
6060

6161
# environment MUST be detected before we can access the metadata
@@ -79,6 +79,7 @@ async def test_configure_context_adds_ec2_metadata_props(aresponses):
7979

8080
configure_response(
8181
aresponses,
82+
fake.pystr(),
8283
json.dumps(
8384
{
8485
"imageId": image_id,
@@ -109,12 +110,20 @@ async def test_configure_context_adds_ec2_metadata_props(aresponses):
109110
# Test helper methods
110111

111112

112-
def configure_response(aresponses, json):
113+
def configure_response(aresponses, token, json):
114+
aresponses.add(
115+
"169.254.169.254",
116+
"/latest/api/token",
117+
"put",
118+
aresponses.Response(text=token, headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"}),
119+
)
113120
aresponses.add(
114121
"169.254.169.254",
115122
"/latest/dynamic/instance-identity/document",
116123
"get",
117124
# the ec2-metdata endpoint does not actually set the correct
118125
# content-type header, it will instead use text/plain
119-
aresponses.Response(text=json, content_type="text/plain"),
126+
aresponses.Response(text=json,
127+
content_type="text/plain",
128+
headers={"X-aws-ec2-metadata-token": token})
120129
)

0 commit comments

Comments
 (0)