diff --git a/Cargo.lock b/Cargo.lock index 58dd6f14f..af255f456 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -637,6 +643,17 @@ dependencies = [ "trident_api", ] +[[package]] +name = "docker_credential" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "documented" version = "0.6.0" @@ -3344,6 +3361,7 @@ dependencies = [ "configparser", "const_format", "derive_more", + "docker_credential", "duct", "enumflags2", "env_logger 0.11.5", diff --git a/Cargo.toml b/Cargo.toml index 3ace94fe1..81888304c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ clap = { version = "4.4.18", features = ["derive"] } configparser = { version = "3.1.0", features = ["indexmap"] } const_format = "0.2.33" derive_more = "0.99.19" +docker_credential = { version = "1.0.1", optional = true } duct = "0.13.7" enumflags2 = { version = "0.7", features = ["serde"] } env_logger = "0.11.5" @@ -84,6 +85,7 @@ sha2 = "0.10.8" dangerous-options = [ "trident_api/dangerous-options", "setsail/dangerous-options", + "docker_credential", ] sysupdate = ["trident_api/sysupdate"] functional-test = [ diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te index 38b2e4fd3..ded852aef 100644 --- a/selinux-policy-trident/trident.te +++ b/selinux-policy-trident/trident.te @@ -40,6 +40,7 @@ require { type cloud_init_exec_t; type cloud_init_state_t; type cloud_init_t; + type container_conf_home_t; type container_unit_t; type container_var_lib_t; type crack_db_t; @@ -299,6 +300,8 @@ allow trident_t cloud_init_exec_t:file getattr; allow trident_t cloud_init_state_t:dir { list_dir_perms relabelto }; allow trident_t cloud_init_state_t:lnk_file read_lnk_file_perms; allow trident_t cloud_init_state_t:file getattr; +allow trident_t container_conf_home_t:dir search; # Allow access to ~/.docker directory +allow trident_t container_conf_home_t:file { open read }; # Allow access to ~/.docker/config.json allow trident_t container_unit_t:file getattr; allow trident_t container_var_lib_t:file getattr; allow trident_t crack_db_t:dir { getattr open search read }; diff --git a/src/osimage/cosi/reader.rs b/src/osimage/cosi/reader.rs index b43c2c564..3d2e38768 100644 --- a/src/osimage/cosi/reader.rs +++ b/src/osimage/cosi/reader.rs @@ -8,6 +8,9 @@ use std::{ time::{Duration, Instant}, }; +#[cfg(feature = "dangerous-options")] +use std::{env, io::BufReader}; + use anyhow::{bail, ensure, Context, Error}; use log::{debug, trace, warn}; use oci_client::{secrets::RegistryAuth, Client as OciClient, Reference}; @@ -15,6 +18,9 @@ use reqwest::blocking::{Client, Response}; use tokio::runtime::Runtime; use url::Url; +#[cfg(feature = "dangerous-options")] +use docker_credential::{self, DockerCredential}; + pub(super) trait ReadSeek: Read + Seek {} impl ReadSeek for HttpFile {} @@ -23,6 +29,9 @@ impl ReadSeek for File {} #[cfg(test)] impl ReadSeek for Cursor> {} +#[cfg(feature = "dangerous-options")] +const DOCKER_CONFIG_FILE_PATH: &str = ".docker/config.json"; + /// An abstraction over a COSI file reader that can be either a local file or an /// HTTP request. /// @@ -140,9 +149,10 @@ impl HttpFile { })?) .with_context(|| format!("Failed to parse URL '{url}'"))?; + let oci_client = OciClient::default(); let rt = Runtime::new().context("Failed to create Tokio runtime")?; - let token = Self::retrieve_access_token(&img_ref, &rt)?; - let digest = Self::retrieve_artifact_digest(&img_ref, &rt)?; + let token = Self::retrieve_access_token(&img_ref, &rt, &oci_client)?; + let digest = Self::retrieve_artifact_digest(&img_ref, &rt, &oci_client)?; trace!("Retrieved artifact digest: {digest}"); // Create HTTP URL @@ -239,18 +249,18 @@ impl HttpFile { /// Retrieve bearer token to access container registry. Even registries allowing anonymous /// access may require a token. - fn retrieve_access_token(img_ref: &Reference, runtime: &Runtime) -> Result { + fn retrieve_access_token( + img_ref: &Reference, + runtime: &Runtime, + client: &OciClient, + ) -> Result { trace!( "Retrieving access token for OCI registry '{}'", img_ref.registry() ); - let client = OciClient::default(); + let auth = Self::get_auth(img_ref); runtime - .block_on(client.auth( - img_ref, - &RegistryAuth::Anonymous, - oci_client::RegistryOperation::Pull, - )) + .block_on(client.auth(img_ref, &auth, oci_client::RegistryOperation::Pull)) .with_context(|| { format!( "Registry '{}' is not accessible or does not exist", @@ -260,8 +270,45 @@ impl HttpFile { .context("Failed to retrieve authorization token") } + /// Get authentication credentials for accessing registry. Unless "dangerous-options" flag is + /// enabled, will default to anonymous access. + fn get_auth(_img_ref: &Reference) -> RegistryAuth { + #[cfg(feature = "dangerous-options")] + if let Ok(docker_config) = File::open( + env::home_dir() + .unwrap_or_default() + .join(DOCKER_CONFIG_FILE_PATH), + ) { + let registry = _img_ref + .resolve_registry() + .strip_suffix('/') + .unwrap_or_else(|| _img_ref.resolve_registry()); + match docker_credential::get_credential_from_reader( + BufReader::new(docker_config), + registry, + ) { + Ok(DockerCredential::UsernamePassword(username, password)) => { + debug!("Found username and password docker credential"); + return RegistryAuth::Basic(username, password); + } + Ok(DockerCredential::IdentityToken(_)) => { + debug!("Found identity token docker credential") + } + Err(_) => debug!("Failed to find docker credentials"), + } + }; + + debug!("Proceeding with anonymous access"); + RegistryAuth::Anonymous + } + /// Retrieve artifact digest, which is necessary to send HTTP request to container registry. - fn retrieve_artifact_digest(img_ref: &Reference, runtime: &Runtime) -> Result { + fn retrieve_artifact_digest( + img_ref: &Reference, + runtime: &Runtime, + client: &OciClient, + ) -> Result { + trace!("Retrieving artifact digest"); Ok(match img_ref.digest() { Some(digest) => digest.to_string(), None => { @@ -269,7 +316,6 @@ impl HttpFile { format!("Failed to retrieve tag from OCI URL '{}'", img_ref.whole()) })?; // Attempt to retrieve digest from manifest - let client = OciClient::default(); let manifest = client.pull_image_manifest(img_ref, &RegistryAuth::Anonymous); let (oci_image_manifest, _) = runtime.block_on(manifest).with_context(|| format!( @@ -504,17 +550,19 @@ mod tests { #[test] fn test_retrieve_access_token() { + let client = OciClient::default(); let rt = Runtime::new().unwrap(); let url = "oci://docker.io/library/hello-world:latest".to_string(); let img_ref = url .strip_prefix("oci://") .and_then(|url| url.parse::().ok()) .unwrap(); - HttpFile::retrieve_access_token(&img_ref, &rt).unwrap(); + HttpFile::retrieve_access_token(&img_ref, &rt, &client).unwrap(); } #[test] fn test_retrieve_artifact_digest() { + let client = OciClient::default(); let rt = Runtime::new().unwrap(); // TODO(12732): Fix this test to use test COSI file instead of hello-world image let url = "oci://docker.io/library/hello-world@sha256:940c619fbd418f9b2b1b63e25d8861f9cc1b46e3fc8b018ccfe8b78f19b8cc4f".to_string(); @@ -523,7 +571,7 @@ mod tests { .and_then(|url| url.parse::().ok()) .unwrap(); assert_eq!( - HttpFile::retrieve_artifact_digest(&img_ref, &rt).unwrap(), + HttpFile::retrieve_artifact_digest(&img_ref, &rt, &client).unwrap(), "sha256:940c619fbd418f9b2b1b63e25d8861f9cc1b46e3fc8b018ccfe8b78f19b8cc4f" ); }