diff --git a/README.md b/README.md
index 4a9a5616612..ab431a13a15 100644
--- a/README.md
+++ b/README.md
@@ -350,6 +350,10 @@ These settings can be changed at any time.
#### Kernel settings
+* `settings.kernel.lockdown`: This allows further restrictions on what the Linux kernel will allow, for example preventing the loading of unsigned modules.
+ May be set to "none" (the default), "integrity", or "confidentiality".
+ **Important note:** this setting cannot be lowered (toward 'none') at runtime.
+ You must reboot for a change to a lower level to take effect.
* `settings.kernel.sysctl`: Key/value pairs representing Linux kernel parameters.
Remember to quote keys (since they often contain ".") and to quote all values.
* Example user data for setting up sysctl:
diff --git a/Release.toml b/Release.toml
index 2999a7aadd6..b0a2615bb92 100644
--- a/Release.toml
+++ b/Release.toml
@@ -12,3 +12,4 @@ version = "1.0.4"
"(1.0.1, 1.0.2)" = ["migrate_v1.0.2_add-enable-spot-instance-draining.lz4"]
"(1.0.2, 1.0.3)" = ["migrate_v1.0.3_add-sysctl.lz4"]
"(1.0.3, 1.0.4)" = []
+"(1.0.4, 1.0.5)" = ["migrate_v1.0.5_add-lockdown.lz4", "migrate_v1.0.5_sysctl-subcommand.lz4"]
diff --git a/packages/kernel/config-bottlerocket b/packages/kernel/config-bottlerocket
index 1494d44e15f..331b57e3fba 100644
--- a/packages/kernel/config-bottlerocket
+++ b/packages/kernel/config-bottlerocket
@@ -47,3 +47,6 @@ CONFIG_IKHEADERS=y
# BTF debug info at /sys/kernel/btf/vmlinux
CONFIG_DEBUG_INFO_BTF=y
+
+# Enable support for the kernel lockdown security module.
+CONFIG_SECURITY_LOCKDOWN_LSM=y
diff --git a/sources/Cargo.lock b/sources/Cargo.lock
index a687d38c50b..efb0e3f9e73 100644
--- a/sources/Cargo.lock
+++ b/sources/Cargo.lock
@@ -273,6 +273,13 @@ dependencies = [
"migration-helpers",
]
+[[package]]
+name = "add-lockdown"
+version = "0.1.0"
+dependencies = [
+ "migration-helpers",
+]
+
[[package]]
name = "add-sysctl"
version = "0.1.0"
@@ -2797,6 +2804,13 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "sysctl-subcommand"
+version = "0.1.0"
+dependencies = [
+ "migration-helpers",
+]
+
[[package]]
name = "tar"
version = "0.4.30"
diff --git a/sources/Cargo.toml b/sources/Cargo.toml
index d70e1ad428d..9bdc3bc9cc2 100644
--- a/sources/Cargo.toml
+++ b/sources/Cargo.toml
@@ -30,6 +30,8 @@ members = [
"api/migration/migrations/v1.0.0/ecr-helper-control",
"api/migration/migrations/v1.0.2/add-enable-spot-instance-draining",
"api/migration/migrations/v1.0.3/add-sysctl",
+ "api/migration/migrations/v1.0.5/add-lockdown",
+ "api/migration/migrations/v1.0.5/sysctl-subcommand",
"bottlerocket-release",
diff --git a/sources/api/corndog/README.md b/sources/api/corndog/README.md
index d9c20db5465..f5a52601673 100644
--- a/sources/api/corndog/README.md
+++ b/sources/api/corndog/README.md
@@ -3,7 +3,9 @@
Current version: 0.1.0
corndog is a delicious way to get at the meat inside the kernels.
-It sets kernel sysctl values based on key/value pairs in `settings.kernel.sysctl`.
+It sets kernel-related settings, for example:
+* sysctl values, based on key/value pairs in `settings.kernel.sysctl`
+* lockdown mode, based on the value of `settings.kernel.lockdown`
## Colophon
diff --git a/sources/api/corndog/src/main.rs b/sources/api/corndog/src/main.rs
index 8bb159ff13c..8824ac29820 100644
--- a/sources/api/corndog/src/main.rs
+++ b/sources/api/corndog/src/main.rs
@@ -1,11 +1,13 @@
/*!
corndog is a delicious way to get at the meat inside the kernels.
-It sets kernel sysctl values based on key/value pairs in `settings.kernel.sysctl`.
+It sets kernel-related settings, for example:
+* sysctl values, based on key/value pairs in `settings.kernel.sysctl`
+* lockdown mode, based on the value of `settings.kernel.lockdown`
*/
#![deny(rust_2018_idioms)]
-use log::{debug, error, trace};
+use log::{debug, error, info, trace, warn};
use simplelog::{Config as LogConfig, LevelFilter, TermLogger, TerminalMode};
use snafu::ResultExt;
use std::collections::HashMap;
@@ -17,9 +19,11 @@ use std::{env, process};
const DEFAULT_API_SOCKET: &str = "/run/api.sock";
const SYSCTL_PATH_PREFIX: &str = "/proc/sys";
+const LOCKDOWN_PATH: &str = "/sys/kernel/security/lockdown";
/// Store the args we receive on the command line.
struct Args {
+ subcommand: String,
log_level: LevelFilter,
socket_path: String,
}
@@ -32,13 +36,24 @@ async fn run() -> Result<()> {
TermLogger::init(args.log_level, LogConfig::default(), TerminalMode::Mixed)
.context(error::Logger)?;
- // If the user has sysctl settings, apply them.
+ // If the user has kernel settings, apply them.
let model = get_model(args.socket_path).await?;
if let Some(settings) = model.settings {
if let Some(kernel) = settings.kernel {
- if let Some(sysctls) = kernel.sysctl {
- debug!("Applying sysctls: {:#?}", sysctls);
- set_sysctls(sysctls)?;
+ match args.subcommand.as_ref() {
+ "sysctl" => {
+ if let Some(sysctls) = kernel.sysctl {
+ debug!("Applying sysctls: {:#?}", sysctls);
+ set_sysctls(sysctls);
+ }
+ }
+ "lockdown" => {
+ if let Some(lockdown) = kernel.lockdown {
+ debug!("Setting lockdown: {:#?}", lockdown);
+ set_lockdown(&lockdown)?;
+ }
+ }
+ _ => usage_msg(format!("Unknown subcommand '{}'", args.subcommand)), // should be unreachable
}
}
}
@@ -46,6 +61,8 @@ async fn run() -> Result<()> {
Ok(())
}
+// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
+
/// Retrieve the current model from the API.
async fn get_model
(socket_path: P) -> Result
where
@@ -85,7 +102,7 @@ where
/// Applies the requested sysctls to the system. The keys are used to generate the appropriate
/// path, and the value its contents.
-fn set_sysctls(sysctls: HashMap) -> Result<()>
+fn set_sysctls(sysctls: HashMap)
where
K: AsRef,
{
@@ -100,16 +117,70 @@ where
error!("Failed to write sysctl value '{}': {}", key, e);
}
}
- Ok(())
}
+/// Sets the requested lockdown mode in the kernel.
+///
+/// The Linux kernel won't allow lowering the lockdown setting, but we want to allow users to
+/// change the Bottlerocket setting and reboot for it to take effect. Changing the Bottlerocket
+/// setting means this code will run to write it out, but it wouldn't be able to convince the
+/// kernel. So, we just warn the user rather than trying to write and causing a failure that could
+/// prevent the rest of a settings-changing transaction from going through. We'll run again after
+/// reboot to set lockdown as it was requested.
+fn set_lockdown(lockdown: &str) -> Result<()> {
+ let current_raw = fs::read_to_string(LOCKDOWN_PATH).unwrap_or_else(|_| "unknown".to_string());
+ let current = parse_kernel_setting(¤t_raw);
+ trace!("Parsed lockdown setting '{}' to '{}'", current_raw, current);
+
+ // The kernel doesn't allow rewriting the current value.
+ if current == lockdown {
+ info!("Requested lockdown setting is already in effect.");
+ return Ok(());
+ // As described above, the kernel doesn't allow lowering the value.
+ } else if current == "confidentiality" || (current == "integrity" && lockdown == "none") {
+ warn!("Can't lower lockdown setting at runtime; please reboot for it to take effect.",);
+ return Ok(());
+ }
+
+ fs::write(LOCKDOWN_PATH, lockdown).context(error::Lockdown { current, lockdown })
+}
+
+/// The Linux kernel provides human-readable output like `[none] integrity confidentiality` when
+/// you read settings from virtual files like /sys/kernel/security/lockdown. This parses out the
+/// current value of the setting from that human-readable output.
+///
+/// There are also some files that only output the current value without the other options, so we
+/// return the output as-is (except for trimming whitespace) if there are no brackets.
+fn parse_kernel_setting(setting: &str) -> &str {
+ let mut setting = setting.trim();
+ // Take after the '['
+ if let Some(idx) = setting.find('[') {
+ if setting.len() > idx + 1 {
+ setting = &setting[idx + 1..];
+ }
+ }
+ // Take before the ']'
+ if let Some(idx) = setting.find(']') {
+ setting = &setting[..idx];
+ }
+ setting
+}
+
+// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
+
/// Print a usage message in the event a bad argument is given.
fn usage() -> ! {
let program_name = env::args().next().unwrap_or_else(|| "program".to_string());
eprintln!(
- r"Usage: {}
- [ --socket-path PATH ]
- [ --log-level trace|debug|info|warn|error ]
+ r"Usage: {} SUBCOMMAND [ ARGUMENTS... ]
+
+ Subcommands:
+ sysctl
+ lockdown
+
+ Global arguments:
+ --socket-path PATH
+ --log-level trace|debug|info|warn|error
Socket path defaults to {}",
program_name, DEFAULT_API_SOCKET,
@@ -127,6 +198,7 @@ fn usage_msg>(msg: S) -> ! {
fn parse_args(args: env::Args) -> Args {
let mut log_level = None;
let mut socket_path = None;
+ let mut subcommand = None;
let mut iter = args.skip(1);
while let Some(arg) = iter.next() {
@@ -147,11 +219,14 @@ fn parse_args(args: env::Args) -> Args {
)
}
+ "sysctl" | "lockdown" => subcommand = Some(arg),
+
_ => usage(),
}
}
Args {
+ subcommand: subcommand.unwrap_or_else(|| usage_msg("Must specify a subcommand.")),
log_level: log_level.unwrap_or_else(|| LevelFilter::Info),
socket_path: socket_path.unwrap_or_else(|| DEFAULT_API_SOCKET.to_string()),
}
@@ -168,9 +243,12 @@ async fn main() {
}
}
+// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
+
mod error {
use http::StatusCode;
use snafu::Snafu;
+ use std::io;
#[derive(Debug, Snafu)]
#[snafu(visibility = "pub(super)")]
@@ -190,6 +268,18 @@ mod error {
response_body: String,
},
+ #[snafu(display(
+ "Failed to change lockdown from '{}' to '{}': {}",
+ current,
+ lockdown,
+ source
+ ))]
+ Lockdown {
+ current: String,
+ lockdown: String,
+ source: io::Error,
+ },
+
#[snafu(display("Logger setup error: {}", source))]
Logger { source: simplelog::TermLogError },
@@ -219,4 +309,29 @@ mod test {
format!("{}/root/file", SYSCTL_PATH_PREFIX)
);
}
+
+ #[test]
+ fn brackets() {
+ assert_eq!(
+ "none",
+ parse_kernel_setting("[none] integrity confidentiality")
+ );
+ assert_eq!(
+ "integrity",
+ parse_kernel_setting("none [integrity] confidentiality\n")
+ );
+ assert_eq!(
+ "confidentiality",
+ parse_kernel_setting("none integrity [confidentiality]")
+ );
+ }
+
+ #[test]
+ fn no_brackets() {
+ assert_eq!("none", parse_kernel_setting("none"));
+ assert_eq!(
+ "none integrity confidentiality",
+ parse_kernel_setting("none integrity confidentiality\n")
+ );
+ }
}
diff --git a/sources/api/migration/migration-helpers/src/common_migrations.rs b/sources/api/migration/migration-helpers/src/common_migrations.rs
index b98fbb95a37..ffbecb30294 100644
--- a/sources/api/migration/migration-helpers/src/common_migrations.rs
+++ b/sources/api/migration/migration-helpers/src/common_migrations.rs
@@ -290,6 +290,242 @@ impl Migration for ReplaceStringMigration {
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
+/// We use this migration when we replace a setting that contains a list of string values.
+// String is the only type we use today, and handling multiple value types is more complicated than
+// we need at the moment. Allowing &[serde_json::Value] seems nice, but it would allow arbitrary
+// data transformations that the API model would then fail to load.
+pub struct ReplaceListMigration {
+ pub setting: &'static str,
+ pub old_vals: &'static [&'static str],
+ pub new_vals: &'static [&'static str],
+}
+
+impl Migration for ReplaceListMigration {
+ fn forward(&mut self, mut input: MigrationData) -> Result {
+ if let Some(data) = input.data.get_mut(self.setting) {
+ match data {
+ serde_json::Value::Array(data) => {
+ // We only handle string lists; convert each value to a str we can compare.
+ let list: Vec<&str> = data
+ .iter()
+ .map(|v| v.as_str())
+ .collect::