Skip to content

Commit 9717452

Browse files
committed
Add support for passing auth creds via fd
Motivated by ostreedev/ostree-rs-ext#413 which is in turn an adaption of code that lives in rpm-ostree today from coreos/rpm-ostree@d661e8f which aims to support privilege separation. This doesn't lower privilege separation into this project (yet), just aids in passing data across boundaries. Signed-off-by: Colin Walters <[email protected]>
1 parent 8b7b64b commit 9717452

File tree

2 files changed

+63
-4
lines changed

2 files changed

+63
-4
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ serde_json = "1.0.64"
2121
semver = "1.0.4"
2222
tokio = { features = ["fs", "io-util", "macros", "process", "rt", "sync"], version = "1" }
2323
tracing = "0.1"
24+
cap-tempfile = "1.0.1"
25+
cap-std-ext = "1.0"
2426

2527
[lib]
2628
path = "src/imageproxy.rs"

src/imageproxy.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
//! More information: <https://github.com/containers/skopeo/pull/1476>
66
77
use anyhow::{anyhow, Context, Result};
8+
use cap_std_ext::cap_std;
9+
use cap_std_ext::prelude::CapStdExtCommandExt;
810
use futures_util::Future;
911
use nix::sys::socket::{self as nixsocket, ControlMessageOwned};
1012
use nix::sys::uio::IoVec;
1113
use once_cell::sync::Lazy;
1214
use serde::{Deserialize, Serialize};
1315
use std::fs::File;
16+
use std::ops::Range;
1417
use std::os::unix::io::AsRawFd;
1518
use std::os::unix::prelude::{CommandExt, FromRawFd, RawFd};
1619
use std::path::PathBuf;
@@ -25,6 +28,11 @@ use tracing::instrument;
2528
pub const OCI_TYPE_LAYER_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
2629
pub const OCI_TYPE_LAYER_TAR: &str = "application/vnd.oci.image.layer.v1.tar";
2730

31+
/// File descriptor range which is reserved for passing data down into the proxy;
32+
/// avoid configuring the command to use files in this range. (Also, stdin is
33+
/// reserved)
34+
pub const RESERVED_FD_RANGE: Range<i32> = 100..200;
35+
2836
// This is defined in skopeo; maximum size of JSON we will read/write.
2937
// Note that payload data (non-metadata) should go over a pipe file descriptor.
3038
const MAX_MSG_SIZE: usize = 32 * 1024;
@@ -117,8 +125,12 @@ fn file_from_scm_rights(cmsg: ControlMessageOwned) -> Option<File> {
117125
#[derive(Debug, Default)]
118126
pub struct ImageProxyConfig {
119127
/// Path to container auth file; equivalent to `skopeo --authfile`.
128+
/// This conflicts with [`auth_data`].
120129
pub authfile: Option<PathBuf>,
121130

131+
/// Data stream for container auth. This conflicts with [`authfile`].
132+
pub auth_data: Option<File>,
133+
122134
/// Do not use default container authentication paths; equivalent to `skopeo --no-creds`.
123135
///
124136
/// Defaults to `false`; in other words, use the default file paths from `man containers-auth.json`.
@@ -158,6 +170,13 @@ impl TryFrom<ImageProxyConfig> for Command {
158170
type Error = anyhow::Error;
159171

160172
fn try_from(config: ImageProxyConfig) -> Result<Self, Self::Error> {
173+
let mut allocated_fds = RESERVED_FD_RANGE.clone();
174+
let mut alloc_fd = || {
175+
allocated_fds
176+
.next()
177+
.ok_or_else(|| anyhow::anyhow!("Ran out of reserved file descriptors for child"))
178+
};
179+
161180
// By default, we set up pdeathsig to "lifecycle bind" the child process to us.
162181
let mut c = config.skopeo_cmd.unwrap_or_else(|| {
163182
let mut c = std::process::Command::new("skopeo");
@@ -170,16 +189,40 @@ impl TryFrom<ImageProxyConfig> for Command {
170189
c
171190
});
172191
c.arg("experimental-image-proxy");
192+
let auth_option_count = [
193+
config.authfile.is_some(),
194+
config.auth_data.is_some(),
195+
config.auth_anonymous,
196+
]
197+
.into_iter()
198+
.filter(|&x| x)
199+
.count();
200+
if auth_option_count > 1 {
201+
// This is a programmer error really
202+
anyhow::bail!("Conflicting authentication options");
203+
}
173204
if let Some(authfile) = config.authfile {
174-
if config.auth_anonymous {
175-
// This is a programmer error really
176-
anyhow::bail!("Cannot use anonymous auth and an authfile");
177-
}
178205
c.arg("--authfile");
179206
c.arg(authfile);
207+
} else if let Some(mut auth_data) = config.auth_data.map(std::io::BufReader::new) {
208+
// If we get the authentication data as a file, we always copy it to a new temporary file under
209+
// the assumption that the caller provided it this way to aid in privilege separation where
210+
// the file is only readable to privileged code.
211+
let target_fd = alloc_fd()?;
212+
let tmpd = &cap_std::fs::Dir::open_ambient_dir("/tmp", cap_std::ambient_authority())?;
213+
let mut tempfile = cap_tempfile::TempFile::new_anonymous(tmpd)
214+
.context("Creating temporary file for auth data")
215+
.map(std::io::BufWriter::new)?;
216+
std::io::copy(&mut auth_data, &mut tempfile)?;
217+
let tempfile = tempfile.into_inner()?.into_std();
218+
let fd = std::sync::Arc::new(tempfile.into());
219+
c.take_fd_n(fd, target_fd);
220+
c.arg("--authfile");
221+
c.arg(format!("/proc/self/fd/{target_fd}"));
180222
} else if config.auth_anonymous {
181223
c.arg("--no-creds");
182224
}
225+
183226
if let Some(certificate_directory) = config.certificate_directory {
184227
c.arg("--cert-dir");
185228
c.arg(certificate_directory);
@@ -454,6 +497,8 @@ impl ImageProxy {
454497

455498
#[cfg(test)]
456499
mod tests {
500+
use std::io::{Seek, Write};
501+
457502
use super::*;
458503

459504
fn validate(c: Command, contains: &[&str], not_contains: &[&str]) {
@@ -471,6 +516,8 @@ mod tests {
471516

472517
#[test]
473518
fn proxy_configs() {
519+
let tmpd = &cap_tempfile::tempdir(cap_std::ambient_authority()).unwrap();
520+
474521
let c = Command::try_from(ImageProxyConfig::default()).unwrap();
475522
validate(
476523
c,
@@ -498,5 +545,15 @@ mod tests {
498545
})
499546
.unwrap();
500547
validate(c, &[r"--tls-verify=false"], &[]);
548+
549+
let mut tmpf = cap_tempfile::TempFile::new_anonymous(tmpd).unwrap();
550+
tmpf.write_all(r#"{ "auths": {} "#.as_bytes()).unwrap();
551+
tmpf.seek(std::io::SeekFrom::Start(0)).unwrap();
552+
let c = Command::try_from(ImageProxyConfig {
553+
auth_data: Some(tmpf.into_std()),
554+
..Default::default()
555+
})
556+
.unwrap();
557+
validate(c, &["--authfile", "/proc/self/fd/100"], &[]);
501558
}
502559
}

0 commit comments

Comments
 (0)