/*!
corndog is a delicious way to get at the meat inside the kernels.
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`
*/
use log::{debug, error, info, trace, warn};
use simplelog::{Config as LogConfig, LevelFilter, SimpleLogger};
use snafu::ResultExt;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::String;
use std::{env, process};
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,
}
/// Main entry point.
async fn run() -> Result<()> {
let args = parse_args(env::args());
// SimpleLogger will send errors to stderr and anything less to stdout.
SimpleLogger::init(args.log_level, LogConfig::default()).context(error::LoggerSnafu)?;
// 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 {
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
}
}
}
Ok(())
}
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
/// Retrieve the current model from the API.
async fn get_model
(socket_path: P) -> Result
where
P: AsRef,
{
let uri = "/";
let method = "GET";
trace!("{}ing from {}", method, uri);
let (code, response_body) = apiclient::raw_request(socket_path, &uri, method, None)
.await
.context(error::APIRequestSnafu { method, uri })?;
if !code.is_success() {
return error::APIResponseSnafu {
method,
uri,
code,
response_body,
}
.fail();
}
trace!("JSON response: {}", response_body);
serde_json::from_str(&response_body).context(error::ResponseJsonSnafu { method, uri })
}
fn sysctl_path(name: S) -> PathBuf
where
S: AsRef,
{
let name = name.as_ref();
let mut path = PathBuf::from(SYSCTL_PATH_PREFIX);
path.extend(name.replace('.', "/").split('/'));
trace!("Path for {}: {}", name, path.display());
path
}
/// 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)
where
K: AsRef,
{
for (key, value) in sysctls {
let key = key.as_ref();
let path = sysctl_path(key);
if let Err(e) = fs::write(path, value) {
// We don't fail because sysctl keys can vary between kernel versions and depend on
// loaded modules. It wouldn't be possible to deploy settings to a mixed-kernel fleet
// if newer sysctl values failed on your older kernels, for example, and we believe
// it's too cumbersome to have to specify in settings which keys are allowed to fail.
error!("Failed to write sysctl value '{}': {}", key, e);
}
}
}
/// 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::LockdownSnafu { 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: {} SUBCOMMAND [ ARGUMENTS... ]
Subcommands:
sysctl
lockdown
Global arguments:
--socket-path PATH
--log-level trace|debug|info|warn|error
Socket path defaults to {}",
program_name,
constants::API_SOCKET,
);
process::exit(2);
}
/// Prints a more specific message before exiting through usage().
fn usage_msg>(msg: S) -> ! {
eprintln!("{}\n", msg.as_ref());
usage();
}
/// Parses the arguments to the program and return a representative `Args`.
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() {
match arg.as_ref() {
"--log-level" => {
let log_level_str = iter
.next()
.unwrap_or_else(|| usage_msg("Did not give argument to --log-level"));
log_level = Some(LevelFilter::from_str(&log_level_str).unwrap_or_else(|_| {
usage_msg(format!("Invalid log level '{}'", log_level_str))
}));
}
"--socket-path" => {
socket_path = Some(
iter.next()
.unwrap_or_else(|| usage_msg("Did not give argument to --socket-path")),
)
}
"sysctl" | "lockdown" => subcommand = Some(arg),
_ => usage(),
}
}
Args {
subcommand: subcommand.unwrap_or_else(|| usage_msg("Must specify a subcommand.")),
log_level: log_level.unwrap_or(LevelFilter::Info),
socket_path: socket_path.unwrap_or_else(|| constants::API_SOCKET.to_string()),
}
}
// 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
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("{}", e);
process::exit(1);
}
}
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
mod error {
use http::StatusCode;
use snafu::Snafu;
use std::io;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(super)))]
pub(super) enum Error {
#[snafu(display("Error {}ing to {}: {}", method, uri, source))]
APIRequest {
method: String,
uri: String,
#[snafu(source(from(apiclient::Error, Box::new)))]
source: Box,
},
#[snafu(display("Error {} when {}ing to {}: {}", code, method, uri, response_body))]
APIResponse {
method: String,
uri: String,
code: StatusCode,
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: log::SetLoggerError },
#[snafu(display(
"Error deserializing response as JSON from {} to '{}': {}",
method,
uri,
source
))]
ResponseJson {
method: &'static str,
uri: String,
source: serde_json::Error,
},
}
}
type Result = std::result::Result;
#[cfg(test)]
mod test {
use super::*;
#[test]
fn no_traversal() {
assert_eq!(
sysctl_path("../../root/file").to_string_lossy(),
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")
);
}
}