Upstream Said It Was Not a Vulnerability; Red Hat Gave It an 8.1

At 06:17:24Z and 07:16:31Z on 19 October 2026, NVD published CVE-2026-16487 and CVE-2026-16460, both assigned by Red Hat, both CWE-345, both reported by the same researcher on the same day in July, and both describing the same thing from opposite ends: an image-mode Linux host that boots OS content nobody verified. The first is rated Important, CVSS 8.1, affects RHEL 8, 9 and 10, and has no errata. The second is 7.4, and Red Hat’s own mitigation field reads, in full, that mitigation “is either not available or the currently available options do not meet the Red Hat Product Security criteria.”

What makes the pair worth reading is not the scores. It is that upstream bootc published a written policy three months ago saying the first one is not a vulnerability — and the vendor shipping bootc, the same company employing the maintainer who wrote that policy, has now issued it a CVE number. We read both CVE records, both Red Hat advisories, the upstream security document, the commit that created it, and the Rust that implements the check, all first-hand.

CVE-2026-16487: the tag moves, the policy shrugs

bootc (bootc-dev/bootc, 2,314 stars, Rust) boots and upgrades a host from a container image: the OS is the image, and bootc upgrade — or the bootc-fetch-apply-updates.timer running it unattended — re-resolves the configured reference and stages whatever it finds for the next boot. If that reference is a mutable tag rather than a digest, then whoever can push to that tag decides what the host’s root filesystem becomes. Signature policy is the control that is supposed to stand between those two facts. Red Hat’s description states the failure plainly: upgrades “may apply unsigned content for registries that fall through to insecureAcceptAnything, allowing an attacker who can update that tag to execute arbitrary OS content as the host root filesystem on reboot.”

The fall-through is not hypothetical, and it is the shipped default. The default-policy.json installed by the containers tooling is six meaningful lines, and its top-level default is a single {"type": "insecureAcceptAnything"} entry. Everything else inherits from it.

bootc does have a guard, and reading it is the most instructive part of the record. The library models verification as three modes, with doc comments we quote verbatim: OstreeRemote(name), which verifies against an ostree remote’s keyring; ContainerPolicy, which will “defer to the containers-policy.json, but we make a best effort to reject default: insecureAcceptAnything policy”; and ContainerPolicyAllowInsecure, marked “NOT RECOMMENDED,” which defers to the default outright. The best effort is a function called container_policy_is_default_insecure, and the struct it deserialises into has exactly one field:

struct ContainerPolicy { default: Option<Vec<PolicyEntry>> }, whose is_default_insecure returns true only when default.split_first() yields Some((v, &[])) and that single entry’s type is insecureAcceptAnything.

Two things follow from those lines, and we offer them as our reading of the code rather than as Red Hat’s stated mechanism. The transports key — where per-registry and per-repository scopes live, and where a real policy does most of its work — is not a field on the struct at all, so it is parsed away and never consulted. A policy whose default is reject but which grants insecureAcceptAnything to a registry scope therefore passes the guard while accepting unsigned images for exactly the registry the host updates from. And because the match arm requires the remainder of the array to be empty, a default list with two entries does not match either. The check answers “is the top-level default a one-element insecure array,” which is a narrower question than “will this pull accept unsigned content.”

That guard also only runs in the ContainerPolicy arm, and a comment elsewhere in the same crate records what the common case actually is: formatting omits the signature source for insecure references “because allow-insecure is the effective default.” The shorthand ostree-unverified-registry: that appears in ordinary install and status output maps to ContainerPolicyAllowInsecure. On such a host the refusal path — whose error message reads containers-policy.json specifies a default of 'insecureAcceptAnything'; refusing usage — is never reached, because the mode that reaches it was never selected.

CVE-2026-16460: hardening the policy does not un-stage what it already let in

The second record is the other half, and it is the one with no mitigation. ostree deployments are staged: content is written and prepared, then finalised into the boot order on shutdown by ostree-finalize-staged.service. Red Hat’s statement is explicit that the signature gate sits at the wrong end of that sequence — policy “gates pull and import of bootc images, but does not revalidate content that is already staged for the next boot,” and so “tightening /etc/containers/policy.json alone does not invalidate previously staged deployments.”

This is the defect that punishes the correct response to the first one. An operator who reads CVE-2026-16487, hardens policy.json to reject unsigned content, and reboots to apply the change will, on that very reboot, finalise the unsigned deployment staged under the old policy. The remediation step and the exploitation step are the same action. Red Hat’s advisory names the escape — clear staged deployments after hardening policy — but declines to class it as a mitigation that meets its criteria, which is an honest answer rather than a reassuring one.

The affected-products tables are worth reading side by side, because they diverge sharply. For CVE-2026-16487, RHEL 8, 9 and 10 are all Affected with errata listed as None; OpenShift 4 is Not affected on the stated and checkable grounds that the Machine Config Operator applies node OS updates from digest-pinned images in the release payload rather than through bootc’s mutable-tag update path. For CVE-2026-16460, RHEL 8, 9 and 10 bootc, ostree and rpm-ostree are all marked Not affected with the justification “Vulnerable Code cannot be Controlled by Adversary” — while Red Hat Hardened Images is Affected at Important for both bootc and ostree. The product line whose name is the security claim is the one carrying both flaws.

The documented position, and when it was written

On 27 July 2026 at 14:12:20Z, bootc maintainer Colin Walters committed 24ee5eac452b, adding a 42-line docs/src/security.md and four lines to SECURITY.md. The commit message states its purpose verbatim: “We’ve had reports treating bootc’s default deference to containers-policy.json as a vulnerability, fix this by having a clear policy.” The document it added says, as its own paragraph and still present on main today: “It is not a vulnerability in bootc that signatures are not required by default.”

Both CVEs were reported to Red Hat on 21 July 2026 — 16460 at 11:36:56Z, 16487 at 15:40:04Z, per the timelines in the CVE records themselves. The upstream policy declaring the behaviour non-vulnerable was therefore written six days after the report arrived, and it is the document SECURITY.md now directs reporters to read first, under the heading “Understanding what is and is not a vulnerability.” Three months later the same company’s product security team published it as Important, 8.1, with RHEL 8, 9 and 10 affected. We are not calling that bad faith in either direction, and we note the commit carries an Assisted-by: AI trailer alongside the maintainer’s sign-off. We are pointing at the gap it exposes, because it is the generalisable part: a library can be correct to defer to a configuration file and a product can still be vulnerable, and the two judgements are made by different people reasoning about different systems.

Upstream’s position is defensible on its own terms and the document is clear about them: bootc runs fully privileged, all its inputs are trusted, it honours whatever policy.json says, and the document urges users to enable signatures. That is a statement about a tool. Red Hat’s CVE is a statement about a fleet of hosts enrolled to a tag, updating on a timer, with a default nobody changed. Both can be true. Only one of them reboots.

One further piece of context sits in the same code and the same window, and we record it as adjacent rather than as a third flaw. Issue #2339, filed 26 July 2026, reported that policy resolution did not follow absolute symlinks — a real pattern, since some image-mode distributions symlink /etc/containers/policy.json into /usr precisely so users cannot break verification. The resolver returned No containers policy.json found and bootc upgrade failed, so this surfaced as a hard error rather than a silent acceptance. PR #2343 (+84/−17, one file) fixed it on 31 July. The policy-resolution path was being actively repaired in the same fortnight the policy document was written.

What to do

Red Hat’s own guidance is the right starting point and we repeat it without improvement: pin bootc updates to digests, or rewrite /etc/containers/policy.json so the top-level default is reject and only explicitly trusted signed scopes are permitted; until that is done, disable bootc-fetch-apply-updates.timer. Given CVE-2026-16460, add one ordering constraint the advisories imply but do not sequence for you: clear staged deployments before the reboot that applies your new policy, not after. Check the current signature source in bootc status and treat any reference printed as ostree-unverified-registry: as unverified in fact, not merely in label. Prefer ostree-remote-registry: with a real keyring, or a sigstoreSigned scope, over relying on the ContainerPolicy guard to catch a bad file — it inspects only the top-level default and never the transport scopes. And if you suspect a malicious image has already been applied, Red Hat is explicit that rollback is insufficient: /var persists across it by design, including /var/roothome, so audit and remediate persistent state and re-enrol to a signed image.

The generalisation is the one we keep arriving at from different directions: verification that runs at fetch time and nowhere else is a property of a transaction, not of a system. We filed the same shape last week when hyper-mcp’s cosign verification accepted any signer, and when runwasi trusted a precompiled OCI layer it never re-checked. Here the content is not a plugin or a Wasm module but the root filesystem, and the re-check that never happens is the one between staging and boot.

Neither CVE is in the CISA KEV catalogue (checked directly, version 2026.10.16, 1,748 entries), and no exploitation has been reported. There is no GHSA for either: the bootc repository’s security-advisory list is empty, so neither identifier will reach a dependency scanner through that route. bootc’s current release is v1.17.1 (7 October 2026); no upstream commit since has touched signature-policy handling, and no fix exists for either record at the time of writing.

Our verification was read-only. Both CVE identifiers, publication timestamps (2026-10-19T06:17:24.203Z and 2026-10-19T07:16:31.253Z), Received status, the secalert@redhat.com source, CWE-345 assignments, CVSS 3.1 vectors and scores were read first-hand from the NVD 2.0 API; the disclosure timelines (both reported 2026-07-21; made public 2026-10-10 and 2026-10-18), CVE titles, per-product affected status and credits from the CVE Program records at cveawg.mitre.org; the severity ratings, statements, mitigations, affected-package tables with their justification strings, errata status and acknowledgements from the Red Hat CVE pages, read directly and last modified 2026-10-19 05:48:52Z and 07:11:03Z; every quoted line of Rust, every code comment and the SignatureSource doc comments from the bootc repository at main; the default policy document from the containers tooling repository; the security document, its 42-line creation commit 24ee5eac452b, that commit’s verbatim message, date and trailers, and SECURITY.md from the GitHub API; issue #2339 and PR #2343 metadata and bodies likewise; repository metadata (2,314 stars, 234 forks, Rust, created 2022-11-30, last pushed 2026-10-19T08:15:24Z, not archived) and release tags from the GitHub API; the empty repository advisory list measured directly. KEV status was checked directly against the published catalogue. Brave Search was used for discovery only and returned nothing but automated CVE mirrors — VulDB, strix.ai, threatint and two scoring aggregators — with no independent technical analysis of either record. We installed nothing, deployed no bootc host, built no image, pushed to no tag, altered no policy file, and executed no proof of concept.

Sources: