diff --git a/CHANGELOG.md b/CHANGELOG.md index 579f1193877..1dcf21a3883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,16 @@ and this project adheres to - [#5175](https://github.com/firecracker-microvm/firecracker/pull/5175): Allow including a custom cpu template directly in the json configuration file passed to `--config-file` under the `cpu_config` key. +- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290): + Extended MMDS to support the EC2 IMDS-compatible session token headers (i.e. + "X-aws-ec2-metadata-token" and "X-aws-ec2-metadata-token-ttl-seconds") + alongside the MMDS-specific ones. +- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290): Added + `mmds.rx_invalid_token` and `mmds.rx_no_token` metrics to track the number of + GET requests that were rejected due to token validation failures in MMDS + version 2. These metrics also count requests that would be rejected in MMDS + version 2 when MMDS version 1 is configured. They helps users assess readiness + for migrating to MMDS version 2. ### Changed @@ -24,6 +34,14 @@ and this project adheres to Incremental snapshots remain in developer preview. - [#5282](https://github.com/firecracker-microvm/firecracker/pull/5282): Updated jailer to no longer require the executable file name to contain `firecracker`. +- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290): Changed + MMDS to validate the value of "X-metadata-token-ttl-seconds" header only if it + is a PUT request to /latest/api/token, as in EC2 IMDS. +- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290): Changed + MMDS version 1 to support the session oriented method as in version 2, + allowing easier migration to version 2. Note that MMDS version 1 accepts a GET + request even with no token or an invalid token so that existing workloads + continue to work. ### Deprecated @@ -40,6 +58,9 @@ and this project adheres to - [#5260](https://github.com/firecracker-microvm/firecracker/pull/5260): Fixed a bug allowing the block device to starve all other devices when backed by a sufficiently slow drive. +- [#XXXX](https://github.com/firecracker-microvm/firecracker/pull/XXXX): Fixed + MMDS to reject PUT requests containing `X-Forwarded-For` header regardless of + its casing (e.g. `x-forwarded-for`). ## [1.12.0] diff --git a/docs/mmds/mmds-user-guide.md b/docs/mmds/mmds-user-guide.md index 50eeb9e41e2..a08ce707d95 100644 --- a/docs/mmds/mmds-user-guide.md +++ b/docs/mmds/mmds-user-guide.md @@ -231,8 +231,16 @@ must be issued. The requested resource can be referenced by its corresponding the MMDS request. The HTTP response content will contain the referenced metadata resource. -The only HTTP method supported by MMDS version 1 is `GET`. Requests containing -any other HTTP method will receive **405 Method Not Allowed** error. +As in version 2, version 1 also supports a session oriented method in order to +make the migration easier. See [the next section](#version-2) for the session +oriented method. Note that version 1 returns a successful response to a `GET` +request even with an invalid token or no token not to break existing workloads. +`mmds.rx_invalid_token` and `mmds.rx_no_token` metrics track the number of `GET` +requests with invalid tokens and missing tokens respectively, helping users +evaluate their readiness for migrating to MMDS version 2. + +Requests containing any other HTTP methods than `GET` and `PUT` will receive +**405 Method Not Allowed** error. ```bash MMDS_IPV4_ADDR=169.254.170.2 @@ -252,9 +260,9 @@ token. In order to be successful, the request must respect the following constraints: - must be directed towards `/latest/api/token` path -- must contain a `X-metadata-token-ttl-seconds` header specifying the token - lifetime in seconds. The value cannot be lower than 1 or greater than 21600 (6 - hours). +- must contain a `X-metadata-token-ttl-seconds` or + `X-aws-ec2-metadata-token-ttl-seconds` header specifying the token lifetime in + seconds. The value cannot be lower than 1 or greater than 21600 (6 hours). - must not contain a `X-Forwarded-For` header. ```bash @@ -266,8 +274,8 @@ TOKEN=`curl -X PUT "http://${MMDS_IPV4_ADDR}/latest/api/token" \ The HTTP response from MMDS is a plaintext containing the session token. During the duration specified by the token's time to live value, all subsequent -`GET` requests must specify the session token through the `X-metadata-token` -header in order to fetch data from MMDS. +`GET` requests must specify the session token through the `X-metadata-token` or +`X-aws-ec2-metadata-token` header in order to fetch data from MMDS. ```bash MMDS_IPV4_ADDR=169.254.170.2 diff --git a/resources/chroot.sh b/resources/chroot.sh index e7177d7e2ca..93f6ca754f0 100755 --- a/resources/chroot.sh +++ b/resources/chroot.sh @@ -11,7 +11,7 @@ PS4='+\t ' cp -ruv $rootfs/* / -packages="udev systemd-sysv openssh-server iproute2 curl socat python3-minimal iperf3 iputils-ping fio kmod tmux hwloc-nox vim-tiny trace-cmd linuxptp strace" +packages="udev systemd-sysv openssh-server iproute2 curl socat python3-minimal iperf3 iputils-ping fio kmod tmux hwloc-nox vim-tiny trace-cmd linuxptp strace python3-boto3" # msr-tools is only supported on x86-64. arch=$(uname -m) diff --git a/resources/rebuild.sh b/resources/rebuild.sh index 56afd1bdbac..c6d5e2dd38d 100755 --- a/resources/rebuild.sh +++ b/resources/rebuild.sh @@ -65,6 +65,12 @@ for d in $dirs; do tar c "/$d" | tar x -C $rootfs; done mkdir -pv $rootfs/{dev,proc,sys,run,tmp,var/lib/systemd} # So apt works mkdir -pv $rootfs/var/lib/dpkg/ + +# Install AWS CLI v2 +curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +unzip awscliv2.zip +./aws/install --install-dir $rootfs/usr/local/aws-cli --bin-dir $rootfs/usr/local/bin +rm -rf awscliv2.zip aws EOF # TBD what abt /etc/hosts? diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index 4a810ee083a..74f03e6b111 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -987,7 +987,7 @@ pub(crate) mod tests { net_builder.build(net_config).unwrap(); let net = net_builder.iter().next().unwrap(); let mut mmds = Mmds::default(); - mmds.set_version(mmds_version).unwrap(); + mmds.set_version(mmds_version); net.lock().unwrap().configure_mmds_network_stack( MmdsNetworkStack::default_ipv4_addr(), Arc::new(Mutex::new(mmds)), diff --git a/src/vmm/src/device_manager/persist.rs b/src/vmm/src/device_manager/persist.rs index 30a6387bc82..f532d13fc83 100644 --- a/src/vmm/src/device_manager/persist.rs +++ b/src/vmm/src/device_manager/persist.rs @@ -569,7 +569,7 @@ impl<'a> Persist<'a> for MMIODeviceManager { // If there's at least one network device having an mmds_ns, it means // that we are restoring from a version that did not persist the `MmdsVersionState`. // Init with the default. - constructor_args.vm_resources.mmds_or_default(); + constructor_args.vm_resources.mmds_or_default()?; } for net_state in &state.net_devices { diff --git a/src/vmm/src/logger/metrics.rs b/src/vmm/src/logger/metrics.rs index e793495e1f1..3f362622b7d 100644 --- a/src/vmm/src/logger/metrics.rs +++ b/src/vmm/src/logger/metrics.rs @@ -556,6 +556,10 @@ pub struct MmdsMetrics { pub rx_accepted_unusual: SharedIncMetric, /// The number of buffers which couldn't be parsed as valid Ethernet frames by the MMDS. pub rx_bad_eth: SharedIncMetric, + /// The number of GET requests with invalid tokens. + pub rx_invalid_token: SharedIncMetric, + /// The number of GET requests with no tokens. + pub rx_no_token: SharedIncMetric, /// The total number of successful receive operations by the MMDS. pub rx_count: SharedIncMetric, /// The total number of bytes sent by the MMDS. @@ -579,6 +583,8 @@ impl MmdsMetrics { rx_accepted_err: SharedIncMetric::new(), rx_accepted_unusual: SharedIncMetric::new(), rx_bad_eth: SharedIncMetric::new(), + rx_invalid_token: SharedIncMetric::new(), + rx_no_token: SharedIncMetric::new(), rx_count: SharedIncMetric::new(), tx_bytes: SharedIncMetric::new(), tx_count: SharedIncMetric::new(), diff --git a/src/vmm/src/mmds/data_store.rs b/src/vmm/src/mmds/data_store.rs index 22b2ee215fb..ac6ae8e5547 100644 --- a/src/vmm/src/mmds/data_store.rs +++ b/src/vmm/src/mmds/data_store.rs @@ -12,9 +12,9 @@ use crate::mmds::token::{MmdsTokenError as TokenError, TokenAuthority}; /// The Mmds is the Microvm Metadata Service represented as an untyped json. #[derive(Debug)] pub struct Mmds { + version: MmdsVersion, data_store: Value, - // None when MMDS V1 is configured, Some for MMDS V2. - token_authority: Option, + token_authority: TokenAuthority, is_initialized: bool, data_store_limit: usize, } @@ -65,19 +65,20 @@ pub enum MmdsDatastoreError { // Used for ease of use in tests. impl Default for Mmds { fn default() -> Self { - Self::default_with_limit(51200) + Self::try_new(51200).unwrap() } } impl Mmds { /// MMDS default instance with limit `data_store_limit` - pub fn default_with_limit(data_store_limit: usize) -> Self { - Mmds { + pub fn try_new(data_store_limit: usize) -> Result { + Ok(Mmds { + version: MmdsVersion::V1, data_store: Value::default(), - token_authority: None, + token_authority: TokenAuthority::try_new()?, is_initialized: false, data_store_limit, - } + }) } /// This method is needed to check if data store is initialized. @@ -92,52 +93,29 @@ impl Mmds { } /// Set the MMDS version. - pub fn set_version(&mut self, version: MmdsVersion) -> Result<(), MmdsDatastoreError> { - match version { - MmdsVersion::V1 => { - self.token_authority = None; - Ok(()) - } - MmdsVersion::V2 => { - if self.token_authority.is_none() { - self.token_authority = Some(TokenAuthority::new()?); - } - Ok(()) - } - } + pub fn set_version(&mut self, version: MmdsVersion) { + self.version = version; } - /// Return the MMDS version by checking the token authority field. + /// Get the MMDS version. pub fn version(&self) -> MmdsVersion { - if self.token_authority.is_none() { - MmdsVersion::V1 - } else { - MmdsVersion::V2 - } + self.version } /// Sets the Additional Authenticated Data to be used for encryption and - /// decryption of the session token when MMDS version 2 is enabled. + /// decryption of the session token. pub fn set_aad(&mut self, instance_id: &str) { - if let Some(ta) = self.token_authority.as_mut() { - ta.set_aad(instance_id); - } + self.token_authority.set_aad(instance_id); } /// Checks if the provided token has not expired. - pub fn is_valid_token(&self, token: &str) -> Result { - self.token_authority - .as_ref() - .ok_or(TokenError::InvalidState) - .map(|ta| ta.is_valid(token)) + pub fn is_valid_token(&self, token: &str) -> bool { + self.token_authority.is_valid(token) } /// Generate a new Mmds token using the token authority. pub fn generate_token(&mut self, ttl_seconds: u32) -> Result { - self.token_authority - .as_mut() - .ok_or(TokenError::InvalidState) - .and_then(|ta| ta.generate_token_secret(ttl_seconds)) + self.token_authority.generate_token_secret(ttl_seconds) } /// set MMDS data store limit to `data_store_limit` @@ -304,11 +282,11 @@ mod tests { assert_eq!(mmds.version(), MmdsVersion::V1); // Test setting MMDS version to v2. - mmds.set_version(MmdsVersion::V2).unwrap(); + mmds.set_version(MmdsVersion::V2); assert_eq!(mmds.version(), MmdsVersion::V2); - // Test setting MMDS version back to default. - mmds.set_version(MmdsVersion::V1).unwrap(); + // Test setting MMDS version back to v1. + mmds.set_version(MmdsVersion::V1); assert_eq!(mmds.version(), MmdsVersion::V1); } @@ -593,37 +571,4 @@ mod tests { assert_eq!(mmds.get_data_str().len(), 2); } - - #[test] - fn test_is_valid() { - let mut mmds = Mmds::default(); - // Set MMDS version to V2. - mmds.set_version(MmdsVersion::V2).unwrap(); - assert_eq!(mmds.version(), MmdsVersion::V2); - - assert!(!mmds.is_valid_token("aaa").unwrap()); - - mmds.token_authority = None; - assert_eq!( - mmds.is_valid_token("aaa").unwrap_err().to_string(), - TokenError::InvalidState.to_string() - ) - } - - #[test] - fn test_generate_token() { - let mut mmds = Mmds::default(); - // Set MMDS version to V2. - mmds.set_version(MmdsVersion::V2).unwrap(); - assert_eq!(mmds.version(), MmdsVersion::V2); - - let token = mmds.generate_token(1).unwrap(); - assert!(mmds.is_valid_token(&token).unwrap()); - - mmds.token_authority = None; - assert_eq!( - mmds.generate_token(1).err().unwrap().to_string(), - TokenError::InvalidState.to_string() - ); - } } diff --git a/src/vmm/src/mmds/mod.rs b/src/vmm/src/mmds/mod.rs index 7f831d384f8..fbe2505f14b 100644 --- a/src/vmm/src/mmds/mod.rs +++ b/src/vmm/src/mmds/mod.rs @@ -17,8 +17,9 @@ use micro_http::{ Body, HttpHeaderError, MediaType, Method, Request, RequestError, Response, StatusCode, Version, }; use serde_json::{Map, Value}; -use token_headers::TokenHeaders; +use token_headers::{XMetadataToken, XMetadataTokenTtlSeconds}; +use crate::logger::{IncMetric, METRICS}; use crate::mmds::data_store::{Mmds, MmdsDatastoreError as MmdsError, MmdsVersion, OutputFormat}; use crate::mmds::token::PATH_TO_TOKEN; use crate::mmds::token_headers::REJECTED_HEADER; @@ -33,9 +34,9 @@ pub enum VmmMmdsError { InvalidURI, /// Not allowed HTTP method. MethodNotAllowed, - /// No MMDS token provided. Use `X-metadata-token` header to specify the session token. + /// No MMDS token provided. Use `X-metadata-token` or `X-aws-ec2-metadata-token` header to specify the session token. NoTokenProvided, - /// Token time to live value not found. Use `X-metadata-token-ttl-seconds` header to specify the token's lifetime. + /// 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. NoTtlProvided, /// Resource not found: {0}. ResourceNotFound(String), @@ -106,6 +107,7 @@ fn sanitize_uri(mut uri: String) -> String { /// Build a response for `request` and return response based on MMDS version pub fn convert_to_response(mmds: Arc>, request: Request) -> Response { + // Check URI is not empty let uri = request.uri().get_abs_path(); if uri.is_empty() { return build_response( @@ -118,16 +120,13 @@ pub fn convert_to_response(mmds: Arc>, request: Request) -> Response let mut mmds_guard = mmds.lock().expect("Poisoned lock"); - match mmds_guard.version() { - MmdsVersion::V1 => respond_to_request_mmdsv1(&mmds_guard, request), - MmdsVersion::V2 => respond_to_request_mmdsv2(&mut mmds_guard, request), - } -} - -fn respond_to_request_mmdsv1(mmds: &Mmds, request: Request) -> Response { - // Allow only GET requests. + // Allow only GET and PUT requests match request.method() { - Method::Get => respond_to_get_request_unchecked(mmds, request), + Method::Get => match mmds_guard.version() { + MmdsVersion::V1 => respond_to_get_request_v1(&mmds_guard, request), + MmdsVersion::V2 => respond_to_get_request_v2(&mmds_guard, request), + }, + Method::Put => respond_to_put_request(&mut mmds_guard, request), _ => { let mut response = build_response( request.http_version(), @@ -136,52 +135,34 @@ fn respond_to_request_mmdsv1(mmds: &Mmds, request: Request) -> Response { Body::new(VmmMmdsError::MethodNotAllowed.to_string()), ); response.allow_method(Method::Get); + response.allow_method(Method::Put); response } } } -fn respond_to_request_mmdsv2(mmds: &mut Mmds, request: Request) -> Response { - // Fetch custom headers from request. - let token_headers = match TokenHeaders::try_from(request.headers.custom_entries()) { - Ok(token_headers) => token_headers, - Err(err) => { - return build_response( - request.http_version(), - StatusCode::BadRequest, - MediaType::PlainText, - Body::new(err.to_string()), - ); +fn respond_to_get_request_v1(mmds: &Mmds, request: Request) -> Response { + match XMetadataToken::from(request.headers.custom_entries()).0 { + Some(token) => { + if !mmds.is_valid_token(&token) { + METRICS.mmds.rx_invalid_token.inc(); + } } - }; - - // Allow only GET and PUT requests. - match request.method() { - Method::Get => respond_to_get_request_checked(mmds, request, token_headers), - Method::Put => respond_to_put_request(mmds, request, token_headers), - _ => { - let mut response = build_response( - request.http_version(), - StatusCode::MethodNotAllowed, - MediaType::PlainText, - Body::new(VmmMmdsError::MethodNotAllowed.to_string()), - ); - response.allow_method(Method::Get); - response.allow_method(Method::Put); - response + None => { + METRICS.mmds.rx_no_token.inc(); } } + + respond_to_get_request(mmds, request) } -fn respond_to_get_request_checked( - mmds: &Mmds, - request: Request, - token_headers: TokenHeaders, -) -> Response { - // Get MMDS token from custom headers. - let token = match token_headers.x_metadata_token() { +fn respond_to_get_request_v2(mmds: &Mmds, request: Request) -> Response { + // Check whether a token exists. + let x_metadata_token = XMetadataToken::from(request.headers.custom_entries()); + let token = match x_metadata_token.0 { Some(token) => token, None => { + METRICS.mmds.rx_no_token.inc(); let error_msg = VmmMmdsError::NoTokenProvided.to_string(); return build_response( request.http_version(), @@ -192,20 +173,22 @@ fn respond_to_get_request_checked( } }; - // Validate MMDS token. - match mmds.is_valid_token(token) { - Ok(true) => respond_to_get_request_unchecked(mmds, request), - Ok(false) => build_response( - request.http_version(), - StatusCode::Unauthorized, - MediaType::PlainText, - Body::new(VmmMmdsError::InvalidToken.to_string()), - ), - Err(_) => unreachable!(), + // Validate the token. + match mmds.is_valid_token(&token) { + true => respond_to_get_request(mmds, request), + false => { + METRICS.mmds.rx_invalid_token.inc(); + build_response( + request.http_version(), + StatusCode::Unauthorized, + MediaType::PlainText, + Body::new(VmmMmdsError::InvalidToken.to_string()), + ) + } } } -fn respond_to_get_request_unchecked(mmds: &Mmds, request: Request) -> Response { +fn respond_to_get_request(mmds: &Mmds, request: Request) -> Response { let uri = request.uri().get_abs_path(); // The data store expects a strict json path, so we need to @@ -248,21 +231,17 @@ fn respond_to_get_request_unchecked(mmds: &Mmds, request: Request) -> Response { } } -fn respond_to_put_request( - mmds: &mut Mmds, - request: Request, - token_headers: TokenHeaders, -) -> Response { +fn respond_to_put_request(mmds: &mut Mmds, request: Request) -> Response { + let custom_headers = request.headers.custom_entries(); + // Reject `PUT` requests that contain `X-Forwarded-For` header. - if request - .headers - .custom_entries() - .contains_key(REJECTED_HEADER) + if let Some((header, __)) = custom_headers + .iter() + .find(|(k, _)| k.to_lowercase() == REJECTED_HEADER) { - let error_msg = RequestError::HeaderError(HttpHeaderError::UnsupportedName( - REJECTED_HEADER.to_string(), - )) - .to_string(); + let error_msg = + RequestError::HeaderError(HttpHeaderError::UnsupportedName(header.to_string())) + .to_string(); return build_response( request.http_version(), StatusCode::BadRequest, @@ -287,16 +266,26 @@ fn respond_to_put_request( } // Get token lifetime value. - let ttl_seconds = match token_headers.x_metadata_token_ttl_seconds() { - Some(ttl_seconds) => ttl_seconds, - None => { + let ttl_seconds = match XMetadataTokenTtlSeconds::try_from(custom_headers) { + Err(err) => { return build_response( request.http_version(), StatusCode::BadRequest, MediaType::PlainText, - Body::new(VmmMmdsError::NoTtlProvided.to_string()), + Body::new(err.to_string()), ); } + Ok(ttl_seconds) => match ttl_seconds.0 { + Some(ttl_seconds) => ttl_seconds, + None => { + return build_response( + request.http_version(), + StatusCode::BadRequest, + MediaType::PlainText, + Body::new(VmmMmdsError::NoTtlProvided.to_string()), + ); + } + }, }; // Generate token. @@ -430,7 +419,7 @@ mod tests { // Test with empty `Accept` header. micro-http defaults to `Accept: text/plain`. let (request, expected_response) = generate_request_and_expected_response( b"GET http://169.254.169.254/ HTTP/1.0\r\n\" - Accept:\r\n\r\n", + Accept:\r\n\r\n", MediaType::PlainText, ); assert_eq!( @@ -441,7 +430,7 @@ mod tests { // Test with `Accept: */*` header. let (request, expected_response) = generate_request_and_expected_response( b"GET http://169.254.169.254/ HTTP/1.0\r\n\" - Accept: */*\r\n\r\n", + Accept: */*\r\n\r\n", MediaType::PlainText, ); assert_eq!( @@ -452,7 +441,7 @@ mod tests { // Test with `Accept: text/plain`. let (request, expected_response) = generate_request_and_expected_response( b"GET http://169.254.169.254/ HTTP/1.0\r\n\ - Accept: text/plain\r\n\r\n", + Accept: text/plain\r\n\r\n", MediaType::PlainText, ); assert_eq!( @@ -463,285 +452,378 @@ mod tests { // Test with `Accept: application/json`. let (request, expected_response) = generate_request_and_expected_response( b"GET http://169.254.169.254/ HTTP/1.0\r\n\ - Accept: application/json\r\n\r\n", + Accept: application/json\r\n\r\n", MediaType::ApplicationJson, ); assert_eq!(convert_to_response(mmds, request), expected_response); } + // Test the version-independent error paths of `convert_to_response()`. #[test] - fn test_respond_to_request_mmdsv1() { - // Populate MMDS with data. - let mmds = populate_mmds(); - - // Set version to V1. - mmds.lock() - .expect("Poisoned lock") - .set_version(MmdsVersion::V1) - .unwrap(); - assert_eq!( - mmds.lock().expect("Poisoned lock").version(), - MmdsVersion::V1 - ); + fn test_convert_to_response_negative() { + for version in [MmdsVersion::V1, MmdsVersion::V2] { + let mmds = populate_mmds(); + mmds.lock().expect("Poisoned lock").set_version(version); - // Test resource not found. - let request_bytes = b"GET http://169.254.169.254/invalid HTTP/1.0\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::NotFound); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new( - VmmMmdsError::ResourceNotFound(String::from("/invalid")).to_string(), - )); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); - - // Test NotImplemented. - let request_bytes = b"GET /age HTTP/1.1\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http11, StatusCode::NotImplemented); - expected_response.set_content_type(MediaType::PlainText); - let body = "Cannot retrieve value. The value has an unsupported type.".to_string(); - expected_response.set_body(Body::new(body)); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); + // Test InvalidURI (empty absolute path). + let request = Request::try_from(b"GET http:// HTTP/1.0\r\n\r\n", None).unwrap(); + let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); + expected_response.set_content_type(MediaType::PlainText); + expected_response.set_body(Body::new(VmmMmdsError::InvalidURI.to_string())); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); - // Test not allowed HTTP Method. - let not_allowed_methods = ["PUT", "PATCH"]; - for method in not_allowed_methods.iter() { - let request_bytes = format!("{} http://169.254.169.255/ HTTP/1.0\r\n\r\n", method); - let request = Request::try_from(request_bytes.as_bytes(), None).unwrap(); + // Test MethodNotAllowed (PATCH method). + let request = + Request::try_from(b"PATCH http://169.254.169.255/ HTTP/1.0\r\n\r\n", None).unwrap(); let mut expected_response = Response::new(Version::Http10, StatusCode::MethodNotAllowed); expected_response.set_content_type(MediaType::PlainText); expected_response.set_body(Body::new(VmmMmdsError::MethodNotAllowed.to_string())); expected_response.allow_method(Method::Get); + expected_response.allow_method(Method::Put); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response, expected_response); } - - // Test invalid (empty absolute path) URI. - let request_bytes = b"GET http:// HTTP/1.0\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new(VmmMmdsError::InvalidURI.to_string())); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); - - // Test invalid custom header value is ignored when V1 is configured. - let request_bytes = b"GET http://169.254.169.254/name/first HTTP/1.0\r\n\ - Accept: application/json\r\n - X-metadata-token-ttl-seconds: application/json\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::OK); - expected_response.set_body(Body::new("\"John\"")); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); - - // Test Ok path. - let request_bytes = b"GET http://169.254.169.254/ HTTP/1.0\r\n\ - Accept: application/json\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::OK); - let mut body = get_json_data().to_string(); - body.retain(|c| !c.is_whitespace()); - expected_response.set_body(Body::new(body)); - let actual_response = convert_to_response(mmds, request); - assert_eq!(actual_response, expected_response); } #[test] - fn test_respond_to_request_mmdsv2() { - // Populate MMDS with data. + fn test_respond_to_request_mmdsv1() { let mmds = populate_mmds(); - - // Set version to V2. mmds.lock() .expect("Poisoned lock") - .set_version(MmdsVersion::V2) - .unwrap(); - assert_eq!( - mmds.lock().expect("Poisoned lock").version(), - MmdsVersion::V2 - ); + .set_version(MmdsVersion::V1); - // Test not allowed PATCH HTTP Method. - let request_bytes = b"PATCH http://169.254.169.255/ HTTP/1.0\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::MethodNotAllowed); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new(VmmMmdsError::MethodNotAllowed.to_string())); - expected_response.allow_method(Method::Get); - expected_response.allow_method(Method::Put); + // Test valid v1 GET request. + let (request, expected_response) = generate_request_and_expected_response( + b"GET http://169.254.169.254/ HTTP/1.0\r\n\ + Accept: application/json\r\n\r\n", + MediaType::ApplicationJson, + ); + let prev_rx_invalid_token = METRICS.mmds.rx_invalid_token.count(); + let prev_rx_no_token = METRICS.mmds.rx_no_token.count(); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response, expected_response); - - // Test invalid value for custom header. - let request_bytes = b"GET http://169.254.169.254/ HTTP/1.0\r\n\ - Accept: application/json\r\n - X-metadata-token-ttl-seconds: application/json\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new( - "Invalid header. Reason: Invalid value. Key:X-metadata-token-ttl-seconds; \ - Value:application/json" - .to_string(), - )); + assert_eq!(prev_rx_invalid_token, METRICS.mmds.rx_invalid_token.count()); + assert_eq!(prev_rx_no_token + 1, METRICS.mmds.rx_no_token.count()); + + // Test valid PUT request to generate a valid token. + let request = Request::try_from( + b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: 60\r\n\r\n", + None, + ) + .unwrap(); let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); + assert_eq!(actual_response.status(), StatusCode::OK); + assert_eq!(actual_response.content_type(), MediaType::PlainText); + let valid_token = String::from_utf8(actual_response.body().unwrap().body).unwrap(); - // Test PUT requests. - // Unsupported `X-Forwarded-For` header present. - let request_bytes = b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ - X-Forwarded-For: 203.0.113.195\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new( - "Invalid header. Reason: Unsupported header name. Key: X-Forwarded-For".to_string(), - )); + // Test valid v2 GET request. + #[rustfmt::skip] + let (request, expected_response) = generate_request_and_expected_response( + format!( + "GET http://169.254.169.254/ HTTP/1.0\r\n\ + Accept: application/json\r\n\ + X-metadata-token: {valid_token}\r\n\r\n", + ) + .as_bytes(), + MediaType::ApplicationJson, + ); + let prev_rx_invalid_token = METRICS.mmds.rx_invalid_token.count(); + let prev_rx_no_token = METRICS.mmds.rx_no_token.count(); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response, expected_response); + assert_eq!(prev_rx_invalid_token, METRICS.mmds.rx_invalid_token.count()); + assert_eq!(prev_rx_no_token, METRICS.mmds.rx_no_token.count()); - // Test invalid path. - let request_bytes = b"PUT http://169.254.169.254/token HTTP/1.0\r\n\ - X-metadata-token-ttl-seconds: 60\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::NotFound); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new( - VmmMmdsError::ResourceNotFound(String::from("/token")).to_string(), - )); - let actual_response = convert_to_response(mmds.clone(), request); + // Test GET request with invalid token is accepted when v1 is configured. + let (request, expected_response) = generate_request_and_expected_response( + b"GET http://169.254.169.254/ HTTP/1.0\r\n\ + Accept: application/json\r\n\ + X-metadata-token: INVALID_TOKEN\r\n\r\n", + MediaType::ApplicationJson, + ); + let prev_rx_invalid_token = METRICS.mmds.rx_invalid_token.count(); + let prev_rx_no_token = METRICS.mmds.rx_no_token.count(); + let actual_response = convert_to_response(mmds, request); assert_eq!(actual_response, expected_response); + assert_eq!( + prev_rx_invalid_token + 1, + METRICS.mmds.rx_invalid_token.count() + ); + assert_eq!(prev_rx_no_token, METRICS.mmds.rx_no_token.count()); + } - // Test invalid lifetime values for token. - let invalid_values = [MIN_TOKEN_TTL_SECONDS - 1, MAX_TOKEN_TTL_SECONDS + 1]; - for invalid_value in invalid_values.iter() { - let request_bytes = format!( - "PUT http://169.254.169.254/latest/api/token \ - HTTP/1.0\r\nX-metadata-token-ttl-seconds: {}\r\n\r\n", - invalid_value - ); - let request = Request::try_from(request_bytes.as_bytes(), None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); - expected_response.set_content_type(MediaType::PlainText); - let error_msg = format!( - "Invalid time to live value provided for token: {}. Please provide a value \ - between {} and {}.", - invalid_value, MIN_TOKEN_TTL_SECONDS, MAX_TOKEN_TTL_SECONDS - ); - expected_response.set_body(Body::new(error_msg)); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); - } - - // Test no lifetime value provided for token. - let request_bytes = b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new(VmmMmdsError::NoTtlProvided.to_string())); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); + #[test] + fn test_respond_to_request_mmdsv2() { + let mmds = populate_mmds(); + mmds.lock() + .expect("Poisoned lock") + .set_version(MmdsVersion::V2); - // Test valid PUT. - let request_bytes = b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ - X-metadata-token-ttl-seconds: 60\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); + // Test valid PUT to generate a valid token. + let request = Request::try_from( + b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: 60\r\n\r\n", + None, + ) + .unwrap(); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response.status(), StatusCode::OK); assert_eq!(actual_response.content_type(), MediaType::PlainText); - - // Test valid GET. let valid_token = String::from_utf8(actual_response.body().unwrap().body).unwrap(); - let request_bytes = format!( - "GET http://169.254.169.254/ HTTP/1.0\r\nAccept: \ - application/json\r\nX-metadata-token: {}\r\n\r\n", - valid_token - ); - let request = Request::try_from(request_bytes.as_bytes(), None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::OK); - let mut body = get_json_data().to_string(); - body.retain(|c| !c.is_whitespace()); - expected_response.set_body(Body::new(body)); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); - // Test GET request towards unsupported value type. - let request_bytes = format!( - "GET /age HTTP/1.1\r\nX-metadata-token: {}\r\n\r\n", - valid_token - ); - let request = Request::try_from(request_bytes.as_bytes(), None).unwrap(); - let mut expected_response = Response::new(Version::Http11, StatusCode::NotImplemented); - expected_response.set_content_type(MediaType::PlainText); - let body = "Cannot retrieve value. The value has an unsupported type.".to_string(); - expected_response.set_body(Body::new(body)); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); - - // Test GET request towards invalid resource. - let request_bytes = format!( - "GET http://169.254.169.254/invalid HTTP/1.0\r\nX-metadata-token: {}\r\n\r\n", - valid_token + // Test valid GET. + #[rustfmt::skip] + let (request, expected_response) = generate_request_and_expected_response( + format!( + "GET http://169.254.169.254/ HTTP/1.0\r\n\ + Accept: application/json\r\n\ + X-metadata-token: {valid_token}\r\n\r\n", + ) + .as_bytes(), + MediaType::ApplicationJson, ); - let request = Request::try_from(request_bytes.as_bytes(), None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::NotFound); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new( - VmmMmdsError::ResourceNotFound(String::from("/invalid")).to_string(), - )); + let prev_rx_invalid_token = METRICS.mmds.rx_invalid_token.count(); + let prev_rx_no_token = METRICS.mmds.rx_no_token.count(); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response, expected_response); + assert_eq!(prev_rx_invalid_token, METRICS.mmds.rx_invalid_token.count()); + assert_eq!(prev_rx_no_token, METRICS.mmds.rx_no_token.count()); // Test GET request without token should return Unauthorized status code. - let request_bytes = b"GET http://169.254.169.254/ HTTP/1.0\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); + let request = + Request::try_from(b"GET http://169.254.169.254/ HTTP/1.0\r\n\r\n", None).unwrap(); let mut expected_response = Response::new(Version::Http10, StatusCode::Unauthorized); expected_response.set_content_type(MediaType::PlainText); expected_response.set_body(Body::new(VmmMmdsError::NoTokenProvided.to_string())); + let prev_rx_no_token = METRICS.mmds.rx_no_token.count(); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response, expected_response); + assert_eq!(prev_rx_no_token + 1, METRICS.mmds.rx_no_token.count()); - // Test GET request with invalid token should return Unauthorized status code. - let request_bytes = b"GET http://169.254.169.254/ HTTP/1.0\r\n\ - X-metadata-token: foo\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); - let mut expected_response = Response::new(Version::Http10, StatusCode::Unauthorized); - expected_response.set_content_type(MediaType::PlainText); - expected_response.set_body(Body::new(VmmMmdsError::InvalidToken.to_string())); - let actual_response = convert_to_response(mmds.clone(), request); - assert_eq!(actual_response, expected_response); - - // Create a new MMDS token that expires in one second. - let request_bytes = b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ - X-metadata-token-ttl-seconds: 1\r\n\r\n"; - let request = Request::try_from(request_bytes, None).unwrap(); + // Create an expired token. + let request = Request::try_from( + b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: 1\r\n\r\n", + None, + ) + .unwrap(); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response.status(), StatusCode::OK); assert_eq!(actual_response.content_type(), MediaType::PlainText); + let expired_token = String::from_utf8(actual_response.body().unwrap().body).unwrap(); + std::thread::sleep(Duration::from_secs(1)); // Test GET request with invalid tokens. - // `valid_token` will become invalid after one second, when it expires. - let valid_token = String::from_utf8(actual_response.body().unwrap().body).unwrap(); - let invalid_token = "a".repeat(58); - let tokens = [invalid_token, valid_token]; + let tokens = ["INVALID_TOKEN", &expired_token]; for token in tokens.iter() { - let request_bytes = format!( - "GET http://169.254.169.254/ HTTP/1.0\r\nX-metadata-token: {}\r\n\r\n", - token - ); - let request = Request::try_from(request_bytes.as_bytes(), None).unwrap(); + #[rustfmt::skip] + let request = Request::try_from( + format!( + "GET http://169.254.169.254/ HTTP/1.0\r\n\ + X-metadata-token: {token}\r\n\r\n", + ) + .as_bytes(), + None, + ) + .unwrap(); let mut expected_response = Response::new(Version::Http10, StatusCode::Unauthorized); expected_response.set_content_type(MediaType::PlainText); expected_response.set_body(Body::new(VmmMmdsError::InvalidToken.to_string())); + let prev_rx_invalid_token = METRICS.mmds.rx_invalid_token.count(); + let prev_rx_no_token = METRICS.mmds.rx_no_token.count(); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); + assert_eq!( + prev_rx_invalid_token + 1, + METRICS.mmds.rx_invalid_token.count() + ); + assert_eq!(prev_rx_no_token, METRICS.mmds.rx_no_token.count()); + } + } + + // Test the version-independent parts of GET request + #[test] + fn test_respond_to_get_request() { + for version in [MmdsVersion::V1, MmdsVersion::V2] { + let mmds = populate_mmds(); + mmds.lock().expect("Poisoned lock").set_version(version); + + // Generate a token + let request = Request::try_from( + b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: 60\r\n\r\n", + None, + ) + .unwrap(); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response.status(), StatusCode::OK); + assert_eq!(actual_response.content_type(), MediaType::PlainText); + let valid_token = String::from_utf8(actual_response.body().unwrap().body).unwrap(); + + // Test invalid path + #[rustfmt::skip] + let request = Request::try_from( + format!( + "GET http://169.254.169.254/invalid HTTP/1.0\r\n\ + X-metadata-token: {valid_token}\r\n\r\n", + ) + .as_bytes(), + None, + ) + .unwrap(); + let mut expected_response = Response::new(Version::Http10, StatusCode::NotFound); + expected_response.set_content_type(MediaType::PlainText); + expected_response.set_body(Body::new( + VmmMmdsError::ResourceNotFound(String::from("/invalid")).to_string(), + )); let actual_response = convert_to_response(mmds.clone(), request); assert_eq!(actual_response, expected_response); - // Wait for the second token to expire. - std::thread::sleep(Duration::from_secs(1)); + // Test unsupported type + #[rustfmt::skip] + let request = Request::try_from( + format!( + "GET /age HTTP/1.1\r\n\ + X-metadata-token: {valid_token}\r\n\r\n", + ) + .as_bytes(), + None, + ) + .unwrap(); + let mut expected_response = Response::new(Version::Http11, StatusCode::NotImplemented); + expected_response.set_content_type(MediaType::PlainText); + let body = "Cannot retrieve value. The value has an unsupported type.".to_string(); + expected_response.set_body(Body::new(body)); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); + + // Test invalid `X-metadata-token-ttl-seconds` value is ignored if not PUT request. + #[rustfmt::skip] + let (request, expected_response) = generate_request_and_expected_response( + format!( + "GET http://169.254.169.254/ HTTP/1.0\r\n\ + X-metadata-token: {valid_token}\r\n\ + X-metadata-token-ttl-seconds: application/json\r\n\r\n", + ) + .as_bytes(), + MediaType::PlainText, + ); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); + } + } + + // Test PUT request (version-independent) + #[test] + fn test_respond_to_put_request() { + for version in [MmdsVersion::V1, MmdsVersion::V2] { + let mmds = populate_mmds(); + mmds.lock().expect("Poisoned lock").set_version(version); + + // Test valid PUT + let request = Request::try_from( + b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: 60\r\n\r\n", + None, + ) + .unwrap(); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response.status(), StatusCode::OK); + assert_eq!(actual_response.content_type(), MediaType::PlainText); + + // Test unsupported `X-Forwarded-For` header + for header in ["X-Forwarded-For", "x-forwarded-for", "X-fOrWaRdEd-FoR"] { + #[rustfmt::skip] + let request = Request::try_from( + format!( + "PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + {header}: 203.0.113.195\r\n\r\n" + ) + .as_bytes(), + None, + ) + .unwrap(); + let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); + expected_response.set_content_type(MediaType::PlainText); + expected_response.set_body(Body::new(format!( + "Invalid header. Reason: Unsupported header name. Key: {header}" + ))); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); + } + + // Test invalid path + let request = Request::try_from( + b"PUT http://169.254.169.254/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: 60\r\n\r\n", + None, + ) + .unwrap(); + let mut expected_response = Response::new(Version::Http10, StatusCode::NotFound); + expected_response.set_content_type(MediaType::PlainText); + expected_response.set_body(Body::new( + VmmMmdsError::ResourceNotFound(String::from("/token")).to_string(), + )); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); + + // Test non-numeric `X-metadata-token-ttl-seconds` value + let request = Request::try_from( + b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: application/json\r\n\r\n", + None, + ) + .unwrap(); + let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); + expected_response.set_content_type(MediaType::PlainText); + #[rustfmt::skip] + expected_response.set_body(Body::new( + "Invalid header. Reason: Invalid value. \ + Key:X-metadata-token-ttl-seconds; Value:application/json" + .to_string(), + )); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); + + // Test out-of-range `X-metadata-token-ttl-seconds` value + let invalid_values = [MIN_TOKEN_TTL_SECONDS - 1, MAX_TOKEN_TTL_SECONDS + 1]; + for invalid_value in invalid_values.iter() { + #[rustfmt::skip] + let request = Request::try_from( + format!( + "PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\ + X-metadata-token-ttl-seconds: {invalid_value}\r\n\r\n", + ) + .as_bytes(), + None, + ) + .unwrap(); + let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); + expected_response.set_content_type(MediaType::PlainText); + #[rustfmt::skip] + let error_msg = format!( + "Invalid time to live value provided for token: {invalid_value}. \ + Please provide a value between {MIN_TOKEN_TTL_SECONDS} and {MAX_TOKEN_TTL_SECONDS}.", + ); + expected_response.set_body(Body::new(error_msg)); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); + } + + // Test lack of `X-metadata-token-ttl-seconds` header + let request = Request::try_from( + b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\r\n", + None, + ) + .unwrap(); + let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest); + expected_response.set_content_type(MediaType::PlainText); + expected_response.set_body(Body::new(VmmMmdsError::NoTtlProvided.to_string())); + let actual_response = convert_to_response(mmds.clone(), request); + assert_eq!(actual_response, expected_response); } } @@ -815,13 +897,14 @@ mod tests { assert_eq!( VmmMmdsError::NoTokenProvided.to_string(), - "No MMDS token provided. Use `X-metadata-token` header to specify the session token." + "No MMDS token provided. Use `X-metadata-token` or `X-aws-ec2-metadata-token` header \ + to specify the session token." ); assert_eq!( VmmMmdsError::NoTtlProvided.to_string(), - "Token time to live value not found. Use `X-metadata-token-ttl-seconds` header to \ - specify the token's lifetime." + "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." ); assert_eq!( diff --git a/src/vmm/src/mmds/token.rs b/src/vmm/src/mmds/token.rs index e902303593d..10c27829d38 100644 --- a/src/vmm/src/mmds/token.rs +++ b/src/vmm/src/mmds/token.rs @@ -59,8 +59,6 @@ pub enum MmdsTokenError { EntropyPool(#[from] io::Error), /// Failed to extract expiry value from token. ExpiryExtraction, - /// Invalid token authority state. - InvalidState, /// Invalid time to live value provided for token: {0}. Please provide a value between {MIN_TOKEN_TTL_SECONDS:} and {MAX_TOKEN_TTL_SECONDS:}. InvalidTtlValue(u32), /// Bincode serialization failed: {0}. @@ -92,7 +90,7 @@ impl fmt::Debug for TokenAuthority { impl TokenAuthority { /// Create a new token authority entity. - pub fn new() -> Result { + pub fn try_new() -> Result { let mut file = File::open(Path::new(RANDOMNESS_POOL))?; Ok(TokenAuthority { @@ -334,7 +332,7 @@ mod tests { #[test] fn test_set_aad() { - let mut token_authority = TokenAuthority::new().unwrap(); + let mut token_authority = TokenAuthority::try_new().unwrap(); assert_eq!(token_authority.aad, "".to_string()); token_authority.set_aad("foo"); @@ -343,7 +341,7 @@ mod tests { #[test] fn test_create_token() { - let mut token_authority = TokenAuthority::new().unwrap(); + let mut token_authority = TokenAuthority::try_new().unwrap(); // Test invalid time to live value. assert_eq!( @@ -384,7 +382,7 @@ mod tests { #[test] fn test_encrypt_decrypt() { - let mut token_authority = TokenAuthority::new().unwrap(); + let mut token_authority = TokenAuthority::try_new().unwrap(); let mut file = File::open(Path::new(RANDOMNESS_POOL)).unwrap(); let mut iv = [0u8; IV_LEN]; file.read_exact(&mut iv).unwrap(); @@ -445,7 +443,7 @@ mod tests { #[test] fn test_generate_token_secret() { - let mut token_authority = TokenAuthority::new().unwrap(); + let mut token_authority = TokenAuthority::try_new().unwrap(); // Test time to live value too small. assert_eq!( @@ -484,7 +482,7 @@ mod tests { #[test] fn test_is_valid() { - let mut token_authority = TokenAuthority::new().unwrap(); + let mut token_authority = TokenAuthority::try_new().unwrap(); // Test token with size bigger than expected. assert!(!token_authority.is_valid(str::repeat("a", TOKEN_LENGTH_LIMIT + 1).as_str())); @@ -496,7 +494,7 @@ mod tests { #[test] fn test_token_authority() { - let mut token_authority = TokenAuthority::new().unwrap(); + let mut token_authority = TokenAuthority::try_new().unwrap(); // Generate token with lifespan of 60 seconds. let token0 = token_authority.generate_token_secret(60).unwrap(); diff --git a/src/vmm/src/mmds/token_headers.rs b/src/vmm/src/mmds/token_headers.rs index edb501d6ab5..7f4dfeb5f44 100644 --- a/src/vmm/src/mmds/token_headers.rs +++ b/src/vmm/src/mmds/token_headers.rs @@ -7,85 +7,63 @@ use std::result::Result; use micro_http::{HttpHeaderError, RequestError}; /// Header rejected by MMDS. -pub const REJECTED_HEADER: &str = "X-Forwarded-For"; - -/// Wrapper over the list of token headers associated with a Request. -#[derive(Debug, PartialEq, Eq)] -pub struct TokenHeaders { - /// The `X-metadata-token` header might be used by HTTP clients to specify a token in order - /// to authenticate to the session. This is used for guest requests to MMDS only. - x_metadata_token: Option, - /// The `X-metadata-token-ttl-seconds` header might be used by HTTP clients to specify - /// the expiry time of a token. This is used for PUT requests issued by the guest to MMDS only. - x_metadata_token_ttl_seconds: Option, -} - -impl Default for TokenHeaders { - /// Token headers are not present in the request by default. - fn default() -> Self { - Self { - x_metadata_token: None, - x_metadata_token_ttl_seconds: None, - } +/// Defined in lowercase since HTTP headers are case-insensitive. +pub const REJECTED_HEADER: &str = "x-forwarded-for"; + +/// `X-metadata-token` header might be used by HTTP clients to specify a token in order to +/// authenticate to the session. This is used for GET requests issued by the guest to MMDS only. +#[derive(Debug)] +pub struct XMetadataToken(pub Option); + +// Defined in lowercase since HTTP headers are case-insensitive. +const X_METADATA_TOKEN_HEADER: &str = "x-metadata-token"; +const X_AWS_EC2_METADATA_TOKEN_HEADER: &str = "x-aws-ec2-metadata-token"; + +impl From<&HashMap> for XMetadataToken { + fn from(custom_headers: &HashMap) -> Self { + Self( + custom_headers + .iter() + .find(|(k, _)| { + let k = k.to_lowercase(); + k == X_METADATA_TOKEN_HEADER || k == X_AWS_EC2_METADATA_TOKEN_HEADER + }) + .map(|(_, v)| v.to_string()), + ) } } -impl TokenHeaders { - /// `X-metadata-token` header. - const X_METADATA_TOKEN: &'static str = "X-metadata-token"; - /// `X-metadata-token-ttl-seconds` header. - const X_METADATA_TOKEN_TTL_SECONDS: &'static str = "X-metadata-token-ttl-seconds"; - - /// Return `TokenHeaders` from headers map. - pub fn try_from(map: &HashMap) -> Result { - let mut headers = Self::default(); - let lowercased_headers: HashMap = map - .iter() - .map(|(k, v)| (k.to_lowercase(), v.clone())) - .collect(); - - if let Some(token) = lowercased_headers.get(&TokenHeaders::X_METADATA_TOKEN.to_lowercase()) - { - headers.x_metadata_token = Some(token.to_string()); - } +/// `X-metadata-token-ttl-seconds` header might be used by HTTP clients to specify the expiry time +/// of a token. This is used for PUT requests issued by the guest to MMDS only. +#[derive(Debug)] +pub struct XMetadataTokenTtlSeconds(pub Option); - if let Some(value) = - lowercased_headers.get(&TokenHeaders::X_METADATA_TOKEN_TTL_SECONDS.to_lowercase()) - { - match value.parse::() { - Ok(seconds) => { - headers.x_metadata_token_ttl_seconds = Some(seconds); - } - Err(_) => { - return Err(RequestError::HeaderError(HttpHeaderError::InvalidValue( - TokenHeaders::X_METADATA_TOKEN_TTL_SECONDS.to_string(), - value.to_string(), - ))); - } - } - } +// Defined in lowercase since HTTP headers are case-insensitive. +const X_METADATA_TOKEN_TTL_SECONDS_HEADER: &str = "x-metadata-token-ttl-seconds"; +const X_AWS_EC2_METADATA_TOKEN_SSL_SECONDS_HEADER: &str = "x-aws-ec2-metadata-token-ttl-seconds"; - Ok(headers) - } +impl TryFrom<&HashMap> for XMetadataTokenTtlSeconds { + type Error = RequestError; - /// Returns the `XMetadataToken` token. - pub fn x_metadata_token(&self) -> Option<&String> { - self.x_metadata_token.as_ref() - } - - /// Returns the `XMetadataTokenTtlSeconds` token. - pub fn x_metadata_token_ttl_seconds(&self) -> Option { - self.x_metadata_token_ttl_seconds - } - - /// Sets the `XMetadataToken` token. - pub fn set_x_metadata_token(&mut self, token: String) { - self.x_metadata_token = Some(token) - } - - /// Sets the `XMetadataTokenTtlSeconds` token. - pub fn set_x_metadata_token_ttl_seconds(&mut self, ttl: u32) { - self.x_metadata_token_ttl_seconds = Some(ttl); + fn try_from(custom_headers: &HashMap) -> Result { + let seconds = custom_headers + .iter() + .find(|(k, _)| { + let k = k.to_lowercase(); + k == X_METADATA_TOKEN_TTL_SECONDS_HEADER + || k == X_AWS_EC2_METADATA_TOKEN_SSL_SECONDS_HEADER + }) + .map(|(k, v)| { + v.parse::().map_err(|_| { + RequestError::HeaderError(HttpHeaderError::InvalidValue( + k.to_string(), + v.to_string(), + )) + }) + }) + .transpose()?; + + Ok(Self(seconds)) } } @@ -93,70 +71,115 @@ impl TokenHeaders { mod tests { use super::*; + fn to_mixed_case(s: &str) -> String { + s.chars() + .enumerate() + .map(|(i, c)| { + if i % 2 == 0 { + c.to_ascii_lowercase() + } else { + c.to_ascii_uppercase() + } + }) + .collect() + } + #[test] - fn test_default() { - let headers = TokenHeaders::default(); - assert_eq!(headers.x_metadata_token(), None); - assert_eq!(headers.x_metadata_token_ttl_seconds(), None); + fn test_x_metadata_token() { + // No custom headers + let custom_headers = HashMap::default(); + let x_metadata_token = XMetadataToken::from(&custom_headers); + assert!(x_metadata_token.0.is_none()); + + // Unrelated custom headers + let custom_headers = HashMap::from([ + ("Some-Header".into(), "10".into()), + ("Another-Header".into(), "value".into()), + ]); + let x_metadata_token = XMetadataToken::from(&custom_headers); + assert!(x_metadata_token.0.is_none()); + + for header in [X_METADATA_TOKEN_HEADER, X_AWS_EC2_METADATA_TOKEN_HEADER] { + let token = "THIS_IS_TOKEN"; + + // Valid header + let custom_headers = HashMap::from([(header.into(), token.into())]); + let x_metadata_token = XMetadataToken::from(&custom_headers); + assert_eq!(&x_metadata_token.0.unwrap(), token); + + // Valid header in unrelated custom headers + let custom_headers = HashMap::from([ + ("Some-Header".into(), "10".into()), + ("Another-Header".into(), "value".into()), + (header.into(), token.into()), + ]); + let x_metadata_token = XMetadataToken::from(&custom_headers); + assert_eq!(&x_metadata_token.0.unwrap(), token); + + // Test case-insensitiveness + let custom_headers = HashMap::from([(to_mixed_case(header), token.into())]); + let x_metadata_token = XMetadataToken::from(&custom_headers); + assert_eq!(&x_metadata_token.0.unwrap(), token); + } } #[test] - fn test_try_from_headers() { - // Empty token header map. - let map: HashMap = HashMap::default(); - let headers = TokenHeaders::try_from(&map).unwrap(); - assert_eq!(headers, TokenHeaders::default()); - - // Unrecognised headers. - let mut map: HashMap = HashMap::default(); - map.insert("Some-Header".to_string(), "10".to_string()); - map.insert("Another-Header".to_string(), "value".to_string()); - let headers = TokenHeaders::try_from(&map).unwrap(); - assert_eq!(headers, TokenHeaders::default()); - - // Valid headers. - let mut map: HashMap = HashMap::default(); - map.insert("Some-Header".to_string(), "10".to_string()); - map.insert( - TokenHeaders::X_METADATA_TOKEN_TTL_SECONDS.to_string(), - "60".to_string(), - ); - map.insert( - TokenHeaders::X_METADATA_TOKEN.to_string(), - "foo".to_string(), - ); - let headers = TokenHeaders::try_from(&map).unwrap(); - assert_eq!(headers.x_metadata_token_ttl_seconds().unwrap(), 60); - assert_eq!(*headers.x_metadata_token().unwrap(), "foo".to_string()); - - let mut map: HashMap = HashMap::default(); - map.insert(TokenHeaders::X_METADATA_TOKEN.to_string(), "".to_string()); - let headers = TokenHeaders::try_from(&map).unwrap(); - assert_eq!(*headers.x_metadata_token().unwrap(), "".to_string()); - - // Lowercased headers - let mut map: HashMap = HashMap::default(); - map.insert( - TokenHeaders::X_METADATA_TOKEN_TTL_SECONDS - .to_string() - .to_lowercase(), - "60".to_string(), - ); - let headers = TokenHeaders::try_from(&map).unwrap(); - assert_eq!(headers.x_metadata_token_ttl_seconds().unwrap(), 60); - - // Invalid value. - let mut map: HashMap = HashMap::default(); - map.insert( - TokenHeaders::X_METADATA_TOKEN_TTL_SECONDS.to_string(), - "-60".to_string(), - ); - assert_eq!( - TokenHeaders::try_from(&map).unwrap_err(), - RequestError::HeaderError(HttpHeaderError::InvalidValue( - TokenHeaders::X_METADATA_TOKEN_TTL_SECONDS.to_string(), - "-60".to_string() - )) - ); + fn test_x_metadata_token_ttl_seconds() { + // No custom headers + let custom_headers = HashMap::default(); + let x_metadata_token_ttl_seconds = + XMetadataTokenTtlSeconds::try_from(&custom_headers).unwrap(); + assert!(x_metadata_token_ttl_seconds.0.is_none()); + + // Unrelated custom headers + let custom_headers = HashMap::from([ + ("Some-Header".into(), "10".into()), + ("Another-Header".into(), "value".into()), + ]); + let x_metadata_token_ttl_seconds = + XMetadataTokenTtlSeconds::try_from(&custom_headers).unwrap(); + assert!(x_metadata_token_ttl_seconds.0.is_none()); + + for header in [ + X_METADATA_TOKEN_TTL_SECONDS_HEADER, + X_AWS_EC2_METADATA_TOKEN_SSL_SECONDS_HEADER, + ] { + let seconds = 60; + + // Valid header + let custom_headers = HashMap::from([(header.into(), seconds.to_string())]); + let x_metadata_token_ttl_seconds = + XMetadataTokenTtlSeconds::try_from(&custom_headers).unwrap(); + assert_eq!(x_metadata_token_ttl_seconds.0.unwrap(), seconds); + + // Valid header in unrelated custom headers + let custom_headers = HashMap::from([ + ("Some-Header".into(), "10".into()), + ("Another-Header".into(), "value".into()), + (header.into(), seconds.to_string()), + ]); + let x_metadata_token_ttl_seconds = + XMetadataTokenTtlSeconds::try_from(&custom_headers).unwrap(); + assert_eq!(x_metadata_token_ttl_seconds.0.unwrap(), seconds); + + // Test case-insensitiveness + let custom_headers = HashMap::from([(to_mixed_case(header), seconds.to_string())]); + let x_metadata_token_ttl_seconds = + XMetadataTokenTtlSeconds::try_from(&custom_headers).unwrap(); + assert_eq!(x_metadata_token_ttl_seconds.0.unwrap(), seconds); + + // Invalid value + let mixed_case_header = to_mixed_case(header); + let invalid_seconds = "-60"; + let custom_headers = + HashMap::from([(mixed_case_header.clone(), invalid_seconds.to_string())]); + assert_eq!( + XMetadataTokenTtlSeconds::try_from(&custom_headers).unwrap_err(), + RequestError::HeaderError(HttpHeaderError::InvalidValue( + mixed_case_header, + invalid_seconds.to_string() + )) + ); + } } } diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index 70c317bb1e1..29c4258b8a7 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -177,7 +177,7 @@ impl VmResources { // Init the data store from file, if present. if let Some(data) = metadata_json { - resources.locked_mmds_or_default().put_data( + resources.locked_mmds_or_default()?.put_data( serde_json::from_str(data).expect("MMDS error: metadata provided not valid json"), )?; info!("Successfully added metadata to mmds from file"); @@ -195,17 +195,16 @@ impl VmResources { } /// If not initialised, create the mmds data store with the default config. - pub fn mmds_or_default(&mut self) -> &Arc> { - self.mmds - .get_or_insert(Arc::new(Mutex::new(Mmds::default_with_limit( - self.mmds_size_limit, - )))) + pub fn mmds_or_default(&mut self) -> Result<&Arc>, MmdsConfigError> { + Ok(self + .mmds + .get_or_insert(Arc::new(Mutex::new(Mmds::try_new(self.mmds_size_limit)?)))) } /// If not initialised, create the mmds data store with the default config. - pub fn locked_mmds_or_default(&mut self) -> MutexGuard<'_, Mmds> { - let mmds = self.mmds_or_default(); - mmds.lock().expect("Poisoned lock") + pub fn locked_mmds_or_default(&mut self) -> Result, MmdsConfigError> { + let mmds = self.mmds_or_default()?; + Ok(mmds.lock().expect("Poisoned lock")) } /// Updates the resources from a restored device (used for configuring resources when @@ -395,10 +394,8 @@ impl VmResources { version: MmdsVersion, instance_id: &str, ) -> Result<(), MmdsConfigError> { - let mut mmds_guard = self.locked_mmds_or_default(); - mmds_guard - .set_version(version) - .map_err(|err| MmdsConfigError::MmdsVersion(version, err))?; + let mut mmds_guard = self.locked_mmds_or_default()?; + mmds_guard.set_version(version); mmds_guard.set_aad(instance_id); Ok(()) @@ -434,7 +431,7 @@ impl VmResources { } // Safe to unwrap because we've just made sure that it's initialised. - let mmds = self.mmds_or_default().clone(); + let mmds = self.mmds_or_default()?.clone(); // Create `MmdsNetworkStack` and configure the IPv4 address for // existing built network devices whose names are defined in the diff --git a/src/vmm/src/rpc_interface.rs b/src/vmm/src/rpc_interface.rs index d868c022dd2..6979190be7a 100644 --- a/src/vmm/src/rpc_interface.rs +++ b/src/vmm/src/rpc_interface.rs @@ -197,14 +197,14 @@ pub enum VmmData { /// The methods get a mutable reference to self because the methods should initialise the data /// store with the defaults if it's not already initialised. trait MmdsRequestHandler { - fn mmds(&mut self) -> MutexGuard<'_, Mmds>; + fn mmds(&mut self) -> Result, VmmActionError>; fn get_mmds(&mut self) -> Result { - Ok(VmmData::MmdsValue(self.mmds().data_store_value())) + Ok(VmmData::MmdsValue(self.mmds()?.data_store_value())) } fn patch_mmds(&mut self, value: serde_json::Value) -> Result { - self.mmds() + self.mmds()? .patch_data(value) .map(|()| VmmData::Empty) .map_err(|err| match err { @@ -218,7 +218,7 @@ trait MmdsRequestHandler { } fn put_mmds(&mut self, value: serde_json::Value) -> Result { - self.mmds() + self.mmds()? .put_data(value) .map(|()| VmmData::Empty) .map_err(|err| match err { @@ -264,8 +264,10 @@ impl fmt::Debug for PrebootApiController<'_> { } impl MmdsRequestHandler for PrebootApiController<'_> { - fn mmds(&mut self) -> MutexGuard<'_, Mmds> { - self.vm_resources.locked_mmds_or_default() + fn mmds(&mut self) -> Result, VmmActionError> { + self.vm_resources + .locked_mmds_or_default() + .map_err(VmmActionError::MmdsConfig) } } @@ -286,10 +288,12 @@ pub type ApiRequest = Box; pub type ApiResponse = Box>; /// Error type for `PrebootApiController::build_microvm_from_requests`. -#[derive(Debug, thiserror::Error, displaydoc::Display, derive_more::From)] +#[derive(Debug, thiserror::Error, displaydoc::Display)] pub enum BuildMicrovmFromRequestsError { + /// Configuring MMDS failed: {0}. + ConfigureMmds(#[from] MmdsConfigError), /// Populating MMDS from file failed: {0}. - Mmds(data_store::MmdsDatastoreError), + PopulateMmds(#[from] data_store::MmdsDatastoreError), /// Loading snapshot failed. Restore, /// Resuming MicroVM after loading snapshot failed. @@ -342,7 +346,7 @@ impl<'a> PrebootApiController<'a> { // Init the data store from file, if present. if let Some(data) = metadata_json { - vm_resources.locked_mmds_or_default().put_data( + vm_resources.locked_mmds_or_default()?.put_data( serde_json::from_str(data).expect("MMDS error: metadata provided not valid json"), )?; @@ -607,8 +611,10 @@ pub struct RuntimeApiController { } impl MmdsRequestHandler for RuntimeApiController { - fn mmds(&mut self) -> MutexGuard<'_, Mmds> { - self.vm_resources.locked_mmds_or_default() + fn mmds(&mut self) -> Result, VmmActionError> { + self.vm_resources + .locked_mmds_or_default() + .map_err(VmmActionError::MmdsConfig) } } diff --git a/src/vmm/src/vmm_config/mmds.rs b/src/vmm/src/vmm_config/mmds.rs index e248095fb97..a9205e51647 100644 --- a/src/vmm/src/vmm_config/mmds.rs +++ b/src/vmm/src/vmm_config/mmds.rs @@ -48,6 +48,6 @@ pub enum MmdsConfigError { InvalidIpv4Addr, /// The list of network interface IDs provided contains at least one ID that does not correspond to any existing network interface. InvalidNetworkInterfaceId, - /// The MMDS could not be configured to version {0}: {1} - MmdsVersion(MmdsVersion, data_store::MmdsDatastoreError), + /// Failed to initialize MMDS data store: {0} + InitMmdsDatastore(#[from] data_store::MmdsDatastoreError), } diff --git a/tests/host_tools/fcmetrics.py b/tests/host_tools/fcmetrics.py index 6e88612d458..1b3cdcb96b1 100644 --- a/tests/host_tools/fcmetrics.py +++ b/tests/host_tools/fcmetrics.py @@ -185,6 +185,8 @@ def validate_fc_metrics(metrics): "rx_accepted_err", "rx_accepted_unusual", "rx_bad_eth", + "rx_invalid_token", + "rx_no_token", "rx_count", "tx_bytes", "tx_count", diff --git a/tests/integration_tests/functional/test_mmds.py b/tests/integration_tests/functional/test_mmds.py index 51ea6358631..4f3480ac7d1 100644 --- a/tests/integration_tests/functional/test_mmds.py +++ b/tests/integration_tests/functional/test_mmds.py @@ -3,9 +3,11 @@ """Tests that verify MMDS related functionality.""" # pylint: disable=too-many-lines +import json import random import string import time +from datetime import datetime, timedelta, timezone import pytest @@ -61,10 +63,7 @@ def _validate_mmds_snapshot( ssh_connection = basevm.ssh run_guest_cmd(ssh_connection, f"ip route add {ipv4_address} dev eth0", "") - # Generate token if needed. - token = None - if version == "V2": - token = generate_mmds_session_token(ssh_connection, ipv4_address, token_ttl=60) + token = generate_mmds_session_token(ssh_connection, ipv4_address, token_ttl=60) # Fetch metadata. cmd = generate_mmds_get_request( @@ -101,15 +100,8 @@ def _validate_mmds_snapshot( response = microvm.api.vm_config.get() assert response.json()["mmds-config"] == expected_mmds_config - if version == "V1": - # Verify that V2 requests don't work - assert ( - generate_mmds_session_token(ssh_connection, ipv4_address, token_ttl=60) - == "Not allowed HTTP method." - ) - - token = None - else: + # Since V1 should accept GET request even with invalid token, don't regenerate a token for V1. + if version == "V2": # Attempting to reuse the token across a restore must fail. cmd = generate_mmds_get_request(ipv4_address, token=token) run_guest_cmd(ssh_connection, cmd, "MMDS token not valid.") @@ -647,7 +639,7 @@ def test_mmds_v2_negative(uvm_plain): # Check `GET` request fails when token is not provided. cmd = generate_mmds_get_request(DEFAULT_IPV4) expected = ( - "No MMDS token provided. Use `X-metadata-token` header " + "No MMDS token provided. Use `X-metadata-token` or `X-aws-ec2-metadata-token` header " "to specify the session token." ) run_guest_cmd(ssh_connection, cmd, expected) @@ -664,9 +656,8 @@ def test_mmds_v2_negative(uvm_plain): # Check `PUT` request fails when token TTL is not provided. cmd = f"curl -m 2 -s -X PUT http://{DEFAULT_IPV4}/latest/api/token" expected = ( - "Token time to live value not found. Use " - "`X-metadata-token-ttl-seconds` header to specify " - "the token's lifetime." + "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." ) run_guest_cmd(ssh_connection, cmd, expected) @@ -748,3 +739,53 @@ def test_deprecated_mmds_config(uvm_plain): ) == 2 ) + + +def test_aws_credential_provider(uvm_plain): + """ + Test AWS CLI credential provider + """ + test_microvm = uvm_plain + test_microvm.spawn() + test_microvm.basic_config() + test_microvm.add_net_iface() + # V2 requires session tokens for GET requests + configure_mmds(test_microvm, iface_ids=["eth0"], version="V2") + now = datetime.now(timezone.utc) + credentials = { + "Code": "Success", + "LastUpdated": now.strftime("%Y-%m-%dT%H:%M:%SZ"), + "Type": "AWS-HMAC", + "AccessKeyId": "AAA", + "SecretAccessKey": "BBB", + "Token": "CCC", + "Expiration": (now + timedelta(seconds=60)).strftime("%Y-%m-%dT%H:%M:%SZ"), + } + data_store = { + "latest": { + "meta-data": { + "iam": { + "security-credentials": {"role": json.dumps(credentials, indent=2)} + }, + "placement": {"availability-zone": "us-east-1a"}, + } + } + } + populate_data_store(test_microvm, data_store) + test_microvm.start() + + ssh_connection = test_microvm.ssh + + run_guest_cmd(ssh_connection, f"ip route add {DEFAULT_IPV4} dev eth0", "") + + cmd = r"""python3 - <