diff --git a/man/portablectl.xml b/man/portablectl.xml
index 5678171a17..531bc2dbfe 100644
--- a/man/portablectl.xml
+++ b/man/portablectl.xml
@@ -141,6 +141,11 @@
immediately started (blocking operation unless is passed) and/or enabled after
attaching the image.
+ When images are attached, an image policy will be generated that pins the attached image by
+ the exact content that was found while attaching, so that it cannot be swapped downgrading security
+ (e.g.: removing dm-verity protection) without a full reinstallation. For more details on policies, see
+ systemd.image-policy7.
+
diff --git a/src/portable/portable.c b/src/portable/portable.c
index b7cb9af1ff..715344f84d 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -27,6 +27,7 @@
#include "fileio.h"
#include "fs-util.h"
#include "glyph-util.h"
+#include "image-policy.h"
#include "install.h"
#include "iovec-util.h"
#include "libmount-util.h"
@@ -364,10 +365,12 @@ static int portable_extract_by_path(
const ImagePolicy *image_policy,
PortableMetadata **ret_os_release,
Hashmap **ret_unit_files,
+ ImagePolicy **ret_pinned_image_policy,
sd_bus_error *error) {
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
_cleanup_(portable_metadata_unrefp) PortableMetadata* os_release = NULL;
+ _cleanup_(image_policy_freep) ImagePolicy *pinned_image_policy = NULL;
_cleanup_(loop_device_unrefp) LoopDevice *d = NULL;
int r;
@@ -397,6 +400,7 @@ static int portable_extract_by_path(
} else if (r < 0)
return log_debug_errno(r, "Failed to set up loopback device for %s: %m", path);
else {
+ _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT;
_cleanup_(dissected_image_unrefp) DissectedImage *m = NULL;
_cleanup_(rmdir_and_freep) char *tmpdir = NULL;
_cleanup_close_pair_ int seq[2] = EBADF_PAIR;
@@ -449,6 +453,24 @@ static int portable_extract_by_path(
if (r < 0)
return r;
+ r = verity_settings_load(&verity, path, NULL, NULL);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to load root hash: %m");
+
+ r = dissected_image_load_verity_sig_partition(m, d->fd, &verity);
+ if (r < 0)
+ return r;
+
+ r = dissected_image_guess_verity_roothash(m, &verity);
+ if (r < 0)
+ return r;
+
+ if (ret_pinned_image_policy) {
+ pinned_image_policy = image_policy_new_from_dissected(m, &verity);
+ if (!pinned_image_policy)
+ return -ENOMEM;
+ }
+
if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0)
return log_debug_errno(errno, "Failed to allocated SOCK_SEQPACKET socket: %m");
@@ -558,6 +580,9 @@ static int portable_extract_by_path(
if (ret_os_release)
*ret_os_release = TAKE_PTR(os_release);
+ if (ret_pinned_image_policy)
+ *ret_pinned_image_policy = TAKE_PTR(pinned_image_policy);
+
return 0;
}
@@ -575,9 +600,12 @@ static int extract_image_and_extensions(
PortableMetadata **ret_os_release,
Hashmap **ret_unit_files,
char ***ret_valid_prefixes,
+ ImagePolicy **ret_pinned_root_image_policy,
+ ImagePolicy **ret_pinned_ext_image_policy,
sd_bus_error *error) {
_cleanup_free_ char *id = NULL, *id_like = NULL, *version_id = NULL, *sysext_level = NULL, *confext_level = NULL;
+ _cleanup_(image_policy_freep) ImagePolicy *pinned_root_image_policy = NULL, *pinned_ext_image_policy = NULL;
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
_cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
@@ -669,6 +697,7 @@ static int extract_image_and_extensions(
image_policy,
&os_release,
&unit_files,
+ &pinned_root_image_policy,
error);
if (r < 0)
return r;
@@ -700,6 +729,7 @@ static int extract_image_and_extensions(
ORDERED_HASHMAP_FOREACH(ext, extension_images) {
_cleanup_(portable_metadata_unrefp) PortableMetadata *extension_release_meta = NULL;
+ _cleanup_(image_policy_freep) ImagePolicy *policy = NULL;
_cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL;
_cleanup_strv_free_ char **extension_release = NULL;
const char *e;
@@ -713,6 +743,7 @@ static int extract_image_and_extensions(
image_policy,
&extension_release_meta,
&extra_unit_files,
+ &policy,
error);
if (r < 0)
return r;
@@ -721,6 +752,19 @@ static int extract_image_and_extensions(
if (r < 0)
return r;
+ if (!pinned_ext_image_policy && policy)
+ pinned_ext_image_policy = TAKE_PTR(policy);
+ else if (policy) {
+ _cleanup_(image_policy_freep) ImagePolicy *intersected_policy = NULL;
+
+ /* There is a single policy for all extension images, so we need a union */
+ r = image_policy_union(pinned_ext_image_policy, policy, &intersected_policy);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to merge extension image policies: %m");
+
+ free_and_replace(pinned_ext_image_policy, intersected_policy);
+ }
+
if (!validate_extension && !ret_valid_prefixes && !ret_extension_releases)
continue;
@@ -768,6 +812,10 @@ static int extract_image_and_extensions(
*ret_unit_files = TAKE_PTR(unit_files);
if (ret_valid_prefixes)
*ret_valid_prefixes = TAKE_PTR(valid_prefixes);
+ if (ret_pinned_root_image_policy)
+ *ret_pinned_root_image_policy = TAKE_PTR(pinned_root_image_policy);
+ if (ret_pinned_ext_image_policy)
+ *ret_pinned_ext_image_policy = TAKE_PTR(pinned_ext_image_policy);
return 0;
}
@@ -808,6 +856,8 @@ int portable_extract(
&os_release,
&unit_files,
ret_valid_prefixes ? &valid_prefixes : NULL,
+ /* pinned_root_image_policy= */ NULL,
+ /* pinned_ext_image_policy= */ NULL,
error);
if (r < 0)
return r;
@@ -1110,6 +1160,8 @@ static int install_chroot_dropin(
ImageType type,
OrderedHashmap *extension_images,
OrderedHashmap *extension_releases,
+ const ImagePolicy *pinned_root_image_policy,
+ const ImagePolicy *pinned_ext_image_policy,
const PortableMetadata *m,
const PortableMetadata *os_release,
const char *dropin_dir,
@@ -1152,6 +1204,18 @@ static int install_chroot_dropin(
"LogExtraFields=PORTABLE=", base_name, "\n"))
return -ENOMEM;
+ if (pinned_root_image_policy) {
+ _cleanup_free_ char *policy_str = NULL;
+
+ r = image_policy_to_string(pinned_root_image_policy, /* simplify= */ true, &policy_str);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to serialize pinned image policy: %m");
+
+ if (!strextend(&text,
+ "RootImagePolicy=", policy_str, "\n"))
+ return -ENOMEM;
+ }
+
/* If we have a single image then PORTABLE= will point to it, so we add
* PORTABLE_NAME_AND_VERSION= with the os-release fields and we are done. But if we have
* extensions, PORTABLE= will point to the image where the current unit was found in. So we
@@ -1201,6 +1265,18 @@ static int install_chroot_dropin(
"LogExtraFields=PORTABLE_EXTENSION=", extension_base_name, "\n"))
return -ENOMEM;
+ if (pinned_ext_image_policy) {
+ _cleanup_free_ char *policy_str = NULL;
+
+ r = image_policy_to_string(pinned_ext_image_policy, /* simplify= */ true, &policy_str);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to serialize pinned image policy: %m");
+
+ if (!strextend(&text,
+ "ExtensionImagePolicy=", policy_str, "\n"))
+ return -ENOMEM;
+ }
+
/* Look for image/version identifiers in the extension release files. We
* look for all possible IDs, but typically only 1 or 2 will be set, so
* the number of fields added shouldn't be too large. We prefix the DDI
@@ -1317,6 +1393,8 @@ static int attach_unit_file(
ImageType type,
OrderedHashmap *extension_images,
OrderedHashmap *extension_releases,
+ const ImagePolicy *pinned_root_image_policy,
+ const ImagePolicy *pinned_ext_image_policy,
const PortableMetadata *m,
const PortableMetadata *os_release,
const char *profile,
@@ -1362,7 +1440,20 @@ static int attach_unit_file(
* is reloaded while we are creating things here: as long as only the drop-ins exist the unit doesn't exist at
* all for PID 1. */
- r = install_chroot_dropin(image_path, type, extension_images, extension_releases, m, os_release, dropin_dir, flags, &chroot_dropin, changes, n_changes);
+ r = install_chroot_dropin(
+ image_path,
+ type,
+ extension_images,
+ extension_releases,
+ pinned_root_image_policy,
+ pinned_ext_image_policy,
+ m,
+ os_release,
+ dropin_dir,
+ flags,
+ &chroot_dropin,
+ changes,
+ n_changes);
if (r < 0)
return r;
@@ -1631,6 +1722,7 @@ int portable_attach(
size_t *n_changes,
sd_bus_error *error) {
+ _cleanup_(image_policy_freep) ImagePolicy *pinned_root_image_policy = NULL, *pinned_ext_image_policy = NULL;
_cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
@@ -1656,6 +1748,8 @@ int portable_attach(
&os_release,
&unit_files,
&valid_prefixes,
+ &pinned_root_image_policy,
+ &pinned_ext_image_policy,
error);
if (r < 0)
return r;
@@ -1720,8 +1814,20 @@ int portable_attach(
}
HASHMAP_FOREACH(item, unit_files) {
- r = attach_unit_file(&paths, image->path, image->type, extension_images, extension_releases,
- item, os_release, profile, flags, changes, n_changes);
+ r = attach_unit_file(
+ &paths,
+ image->path,
+ image->type,
+ extension_images,
+ extension_releases,
+ pinned_root_image_policy,
+ pinned_ext_image_policy,
+ item,
+ os_release,
+ profile,
+ flags,
+ changes,
+ n_changes);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to attach unit '%s': %m", item->name);
}
diff --git a/test/units/TEST-29-PORTABLE.image.sh b/test/units/TEST-29-PORTABLE.image.sh
index d034e2c48c..8b930d40f2 100755
--- a/test/units/TEST-29-PORTABLE.image.sh
+++ b/test/units/TEST-29-PORTABLE.image.sh
@@ -25,6 +25,10 @@ systemctl is-active minimal-app0.service
systemctl is-active minimal-app0-foo.service
systemctl is-active minimal-app0-bar.service && exit 1
+# Ensure pinning by policy works
+cat /run/systemd/system.attached/minimal-app0-foo.service.d/20-portable.conf
+grep -q -F 'root=signed+squashfs:' /run/systemd/system.attached/minimal-app0-foo.service.d/20-portable.conf
+
portablectl "${ARGS[@]}" reattach --now --runtime /usr/share/minimal_1.raw minimal-app0
portablectl is-attached minimal-app0
@@ -91,6 +95,9 @@ status="$(portablectl is-attached --extension app0 minimal_0)"
grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+# Ensure pinning by policy works
+grep -q -F 'RootImagePolicy=root=signed+squashfs:' /run/systemd/system.attached/app0.service.d/20-portable.conf >/dev/null
+grep -q -F 'ExtensionImagePolicy=root=signed+squashfs:' /run/systemd/system.attached/app0.service.d/20-portable.conf >/dev/null
portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0