-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.rs
158 lines (138 loc) · 5.71 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
use anyhow::{ensure, Context, Result};
use nix::unistd::{initgroups, setresgid, setresuid, Gid, Uid};
use sha2::{Digest, Sha512};
use std::env;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufReader, Read};
use std::os::unix::process::CommandExt;
use std::{ffi::CString, process::Command};
use users::{get_group_by_name, get_user_by_name, group_access_list};
/// A comma separated list of users for which to allow elevation of privileges using this utility.
/// Leave unset for an empty list.
/// Optional.
const ALLOWED_USERS: Option<&str> = option_env!("ELEWRAP_ALLOWED_USERS");
/// A comma separated list of groups for which to allow elevation of privileges using this utility.
/// Leave unset for an empty list.
/// Optional.
const ALLOWED_GROUPS: Option<&str> = option_env!("ELEWRAP_ALLOWED_GROUPS");
/// The target user to change to before executing the command.
/// Required.
const TARGET_USER: &str = env!("ELEWRAP_TARGET_USER");
/// The delimiter on which to split the target command.
/// Default: "\t"
const TARGET_COMMAND_DELIMITER: &str = match option_env!("ELEWRAP_TARGET_COMMAND_DELIMITER") {
Some(x) => x,
None => "\t",
};
/// The command to execute after changing to the target user. This must be an absolute path.
/// Required.
const TARGET_COMMAND: &[&str] =
&const_str::split!(env!("ELEWRAP_TARGET_COMMAND"), TARGET_COMMAND_DELIMITER);
/// If set, authenticates the target command via its sha512 hash at runtime.
/// Optional.
const TARGET_COMMAND_SHA512: Option<&str> = option_env!("ELEWRAP_TARGET_COMMAND_SHA512");
/// A comma separated list of environment variables which should be allowed to be
/// passed to the target command.
/// Leave unset for an empty list.
/// Optional.
const PASS_ENVIRONMENT: Option<&str> = option_env!("ELEWRAP_PASS_ENVIRONMENT");
/// Whether any additional runtime arguments should be appended to the executed command.
/// Default: false
const PASS_ARGUMENTS: bool = match option_env!("ELEWRAP_PASS_ARGUMENTS") {
Some(x) => const_str::equal!(x, "true") || const_str::equal!(x, "1"),
None => false,
};
// The target command must have at least one component
static_assertions::const_assert!(!TARGET_COMMAND.is_empty());
/// Drop all privileges and change to the target user.
fn drop_privileges() -> Result<()> {
let user =
get_user_by_name(TARGET_USER).context(format!("Invalid target user: {}", TARGET_USER))?;
let target_uid: Uid = user.uid().into();
let target_gid: Gid = user.primary_group_id().into();
let setgroups_res = unsafe { libc::setgroups(0, std::ptr::null()) };
ensure!(setgroups_res == 0, "Failed to drop groups with setgroups!");
let cstr_target_user = CString::new(TARGET_USER.as_bytes())?;
initgroups(&cstr_target_user, target_gid)?;
setresgid(target_gid, target_gid, target_gid)?;
setresuid(target_uid, target_uid, target_uid)?;
Ok(())
}
/// Authorizes the given caller.
fn authorize(caller_uid: Uid, caller_gids: &[u32]) -> Result<()> {
let is_allowed_user = || {
ALLOWED_USERS.map_or(false, |xs| {
xs.split(',')
.any(|x| get_user_by_name(x).map_or(false, |x| x.uid() == caller_uid.as_raw()))
})
};
let has_allowed_group = || {
ALLOWED_GROUPS.map_or(false, |xs| {
xs.split(',')
.any(|x| get_group_by_name(x).map_or(false, |x| caller_gids.contains(&x.gid())))
})
};
ensure!(is_allowed_user() || has_allowed_group(), "Unauthorized.");
Ok(())
}
/// Drops all environment variables that are not in the allowlist
fn drop_environment() {
let allowed_vars =
PASS_ENVIRONMENT.map_or_else(|| Vec::new(), |xs| xs.split(',').map(OsStr::new).collect());
for (name, _) in env::vars_os() {
if !allowed_vars.contains(&name.as_os_str()) {
env::remove_var(&name);
}
}
}
/// Computes the sha512 digest of the given file path in lowercase hexadecimal notation
fn sha512_digest(path: &str) -> Result<String> {
let input = File::open(path)?;
let mut reader = BufReader::new(input);
let mut hasher = Sha512::new();
let mut buffer = [0; 4096];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
hasher.update(&buffer[..count]);
}
Ok(format!("{:x}", hasher.finalize()))
}
fn main() -> Result<()> {
// Drop all environment variables that were not explicitly allowed right now
drop_environment();
// The target command must be an absolute path
// XXX: this can be done statically, but not in ed 2021 without going unstable
ensure!(
TARGET_COMMAND[0].starts_with('/'),
"The target command must use an absolute path"
);
// Remember the calling uid and gid for checking access later
let caller_uid = Uid::current();
let mut caller_gids: Vec<_> = group_access_list()?.iter().map(|x| x.gid()).collect();
caller_gids.push(Gid::current().as_raw());
// If the sha512 was baked-in, ensure that the called program has the correct hash.
// Do that before dropping privileges, otherwise we might not be able to read the file.
if let Some(expected_digest) = TARGET_COMMAND_SHA512 {
let hex_digest = sha512_digest(TARGET_COMMAND[0])?;
ensure!(
hex_digest == expected_digest,
"Target executable failed sha512 digest verification. Bailing."
);
}
// Drop privileges as soon as possible
drop_privileges()?;
// Authorization the calling user
authorize(caller_uid, &caller_gids)?;
// Execute command
let args: Vec<_> = std::env::args_os().skip(1).collect();
let mut cmd = Command::new(TARGET_COMMAND[0]);
cmd.args(&TARGET_COMMAND[1..]);
if PASS_ARGUMENTS {
cmd.args(&args);
}
Err(cmd.exec().into())
}