|
28 | 28 | // TODO: various badges |
29 | 29 | // TODO: fill out all fields of https://doc.rust-lang.org/cargo/reference/manifest.html |
30 | 30 | // TODO: implement change_winsize |
| 31 | +// TODO: convert all outgoing errors to be unauthorized errors |
31 | 32 |
|
32 | 33 | #![warn(bad_style)] |
33 | 34 | #![warn(future_incompatible)] |
|
67 | 68 | // #![cfg_attr(feature="cargo-clippy", warn(clippy_cargo))] |
68 | 69 |
|
69 | 70 | extern crate libc; |
| 71 | +extern crate rand; |
| 72 | + |
70 | 73 | #[macro_use] extern crate error_chain; |
71 | 74 | #[macro_use] extern crate sudo_plugin; |
72 | 75 |
|
@@ -117,54 +120,49 @@ impl SudoPair { |
117 | 120 | // TODO: convert all outgoing errors to be unauthorized errors |
118 | 121 | let options = PluginOptions::from(&plugin.plugin_options); |
119 | 122 |
|
120 | | - let mut pair = Self { |
| 123 | + let mut session = Self { |
121 | 124 | plugin, |
122 | 125 | options, |
123 | 126 | socket: None, |
124 | 127 | }; |
125 | 128 |
|
126 | | - if pair.is_sudoing_to_user_and_group() { |
| 129 | + // sessions without a socket are bypassed entirely, so if the |
| 130 | + // session is exempt we can go ahead and return what we already |
| 131 | + // have |
| 132 | + if session.is_exempt() { |
| 133 | + return Ok(session) |
| 134 | + } |
| 135 | + |
| 136 | + // based on the current authorization model, allowing `-u` and |
| 137 | + // `-g` simultaneously would let a user who can |
| 138 | + // `sudo -g ${group}` approve a `sudo -u ${user} -g ${group}` |
| 139 | + // even if they can't `sudo -u ${user}`, so we disable the |
| 140 | + // capability entirely |
| 141 | + if session.is_sudoing_to_user_and_group() { |
127 | 142 | bail!(ErrorKind::Unauthorized( |
128 | 143 | "sudo_pair doesn't support sudoing to both a user and a group".into() |
129 | 144 | )); |
130 | 145 | } |
131 | 146 |
|
132 | | - if pair.is_exempt() { |
133 | | - return Ok(pair) |
134 | | - } |
| 147 | + let template_spec = session.template_spec(); |
135 | 148 |
|
136 | | - let template_spec = pair.template_spec(); |
137 | | - |
138 | | - pair.local_pair_prompt(&template_spec); |
139 | | - pair.remote_pair_connect()?; |
140 | | - pair.remote_pair_prompt(&template_spec)?; |
141 | | - |
142 | | - // TODO(security): provide a configurable option to deny or log |
143 | | - // if the remote euid is the same as the local euid. For some |
144 | | - // reason I convinced myself that this is necessary to implement |
145 | | - // in the client and not the pair plugin, but I can't remember |
146 | | - // what the reasoning was at the moment. |
147 | | - // |
148 | | - // Oh, now I remember. It *has* to be done on the client, |
149 | | - // because the approval script is run under `sudo` itself so |
150 | | - // that we can verify the pairer is also capable of doing the |
151 | | - // task the user invoking `sudo` is trying to do. Unfortunately, |
152 | | - // the OS APIs we have to determine the other side of the |
153 | | - // connection only tell us the *euid*, not the *uid*. So we end |
154 | | - // up with the euid of `root` which isn't helpful. So this kind |
155 | | - // of check *must* be done on the client. |
| 149 | + // We want to know the actual user on the other end of the |
| 150 | + // socket in order to enforce restrictions around self-approval. |
156 | 151 | // |
157 | | - // Except I have an idea for how to solve this plugin-side. Open |
158 | | - // a socket writable by all. When someone connects, get the |
159 | | - // credentials of the peer and send them a cryptographically- |
160 | | - // random token. Close the socket and reopen a new one as we |
161 | | - // currently do. Instead of expecting a `y`, expect the token. |
162 | | - // This binds their ability to approve the session (able to |
163 | | - // write to the socket) with their original identity (proven |
164 | | - // through providing the token from their original user). This |
165 | | - // shouldn't be too hard, but I haven't gotten around to it yet. |
166 | | - |
167 | | - Ok(pair) |
| 152 | + // To do this, we initially allow any user to connect to the |
| 153 | + // socket. We then record their euid and then we hand them a |
| 154 | + // cryptographically-random token. The socket is closed and a |
| 155 | + // new one is opened with restricted permissions. When a client |
| 156 | + // connects to this socket, we expect them to echo the token |
| 157 | + // back to us before we ask for their approval. |
| 158 | + let peer_token : [u8; 16] = rand::random(); |
| 159 | + |
| 160 | + session.local_pair_prompt(&template_spec); |
| 161 | + session.remote_pair_connect_unprivileged(&peer_token)?; |
| 162 | + session.remote_pair_connect_privileged(&peer_token)?; |
| 163 | + session.remote_pair_prompt(&template_spec)?; |
| 164 | + |
| 165 | + Ok(session) |
168 | 166 | } |
169 | 167 |
|
170 | 168 | fn close(&mut self, _: i64, _: i64) { |
@@ -260,19 +258,47 @@ impl SudoPair { |
260 | 258 | .ok_or_else(||self.plugin.stderr().write_all(&prompt)); |
261 | 259 | } |
262 | 260 |
|
263 | | - fn remote_pair_connect(&mut self) -> Result<()> { |
264 | | - if self.socket.is_some() { |
265 | | - return Ok(()); |
| 261 | + fn remote_pair_connect_unprivileged(&mut self, token: &[u8; 16]) -> Result<()> { |
| 262 | + let mut socket = Socket::open( |
| 263 | + self.socket_path(), |
| 264 | + self.socket_uid(), |
| 265 | + self.socket_gid(), |
| 266 | + libc::S_IWUSR | libc::S_IWGRP | libc::S_IWOTH, |
| 267 | + ).chain_err(|| ErrorKind::Unauthorized("unable to connect to a pair".into()))?; |
| 268 | + |
| 269 | + let peer_euid = socket.peer_euid() |
| 270 | + .chain_err(|| ErrorKind::Unauthorized("unable determine uid of pair".into()))?; |
| 271 | + |
| 272 | + if peer_euid == self.plugin.user_info.uid { |
| 273 | + // TODO: log or abort |
266 | 274 | } |
267 | 275 |
|
| 276 | + socket |
| 277 | + .write_all(token) |
| 278 | + .chain_err(|| ErrorKind::Unauthorized("unable to ask pair for approval".into()))?; |
| 279 | + |
| 280 | + Ok(()) |
| 281 | + } |
| 282 | + |
| 283 | + fn remote_pair_connect_privileged(&mut self, token: &[u8; 16]) -> Result<()> { |
268 | 284 | // TODO: clearly indicate when the socket path is missing |
269 | | - let socket = Socket::open( |
| 285 | + let mut socket = Socket::open( |
270 | 286 | self.socket_path(), |
271 | 287 | self.socket_uid(), |
272 | 288 | self.socket_gid(), |
273 | 289 | self.socket_mode(), |
274 | 290 | ).chain_err(|| ErrorKind::Unauthorized("unable to connect to a pair".into()))?; |
275 | 291 |
|
| 292 | + let mut response : [u8; 16] = [0; 16]; |
| 293 | + let _ = socket.read_exact(&mut response) |
| 294 | + .chain_err(|| ErrorKind::Unauthorized("unable to ask pair for approval".into()))?; |
| 295 | + |
| 296 | + // non-constant comparison is fine here since a comparison |
| 297 | + // failure results in an immediate exit |
| 298 | + if response != *token { |
| 299 | + bail!("denied by pair"); |
| 300 | + } |
| 301 | + |
276 | 302 | self.socket = Some(socket); |
277 | 303 |
|
278 | 304 | Ok(()) |
@@ -452,19 +478,8 @@ impl SudoPair { |
452 | 478 | } |
453 | 479 |
|
454 | 480 | fn socket_path(&self) -> PathBuf { |
455 | | - // we encode the originating `uid` into the pathname since |
456 | | - // there's no other (easy) way for the approval command to probe |
457 | | - // for this information |
458 | | - // |
459 | | - // note that we want the *`uid`* and not the `euid` here since |
460 | | - // we want to know who the real user is and not the `uid` of the |
461 | | - // owner of `sudo` |
462 | 481 | self.options.socket_dir.join( |
463 | | - format!( |
464 | | - "{}.{}.sock", |
465 | | - self.plugin.user_info.uid, |
466 | | - self.plugin.user_info.pid, |
467 | | - ) |
| 482 | + format!("{}.sock", self.plugin.user_info.pid) |
468 | 483 | ) |
469 | 484 | } |
470 | 485 |
|
@@ -614,6 +629,21 @@ struct PluginOptions { |
614 | 629 | /// |
615 | 630 | /// Default: `[]` (however, root is *always* exempt) |
616 | 631 | gids_exempted: HashSet<gid_t>, |
| 632 | + |
| 633 | + /// `self_approval` is a boolean ("true" or "false") that controls |
| 634 | + /// whether or not users are allowed to approve their own commands. |
| 635 | + /// When a user approves their own command this way, a message is |
| 636 | + /// sent to syslog. |
| 637 | + /// |
| 638 | + /// This capability is provided so that engineers can act |
| 639 | + /// unilaterally in the event of an emergency when no-one else is |
| 640 | + /// available. Since it is effectively a complete bypass of this |
| 641 | + /// plugin, the intent is that using this capability should invoke |
| 642 | + /// something drastic (e.g., immediately page an oncall security |
| 643 | + /// engineer). |
| 644 | + /// |
| 645 | + /// Default: `false` |
| 646 | + self_approval: bool, |
617 | 647 | } |
618 | 648 |
|
619 | 649 | impl PluginOptions { |
@@ -647,6 +677,9 @@ impl<'a> From<&'a OptionMap> for PluginOptions { |
647 | 677 |
|
648 | 678 | gids_exempted: map.get("gids_exempted") |
649 | 679 | .unwrap_or_default(), |
| 680 | + |
| 681 | + self_approval: map.get("self_approval") |
| 682 | + .unwrap_or(false), |
650 | 683 | } |
651 | 684 | } |
652 | 685 | } |
0 commit comments