Skip to content

Commit

Permalink
Merge pull request #218 from ginglis13/fips-aws-endpoints
Browse files Browse the repository at this point in the history
Configure FIPS AWS service endpoints
  • Loading branch information
ginglis13 authored Oct 28, 2024
2 parents a10182f + 708d599 commit 8e173a1
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 2 deletions.
2 changes: 2 additions & 0 deletions sources/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions sources/api/pluto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fips = ["aws-lc-rs/fips", "aws-smithy-experimental/crypto-aws-lc-fips", "rustls/
source-groups = ["aws-smithy-experimental"]

[dependencies]
base64.workspace = true
bottlerocket-modeled-types.workspace = true
bottlerocket-settings-models.workspace = true
bytes.workspace = true
Expand All @@ -38,6 +39,7 @@ rustls.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
snafu.workspace = true
tempfile.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
tokio-retry.workspace = true
tokio-rustls.workspace = true
Expand Down
112 changes: 110 additions & 2 deletions sources/api/pluto/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ mod hyper_proxy;
mod proxy;

use api::{settings_view_get, settings_view_set, SettingsViewDelta};
use base64::Engine;
use bottlerocket_modeled_types::{KubernetesClusterDnsIp, KubernetesHostnameOverrideSource};
use imdsclient::ImdsClient;
use snafu::{ensure, OptionExt, ResultExt};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Write};
use std::net::IpAddr;
use std::process;
use std::path::Path;
use std::str::FromStr;
use std::string::String;
use std::{env, process};

// This is the default DNS unless our CIDR block begins with "10."
const DEFAULT_DNS_CLUSTER_IP: &str = "10.100.0.10";
Expand All @@ -56,6 +58,12 @@ const DEFAULT_10_RANGE_DNS_CLUSTER_IP: &str = "172.20.0.10";

const ENI_MAX_PODS_PATH: &str = "/usr/share/eks/eni-max-pods";

/// The name of the AWS config file used by pluto. The file is placed in a tempdir, and
/// the contents of settings.aws.config are decoded and written here.
const AWS_CONFIG_FILE: &str = "config.pluto";
/// The environment variable that specifies the path to the AWS config file.
const AWS_CONFIG_FILE_ENV_VAR: &str = "AWS_CONFIG_FILE";

mod error {
use crate::{api, ec2, eks};
use snafu::Snafu;
Expand All @@ -73,6 +81,9 @@ mod error {
#[snafu(display("Missing AWS region"))]
AwsRegion,

#[snafu(display("Unable to decode base64 in of AWS config: {}", source))]
AwsBase64Decode { source: base64::DecodeError },

#[snafu(display("Failed to parse setting {} as u32: {}", setting, source))]
ParseToU32 {
setting: String,
Expand Down Expand Up @@ -130,6 +141,21 @@ mod error {
instance_type
))]
NoInstanceTypeMaxPods { instance_type: String },

#[snafu(display("Unable to create AWS config file '{}': {}", filepath, source))]
CreateAwsConfigFile {
filepath: String,
source: std::io::Error,
},

#[snafu(display("Unable to write AWS config file to '{}': {}", filepath, source))]
WriteAwsConfigFile {
filepath: String,
source: std::io::Error,
},

#[snafu(display("Unable to create tempdir: {}", source))]
Tempdir { source: std::io::Error },
}
}

Expand Down Expand Up @@ -424,13 +450,42 @@ async fn generate_node_name(
Ok(())
}

/// Temporarily copy the yet-to-be-committed settings.aws.config value to a file
/// and set the environment variable AWS_CONFIG_FILE to this file's location.
/// This ensures that subsequent calls via pluto to the AWS SDK will respect settings.aws.config.
fn set_aws_config(aws_k8s_info: &SettingsViewDelta, filepath: &Path) -> Result<()> {
if let Some(config_contents) = settings_view_get!(aws_k8s_info.aws.config) {
// Decode settings.aws.config.
let decoded_bytes = base64::engine::general_purpose::STANDARD
.decode(config_contents.as_bytes())
.context(error::AwsBase64DecodeSnafu)?;

// Write the decoded bytes to the provided filepath.
let mut file = File::create(filepath).context(error::CreateAwsConfigFileSnafu {
filepath: filepath.to_str().unwrap(),
})?;
file.write_all(&decoded_bytes)
.context(error::WriteAwsConfigFileSnafu {
filepath: filepath.to_str().unwrap(),
})?;

env::set_var(AWS_CONFIG_FILE_ENV_VAR, filepath);
}

Ok(())
}

async fn run() -> Result<()> {
let mut client = ImdsClient::new();
let current_settings = api::get_aws_k8s_info().await.context(error::AwsInfoSnafu)?;
let mut aws_k8s_info = SettingsViewDelta::from_api_response(current_settings);

let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();

let temp_dir = tempfile::tempdir().context(error::TempdirSnafu)?;
let aws_config_file_path = temp_dir.path().join(AWS_CONFIG_FILE);
set_aws_config(&aws_k8s_info, Path::new(&aws_config_file_path))?;

generate_cluster_dns_ip(&mut client, &mut aws_k8s_info).await?;
generate_node_ip(&mut client, &mut aws_k8s_info).await?;
generate_max_pods(&mut client, &mut aws_k8s_info).await?;
Expand Down Expand Up @@ -471,6 +526,7 @@ mod test {
use super::*;
use crate::api::SettingsViewDelta;
use api::SettingsView;
use bottlerocket_modeled_types::ValidBase64;
use bottlerocket_settings_models::AwsSettingsV1;
use httptest::{matchers::*, responders::*, Expectation, Server};

Expand All @@ -489,6 +545,58 @@ mod test {
assert!(result.is_err());
}

// Because of test parallelization, serialize the AWS config tests such that
// the AWS_CONFIG_FILE env variable is deterministically set or unset.
#[test]
fn test_aws_config_sequential() {
test_set_aws_config();
test_set_aws_config_is_not_set();
}

fn test_set_aws_config() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_file_path = temp_dir.path().join("config.fake");

// base64 encoded string:
// [default]
// use_fips_endpoint=false
let config_base64 =
ValidBase64::try_from("W2RlZmF1bHRdCnVzZV9maXBzX2VuZHBvaW50PWZhbHNl").unwrap();
let input = SettingsViewDelta::from_api_response(SettingsView {
aws: Some(AwsSettingsV1 {
config: Some(config_base64),
..Default::default()
}),
..Default::default()
});
let result = set_aws_config(&input, &temp_file_path);

assert!(result.is_ok());
assert!(env::var(AWS_CONFIG_FILE_ENV_VAR).is_ok());
assert_eq!(
env::var(AWS_CONFIG_FILE_ENV_VAR).unwrap(),
temp_file_path.to_str().unwrap()
);

// Remove the env variable such that it's no longer set.
env::remove_var(AWS_CONFIG_FILE_ENV_VAR);
}

fn test_set_aws_config_is_not_set() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_file_path = temp_dir.path().join("config.fake");

let input = SettingsViewDelta::from_api_response(SettingsView {
aws: Some(AwsSettingsV1 {
..Default::default()
}),
..Default::default()
});
let result = set_aws_config(&input, &temp_file_path);
assert!(result.is_ok());
assert!(env::var(AWS_CONFIG_FILE_ENV_VAR).is_err()); // NotPresent
}

#[tokio::test]
async fn test_hostname_override_source() {
let server = Server::run();
Expand Down
133 changes: 133 additions & 0 deletions sources/api/schnauzer/src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// be registered with the Handlebars library to assist in manipulating
// text at render time.

use base64::Engine;
use bottlerocket_modeled_types::{OciDefaultsCapability, OciDefaultsResourceLimitType};
use cidr::AnyIpCidr;
use dns_lookup::lookup_host;
Expand Down Expand Up @@ -535,6 +536,81 @@ pub fn tuf_prefix(
Ok(())
}

/// The `aws-config` helper is used to create an AWS config file
/// with `use_fips_endpoint` value set based on if the variant is FIPS enabled.
///
/// # Fallback
///
/// If this helper runs and `settings.aws.config` is set or `settings.aws.config`
/// is set to a profile other than "default" the helper will return early, leaving the
/// existing `settings.aws.config` setting in place.
///
/// # Example
///
/// The AWS config value is generated via
/// `{{ aws-config settings.aws.config settings.aws.profile }}`
///
/// This would result in something like:
/// ```toml
/// [default]
/// use_fips_endpoint=false
/// ```
///
/// The helper will then base64 encode this content and set as `settings.aws.config`.
pub fn aws_config(
helper: &Helper<'_, '_>,
_: &Handlebars,
_: &Context,
renderctx: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("Starting aws-config helper");
let template_name = template_name(renderctx);

check_param_count(helper, template_name, 2)?;

let aws_config = get_param(helper, 0)?;
if !aws_config.is_null() {
trace!("settings.aws.config already configured. Exiting aws-config helper early");
return Ok(());
}

// settings.aws.profile may be null. If so, we'll use the "default" profile in constructing
// the AWS config.
let aws_profile = match get_param(helper, 1)? {
Value::Null => "default",
Value::String(s) => s,
_ => {
return Err(RenderError::from(
error::TemplateHelperError::InvalidTemplateValue {
expected: "string",
value: get_param(helper, 1)?.to_owned(),
template: template_name.to_owned(),
},
))
}
};

if aws_profile != "default" {
return Ok(());
}

// construct the base64 encoded AWS config
let aws_config_str = format!(
r#"[default]
use_fips_endpoint={}"#,
fips_enabled()
);
let new_aws_config = base64::engine::general_purpose::STANDARD.encode(&aws_config_str);

out.write(&new_aws_config)
.with_context(|_| error::TemplateWriteSnafu {
template: template_name.to_owned(),
})?;

Ok(())
}

/// Utility function to determine if a variant is in FIPS mode based
/// on /proc/sys/crypto/fips_enabled.
fn fips_enabled() -> bool {
Expand Down Expand Up @@ -1742,6 +1818,63 @@ mod test_tuf_repository {
}
}

#[cfg(test)]
mod test_aws_config_fips_endpoint {
use super::*;
use handlebars::RenderError;
use serde::Serialize;
use serde_json::json;

// A thin wrapper around the handlebars render_template method that includes
// setup and registration of helpers
fn setup_and_render_template<T>(tmpl: &str, data: &T) -> Result<String, RenderError>
where
T: Serialize,
{
let mut aws_config_helper = Handlebars::new();
aws_config_helper.register_helper("aws-config", Box::new(aws_config));

aws_config_helper.render_template(tmpl, data)
}

const METADATA_TEMPLATE: &str = "{{ aws-config settings.aws.config settings.aws.profile }}";

// base64 encoded string:
// [default]
// use_fips_endpoint=false
const EXPECTED_CONFIG_DEFAULT: &str = "W2RlZmF1bHRdCnVzZV9maXBzX2VuZHBvaW50PWZhbHNl";

#[test]
fn config_default() {
let result = setup_and_render_template(
METADATA_TEMPLATE,
&json!({"settings": {"aws": {"profile": "default"}}}),
)
.unwrap();
assert_eq!(result, EXPECTED_CONFIG_DEFAULT);
}

#[test]
fn config_already_exists() {
let result = setup_and_render_template(
METADATA_TEMPLATE,
&json!({"settings": {"aws": {"profile": "default", "config": "abc"}}}),
)
.unwrap();
assert_eq!(result, "");
}

#[test]
fn config_non_default_profile() {
let result = setup_and_render_template(
METADATA_TEMPLATE,
&json!({"settings": {"aws": {"profile": "custom"}}}),
)
.unwrap();
assert_eq!(result, "");
}
}

#[cfg(test)]
mod test_host {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions sources/api/schnauzer/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ pub fn build_template_registry() -> Result<handlebars::Handlebars<'static>> {
template_registry.register_helper("join_node_taints", Box::new(helpers::join_node_taints));
template_registry.register_helper("default", Box::new(helpers::default));
template_registry.register_helper("ecr-prefix", Box::new(helpers::ecr_prefix));
template_registry.register_helper("aws-config", Box::new(helpers::aws_config));
template_registry.register_helper("tuf-prefix", Box::new(helpers::tuf_prefix));
template_registry.register_helper("metadata-prefix", Box::new(helpers::metadata_prefix));
template_registry.register_helper("host", Box::new(helpers::host));
Expand Down
1 change: 1 addition & 0 deletions sources/api/schnauzer/src/v2/import/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn all_helpers() -> HashMap<ExtensionName, HashMap<HelperName, Box<dyn HelperDef
hashmap! {
"aws" => hashmap! {
"ecr-prefix" => helper!(handlebars_helpers::ecr_prefix),
"aws-config" => helper!(handlebars_helpers::aws_config)
},

"ecs" => hashmap! {
Expand Down

0 comments on commit 8e173a1

Please sign in to comment.