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 ;
@@ -119,8 +127,12 @@ fn file_from_scm_rights(cmsg: ControlMessageOwned) -> Option<File> {
119127#[ derive( Debug , Default ) ]
120128pub struct ImageProxyConfig {
121129 /// Path to container auth file; equivalent to `skopeo --authfile`.
130+ /// This conflicts with [`auth_data`].
122131 pub authfile : Option < PathBuf > ,
123132
133+ /// Data stream for container auth. This conflicts with [`authfile`].
134+ pub auth_data : Option < File > ,
135+
124136 /// Do not use default container authentication paths; equivalent to `skopeo --no-creds`.
125137 ///
126138 /// Defaults to `false`; in other words, use the default file paths from `man containers-auth.json`.
@@ -160,6 +172,13 @@ impl TryFrom<ImageProxyConfig> for Command {
160172 type Error = anyhow:: Error ;
161173
162174 fn try_from ( config : ImageProxyConfig ) -> Result < Self , Self :: Error > {
175+ let mut allocated_fds = RESERVED_FD_RANGE . clone ( ) ;
176+ let mut alloc_fd = || {
177+ allocated_fds
178+ . next ( )
179+ . ok_or_else ( || anyhow:: anyhow!( "Ran out of reserved file descriptors for child" ) )
180+ } ;
181+
163182 // By default, we set up pdeathsig to "lifecycle bind" the child process to us.
164183 let mut c = config. skopeo_cmd . unwrap_or_else ( || {
165184 let mut c = std:: process:: Command :: new ( "skopeo" ) ;
@@ -172,16 +191,40 @@ impl TryFrom<ImageProxyConfig> for Command {
172191 c
173192 } ) ;
174193 c. arg ( "experimental-image-proxy" ) ;
194+ let auth_option_count = [
195+ config. authfile . is_some ( ) ,
196+ config. auth_data . is_some ( ) ,
197+ config. auth_anonymous ,
198+ ]
199+ . into_iter ( )
200+ . filter ( |& x| x)
201+ . count ( ) ;
202+ if auth_option_count > 1 {
203+ // This is a programmer error really
204+ anyhow:: bail!( "Conflicting authentication options" ) ;
205+ }
175206 if let Some ( authfile) = config. authfile {
176- if config. auth_anonymous {
177- // This is a programmer error really
178- anyhow:: bail!( "Cannot use anonymous auth and an authfile" ) ;
179- }
180207 c. arg ( "--authfile" ) ;
181208 c. arg ( authfile) ;
209+ } else if let Some ( mut auth_data) = config. auth_data . map ( std:: io:: BufReader :: new) {
210+ // If we get the authentication data as a file, we always copy it to a new temporary file under
211+ // the assumption that the caller provided it this way to aid in privilege separation where
212+ // the file is only readable to privileged code.
213+ let target_fd = alloc_fd ( ) ?;
214+ let tmpd = & cap_std:: fs:: Dir :: open_ambient_dir ( "/tmp" , cap_std:: ambient_authority ( ) ) ?;
215+ let mut tempfile = cap_tempfile:: TempFile :: new_anonymous ( tmpd)
216+ . context ( "Creating temporary file for auth data" )
217+ . map ( std:: io:: BufWriter :: new) ?;
218+ std:: io:: copy ( & mut auth_data, & mut tempfile) ?;
219+ let tempfile = tempfile. into_inner ( ) ?. into_std ( ) ;
220+ let fd = std:: sync:: Arc :: new ( tempfile. into ( ) ) ;
221+ c. take_fd_n ( fd, target_fd) ;
222+ c. arg ( "--authfile" ) ;
223+ c. arg ( format ! ( "/proc/self/fd/{target_fd}" ) ) ;
182224 } else if config. auth_anonymous {
183225 c. arg ( "--no-creds" ) ;
184226 }
227+
185228 if let Some ( certificate_directory) = config. certificate_directory {
186229 c. arg ( "--cert-dir" ) ;
187230 c. arg ( certificate_directory) ;
@@ -482,6 +525,8 @@ impl ImageProxy {
482525
483526#[ cfg( test) ]
484527mod tests {
528+ use std:: io:: { Seek , Write } ;
529+
485530 use super :: * ;
486531
487532 fn validate ( c : Command , contains : & [ & str ] , not_contains : & [ & str ] ) {
@@ -499,6 +544,8 @@ mod tests {
499544
500545 #[ test]
501546 fn proxy_configs ( ) {
547+ let tmpd = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) . unwrap ( ) ;
548+
502549 let c = Command :: try_from ( ImageProxyConfig :: default ( ) ) . unwrap ( ) ;
503550 validate (
504551 c,
@@ -526,5 +573,15 @@ mod tests {
526573 } )
527574 . unwrap ( ) ;
528575 validate ( c, & [ r"--tls-verify=false" ] , & [ ] ) ;
576+
577+ let mut tmpf = cap_tempfile:: TempFile :: new_anonymous ( tmpd) . unwrap ( ) ;
578+ tmpf. write_all ( r#"{ "auths": {} "# . as_bytes ( ) ) . unwrap ( ) ;
579+ tmpf. seek ( std:: io:: SeekFrom :: Start ( 0 ) ) . unwrap ( ) ;
580+ let c = Command :: try_from ( ImageProxyConfig {
581+ auth_data : Some ( tmpf. into_std ( ) ) ,
582+ ..Default :: default ( )
583+ } )
584+ . unwrap ( ) ;
585+ validate ( c, & [ "--authfile" , "/proc/self/fd/100" ] , & [ ] ) ;
529586 }
530587}
0 commit comments