Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure FIPS AWS service endpoints #218

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you could return early here if you check s == "default"

_ => {
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