From ed24ffd8c394f73c3d2a747c7b7ed0e27e16b9c1 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 11:31:07 -0400 Subject: [PATCH 01/21] update clap as per atty dependency vulnerability: RUSTSEC-2021-0145 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ba9e204..166d2d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,4 @@ keywords = ["shell", "ssh", "restricted", "nrpe"] serde = "1.0" serde_derive = "1.0" toml = "0.5" -clap = { version = "3", features = ["cargo"] } +clap = { version = "4", features = ["cargo"] } From f9c212910e84f7d898ddd89020788dbeadf392d3 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 13:54:36 -0400 Subject: [PATCH 02/21] implement user-specific whitelist --- src/main.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index 19e1938..d0f4aa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,23 @@ #[macro_use] extern crate serde_derive; +use clap::{Arg, Command}; use std::collections::BTreeMap; use std::fs::File; use std::io::prelude::*; -use clap::{App, Arg}; - macro_rules! die( - ($($arg:tt)*) => { { - writeln!(std::io::stderr(), $($arg)*) - .expect("Failed to print to stderr"); - std::process::exit(1); - } } + ($($arg:tt)*) => { { + writeln!(std::io::stderr(), $($arg)*) + .expect("Failed to print to stderr"); + std::process::exit(1); + } } ); #[derive(Deserialize)] struct Config { commands: BTreeMap, + user_commands: Option>>, } fn read_config>(path: P) -> Result> { @@ -42,20 +42,21 @@ fn run_command(command: &str) -> Result> { } fn main() { - let matches = App::new(clap::crate_name!()) + let matches = Command::new(clap::crate_name!()) .version(clap::crate_version!()) .author(clap::crate_authors!()) .about("resh is a restricted (ssh) shell that only allows whitelisted commands") .arg( - Arg::with_name("command") + Arg::new("command") .short('c') .help("Alias of command to execute") .value_name("COMMAND"), ) .get_matches(); - let command_alias = match matches.value_of("command") { - Some(cmd) => String::from(cmd), + let command_alias = match matches.get_one::("command") { + Some(cmd) => cmd.clone(), + None => match std::env::var("SSH_ORIGINAL_COMMAND") { Ok(cmd) => cmd, _ => die!("Usage: {} ", clap::crate_name!()), @@ -68,7 +69,15 @@ fn main() { die!("Failed to read {}: {}", config_file, e); }); - let full_command = match config.commands.get(&command_alias) { + let username = std::env::var("USER").unwrap_or_else(|_| "default".to_string()); + + let commands = config + .user_commands + .as_ref() + .and_then(|user_cmds| user_cmds.get(&username)) + .unwrap_or_else(|| &config.commands); + + let full_command = match commands.get(&command_alias) { Some(cmd) => cmd, None => die!("Undefined command alias: {}", command_alias), }; From 8a5a9b6823132f51fc831842c41b4c8070a7ae12 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 13:54:44 -0400 Subject: [PATCH 03/21] update cargo --- Cargo.lock | 201 +++++++++++++++++++++++++++++------------------------ 1 file changed, 112 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aade198..0883ef9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,95 +3,85 @@ version = 3 [[package]] -name = "atty" -version = "0.2.14" +name = "anstream" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", ] [[package]] -name = "autocfg" -version = "1.1.0" +name = "anstyle" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] -name = "bitflags" -version = "1.3.2" +name = "anstyle-parse" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "clap" -version = "3.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54635806b078b7925d6e36810b1755f2a4b5b4d57560432c1ecf60bcbe10602b" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ - "atty", - "bitflags", - "clap_lex", - "indexmap", - "once_cell", - "strsim", - "termcolor", - "textwrap", + "utf8parse", ] [[package]] -name = "clap_lex" -version = "0.2.4" +name = "anstyle-query" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "os_str_bytes", + "windows-sys", ] [[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hermit-abi" -version = "0.1.19" +name = "anstyle-wincon" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ - "libc", + "anstyle", + "windows-sys", ] [[package]] -name = "indexmap" -version = "1.9.1" +name = "clap" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ - "autocfg", - "hashbrown", + "clap_builder", ] [[package]] -name = "libc" -version = "0.2.126" +name = "clap_builder" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] [[package]] -name = "once_cell" -version = "1.13.0" +name = "clap_lex" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] -name = "os_str_bytes" -version = "6.2.0" +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "proc-macro2" @@ -113,7 +103,7 @@ dependencies = [ [[package]] name = "resh" -version = "0.1.4" +version = "0.1.5" dependencies = [ "clap", "serde", @@ -140,9 +130,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" @@ -155,21 +145,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "toml" version = "0.5.9" @@ -186,32 +161,80 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] -name = "winapi" -version = "0.3.9" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] [[package]] -name = "winapi-util" -version = "0.1.5" +name = "windows-targets" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "winapi", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" From d34d38a98ebf9a3456a44fe29381f2ece2f6e51e Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 14:43:29 -0400 Subject: [PATCH 04/21] add back in handling for SSH_ORIGINAL_COMMAND --- src/main.rs | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index d0f4aa0..cec7d11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate serde_derive; -use clap::{Arg, Command}; +use clap::Arg; use std::collections::BTreeMap; use std::fs::File; use std::io::prelude::*; @@ -29,10 +29,20 @@ fn read_config>(path: P) -> Result Result> { +fn run_command(command: &str, args: &str) -> Result> { + let mut formatted_command = command.to_owned(); + + let args_vec: Vec<&str> = args.split_whitespace().collect(); + + for (i, arg) in args_vec.iter().enumerate() { + let placeholder = format!("%{}", i + 1); + formatted_command = formatted_command.replace(&placeholder, arg); + } + formatted_command = formatted_command.replace("%@", args); + let mut child = std::process::Command::new("/bin/sh") .arg("-c") - .arg(command) + .arg(&formatted_command) .spawn()?; child @@ -42,10 +52,10 @@ fn run_command(command: &str) -> Result> { } fn main() { - let matches = Command::new(clap::crate_name!()) + let matches = clap::Command::new(clap::crate_name!()) .version(clap::crate_version!()) .author(clap::crate_authors!()) - .about("resh is a restricted (ssh) shell that only allows whitelisted commands") + .about("resh is a restricted shell that only allows whitelisted commands") .arg( Arg::new("command") .short('c') @@ -54,13 +64,19 @@ fn main() { ) .get_matches(); - let command_alias = match matches.get_one::("command") { - Some(cmd) => cmd.clone(), + let command_alias_and_args = match std::env::var("SSH_ORIGINAL_COMMAND") { + Ok(cmd) => cmd, + Err(_) => matches + .get_one::("command") + .cloned() + .unwrap_or_else(|| die!("Usage: {} ", clap::crate_name!())), + }; + + let command_args: Vec<&str> = command_alias_and_args.split_whitespace().collect(); - None => match std::env::var("SSH_ORIGINAL_COMMAND") { - Ok(cmd) => cmd, - _ => die!("Usage: {} ", clap::crate_name!()), - }, + let (command_alias, command_args) = match command_args.split_at(1) { + (c, a) if !c.is_empty() => (c[0].to_string(), a.join(" ")), + _ => die!("Usage: {} ", clap::crate_name!()), }; let config_file = std::env::var("RESH_CONFIG").unwrap_or_else(|_| "/etc/resh.toml".to_string()); @@ -82,7 +98,7 @@ fn main() { None => die!("Undefined command alias: {}", command_alias), }; - let exitcode = run_command(full_command).unwrap_or(1); + let exitcode = run_command(full_command, &command_args).unwrap_or(1); std::process::exit(exitcode); } From 9c4da1276bc048676a7cc864127b491b4e8bfbc2 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 14:47:29 -0400 Subject: [PATCH 05/21] bugfix: fallback to global --- src/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index cec7d11..00904a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,18 +87,18 @@ fn main() { let username = std::env::var("USER").unwrap_or_else(|_| "default".to_string()); - let commands = config + let command = config .user_commands .as_ref() - .and_then(|user_cmds| user_cmds.get(&username)) - .unwrap_or_else(|| &config.commands); - - let full_command = match commands.get(&command_alias) { - Some(cmd) => cmd, - None => die!("Undefined command alias: {}", command_alias), - }; - - let exitcode = run_command(full_command, &command_args).unwrap_or(1); + .and_then(|user_cmds| { + user_cmds + .get(&username) + .and_then(|cmds| cmds.get(&command_alias)) + }) + .or_else(|| config.commands.get(&command_alias)) + .unwrap_or_else(|| die!("Undefined command alias: {}", command_alias)); + + let exitcode = run_command(command, &command_args).unwrap_or(1); std::process::exit(exitcode); } From b6e0435c7281cf5a58175d00ca29fd6f551df099 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 15:01:12 -0400 Subject: [PATCH 06/21] refactor for cleanliness --- src/main.rs | 101 ++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index 00904a4..a2afa90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,20 @@ #[macro_use] extern crate serde_derive; -use clap::Arg; -use std::collections::BTreeMap; -use std::fs::File; -use std::io::prelude::*; +use clap::{crate_authors, crate_name, crate_version, Arg, Command}; +use std::{ + collections::BTreeMap, + error::Error, + fs::File, + io::{self, prelude::*}, + process::Command as ProcessCommand, +}; macro_rules! die( - ($($arg:tt)*) => { { - writeln!(std::io::stderr(), $($arg)*) - .expect("Failed to print to stderr"); - std::process::exit(1); - } } + ($($arg:tt)*) => { { + writeln!(io::stderr(), $($arg)*).expect("Failed to print to stderr"); + std::process::exit(1); + } } ); #[derive(Deserialize)] @@ -20,41 +23,35 @@ struct Config { user_commands: Option>>, } -fn read_config>(path: P) -> Result> { +fn read_config>(path: P) -> Result> { let mut contents = String::new(); - File::open(path)?.read_to_string(&mut contents)?; - - let config: Config = toml::from_str(&contents)?; - Ok(config) + toml::from_str(&contents).map_err(|e| e.into()) } -fn run_command(command: &str, args: &str) -> Result> { - let mut formatted_command = command.to_owned(); - - let args_vec: Vec<&str> = args.split_whitespace().collect(); +fn run_command(command: &str, args: &str) -> Result> { + let formatted_command = args + .split_whitespace() + .enumerate() + .fold(command.replace("%@", args), |cmd, (i, arg)| { + cmd.replace(&format!("%{}", i + 1), arg) + }); - for (i, arg) in args_vec.iter().enumerate() { - let placeholder = format!("%{}", i + 1); - formatted_command = formatted_command.replace(&placeholder, arg); - } - formatted_command = formatted_command.replace("%@", args); - - let mut child = std::process::Command::new("/bin/sh") + ProcessCommand::new("/bin/sh") .arg("-c") .arg(&formatted_command) - .spawn()?; - - child + .spawn()? .wait()? .code() - .ok_or_else(|| std::io::Error::last_os_error().into()) + .ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::Other, "No exit code available").into() + }) } fn main() { - let matches = clap::Command::new(clap::crate_name!()) - .version(clap::crate_version!()) - .author(clap::crate_authors!()) + let matches = Command::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!()) .about("resh is a restricted shell that only allows whitelisted commands") .arg( Arg::new("command") @@ -64,41 +61,37 @@ fn main() { ) .get_matches(); - let command_alias_and_args = match std::env::var("SSH_ORIGINAL_COMMAND") { - Ok(cmd) => cmd, - Err(_) => matches - .get_one::("command") - .cloned() - .unwrap_or_else(|| die!("Usage: {} ", clap::crate_name!())), - }; - - let command_args: Vec<&str> = command_alias_and_args.split_whitespace().collect(); + let command_alias_and_args = std::env::var("SSH_ORIGINAL_COMMAND") + .or_else(|_| { + matches + .get_one::("command") + .cloned() + .ok_or_else(|| "Command not found") + }) + .unwrap_or_else(|_| die!("Usage: {} ", crate_name!())); - let (command_alias, command_args) = match command_args.split_at(1) { - (c, a) if !c.is_empty() => (c[0].to_string(), a.join(" ")), - _ => die!("Usage: {} ", clap::crate_name!()), - }; + let mut command_args = command_alias_and_args.split_whitespace(); + let command_alias = command_args + .next() + .unwrap_or_else(|| die!("Usage: {} ", crate_name!())); let config_file = std::env::var("RESH_CONFIG").unwrap_or_else(|_| "/etc/resh.toml".to_string()); - - let config: Config = read_config(&config_file).unwrap_or_else(|e| { - die!("Failed to read {}: {}", config_file, e); - }); + let config = + read_config(&config_file).unwrap_or_else(|e| die!("Failed to read {}: {}", config_file, e)); let username = std::env::var("USER").unwrap_or_else(|_| "default".to_string()); - let command = config .user_commands .as_ref() .and_then(|user_cmds| { user_cmds .get(&username) - .and_then(|cmds| cmds.get(&command_alias)) + .and_then(|cmds| cmds.get(command_alias)) }) - .or_else(|| config.commands.get(&command_alias)) + .or_else(|| config.commands.get(command_alias)) .unwrap_or_else(|| die!("Undefined command alias: {}", command_alias)); - let exitcode = run_command(command, &command_args).unwrap_or(1); - + let exitcode = + run_command(command, &command_args.collect::>().join(" ")).unwrap_or(1); std::process::exit(exitcode); } From 47f50c9ec2e17b052851dd565ae80152f6239e65 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 15:05:14 -0400 Subject: [PATCH 07/21] Version resh to 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0883ef9..9b966b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ [[package]] name = "resh" -version = "0.1.5" +version = "0.2.0" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 166d2d9..17a38dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "resh" -version = "0.1.5" +version = "0.2.0" authors = ["Koen Wilde "] description = "A restricted shell that only allows execution of previously defined aliases" edition = "2018" From 549071a0c4739e18f5f5c9a596611f9e17b8ed75 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 15:11:33 -0400 Subject: [PATCH 08/21] Add interactive mod --- src/main.rs | 72 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/src/main.rs b/src/main.rs index a2afa90..5511345 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use std::{ collections::BTreeMap, error::Error, fs::File, - io::{self, prelude::*}, + io::{self, prelude::*, BufRead, Write}, process::Command as ProcessCommand, }; @@ -61,24 +61,30 @@ fn main() { ) .get_matches(); - let command_alias_and_args = std::env::var("SSH_ORIGINAL_COMMAND") - .or_else(|_| { - matches - .get_one::("command") - .cloned() - .ok_or_else(|| "Command not found") - }) - .unwrap_or_else(|_| die!("Usage: {} ", crate_name!())); - - let mut command_args = command_alias_and_args.split_whitespace(); - let command_alias = command_args - .next() - .unwrap_or_else(|| die!("Usage: {} ", crate_name!())); - let config_file = std::env::var("RESH_CONFIG").unwrap_or_else(|_| "/etc/resh.toml".to_string()); let config = read_config(&config_file).unwrap_or_else(|e| die!("Failed to read {}: {}", config_file, e)); + if let Some(command_alias_and_args) = std::env::var("SSH_ORIGINAL_COMMAND") + .ok() + .or_else(|| matches.get_one::("command").cloned()) + { + let mut command_args = command_alias_and_args.split_whitespace(); + execute_command(&config, &mut command_args); + } else { + interactive_mode(&config); + } +} + +fn execute_command(config: &Config, command_args: &mut std::str::SplitWhitespace) -> i32 { + let command_alias = match command_args.next() { + Some(alias) => alias, + None => { + eprintln!("Usage: {} ", crate_name!()); + return 1; + } + }; + let username = std::env::var("USER").unwrap_or_else(|_| "default".to_string()); let command = config .user_commands @@ -88,10 +94,36 @@ fn main() { .get(&username) .and_then(|cmds| cmds.get(command_alias)) }) - .or_else(|| config.commands.get(command_alias)) - .unwrap_or_else(|| die!("Undefined command alias: {}", command_alias)); + .or_else(|| config.commands.get(command_alias)); + + match command { + Some(cmd) => match run_command(cmd, &command_args.collect::>().join(" ")) { + Ok(code) => code, + Err(e) => { + eprintln!("Error executing command: {}", e); + 1 + } + }, + None => { + eprintln!("Undefined command alias: {}", command_alias); + 1 + } + } +} + +fn interactive_mode(config: &Config) { + let stdin = io::stdin(); + let mut reader = stdin.lock(); + let mut line = String::new(); - let exitcode = - run_command(command, &command_args.collect::>().join(" ")).unwrap_or(1); - std::process::exit(exitcode); + loop { + print!("resh> "); + io::stdout().flush().unwrap(); + line.clear(); + if reader.read_line(&mut line).unwrap() == 0 { + break; // EOF + } + let mut command_args = line.trim().split_whitespace(); + execute_command(config, &mut command_args); + } } From eb136d36d3ef516b607b7376ad0f5ee2023b5cc7 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 15:20:02 -0400 Subject: [PATCH 09/21] Update README.md --- README.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6e19093..3ed6745 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,16 @@ whitelisted commands. Use it to restrict automated ssh logins. ## Usage -Define aliases for the commands you want to allow in the *commands* section of +Define aliases for the commands you want to allow in the _commands_ section of `/etc/resh.toml`: ```sh $ cat /etc/resh.toml [commands] foo = "echo hello" +[user_commands.john] +echo = "echo %@" +foo = "echo hello override" ``` Next set resh as the login shell for the user you want to restrict. The user @@ -28,12 +31,23 @@ Undefined command alias: bar hello ``` -Or using ssh: +using ssh: + ```sh $ ssh example_user@localhost foo hello ``` +Or interactive mode: + +```sh +# ./resh +resh > +# foo +hello +resh > +``` + ### Alternative config file locations You can specify an alternative config file by setting the `RESH_CONFIG` @@ -54,7 +68,7 @@ $ cat ~example_user/.ssh/authorized_keys command="/usr/local/bin/resh" AAAAB3NzaC1yc2EAAAADAQABAAACAQD7BsnSaa0gkPJDGZM7psAEkx+68ILJlKHS6MlUfVpQu7UoercvJXqctHczeIEf1eJToK7RmiKufoicLkHQplRpI9kP4IDAx2V0LO4BRncIOyF8wk6I7N6k6glAxePA4MgPaSsFp8SyXYW9wy+0491YHr9sWaqaKG78OQSCyf+/wwynRnwdn2u0dcRl064CGxrYleGe0AHHOSl9jj9J2Ve6M7pjZLuixRLqB2VBYyIAwy/zO7dvuxxvLIGr31TqKdLnnUvLKeInn5IU+UPMxuHG9DC9yLnif29OUzNRERTF4utkRI+ywByFTj/QePp+uTvmVv0PtkGwm77LKxeBP7jP3Hhe2uvf5clApcF+6EjFBNKWxVReH35NGPasY8DNL7Mt5CfBZcdi4nhQZyCQ7Z/XlXmJRMxmYsowhHQB8HkOM8MpHPqP9EBf9eTnxhMaA5qnrSy/z+1vdKHVXc4camSF8z7dRJKDmuoYl+aPcjS5MX6AEVz5gtFsizjhLq+mp2HkvskSZCPY87D0/hriPPtSMUlhh4XKyFJ2VzkfIr1uqQlaN1tIPdCAdUDjH5o5fnqSFHqkD8iah8OiNhmGLk2VPiYohnMLcDdLGtPMkOpX3ODgjNOTcaUfaMZW4IacVcHA2A11Zxe8r73qcjKjcX5mEppMa1Z2vosqJn2dGTasHQ== example_user@example ``` -### Full ssh *`authorized_keys`* example +### Full ssh _`authorized_keys`_ example ```sh $ cat ~example_user/.ssh/authorized_keys @@ -62,7 +76,7 @@ command="/usr/local/bin/resh",environment="RESH_CONFIG=/usr/local/etc/resh.toml" ``` For more information on the options you can specify in the `authorized_keys` -file, refer to the *`AUTHORIZED_KEYS FILE FORMAT`* section of `man 8 sshd`. You +file, refer to the _`AUTHORIZED_KEYS FILE FORMAT`_ section of `man 8 sshd`. You may especially be interested in the `restrict` option, which disables features like tcp port forwarding. @@ -86,9 +100,9 @@ your `$PATH` if cargo is installed correctly. ## Future ideas -* Possibly support an *IncludeDir* option in the config file, for easier +- Possibly support an _IncludeDir_ option in the config file, for easier provisioning from e.g. puppet or ansible. -* Provide pre-build binaries for OpenBSD, FreeBSD and linux. +- Provide pre-build binaries for OpenBSD, FreeBSD and linux. ## Feedback & Questions From 21fab156c569f75416ab87ac5909fbd682fc4328 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Tue, 30 Apr 2024 15:23:34 -0400 Subject: [PATCH 10/21] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3ed6745..7d09b09 100644 --- a/README.md +++ b/README.md @@ -31,21 +31,21 @@ Undefined command alias: bar hello ``` -using ssh: +Or interactive mode: ```sh -$ ssh example_user@localhost foo +# su - example_user +resh > +# foo hello +resh > ``` -Or interactive mode: +using ssh: ```sh -# ./resh -resh > -# foo +$ ssh example_user@localhost foo hello -resh > ``` ### Alternative config file locations From a95e7d66cc4bfb6c703db48f791c34c667082740 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 11:06:25 -0400 Subject: [PATCH 11/21] Use BATS+docker for testing --- .gitignore | 3 +++ package-lock.json | 40 ++++++++++++++++++++++++++++++++++++++++ package.json | 6 ++++++ 3 files changed, 49 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 0975025..89734ae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ target/ # Ignore vim temp and swap files *~ *.sw? + +# npm +node_modules/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e71cad2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,40 @@ +{ + "name": "resh", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "bats-assert": "github:bats-core/bats-assert", + "bats-support": "github:bats-core/bats-support" + } + }, + "node_modules/bats": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/bats/-/bats-1.11.0.tgz", + "integrity": "sha512-qiKdnS4ID3bJ1MaEOKuZe12R4w+t+psJF0ICj+UdkiHBBoObPMHv8xmD3w6F4a5qwUyZUHS+413lxENBNy8xcQ==", + "dev": true, + "peer": true, + "bin": { + "bats": "bin/bats" + } + }, + "node_modules/bats-assert": { + "version": "2.1.0", + "resolved": "git+ssh://git@github.com/bats-core/bats-assert.git#e2d855bc78619ee15b0c702b5c30fb074101159f", + "dev": true, + "peerDependencies": { + "bats": "0.4 || ^1", + "bats-support": "^0.3" + } + }, + "node_modules/bats-support": { + "version": "0.3.0", + "resolved": "git+ssh://git@github.com/bats-core/bats-support.git#9bf10e876dd6b624fe44423f0b35e064225f7556", + "dev": true, + "peerDependencies": { + "bats": "0.4 || ^1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..40f3d8c --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "bats-assert": "github:bats-core/bats-assert", + "bats-support": "github:bats-core/bats-support" + } +} From d2b2d19fa564a020267a20111b7d056e0b8625c5 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 12:31:35 -0400 Subject: [PATCH 12/21] move npm to git submodules --- .gitignore | 3 ++- .gitmodules | 9 +++++++++ package-lock.json | 40 ---------------------------------------- package.json | 6 ------ 4 files changed, 11 insertions(+), 47 deletions(-) create mode 100644 .gitmodules delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/.gitignore b/.gitignore index 89734ae..ffbb106 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ target/ *.sw? # npm -node_modules/ +test/bats +test/test_helper diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b7efcb4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "test/bats"] + path = test/bats + url = https://github.com/bats-core/bats-core.git +[submodule "test/test_helper/bats-support"] + path = test/test_helper/bats-support + url = https://github.com/bats-core/bats-support.git +[submodule "test/test_helper/bats-assert"] + path = test/test_helper/bats-assert + url = https://github.com/bats-core/bats-assert.git diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index e71cad2..0000000 --- a/package-lock.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "resh", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "bats-assert": "github:bats-core/bats-assert", - "bats-support": "github:bats-core/bats-support" - } - }, - "node_modules/bats": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/bats/-/bats-1.11.0.tgz", - "integrity": "sha512-qiKdnS4ID3bJ1MaEOKuZe12R4w+t+psJF0ICj+UdkiHBBoObPMHv8xmD3w6F4a5qwUyZUHS+413lxENBNy8xcQ==", - "dev": true, - "peer": true, - "bin": { - "bats": "bin/bats" - } - }, - "node_modules/bats-assert": { - "version": "2.1.0", - "resolved": "git+ssh://git@github.com/bats-core/bats-assert.git#e2d855bc78619ee15b0c702b5c30fb074101159f", - "dev": true, - "peerDependencies": { - "bats": "0.4 || ^1", - "bats-support": "^0.3" - } - }, - "node_modules/bats-support": { - "version": "0.3.0", - "resolved": "git+ssh://git@github.com/bats-core/bats-support.git#9bf10e876dd6b624fe44423f0b35e064225f7556", - "dev": true, - "peerDependencies": { - "bats": "0.4 || ^1" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 40f3d8c..0000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "devDependencies": { - "bats-assert": "github:bats-core/bats-assert", - "bats-support": "github:bats-core/bats-support" - } -} From 411e7cf87c7cab11eba4e4207a6dd8628da42a28 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 14:42:01 -0400 Subject: [PATCH 13/21] bugfix: revert back to whoami --- Cargo.lock | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/main.rs | 4 +- 3 files changed, 149 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b966b4..747cb3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.4" @@ -83,24 +101,54 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + [[package]] name = "resh" version = "0.2.0" @@ -109,6 +157,7 @@ dependencies = [ "serde", "serde_derive", "toml", + "whoami", ] [[package]] @@ -125,7 +174,7 @@ checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -145,6 +194,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "toml" version = "0.5.9" @@ -166,6 +226,87 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall", + "wasite", + "web-sys", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 17a38dc..cd95c9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ serde = "1.0" serde_derive = "1.0" toml = "0.5" clap = { version = "4", features = ["cargo"] } +whoami = "1.5.1" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5511345..9330d0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use std::{ io::{self, prelude::*, BufRead, Write}, process::Command as ProcessCommand, }; +use whoami; macro_rules! die( ($($arg:tt)*) => { { @@ -60,7 +61,6 @@ fn main() { .value_name("COMMAND"), ) .get_matches(); - let config_file = std::env::var("RESH_CONFIG").unwrap_or_else(|_| "/etc/resh.toml".to_string()); let config = read_config(&config_file).unwrap_or_else(|e| die!("Failed to read {}: {}", config_file, e)); @@ -85,7 +85,7 @@ fn execute_command(config: &Config, command_args: &mut std::str::SplitWhitespace } }; - let username = std::env::var("USER").unwrap_or_else(|_| "default".to_string()); + let username = whoami::username(); let command = config .user_commands .as_ref() From 38e0dd33919027a906fc1ed0daf2b2da07bb8c7c Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 14:42:15 -0400 Subject: [PATCH 14/21] Use bats+docker to do testing --- test/test.bats | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100755 test/test.bats diff --git a/test/test.bats b/test/test.bats new file mode 100755 index 0000000..65fa55c --- /dev/null +++ b/test/test.bats @@ -0,0 +1,114 @@ +#!/usr/bin/env bats + +setup() { + load '../test/test_helper/bats-support/load' + load '../test/test_helper/bats-assert/load' +} + +exit_if_fail() { + if [ ! -z "${TEST_FAILURE}" ]; then + echo "Previous test failed. Aborting." + teardown_once + exit 1 + fi +} + +@test "Setup" { + # Define docker-compose.yml using a Heredoc + dockerfile=$( + cat <<'EOF' +FROM rust:alpine as builder +WORKDIR /usr/src +RUN apk add --no-cache musl-dev + +# The standard way to build +#COPY . . +#RUN cargo build --release + +# A workaround to avoid redownloading and recompling everything when only the source code changes +# This is in leui of a cargo build --deps-only command +COPY ./Cargo.toml ./Cargo.lock ./ +RUN mkdir src \ + && echo "fn main() {println!(\"if you see this, the build failed\")}" > src/main.rs \ + && cargo build +RUN rm src/main.rs + +# Copy the source code and build the release binary +COPY ./src ./src +RUN cargo build --release + +FROM alpine:latest +WORKDIR /root +COPY --from=builder /usr/src/target/release/resh . +CMD tail -f /dev/null +EOF + ) + + # Write the config to a docker-compose.yml file + echo "$dockerfile" >Dockerfile +} + +@test "Cache" { + # Cache the Docker images + run docker pull alpine:latest + [ $status -eq 0 ] + run docker pull rust:alpine + [ $status -eq 0 ] + exit_if_fail +} + +@test "Build" { + run docker build -t resh-test . + [ $status -eq 0 ] + exit_if_fail +} + +@test "Up" { + run docker run -d --name resh-test resh-test + [ $status -eq 0 ] + exit_if_fail +} + +@test "resh-test-finds-toml" { + resh_toml=$( + cat <<'EOF' +[commands] +ls = "ls -l" +[user_commands.root] +lsa = "ls -lah" +EOF + ) + + echo "$resh_toml" >resh.toml + run docker cp resh.toml resh-test:/etc/resh.toml + [ $status -eq 0 ] + run docker exec -it resh-test ./resh -c + [ $status -ne 0 ] + assert_output --partial 'a value is required for' +} + +@test "resh-test-toml-global-valid-command" { + run docker exec -it resh-test ./resh -c ls + assert_output --partial 'root' +} + +@test "resh-test-toml-global-unavailable-command" { + run docker exec -it resh-test ./resh -c touch + assert_output --partial 'Undefined command alias' +} + +@test "resh-test-toml-global-user-command" { + run docker exec -it resh-test ./resh -c lsa + assert_output --partial '..' +} + +@test "Teardown" { + teardown_once +} + +teardown_once() { + rm Dockerfile >/dev/null 2>&1 + rm resh.toml >/dev/null 2>&1 + docker kill resh-test >/dev/null 2>&1 + docker rm resh-test >/dev/null 2>&1 +} From f31532e1f82a58e4bab6523fa782f244d6ddeca1 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 15:07:32 -0400 Subject: [PATCH 15/21] move to defined Dockerfile --- test/Dockerfile | 24 +++++++++++++++++++ test/test.bats | 62 ++----------------------------------------------- 2 files changed, 26 insertions(+), 60 deletions(-) create mode 100644 test/Dockerfile diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..1521f40 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,24 @@ +FROM rust:alpine as builder +WORKDIR /usr/src +RUN apk add --no-cache musl-dev + +# The standard way to build +#COPY . . +#RUN cargo build --release + +# A workaround to avoid redownloading and recompling everything when only the source code changes +# This is in leui of a cargo build --deps-only command +COPY ./Cargo.toml ./Cargo.lock ./ +RUN mkdir src \ + && echo "fn main() {println!(\"if you see this, the build failed\")}" > src/main.rs \ + && cargo build +RUN rm src/main.rs + +# Copy the source code and build the release binary +COPY ./src ./src +RUN cargo build --release + +FROM alpine:latest +WORKDIR /root +COPY --from=builder /usr/src/target/release/resh . +CMD tail -f /dev/null diff --git a/test/test.bats b/test/test.bats index 65fa55c..69215e1 100755 --- a/test/test.bats +++ b/test/test.bats @@ -5,71 +5,14 @@ setup() { load '../test/test_helper/bats-assert/load' } -exit_if_fail() { - if [ ! -z "${TEST_FAILURE}" ]; then - echo "Previous test failed. Aborting." - teardown_once - exit 1 - fi -} - @test "Setup" { - # Define docker-compose.yml using a Heredoc - dockerfile=$( - cat <<'EOF' -FROM rust:alpine as builder -WORKDIR /usr/src -RUN apk add --no-cache musl-dev - -# The standard way to build -#COPY . . -#RUN cargo build --release - -# A workaround to avoid redownloading and recompling everything when only the source code changes -# This is in leui of a cargo build --deps-only command -COPY ./Cargo.toml ./Cargo.lock ./ -RUN mkdir src \ - && echo "fn main() {println!(\"if you see this, the build failed\")}" > src/main.rs \ - && cargo build -RUN rm src/main.rs - -# Copy the source code and build the release binary -COPY ./src ./src -RUN cargo build --release - -FROM alpine:latest -WORKDIR /root -COPY --from=builder /usr/src/target/release/resh . -CMD tail -f /dev/null -EOF - ) - - # Write the config to a docker-compose.yml file - echo "$dockerfile" >Dockerfile -} - -@test "Cache" { - # Cache the Docker images - run docker pull alpine:latest + run docker build -f test/Dockerfile -t resh-test . [ $status -eq 0 ] - run docker pull rust:alpine - [ $status -eq 0 ] - exit_if_fail -} - -@test "Build" { - run docker build -t resh-test . - [ $status -eq 0 ] - exit_if_fail -} - -@test "Up" { run docker run -d --name resh-test resh-test [ $status -eq 0 ] - exit_if_fail } -@test "resh-test-finds-toml" { +@test "resh-test-toml-found" { resh_toml=$( cat <<'EOF' [commands] @@ -107,7 +50,6 @@ EOF } teardown_once() { - rm Dockerfile >/dev/null 2>&1 rm resh.toml >/dev/null 2>&1 docker kill resh-test >/dev/null 2>&1 docker rm resh-test >/dev/null 2>&1 From 8b267cd93ed4a1f880d17f1fd63107249db0e25e Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 15:11:37 -0400 Subject: [PATCH 16/21] add additional tests --- test/test.bats | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/test.bats b/test/test.bats index 69215e1..61f0609 100755 --- a/test/test.bats +++ b/test/test.bats @@ -17,8 +17,11 @@ setup() { cat <<'EOF' [commands] ls = "ls -l" +foo = 'echo bar' [user_commands.root] lsa = "ls -lah" +echo = "echo %@" +foo = 'echo bar override' EOF ) @@ -30,12 +33,12 @@ EOF assert_output --partial 'a value is required for' } -@test "resh-test-toml-global-valid-command" { +@test "resh-test-toml-global-command-valid" { run docker exec -it resh-test ./resh -c ls assert_output --partial 'root' } -@test "resh-test-toml-global-unavailable-command" { +@test "resh-test-toml-global-command-unavailable" { run docker exec -it resh-test ./resh -c touch assert_output --partial 'Undefined command alias' } @@ -45,6 +48,16 @@ EOF assert_output --partial '..' } +@test "resh-test-toml-global-user-command-with-args" { + run docker exec -it resh-test ./resh -c 'echo hello world!' + assert_output --partial 'hello world!' +} + +@test "resh-test-toml-global-user-command-overrides-global" { + run docker exec -it resh-test ./resh -c 'foo' + assert_output --partial 'bar override' +} + @test "Teardown" { teardown_once } From a1f07389d9712fd59ad3485bedc95b505cca514c Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 17:13:31 -0400 Subject: [PATCH 17/21] test.sh file for accessability and mental overhead --- test.sh | 1 + 1 file changed, 1 insertion(+) create mode 100755 test.sh diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..1d3dd16 --- /dev/null +++ b/test.sh @@ -0,0 +1 @@ +./test/bats/bin/bats test/test.bats \ No newline at end of file From 2c5b22dd8f8d052c1eccbdb57ea71a08a3424cfb Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Wed, 1 May 2024 17:14:29 -0400 Subject: [PATCH 18/21] hardfile for resh.toml --- test/resh.toml | 7 +++++++ test/test.bats | 16 +--------------- 2 files changed, 8 insertions(+), 15 deletions(-) create mode 100644 test/resh.toml diff --git a/test/resh.toml b/test/resh.toml new file mode 100644 index 0000000..ab7384b --- /dev/null +++ b/test/resh.toml @@ -0,0 +1,7 @@ +[commands] +ls = "ls -l" +foo = 'echo bar' +[user_commands.root] +lsa = "ls -lah" +echo = "echo %@" +foo = 'echo bar override' \ No newline at end of file diff --git a/test/test.bats b/test/test.bats index 61f0609..9ebf37b 100755 --- a/test/test.bats +++ b/test/test.bats @@ -13,20 +13,7 @@ setup() { } @test "resh-test-toml-found" { - resh_toml=$( - cat <<'EOF' -[commands] -ls = "ls -l" -foo = 'echo bar' -[user_commands.root] -lsa = "ls -lah" -echo = "echo %@" -foo = 'echo bar override' -EOF - ) - - echo "$resh_toml" >resh.toml - run docker cp resh.toml resh-test:/etc/resh.toml + run docker cp test/resh.toml resh-test:/etc/resh.toml [ $status -eq 0 ] run docker exec -it resh-test ./resh -c [ $status -ne 0 ] @@ -63,7 +50,6 @@ EOF } teardown_once() { - rm resh.toml >/dev/null 2>&1 docker kill resh-test >/dev/null 2>&1 docker rm resh-test >/dev/null 2>&1 } From 5d7f029254155c4fff9b19e847b65427df15bb88 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall Date: Thu, 2 May 2024 11:06:37 -0400 Subject: [PATCH 19/21] tests for resh ssh --- test/Dockerfile | 24 ------------- test/docker-compose.yaml | 74 ++++++++++++++++++++++++++++++++++++++++ test/test.bats | 41 +++++++++++++++++++--- 3 files changed, 111 insertions(+), 28 deletions(-) delete mode 100644 test/Dockerfile create mode 100644 test/docker-compose.yaml diff --git a/test/Dockerfile b/test/Dockerfile deleted file mode 100644 index 1521f40..0000000 --- a/test/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM rust:alpine as builder -WORKDIR /usr/src -RUN apk add --no-cache musl-dev - -# The standard way to build -#COPY . . -#RUN cargo build --release - -# A workaround to avoid redownloading and recompling everything when only the source code changes -# This is in leui of a cargo build --deps-only command -COPY ./Cargo.toml ./Cargo.lock ./ -RUN mkdir src \ - && echo "fn main() {println!(\"if you see this, the build failed\")}" > src/main.rs \ - && cargo build -RUN rm src/main.rs - -# Copy the source code and build the release binary -COPY ./src ./src -RUN cargo build --release - -FROM alpine:latest -WORKDIR /root -COPY --from=builder /usr/src/target/release/resh . -CMD tail -f /dev/null diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml new file mode 100644 index 0000000..8d422e0 --- /dev/null +++ b/test/docker-compose.yaml @@ -0,0 +1,74 @@ +services: + resh-test: + hostname: resh-test + container_name: resh-test + build: + context: ./.. + dockerfile_inline: | + FROM rust:alpine as builder + WORKDIR /usr/src + RUN apk add --no-cache musl-dev + + # The standard way to build + #COPY . . + #RUN cargo build --release + + # A workaround to avoid redownloading and recompling everything when only the source code changes + # This is in leui of a cargo build --deps-only command + COPY ./Cargo.toml ./Cargo.lock ./ + RUN mkdir src \ + && echo "fn main() {println!(\"if you see this, the build failed\")}" > src/main.rs \ + && cargo build + RUN rm src/main.rs + + # Copy the source code and build the release binary + COPY ./src ./src + RUN cargo build --release + + FROM alpine:latest + WORKDIR /root + COPY --from=builder /usr/src/target/release/resh . + + # SSH Server + RUN apk add --no-cache openssh-server + RUN sed -i 's/#Port 22/Port 1234/' /etc/ssh/sshd_config + RUN sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config + RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config + RUN sed -i 's/#ChallengeResponseAuthentication yes/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config + RUN sed -i 's/#KbdInteractiveAuthentication yes/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config + RUN echo 'AuthenticationMethods publickey' >> /etc/ssh/sshd_config + + # SSH Server + RUN ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N '' + # Root User Authoirzed Keys + RUN mkdir -p /root/.ssh/ + RUN cat /etc/ssh/ssh_host_ed25519_key.pub >> /root/.ssh/authorized_keys + RUN chmod 600 /root/.ssh/authorized_keys + # Start the SSHD server and keep it up + CMD ["/usr/sbin/sshd", "-D"] + volumes: + - resh-ssh-volume:/etc/ssh/ + networks: + - resh-isolated-network + + resh-test-ssh-client: + hostname: resh-test-ssh-client + container_name: resh-test-ssh-client + build: + context: . + dockerfile_inline: | + FROM alpine:latest + RUN apk add --no-cache openssh-client + RUN mkdir -p /root/.ssh + CMD tail -f /dev/null + volumes: + - resh-ssh-volume:/mnt/resh-ssh-volume/ + networks: + - resh-isolated-network + +networks: + resh-isolated-network: + driver: bridge + +volumes: + resh-ssh-volume: diff --git a/test/test.bats b/test/test.bats index 9ebf37b..bbf7231 100755 --- a/test/test.bats +++ b/test/test.bats @@ -6,10 +6,25 @@ setup() { } @test "Setup" { - run docker build -f test/Dockerfile -t resh-test . + run docker-compose -f test/docker-compose.yaml up --build -d [ $status -eq 0 ] - run docker run -d --name resh-test resh-test + run docker exec -it resh-test-ssh-client cp /mnt/resh-ssh-volume/ssh_host_ed25519_key /root/.ssh/ssh_host_ed25519_key [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client chmod 700 /root/.ssh + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client sh -c 'echo [resh-test]:1234 $(cat /mnt/resh-ssh-volume/ssh_host_ed25519_key.pub) >> /root/.ssh/known_hosts' + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client chmod 700 /root/.ssh/known_hosts + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client cat /root/.ssh/known_hosts + [ $status -eq 0 ] + + # Docker resh-test-ssh-client is authorized into resh-test + run docker exec -it resh-test-ssh-client ssh -i /root/.ssh/ssh_host_ed25519_key root@resh-test -p 1234 echo 'hello world!' + [ $status -eq 0 ] + refute_output --partial 'Connection refused' + refute_output --partial 'Permission denied' + assert_output --partial 'hello world!' } @test "resh-test-toml-found" { @@ -45,11 +60,29 @@ setup() { assert_output --partial 'bar override' } +@test "resh-test-ssh-entrypoint-full" { + run docker exec -it resh-test mkdir -p /usr/local/etc + [ $status -eq 0 ] + run docker cp test/resh.toml resh-test:/usr/local/etc/resh.toml + [ $status -eq 0 ] + run docker exec resh-test /bin/sh -c "sed -i 's/^/command=\"\\/root\\/resh\",environment=\"RESH_CONFIG=\\/usr\\/local\\/etc\\/resh.toml\", /' /root/.ssh/authorized_keys" + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client ssh -i /root/.ssh/ssh_host_ed25519_key root@resh-test -p 1234 foo + [ $status -eq 0 ] + refute_output --partial 'Connection refused' + refute_output --partial 'Permission denied' + assert_output --partial 'bar override' +} + @test "Teardown" { teardown_once } teardown_once() { - docker kill resh-test >/dev/null 2>&1 - docker rm resh-test >/dev/null 2>&1 + docker kill resh-test + docker kill resh-test-ssh-client + docker rm resh-test + docker rm resh-test-ssh-client + docker volume rm test_resh-ssh-volume + docker network rm test_resh-isolated-network } From ed3bd1ceb1cfd8da33eab4c3b636d3e18236bec7 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall <157645696+evanschoffstall@users.noreply.github.com> Date: Sun, 5 May 2024 15:29:56 +0000 Subject: [PATCH 20/21] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d09b09..d230f1e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # resh -[![Build Status](https://api.travis-ci.org/koenw/resh.svg?branch=master)](https://travis-ci.org/koenw/resh) - -`resh` is a shell that only allows the execution of previously -whitelisted commands. Use it to restrict automated ssh logins. +`resh` is a secure restricted production-grade ssh-combatible shell built in rust ## Usage From 07a2feaa558827551a4f1cba330beb3ca30ef4d7 Mon Sep 17 00:00:00 2001 From: Evan Schoffstall <157645696+evanschoffstall@users.noreply.github.com> Date: Mon, 6 May 2024 01:44:52 +0000 Subject: [PATCH 21/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d230f1e..f89cdb9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # resh -`resh` is a secure restricted production-grade ssh-combatible shell built in rust +`resh` is a secure restricted production-grade ssh-compatible shell built in rust ## Usage