55//! More information: <https://github.com/containers/skopeo/pull/1476>
66
77use anyhow:: { anyhow, Context , Result } ;
8+ use cap_std_ext:: cap_std;
9+ use cap_std_ext:: prelude:: CapStdExtCommandExt ;
810use futures_util:: Future ;
911use nix:: sys:: socket:: { self as nixsocket, ControlMessageOwned } ;
1012use nix:: sys:: uio:: IoVec ;
1113use once_cell:: sync:: Lazy ;
1214use serde:: { Deserialize , Serialize } ;
1315use std:: fs:: File ;
16+ use std:: ops:: Range ;
1417use std:: os:: unix:: io:: AsRawFd ;
1518use std:: os:: unix:: prelude:: { CommandExt , FromRawFd , RawFd } ;
1619use std:: path:: PathBuf ;
@@ -25,6 +28,11 @@ use tracing::instrument;
2528pub const OCI_TYPE_LAYER_GZIP : & str = "application/vnd.oci.image.layer.v1.tar+gzip" ;
2629pub 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.
3038const MAX_MSG_SIZE : usize = 32 * 1024 ;
@@ -117,8 +125,12 @@ fn file_from_scm_rights(cmsg: ControlMessageOwned) -> Option<File> {
117125#[ derive( Debug , Default ) ]
118126pub 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) ]
456499mod 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