Skip to content

Conversation

@alexlarsson
Copy link
Member

When using bootc, if you convert a signed ostree commit into an OCI image rpm-ostree compose container-encapsulate you end up with a new commit that isn't signed. However, the base commit object, and its commitmeta are still in the image and will end up the repo.

The base commit id is available in the container image config as a Label. So, we change ostree-prepare-root to fall back to using this base commit+commitmeta to find the expected composefs digest if the main commit is not signed.

@alexlarsson
Copy link
Member Author

I know this is the "old" approach and there is work on sealing images with the new native composefs backend. However, this is a minor change and lets existing users move to bootc easily while keeping this working.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for using composefs signatures from a base commit, which is useful for bootc generated images. The changes in otcore-prepare-root.c correctly implement a fallback mechanism to load the base commit and its metadata if the main commit is not signed. My review focuses on improving the robustness of the implementation, reducing code duplication, and ensuring logging consistency. I've identified a potential issue with the regular expression used for parsing JSON, which could be brittle. I've also suggested refactoring duplicated error handling logic and improving logging and conditional checks for better code clarity and maintainability.

@cgwalters
Copy link
Member

Not reviewing the code in detail yet...yeah I totally understand this and have thought about it myself...

But...there's obviously a lot of suboptimal things about this (many of which get fixed with the big composefs-native work of course).

In the short term though, I think we could change bootc to store the manifest and config data out of band if the input image is "fully" ostree (i.e. does not have any non-ostree layers).
That would dramatically simplify this logic right?

@alexlarsson
Copy link
Member Author

@cgwalters You mean re-use the existing commit in that case? And add the manifest/commit to the commitmeta?

@alexlarsson
Copy link
Member Author

I wish we had signed the composefs digest instead of the complete commit. Then we could just have copied the composefs digest + its signature into the new commit.

@cgwalters
Copy link
Member

@cgwalters You mean re-use the existing commit in that case? And add the manifest/commit to the commitmeta?

Yep

return FALSE;

/* In case the commit is one created by bootc when importing a container, it will not
be signed. However, we can still look at the base commit which may be. */
Copy link
Collaborator

@champtar champtar Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tried to follow the logic, but have you made sure this doesn't break layering:

  • rpm-ostree local layering (not sure if metadata like ostree.container.image-config is copied between commit when doing local layering)
  • container layering (LABEL are kept when adding layers I think)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember that people doing sealed images don't want either of those things to work.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but silently ignoring the extra layers would be super surprising behavior.
You can imagine a vendor delivering a signed commit encapsulated in a container, and a customer layering their favorite security/monitoring agent.
This should error out (during deployment ?), telling the customer they must disable signature checks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, sealing fundamentally breaks layering. In this case, the boot will read the expected composefs digest from the base ostree commit, and if there were added layers the deployed composefs image will have a different commit, so it will fail to boot with an "invalid digest". But this is sort of the point of sealing it in the first place.

If you truly want to layer something you need to at the end run something that re-signs the completed image, and that could if you wanted re-create a full signed ostree commit to make it boot again.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you truly want to layer something you need to at the end run something that re-signs the completed image, and that could if you wanted re-create a full signed ostree commit to make it boot again.

Or disable signature checking at runtime, because you just want to run a quick test and don't have the signing key.

@jlebon
Copy link
Member

jlebon commented Sep 9, 2025

@cgwalters You mean re-use the existing commit in that case? And add the manifest/commit to the commitmeta?

Yep

IIUC, I'm not sure we could do this by default. Wouldn't this change the output of rpm-ostree status --json? This is used by at least CoreOS stuff to read manifests/configs via the base-commit-meta key (but I wouldn't be surprised there's other code out there that does this).

Well... I guess we could change rpm-ostree status --json to also proxy detached metadata. Though I would be very hesitant to fake it and still have it be under the same key alongside non-detached metadata.

@cgwalters
Copy link
Member

IIUC, I'm not sure we could do this by default. Wouldn't this change the output of rpm-ostree status --json? This is used by at least CoreOS stuff to read manifests/configs via the base-commit-meta key (but I wouldn't be surprised there's other code out there that does this).

Probably but I think things that are doing that are broken and should have been instead using the ostree container image metadata command

@jlebon
Copy link
Member

jlebon commented Sep 9, 2025

IIUC, I'm not sure we could do this by default. Wouldn't this change the output of rpm-ostree status --json? This is used by at least CoreOS stuff to read manifests/configs via the base-commit-meta key (but I wouldn't be surprised there's other code out there that does this).

Probably but I think things that are doing that are broken and should have been instead using the ostree container image metadata command

I'm not sure I buy that. :) status --json is used as an API by many clients at this point. We probably should've filtered out those keys if we didn't want to consider them part of that API.

Anyway, we could adapt obviously (and I actually much prefer if we had it detached from the start), though given that this is all pretty stable stuff at this point, we need to be careful. I'd prefer having it opt-in to start.

cgwalters added a commit to cgwalters/bootc that referenced this pull request Sep 9, 2025
Motivated by ostreedev/ostree#3523

This is an obvious and trivially easy thing to do here, and
makes dereferencing from "merge -> base" client side also
trivial which is especially important in the initramfs.
@cgwalters
Copy link
Member

I did bootc-dev/bootc#1600 which should dramatically simplify this.

Sure, but silently ignoring the extra layers would be super surprising behavior.

Yes. I think a clear next enhancement to bootc (ostree-ext) is something like ostree.sealed or something that disallows any non-ostree layers.

@cgwalters
Copy link
Member

Let's go back to the start here though:

When using bootc, if you convert a signed ostree commit into an OCI image rpm-ostree compose container-encapsulate you end up with a new commit that isn't signed. However, the base commit object, and its commitmeta are still in the image and will end up the repo.

This isn't true. The problem solely lies in how bootc always creates a local commit (in order to handle non-ostree layers).

cgwalters added a commit to cgwalters/bootc that referenced this pull request Sep 9, 2025
Motivated by ostreedev/ostree#3523

This is an obvious and trivially easy thing to do here, and
makes dereferencing from "merge -> base" client side also
trivial which is especially important in the initramfs.

Signed-off-by: Colin Walters <[email protected]>
@cgwalters
Copy link
Member

The more I think about this the more I feel this all just needs to be fixed on the bootc side.

@alexlarsson
Copy link
Member Author

I feel the opposite. This is a very localized hack that makes it work with minimal changes. Changing the format of the commits is a much more major change. Especially given that we want to do something else long-term.

@alexlarsson
Copy link
Member Author

One thing I was considering, is could we have bootc embedd the original commit and commitmeta variants as metadata keys in the bootc created commit as "base-commit" and "base-commitmeta"? That would make this workaround much cleaner.

@alexlarsson
Copy link
Member Author

alexlarsson commented Sep 9, 2025

Sure, but silently ignoring the extra layers would be super surprising behavior.

Yes. I think a clear next enhancement to bootc (ostree-ext) is something like ostree.sealed or something that disallows any non-ostree layers.

Not sure where the above comment came from, but that is not quite true. The local commit will be used to deploy. The base commit is only used to find the expected composefs digest, so what will happen is that you deploy something that will then not boot. I mean, this isn't exactly great either, but its not just "silently ignoring" some content.

Also, it is possible to derive from the container if you either override the composefs=signed option in the derived image. Or if you later use some tool to re-sign it (convert it back to a pure ostree image).

@cgwalters
Copy link
Member

One thing I was considering, is could we have bootc embedd the original commit and commitmeta variants as metadata keys in the bootc created commit as "base-commit" and "base-commitmeta"? That would make this workaround much cleaner.

See bootc-dev/bootc#1600

@cgwalters
Copy link
Member

I feel the opposite. This is a very localized hack that makes it work with minimal changes. Changing the format of the commits is a much more major change. Especially given that we want to do something else long-term.

I took a look at it, it's not that major...did some prep work in bootc-dev/bootc#1602

Basically in this flow we'd skip write_merge_commit_impl and write the container metadata to detached instead, and change the lookup functions to look for this.

Something to bear in mind of course here is that in theory in this case, the client side contents should be the same as the server side generated contents...but we're definitely not strictly validating that right now. Hmm, let me try out adding an assertion that they're content identical and see...

There's some potentially kind of tricky skew here if what we do at mount time is different from the filesystem/commit used by all of the rest of the code.


I'm OK to do something here but but the regex parsing JSON feels a bit too hacky for something security sensitive. How about the logic is:

  • Detect if the target commit has a metadata key named ostree.container.image-config (without parsing it)
  • If it does, resolve to the parent

?

@alexlarsson
Copy link
Member Author

Detect if the target commit has a metadata key named ostree.container.image-config (without parsing it)
If it does, resolve to the parent

I think this is good. Although i think looking for the ostree.importer.version key makes it more clear what is going on.

@alexlarsson
Copy link
Member Author

One thing I was considering, is could we have bootc embedd the original commit and commitmeta variants as metadata keys in the bootc created commit as "base-commit" and "base-commitmeta"? That would make this workaround much cleaner.

See bootc-dev/bootc#1600

I mean, that MR makes this code much cleaner, so i love it, but what I mean was embedding the actual commit data, not the digests.

@cgwalters
Copy link
Member

I mean, that MR makes this code much cleaner, so i love it, but what I mean was embedding the actual commit data, not the digests.

Hmm. But if we just copied the composefs digest, I think we'd want to verify that they're actually the same thing. They should be the same thing...but now that I look they're not. Looking at current quay.io/centos-bootc/centos-bootc:stream10...first it is a layered build because of some Konflux cruft at top, but we can ignore that.

What's a bit more concerning there is the labels seem different e.g.

# ostree ls -X $base /usr
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:root_t:s0')] } /usr
l00777 0 0      0 { [(b'security.selinux', b'system_u:object_r:tmp_t:s0')] } /usr/tmp -> ../var/tmp
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:bin_t:s0')] } /usr/bin
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:root_t:s0')] } /usr/etc
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:default_t:s0')] } /usr/games
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:default_t:s0')] } /usr/include
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:lib_t:s0')] } /usr/lib
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:lib_t:s0')] } /usr/lib64
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:default_t:s0')] } /usr/libexec
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:default_t:s0')] } /usr/local
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:bin_t:s0')] } /usr/sbin
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:default_t:s0')] } /usr/share
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:default_t:s0')] } /usr/src
# ostree ls -X $local /usr 
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:usr_t:s0')] } /usr
l00777 0 0      0 { [(b'security.selinux', b'system_u:object_r:usr_t:s0')] } /usr/tmp -> ../var/tmp
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:bin_t:s0')] } /usr/bin
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:etc_t:s0')] } /usr/etc
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:usr_t:s0')] } /usr/games
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:usr_t:s0')] } /usr/include
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:lib_t:s0')] } /usr/lib
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:lib_t:s0')] } /usr/lib64
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:bin_t:s0')] } /usr/libexec
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:usr_t:s0')] } /usr/local
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:bin_t:s0')] } /usr/sbin
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:usr_t:s0')] } /usr/share
d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:usr_t:s0')] } /usr/src

Where the base labels look wrong.

Actually though, quay.io/fedora/fedora-bootc:42 looks fine, but both c10s and c9s have this issue.

This is probably worth chasing - must be an rpm-ostree side issue. Although it might somehow be specific to the container build path we're using; do you see the default_t in the labels in your builds?

@champtar
Copy link
Collaborator

Detect if the target commit has a metadata key named ostree.container.image-config (without parsing it)
If it does, resolve to the parent

I think this is good. Although i think looking for the ostree.importer.version key makes it more clear what is going on.

ostree.importer.version breaks reproducibility and should be removed IMO / we should not start to depend on it

@alexlarsson
Copy link
Member Author

alexlarsson commented Sep 11, 2025

I don't see that difference. I did see some differences due to coreos/rpm-ostree#5485 but when I fixed that i get identical content checksums.

Honestly, having this break something is pretty good, because these shouldn't diverge. Any such divergence is some kind of bug.

In my builds, the only default_t is:

d00755 0 0      0 { [(b'security.selinux', b'system_u:object_r:default_t:s0')] } /sysroot

This is a minor preparation for a later change. Instead of
hand-rolling the G_FILE_ERROR_NOENT error check we add
a new allow_noent option.

Additionally, we move the handling of a no commitmeta being
an error to the caller of load_commit_for_deploy(), because
this check will be slightly more complex in the future.
When using bootc, if you convert a signed ostree commit into an OCI
image `rpm-ostree compose container-encapsulate` you end up with a new
commit that isn't signed. However, the base commit object, and its
commitmeta are still in the image and will end up the repo, and
since bootc-dev/bootc#1600 the base commit
id is available as the parent commit.

So, we change ostree-prepare-root to fall back to using the base
commit+commitmeta to find the expected composefs digest if the main
commit is not signed.

Note: This will only work with ostree-only commits. If you have any
layered data, then the content will change, and the composefs digest
in the base commit will not match the deployed one. This is expected
with such sealed commits though. If you want to layer, either disable
sealing, or create a new sealed ostree commit for the new image.
@alexlarsson alexlarsson force-pushed the signed-composefs-with-bootc branch from 6461363 to 92f6d8e Compare September 15, 2025 12:46
@alexlarsson
Copy link
Member Author

I updated this to be based on the new "parent commit". Also, i split out some helper work in a separate commit to make it more reviewable.

@cgwalters cgwalters merged commit aac962c into ostreedev:main Sep 18, 2025
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants