Skip to content

Commit 5725ae0

Browse files
committed
feat(mmds): Accept EC2 IMDS-compatible custom headers
The custom headers supported by MMDS (X-metadata-token and X-metadata-token-ttl-seconds) are different from what EC2 IMDS supports (X-aws-ec2-metadata-token and X-aws-ec2-metadata-token-ttl-seconds). Supporting EC2 IMDS-compatible custom headers in MMDS, users become able to make their workloads work with MMDS out of the box. Signed-off-by: Takahiro Itazuri <[email protected]>
1 parent 9fed0e3 commit 5725ae0

File tree

6 files changed

+138
-79
lines changed

6 files changed

+138
-79
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ and this project adheres to
2020
taking diff snapshots even if dirty page tracking is disabled, by using
2121
`mincore(2)` to overapproximate the set of dirty pages. Only works if swap is
2222
disabled.
23+
- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290):
24+
Extended MMDS to support the EC2 IMDS-compatible session token headers (i.e.
25+
"X-aws-ec2-metadata-token" and "X-aws-ec2-metadata-token-ttl-seconds")
26+
alongside the MMDS-specific ones.
2327

2428
### Changed
2529

docs/mmds/mmds-user-guide.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,9 @@ token. In order to be successful, the request must respect the following
252252
constraints:
253253

254254
- must be directed towards `/latest/api/token` path
255-
- must contain a `X-metadata-token-ttl-seconds` header specifying the token
256-
lifetime in seconds. The value cannot be lower than 1 or greater than 21600 (6
257-
hours).
255+
- must contain a `X-metadata-token-ttl-seconds` or
256+
`X-aws-ec2-metadata-token-ttl-seconds` header specifying the token lifetime in
257+
seconds. The value cannot be lower than 1 or greater than 21600 (6 hours).
258258
- must not contain a `X-Forwarded-For` header.
259259

260260
```bash
@@ -266,8 +266,8 @@ TOKEN=`curl -X PUT "http://${MMDS_IPV4_ADDR}/latest/api/token" \
266266
The HTTP response from MMDS is a plaintext containing the session token.
267267

268268
During the duration specified by the token's time to live value, all subsequent
269-
`GET` requests must specify the session token through the `X-metadata-token`
270-
header in order to fetch data from MMDS.
269+
`GET` requests must specify the session token through the `X-metadata-token` or
270+
`X-aws-ec2-metadata-token` header in order to fetch data from MMDS.
271271

272272
```bash
273273
MMDS_IPV4_ADDR=169.254.170.2

src/vmm/src/mmds/mod.rs

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ use serde_json::{Map, Value};
2121
use crate::mmds::data_store::{Mmds, MmdsDatastoreError as MmdsError, MmdsVersion, OutputFormat};
2222
use crate::mmds::token::PATH_TO_TOKEN;
2323
use crate::mmds::token_headers::{
24-
REJECTED_HEADER, X_METADATA_TOKEN_HEADER, X_METADATA_TOKEN_TTL_SECONDS_HEADER,
25-
get_header_value_pair,
24+
REJECTED_HEADER, X_AWS_EC2_METADATA_TOKEN_HEADER, X_AWS_EC2_METADATA_TOKEN_SSL_SECONDS_HEADER,
25+
X_METADATA_TOKEN_HEADER, X_METADATA_TOKEN_TTL_SECONDS_HEADER, get_header_value_pair,
2626
};
2727

2828
#[rustfmt::skip]
@@ -35,9 +35,9 @@ pub enum VmmMmdsError {
3535
InvalidURI,
3636
/// Not allowed HTTP method.
3737
MethodNotAllowed,
38-
/// No MMDS token provided. Use `X-metadata-token` header to specify the session token.
38+
/// No MMDS token provided. Use `X-metadata-token` or `X-aws-ec2-metadata-token` header to specify the session token.
3939
NoTokenProvided,
40-
/// Token time to live value not found. Use `X-metadata-token-ttl-seconds` header to specify the token's lifetime.
40+
/// Token time to live value not found. Use `X-metadata-token-ttl-seconds` or `X-aws-ec2-metadata-token-ttl-seconds` header to specify the token's lifetime.
4141
NoTtlProvided,
4242
/// Resource not found: {0}.
4343
ResourceNotFound(String),
@@ -164,19 +164,21 @@ fn respond_to_request_mmdsv2(mmds: &mut Mmds, request: Request) -> Response {
164164

165165
fn respond_to_get_request_checked(mmds: &Mmds, request: Request) -> Response {
166166
// Check whether a token exists.
167-
let token =
168-
match get_header_value_pair(request.headers.custom_entries(), X_METADATA_TOKEN_HEADER) {
169-
Some((_, token)) => token,
170-
None => {
171-
let error_msg = VmmMmdsError::NoTokenProvided.to_string();
172-
return build_response(
173-
request.http_version(),
174-
StatusCode::Unauthorized,
175-
MediaType::PlainText,
176-
Body::new(error_msg),
177-
);
178-
}
179-
};
167+
let token = match get_header_value_pair(
168+
request.headers.custom_entries(),
169+
&[X_METADATA_TOKEN_HEADER, X_AWS_EC2_METADATA_TOKEN_HEADER],
170+
) {
171+
Some((_, token)) => token,
172+
None => {
173+
let error_msg = VmmMmdsError::NoTokenProvided.to_string();
174+
return build_response(
175+
request.http_version(),
176+
StatusCode::Unauthorized,
177+
MediaType::PlainText,
178+
Body::new(error_msg),
179+
);
180+
}
181+
};
180182

181183
// Validate the token.
182184
match mmds.is_valid_token(token) {
@@ -267,36 +269,41 @@ fn respond_to_put_request(mmds: &mut Mmds, request: Request) -> Response {
267269
}
268270

269271
// Get token lifetime value.
270-
let ttl_seconds =
271-
match get_header_value_pair(custom_headers, X_METADATA_TOKEN_TTL_SECONDS_HEADER) {
272-
// Header found
273-
Some((k, v)) => match v.parse::<u32>() {
274-
Ok(ttl_seconds) => ttl_seconds,
275-
Err(_) => {
276-
return build_response(
277-
request.http_version(),
278-
StatusCode::BadRequest,
279-
MediaType::PlainText,
280-
Body::new(
281-
RequestError::HeaderError(HttpHeaderError::InvalidValue(
282-
k.into(),
283-
v.into(),
284-
))
285-
.to_string(),
286-
),
287-
);
288-
}
289-
},
290-
// Header not found
291-
None => {
272+
let ttl_seconds = match get_header_value_pair(
273+
custom_headers,
274+
&[
275+
X_METADATA_TOKEN_TTL_SECONDS_HEADER,
276+
X_AWS_EC2_METADATA_TOKEN_SSL_SECONDS_HEADER,
277+
],
278+
) {
279+
// Header found
280+
Some((k, v)) => match v.parse::<u32>() {
281+
Ok(ttl_seconds) => ttl_seconds,
282+
Err(_) => {
292283
return build_response(
293284
request.http_version(),
294285
StatusCode::BadRequest,
295286
MediaType::PlainText,
296-
Body::new(VmmMmdsError::NoTtlProvided.to_string()),
287+
Body::new(
288+
RequestError::HeaderError(HttpHeaderError::InvalidValue(
289+
k.into(),
290+
v.into(),
291+
))
292+
.to_string(),
293+
),
297294
);
298295
}
299-
};
296+
},
297+
// Header not found
298+
None => {
299+
return build_response(
300+
request.http_version(),
301+
StatusCode::BadRequest,
302+
MediaType::PlainText,
303+
Body::new(VmmMmdsError::NoTtlProvided.to_string()),
304+
);
305+
}
306+
};
300307

301308
// Generate token.
302309
let result = mmds.generate_token(ttl_seconds);
@@ -854,13 +861,14 @@ mod tests {
854861

855862
assert_eq!(
856863
VmmMmdsError::NoTokenProvided.to_string(),
857-
"No MMDS token provided. Use `X-metadata-token` header to specify the session token."
864+
"No MMDS token provided. Use `X-metadata-token` or `X-aws-ec2-metadata-token` header \
865+
to specify the session token."
858866
);
859867

860868
assert_eq!(
861869
VmmMmdsError::NoTtlProvided.to_string(),
862-
"Token time to live value not found. Use `X-metadata-token-ttl-seconds` header to \
863-
specify the token's lifetime."
870+
"Token time to live value not found. Use `X-metadata-token-ttl-seconds` or \
871+
`X-aws-ec2-metadata-token-ttl-seconds` header to specify the token's lifetime."
864872
);
865873

866874
assert_eq!(

src/vmm/src/mmds/token_headers.rs

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ pub const REJECTED_HEADER: &str = "X-Forwarded-For";
88

99
// `X-metadata-token`
1010
pub(crate) const X_METADATA_TOKEN_HEADER: &str = "x-metadata-token";
11+
// `X-aws-ec2-metadata-token`
12+
pub(crate) const X_AWS_EC2_METADATA_TOKEN_HEADER: &str = "x-aws-ec2-metadata-token";
1113
// `X-metadata-token-ttl-seconds`
1214
pub(crate) const X_METADATA_TOKEN_TTL_SECONDS_HEADER: &str = "x-metadata-token-ttl-seconds";
15+
// `X-aws-ec2-metadata-token-ttl-seconds`
16+
pub(crate) const X_AWS_EC2_METADATA_TOKEN_SSL_SECONDS_HEADER: &str =
17+
"x-aws-ec2-metadata-token-ttl-seconds";
1318

1419
pub(crate) fn get_header_value_pair<'a>(
1520
custom_headers: &'a HashMap<String, String>,
16-
header: &'static str,
21+
headers: &'a [&'static str],
1722
) -> Option<(&'a String, &'a String)> {
1823
custom_headers
1924
.iter()
20-
.find(|(k, _)| k.eq_ignore_ascii_case(header))
25+
.find(|(k, _)| headers.iter().any(|header| k.eq_ignore_ascii_case(header)))
2126
}
2227

2328
#[cfg(test)]
@@ -39,38 +44,42 @@ mod tests {
3944

4045
#[test]
4146
fn test_get_header_value_pair() {
47+
let headers = [X_METADATA_TOKEN_HEADER, X_AWS_EC2_METADATA_TOKEN_HEADER];
48+
4249
// No custom headers
4350
let custom_headers = HashMap::default();
44-
let token = get_header_value_pair(&custom_headers, X_METADATA_TOKEN_HEADER);
51+
let token = get_header_value_pair(&custom_headers, &headers);
4552
assert!(token.is_none());
4653

4754
// Unrelated custom headers
4855
let custom_headers = HashMap::from([
4956
("Some-Header".into(), "10".into()),
5057
("Another-Header".into(), "value".into()),
5158
]);
52-
let token = get_header_value_pair(&custom_headers, X_METADATA_TOKEN_HEADER);
59+
let token = get_header_value_pair(&custom_headers, &headers);
5360
assert!(token.is_none());
5461

55-
// Valid header
56-
let expected = "THIS_IS_TOKEN";
57-
let custom_headers = HashMap::from([(X_METADATA_TOKEN_HEADER.into(), expected.into())]);
58-
let token = get_header_value_pair(&custom_headers, X_METADATA_TOKEN_HEADER).unwrap();
59-
assert_eq!(token, (&X_METADATA_TOKEN_HEADER.into(), &expected.into()));
62+
for header in headers {
63+
// Valid header
64+
let expected = "THIS_IS_TOKEN";
65+
let custom_headers = HashMap::from([(header.into(), expected.into())]);
66+
let token = get_header_value_pair(&custom_headers, &headers).unwrap();
67+
assert_eq!(token, (&header.into(), &expected.into()));
6068

61-
// Valid header in unrelated custom headers
62-
let custom_headers = HashMap::from([
63-
("Some-Header".into(), "10".into()),
64-
("Another-Header".into(), "value".into()),
65-
(X_METADATA_TOKEN_HEADER.into(), expected.into()),
66-
]);
67-
let token = get_header_value_pair(&custom_headers, X_METADATA_TOKEN_HEADER).unwrap();
68-
assert_eq!(token, (&X_METADATA_TOKEN_HEADER.into(), &expected.into()));
69+
// Valid header in unrelated custom headers
70+
let custom_headers = HashMap::from([
71+
("Some-Header".into(), "10".into()),
72+
("Another-Header".into(), "value".into()),
73+
(header.into(), expected.into()),
74+
]);
75+
let token = get_header_value_pair(&custom_headers, &headers).unwrap();
76+
assert_eq!(token, (&header.into(), &expected.into()));
6977

70-
// Test case-insensitiveness
71-
let header = to_mixed_case(X_METADATA_TOKEN_HEADER);
72-
let custom_headers = HashMap::from([(header.clone(), expected.into())]);
73-
let token = get_header_value_pair(&custom_headers, X_METADATA_TOKEN_HEADER).unwrap();
74-
assert_eq!(token, (&header, &expected.into()));
78+
// Test case-insensitiveness
79+
let header = to_mixed_case(header);
80+
let custom_headers = HashMap::from([(header.clone(), expected.into())]);
81+
let token = get_header_value_pair(&custom_headers, &headers).unwrap();
82+
assert_eq!(token, (&header, &expected.into()));
83+
}
7584
}
7685
}

tests/framework/utils.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,25 +397,35 @@ def get_kernel_version(level=2):
397397
return linux_version
398398

399399

400-
def generate_mmds_session_token(ssh_connection, ipv4_address, token_ttl):
400+
def generate_mmds_session_token(
401+
ssh_connection, ipv4_address, token_ttl, imds_compat=False
402+
):
401403
"""Generate session token used for MMDS V2 requests."""
402404
cmd = "curl -m 2 -s"
403405
cmd += " -X PUT"
404-
cmd += ' -H "X-metadata-token-ttl-seconds: {}"'.format(token_ttl)
406+
if imds_compat:
407+
cmd += ' -H "X-aws-ec2-metadata-token-ttl-seconds: {}"'.format(token_ttl)
408+
else:
409+
cmd += ' -H "X-metadata-token-ttl-seconds: {}"'.format(token_ttl)
405410
cmd += " http://{}/latest/api/token".format(ipv4_address)
406411
_, stdout, _ = ssh_connection.run(cmd)
407412
token = stdout
408413

409414
return token
410415

411416

412-
def generate_mmds_get_request(ipv4_address, token=None, app_json=True):
417+
def generate_mmds_get_request(
418+
ipv4_address, token=None, app_json=True, imds_compat=False
419+
):
413420
"""Build `GET` request to fetch metadata from MMDS."""
414421
cmd = "curl -m 2 -s"
415422

416423
if token is not None:
417424
cmd += " -X GET"
418-
cmd += ' -H "X-metadata-token: {}"'.format(token)
425+
if imds_compat:
426+
cmd += ' -H "X-aws-ec2-metadata-token: {}"'.format(token)
427+
else:
428+
cmd += ' -H "X-metadata-token: {}"'.format(token)
419429

420430
if app_json:
421431
cmd += ' -H "Accept: application/json"'

tests/integration_tests/functional/test_mmds.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,35 @@ def _validate_mmds_snapshot(
128128
run_guest_cmd(ssh_connection, cmd, data_store, use_json=True)
129129

130130

131+
@pytest.mark.parametrize("version", MMDS_VERSIONS)
132+
@pytest.mark.parametrize("imds_compat", [True, False])
133+
def test_token_generation(uvm_plain, version, imds_compat):
134+
"""
135+
Test MMDS token generation.
136+
"""
137+
test_microvm = uvm_plain
138+
test_microvm.spawn()
139+
140+
test_microvm.add_net_iface()
141+
configure_mmds(test_microvm, iface_ids=["eth0"], version=version)
142+
populate_data_store(test_microvm, {"foo": "bar"})
143+
144+
test_microvm.basic_config(vcpu_count=1)
145+
test_microvm.start()
146+
ssh_connection = test_microvm.ssh
147+
148+
cmd = "ip route add {} dev eth0".format(DEFAULT_IPV4)
149+
run_guest_cmd(ssh_connection, cmd, "")
150+
151+
token = generate_mmds_session_token(ssh_connection, DEFAULT_IPV4, 60, imds_compat)
152+
if version == "V1":
153+
assert token == "Not allowed HTTP method."
154+
# V1 accepts GET request even with an invalid token. So keep going.
155+
156+
cmd = generate_mmds_get_request(DEFAULT_IPV4, token, False, imds_compat) + "foo"
157+
run_guest_cmd(ssh_connection, cmd, "bar")
158+
159+
131160
@pytest.mark.parametrize("version", MMDS_VERSIONS)
132161
def test_custom_ipv4(uvm_plain, version):
133162
"""
@@ -647,7 +676,7 @@ def test_mmds_v2_negative(uvm_plain):
647676
# Check `GET` request fails when token is not provided.
648677
cmd = generate_mmds_get_request(DEFAULT_IPV4)
649678
expected = (
650-
"No MMDS token provided. Use `X-metadata-token` header "
679+
"No MMDS token provided. Use `X-metadata-token` or `X-aws-ec2-metadata-token` header "
651680
"to specify the session token."
652681
)
653682
run_guest_cmd(ssh_connection, cmd, expected)
@@ -664,9 +693,8 @@ def test_mmds_v2_negative(uvm_plain):
664693
# Check `PUT` request fails when token TTL is not provided.
665694
cmd = f"curl -m 2 -s -X PUT http://{DEFAULT_IPV4}/latest/api/token"
666695
expected = (
667-
"Token time to live value not found. Use "
668-
"`X-metadata-token-ttl-seconds` header to specify "
669-
"the token's lifetime."
696+
"Token time to live value not found. Use `X-metadata-token-ttl-seconds` or "
697+
"`X-aws-ec2-metadata-token-ttl-seconds` header to specify the token's lifetime."
670698
)
671699
run_guest_cmd(ssh_connection, cmd, expected)
672700

0 commit comments

Comments
 (0)