Skip to content

Commit 1dc4dcc

Browse files
cgwaltersJohan-Liebert1
authored andcommitted
ostree-ext: Adapt to new containers policy config load order
Closes: #2258 The real fix here will be to migrate to `--require-signed` from skopeo in the future. Generated-by: AI (with only a few tweaks) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 946874c commit 1dc4dcc

8 files changed

Lines changed: 189 additions & 13 deletions

File tree

crates/lib/src/cli.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,9 @@ pub(crate) struct SwitchOpts {
165165
/// This is the inverse of the previous `--target-no-signature-verification` (which is now
166166
/// a no-op).
167167
///
168-
/// Enabling this option enforces that `/etc/containers/policy.json` includes a
169-
/// default policy which requires signatures.
168+
/// Enabling this option enforces that `containers-policy.json` (see `man
169+
/// containers-policy.json` for the full search path) includes a default
170+
/// policy which requires signatures.
170171
#[clap(long)]
171172
pub(crate) enforce_container_sigpolicy: bool,
172173

crates/lib/src/install.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,9 @@ pub(crate) struct InstallTargetOpts {
265265
pub(crate) target_no_signature_verification: bool,
266266

267267
/// This is the inverse of the previous `--target-no-signature-verification` (which is now
268-
/// a no-op). Enabling this option enforces that `/etc/containers/policy.json` includes a
269-
/// default policy which requires signatures.
268+
/// a no-op). Enabling this option enforces that `containers-policy.json` (see `man
269+
/// containers-policy.json` for the full search path) includes a default policy which
270+
/// requires signatures.
270271
#[clap(long)]
271272
#[serde(default)]
272273
pub(crate) enforce_container_sigpolicy: bool,

crates/ostree-ext/src/container/skopeo.rs

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use super::ImageReference;
44
use anyhow::{Context, Result};
5+
use cap_std_ext::cap_std;
56
use cap_std_ext::cmdext::{CapStdExtCommandExt, CmdFds};
67
use containers_image_proxy::oci_spec::image as oci_image;
78
use fn_error_context::context;
@@ -17,9 +18,84 @@ use tokio::process::Command;
1718
// https://github.com/containers/image/blob/main/signature/policy_types.go
1819
// Ideally we add something like `skopeo pull --disallow-insecure-accept-anything`
1920
// but for now we parse the policy.
20-
const POLICY_PATH: &str = "/etc/containers/policy.json";
2121
const INSECURE_ACCEPT_ANYTHING: &str = "insecureAcceptAnything";
2222

23+
/// The env var that overrides the policy path, matching the upstream Go
24+
/// containers/image library behavior.
25+
const POLICY_ENV_VAR: &str = "CONTAINERS_POLICY_JSON";
26+
27+
/// Well-known system paths for `containers-policy.json`, checked in order.
28+
const SYSTEM_POLICY_PATHS: &[&str] = &[
29+
"etc/containers/policy.json",
30+
"usr/share/containers/policy.json",
31+
];
32+
33+
/// Suffix appended under `$XDG_CONFIG_HOME` (or `$HOME/.config`).
34+
const USER_POLICY_SUFFIX: &str = "containers/policy.json";
35+
36+
/// Resolve the containers policy path using the same load order as the
37+
/// upstream Go containers/image library, with all lookups relative to `root`:
38+
///
39+
/// 1. `CONTAINERS_POLICY_JSON` env var (trusted, no existence check)
40+
/// 2. `$XDG_CONFIG_HOME/containers/policy.json` (or `$HOME/.config/…`)
41+
/// 3. `/etc/containers/policy.json`
42+
/// 4. `/usr/share/containers/policy.json`
43+
///
44+
/// For candidates 2–4 we only return a path when the file exists on disk.
45+
///
46+
/// Absolute paths (from env vars) have their leading `/` stripped so they
47+
/// resolve under `root`. Passing `root` opened on `/` gives normal behaviour;
48+
/// tests can pass a cap-std `Dir` backed by a temporary directory.
49+
fn resolve_policy_path(
50+
root: &cap_std::fs::Dir,
51+
env_override: Option<&Path>,
52+
xdg_config_home: Option<&Path>,
53+
home: Option<&Path>,
54+
) -> Result<cap_std::fs::File> {
55+
// Helper: strip a leading `/` so the path is relative to root.
56+
fn strip_abs(p: &Path) -> &Path {
57+
p.strip_prefix("/").unwrap_or(p)
58+
}
59+
60+
// 1. Env var override – trust unconditionally (no existence check).
61+
if let Some(raw) = env_override.filter(|v| !v.as_os_str().is_empty()) {
62+
let relative = strip_abs(raw);
63+
tracing::debug!("Using policy path from {POLICY_ENV_VAR}: {}", raw.display());
64+
return root.open(relative).with_context(|| {
65+
format!(
66+
"Opening policy file from {POLICY_ENV_VAR}={}",
67+
raw.display()
68+
)
69+
});
70+
}
71+
72+
// 2. Per-user config dir.
73+
let user_candidate = if let Some(xdg) = xdg_config_home {
74+
Some(strip_abs(xdg).join(USER_POLICY_SUFFIX))
75+
} else {
76+
home.map(|h| strip_abs(h).join(".config").join(USER_POLICY_SUFFIX))
77+
};
78+
if let Some(p) = &user_candidate {
79+
if let Ok(f) = root.open(p) {
80+
tracing::debug!("Using user policy path: {}", p.display());
81+
return Ok(f);
82+
}
83+
}
84+
85+
// 3–4. System paths.
86+
for candidate in SYSTEM_POLICY_PATHS {
87+
if let Ok(f) = root.open(candidate) {
88+
tracing::debug!("Using system policy path: {candidate}");
89+
return Ok(f);
90+
}
91+
}
92+
93+
anyhow::bail!(
94+
"No containers policy.json found; \
95+
checked ${POLICY_ENV_VAR}, user config dir, and system paths"
96+
)
97+
}
98+
2399
#[derive(Deserialize)]
24100
struct PolicyEntry {
25101
#[serde(rename = "type")]
@@ -43,8 +119,17 @@ impl ContainerPolicy {
43119
}
44120
}
45121

46-
pub(crate) fn container_policy_is_default_insecure() -> Result<bool> {
47-
let r = std::io::BufReader::new(std::fs::File::open(POLICY_PATH)?);
122+
pub(crate) fn container_policy_is_default_insecure(root: &cap_std::fs::Dir) -> Result<bool> {
123+
let f = resolve_policy_path(
124+
root,
125+
std::env::var_os(POLICY_ENV_VAR).as_deref().map(Path::new),
126+
std::env::var_os("XDG_CONFIG_HOME")
127+
.as_deref()
128+
.map(Path::new),
129+
std::env::var_os("HOME").as_deref().map(Path::new),
130+
)
131+
.context("Resolving containers policy path")?;
132+
let r = std::io::BufReader::new(f);
48133
let policy: ContainerPolicy = serde_json::from_reader(r)?;
49134
Ok(policy.is_default_insecure())
50135
}
@@ -106,6 +191,7 @@ pub async fn copy(
106191
#[cfg(test)]
107192
mod tests {
108193
use super::*;
194+
use cap_std_ext::cap_tempfile;
109195

110196
// Default value as of the Fedora 34 containers-common-1-21.fc34.noarch package.
111197
const DEFAULT_POLICY: &str = indoc::indoc! {r#"
@@ -155,4 +241,84 @@ mod tests {
155241
assert!(!p.is_default_insecure());
156242
}
157243
}
244+
245+
/// Create `<dir>/<path>` with empty JSON content, creating parent dirs.
246+
/// Returns the (dev, ino) of the created file for identity checks.
247+
fn touch(dir: &cap_std::fs::Dir, path: &str) -> (u64, u64) {
248+
use cap_std::fs::MetadataExt;
249+
if let Some(parent) = Path::new(path).parent() {
250+
dir.create_dir_all(parent).unwrap();
251+
}
252+
dir.write(path, b"{}").unwrap();
253+
let m = dir.metadata(path).unwrap();
254+
(m.dev(), m.ino())
255+
}
256+
257+
/// Return (dev, ino) for an open cap-std file.
258+
fn file_id(f: &cap_std::fs::File) -> (u64, u64) {
259+
use cap_std::fs::MetadataExt;
260+
let m = f.metadata().unwrap();
261+
(m.dev(), m.ino())
262+
}
263+
264+
#[test]
265+
fn resolve_policy_path_cases() -> Result<()> {
266+
let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
267+
268+
let etc_id = touch(&td, "etc/containers/policy.json");
269+
let _usr_id = touch(&td, "usr/share/containers/policy.json");
270+
271+
// Env var override wins (trusted — errors if file missing)
272+
let custom = Path::new("/custom/policy.json");
273+
assert!(resolve_policy_path(&td, Some(custom), None, None).is_err());
274+
let custom_id = touch(&td, "custom/policy.json");
275+
let f = resolve_policy_path(&td, Some(custom), None, None)?;
276+
assert_eq!(
277+
file_id(&f),
278+
custom_id,
279+
"env var should open the custom file"
280+
);
281+
282+
// Empty env var is ignored, falls through to /etc
283+
let f = resolve_policy_path(&td, Some(Path::new("")), None, None)?;
284+
assert_eq!(
285+
file_id(&f),
286+
etc_id,
287+
"empty env var should fall through to /etc"
288+
);
289+
290+
// XDG_CONFIG_HOME wins when file exists
291+
let xdg_id = touch(&td, "xdg/containers/policy.json");
292+
let f = resolve_policy_path(&td, None, Some(Path::new("/xdg")), None)?;
293+
assert_eq!(file_id(&f), xdg_id, "XDG_CONFIG_HOME should win");
294+
295+
// XDG_CONFIG_HOME skipped when file missing, falls through to /etc
296+
let f = resolve_policy_path(&td, None, Some(Path::new("/xdg-empty")), None)?;
297+
assert_eq!(file_id(&f), etc_id, "missing XDG dir should fall through");
298+
299+
// HOME/.config fallback when XDG unset
300+
let home_id = touch(&td, "home/.config/containers/policy.json");
301+
let f = resolve_policy_path(&td, None, None, Some(Path::new("/home")))?;
302+
assert_eq!(file_id(&f), home_id, "HOME fallback should work");
303+
304+
// /etc preferred over /usr/share
305+
let f = resolve_policy_path(&td, None, None, None)?;
306+
assert_eq!(
307+
file_id(&f),
308+
etc_id,
309+
"/etc should be preferred over /usr/share"
310+
);
311+
312+
// Falls through to /usr/share when /etc missing
313+
let td2 = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
314+
let usr2_id = touch(&td2, "usr/share/containers/policy.json");
315+
let f = resolve_policy_path(&td2, None, None, None)?;
316+
assert_eq!(file_id(&f), usr2_id, "should fall through to /usr/share");
317+
318+
// Nothing found returns error
319+
let td3 = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
320+
assert!(resolve_policy_path(&td3, None, None, None).is_err());
321+
322+
Ok(())
323+
}
158324
}

crates/ostree-ext/src/container/store.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ struct LayerRef {
302302
#[derive(Debug)]
303303
pub struct ImageImporter {
304304
repo: ostree::Repo,
305+
/// The root filesystem directory, used for policy lookups.
306+
root: Dir,
305307
pub(crate) proxy: ImageProxy,
306308
imgref: OstreeImageReference,
307309
target_imgref: Option<OstreeImageReference>,
@@ -648,8 +650,11 @@ impl ImageImporter {
648650

649651
let diffid_to_digest = Self::build_diffid_to_digest_map(&repo)?;
650652

653+
let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
654+
651655
Ok(ImageImporter {
652656
repo,
657+
root,
653658
proxy,
654659
target_imgref: None,
655660
no_imgref: false,
@@ -935,7 +940,9 @@ impl ImageImporter {
935940
#[context("Fetching manifest")]
936941
pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
937942
match &self.imgref.sigverify {
938-
SignatureSource::ContainerPolicy if skopeo::container_policy_is_default_insecure()? => {
943+
SignatureSource::ContainerPolicy
944+
if skopeo::container_policy_is_default_insecure(&self.root)? =>
945+
{
939946
return Err(anyhow!(
940947
"containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
941948
));
@@ -1039,7 +1046,7 @@ impl ImageImporter {
10391046
) -> Result<()> {
10401047
tracing::debug!("Fetching base");
10411048
if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
1042-
&& skopeo::container_policy_is_default_insecure()?
1049+
&& skopeo::container_policy_is_default_insecure(&self.root)?
10431050
{
10441051
return Err(anyhow!(
10451052
"containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"

docs/src/man/bootc-install-config.5.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ The `install` section supports these subfields:
4040
needs the `bli` module (available in newer builds). Defaults to `true`
4141
when using systemd-boot, `false` otherwise.
4242
- `enforce-container-sigpolicy`: A boolean that controls whether to enforce that
43-
`/etc/containers/policy.json` includes a default policy which requires signatures.
43+
`containers-policy.json` (see `man containers-policy.json` for the full search
44+
path) includes a default policy which requires signatures.
4445
When `true`, image pulls will be rejected if the policy file specifies
4546
`insecureAcceptAnything` as the default. Defaults to `false`.
4647
This is equivalent to the `--enforce-container-sigpolicy` CLI flag.

docs/src/man/bootc-install-to-disk.8.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ set `discoverable-partitions = true` in their install configuration
111111

112112
**--enforce-container-sigpolicy**
113113

114-
This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op). Enabling this option enforces that `/etc/containers/policy.json` includes a default policy which requires signatures
114+
This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op). Enabling this option enforces that `containers-policy.json` (see `man containers-policy.json` for the full search path) includes a default policy which requires signatures
115115

116116
**--run-fetch-check**
117117

docs/src/man/bootc-install-to-existing-root.8.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ of migrating the fstab entries. See the "Injecting kernel arguments" section abo
157157

158158
**--enforce-container-sigpolicy**
159159

160-
This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op). Enabling this option enforces that `/etc/containers/policy.json` includes a default policy which requires signatures
160+
This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op). Enabling this option enforces that `containers-policy.json` (see `man containers-policy.json` for the full search path) includes a default policy which requires signatures
161161

162162
**--run-fetch-check**
163163

docs/src/man/bootc-install-to-filesystem.8.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ is currently expected to be empty by default.
6565

6666
**--enforce-container-sigpolicy**
6767

68-
This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op). Enabling this option enforces that `/etc/containers/policy.json` includes a default policy which requires signatures
68+
This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op). Enabling this option enforces that `containers-policy.json` (see `man containers-policy.json` for the full search path) includes a default policy which requires signatures
6969

7070
**--run-fetch-check**
7171

0 commit comments

Comments
 (0)