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::>>() + .with_context(|| error::ReplaceListContents { + setting: self.setting, + data: data.clone(), + })?; + + if list == self.old_vals { + // Convert back to the original type so we can store it. + *data = self.new_vals.iter().map(|s| (*s).into()).collect(); + println!( + "Changed value of '{}' from {:?} to {:?} on upgrade", + self.setting, self.old_vals, self.new_vals + ); + } else { + println!("'{}' is not set to {:?}, leaving alone", self.setting, list); + } + } + _ => { + println!( + "'{}' is set to non-list value '{}'; ReplaceListMigration only handles lists", + self.setting, data + ); + } + } + } else { + println!("Found no '{}' to change on upgrade", self.setting); + } + Ok(input) + } + + fn backward(&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::>>() + .with_context(|| error::ReplaceListContents { + setting: self.setting, + data: data.clone(), + })?; + + if list == self.new_vals { + // Convert back to the original type so we can store it. + *data = self.old_vals.iter().map(|s| (*s).into()).collect(); + println!( + "Changed value of '{}' from {:?} to {:?} on downgrade", + self.setting, self.new_vals, self.old_vals + ); + } else { + println!("'{}' is not set to {:?}, leaving alone", self.setting, list); + } + } + _ => { + println!( + "'{}' is set to non-list value '{}'; ReplaceListMigration only handles lists", + self.setting, data + ); + } + } + } else { + println!("Found no '{}' to change on downgrade", self.setting); + } + Ok(input) + } +} + +#[cfg(test)] +mod test_replace_list { + use super::ReplaceListMigration; + use crate::{Migration, MigrationData}; + use maplit::hashmap; + use std::collections::HashMap; + + #[test] + fn single() { + let data = MigrationData { + data: hashmap! { + "hi".into() => vec!["there"].into(), + }, + metadata: HashMap::new(), + }; + let result = ReplaceListMigration { + setting: "hi", + old_vals: &["there"], + new_vals: &["sup"], + } + .forward(data) + .unwrap(); + assert_eq!( + result.data, + hashmap! { + "hi".into() => vec!["sup"].into(), + } + ); + } + + #[test] + fn backward() { + let data = MigrationData { + data: hashmap! { + "hi".into() => vec!["there"].into(), + }, + metadata: HashMap::new(), + }; + let result = ReplaceListMigration { + setting: "hi", + old_vals: &["sup"], + new_vals: &["there"], + } + .backward(data) + .unwrap(); + assert_eq!( + result.data, + hashmap! { + "hi".into() => vec!["sup"].into(), + } + ); + } + + #[test] + fn multiple() { + let data = MigrationData { + data: hashmap! { + "hi".into() => vec!["there", "you"].into(), + "ignored".into() => vec!["no", "change"].into(), + }, + metadata: HashMap::new(), + }; + let result = ReplaceListMigration { + setting: "hi", + old_vals: &["there", "you"], + new_vals: &["sup", "hey"], + } + .forward(data) + .unwrap(); + assert_eq!( + result.data, + hashmap! { + "hi".into() => vec!["sup", "hey"].into(), + "ignored".into() => vec!["no", "change"].into(), + } + ); + } + + #[test] + fn no_match() { + let data = MigrationData { + data: hashmap! { + "hi".into() => vec!["no", "change"].into(), + "hi2".into() => vec!["no", "change"].into(), + }, + metadata: HashMap::new(), + }; + let result = ReplaceListMigration { + setting: "hi", + old_vals: &["there"], + new_vals: &["sup", "hey"], + } + .forward(data) + .unwrap(); + // No change + assert_eq!( + result.data, + hashmap! { + "hi".into() => vec!["no", "change"].into(), + "hi2".into() => vec!["no", "change"].into(), + } + ); + } + + #[test] + fn not_list() { + let data = MigrationData { + data: hashmap! { + "hi".into() => "just a string, not a list".into(), + }, + metadata: HashMap::new(), + }; + let result = ReplaceListMigration { + setting: "hi", + old_vals: &["there"], + new_vals: &["sup", "hey"], + } + .forward(data) + .unwrap(); + // No change + assert_eq!( + result.data, + hashmap! { + "hi".into() => "just a string, not a list".into(), + } + ); + } + + #[test] + fn not_string() { + let data = MigrationData { + data: hashmap! { + "hi".into() => vec![0].into(), + }, + metadata: HashMap::new(), + }; + ReplaceListMigration { + setting: "hi", + old_vals: &["there"], + new_vals: &["sup", "hey"], + } + .forward(data) + .unwrap_err(); + } +} + +// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= + /// We use this migration when we replace an existing template for generating some setting. pub struct ReplaceTemplateMigration { pub setting: &'static str, diff --git a/sources/api/migration/migration-helpers/src/error.rs b/sources/api/migration/migration-helpers/src/error.rs index 028c760c00e..2add985a5c4 100644 --- a/sources/api/migration/migration-helpers/src/error.rs +++ b/sources/api/migration/migration-helpers/src/error.rs @@ -85,6 +85,12 @@ pub enum Error { NewKey { source: apiserver::datastore::error::Error, }, + + #[snafu(display("Setting '{}' contains non-string item: {:?}", setting, data))] + ReplaceListContents { + setting: String, + data: Vec, + }, } /// Result alias containing our Error type. diff --git a/sources/api/migration/migrations/v1.0.5/add-lockdown/Cargo.toml b/sources/api/migration/migrations/v1.0.5/add-lockdown/Cargo.toml new file mode 100644 index 00000000000..895ec56ec40 --- /dev/null +++ b/sources/api/migration/migrations/v1.0.5/add-lockdown/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "add-lockdown" +version = "0.1.0" +authors = ["Tom Kirchner "] +license = "Apache-2.0 OR MIT" +edition = "2018" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +migration-helpers = { path = "../../../migration-helpers" } diff --git a/sources/api/migration/migrations/v1.0.5/add-lockdown/src/main.rs b/sources/api/migration/migrations/v1.0.5/add-lockdown/src/main.rs new file mode 100644 index 00000000000..b3b6f93a9a0 --- /dev/null +++ b/sources/api/migration/migrations/v1.0.5/add-lockdown/src/main.rs @@ -0,0 +1,24 @@ +#![deny(rust_2018_idioms)] + +use migration_helpers::common_migrations::AddPrefixesMigration; +use migration_helpers::{migrate, Result}; +use std::process; + +/// We added the ability to set kernel lockdown mode through a setting, so on downgrade we need to +/// remove the setting and the associated settings for the service that writes out changes. +fn run() -> Result<()> { + migrate(AddPrefixesMigration(vec![ + "settings.kernel.lockdown", + "services.lockdown", + ])) +} + +// Returning a Result from main makes it print a Debug representation of the error, but with Snafu +// we have nice Display representations of the error, so we wrap "main" (run) and print any error. +// https://github.com/shepmaster/snafu/issues/110 +fn main() { + if let Err(e) = run() { + eprintln!("{}", e); + process::exit(1); + } +} diff --git a/sources/api/migration/migrations/v1.0.5/sysctl-subcommand/Cargo.toml b/sources/api/migration/migrations/v1.0.5/sysctl-subcommand/Cargo.toml new file mode 100644 index 00000000000..94cb36c874c --- /dev/null +++ b/sources/api/migration/migrations/v1.0.5/sysctl-subcommand/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sysctl-subcommand" +version = "0.1.0" +authors = ["Tom Kirchner "] +license = "Apache-2.0 OR MIT" +edition = "2018" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +migration-helpers = { path = "../../../migration-helpers" } diff --git a/sources/api/migration/migrations/v1.0.5/sysctl-subcommand/src/main.rs b/sources/api/migration/migrations/v1.0.5/sysctl-subcommand/src/main.rs new file mode 100644 index 00000000000..4aa2145a481 --- /dev/null +++ b/sources/api/migration/migrations/v1.0.5/sysctl-subcommand/src/main.rs @@ -0,0 +1,25 @@ +#![deny(rust_2018_idioms)] + +use migration_helpers::common_migrations::ReplaceListMigration; +use migration_helpers::{migrate, Result}; +use std::process; + +/// We changed corndog to use subcommands so it can handle different kernel settings without having +/// to apply them all every time. +fn run() -> Result<()> { + migrate(ReplaceListMigration { + setting: "services.sysctl.restart-commands", + old_vals: &["/usr/bin/corndog"], + new_vals: &["/usr/bin/corndog sysctl"], + }) +} + +// Returning a Result from main makes it print a Debug representation of the error, but with Snafu +// we have nice Display representations of the error, so we wrap "main" (run) and print any error. +// https://github.com/shepmaster/snafu/issues/110 +fn main() { + if let Err(e) = run() { + eprintln!("{}", e); + process::exit(1); + } +} diff --git a/sources/models/defaults.toml b/sources/models/defaults.toml index 6e440f3885e..cc14fb36541 100644 --- a/sources/models/defaults.toml +++ b/sources/models/defaults.toml @@ -95,9 +95,19 @@ affected-services = ["chronyd"] # Kernel +[settings.kernel] +lockdown = "none" + [services.sysctl] configuration-files = [] -restart-commands = ["/usr/bin/corndog"] +restart-commands = ["/usr/bin/corndog sysctl"] [metadata.settings.kernel.sysctl] affected-services = ["sysctl"] + +[services.lockdown] +configuration-files = [] +restart-commands = ["/usr/bin/corndog lockdown"] + +[metadata.settings.kernel.lockdown] +affected-services = ["lockdown"] diff --git a/sources/models/src/lib.rs b/sources/models/src/lib.rs index 5d1fecdc11e..6fd2de76863 100644 --- a/sources/models/src/lib.rs +++ b/sources/models/src/lib.rs @@ -88,7 +88,7 @@ use std::net::Ipv4Addr; use crate::modeled_types::{ DNSDomain, ECSAgentLogLevel, ECSAttributeKey, ECSAttributeValue, FriendlyVersion, KubernetesClusterName, KubernetesLabelKey, KubernetesLabelValue, KubernetesTaintValue, - SingleLineString, SysctlKey, Url, ValidBase64, + Lockdown, SingleLineString, SysctlKey, Url, ValidBase64, }; // Kubernetes related settings. The dynamic settings are retrieved from @@ -149,6 +149,7 @@ struct NtpSettings { // Kernel settings #[model] struct KernelSettings { + lockdown: Lockdown, // Values are almost always a single line and often just an integer... but not always. sysctl: HashMap, } diff --git a/sources/models/src/modeled_types/mod.rs b/sources/models/src/modeled_types/mod.rs index bd9e2d1bc5d..3a632183ae9 100644 --- a/sources/models/src/modeled_types/mod.rs +++ b/sources/models/src/modeled_types/mod.rs @@ -48,6 +48,9 @@ pub mod error { #[snafu(display("Invalid domain name '{}': {}", input, msg))] InvalidDomainName { input: String, msg: String }, + #[snafu(display("Invalid Linux lockdown mode '{}'", input))] + InvalidLockdown { input: String }, + #[snafu(display("Invalid sysctl key '{}': {}", input, msg))] InvalidSysctlKey { input: String, msg: String }, diff --git a/sources/models/src/modeled_types/shared.rs b/sources/models/src/modeled_types/shared.rs index b207637802c..fc398445969 100644 --- a/sources/models/src/modeled_types/shared.rs +++ b/sources/models/src/modeled_types/shared.rs @@ -530,3 +530,28 @@ mod test_sysctl_key { } } } + +// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= + +/// Lockdown represents a string that is a valid Linux kernel lockdown mode name. It stores the +/// original string and makes it accessible through standard traits. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Lockdown { + inner: String, +} + +impl TryFrom<&str> for Lockdown { + type Error = error::Error; + + fn try_from(input: &str) -> Result { + ensure!( + matches!(input, "none" | "integrity" | "confidentiality"), + error::InvalidLockdown { input } + ); + Ok(Lockdown { + inner: input.to_string(), + }) + } +} + +string_impls_for!(Lockdown, "Lockdown");