diff --git a/man/crypttab.xml b/man/crypttab.xml
index 8994e4f0d2..1d92745eb4 100644
--- a/man/crypttab.xml
+++ b/man/crypttab.xml
@@ -260,6 +260,29 @@
+
+
+
+ Specifies the kernel keyring and key description
+ (see keyrings7)
+ where LUKS2 volume key gets linked during device activation. The kernel keyring
+ description and key description must be separated by ::.
+
+ The kernel keyring part can be a string description or a predefined
+ kernel keyring prefixed with @ (e.g.: to use @s session or
+ @u user keyring directly). The type prefix text in the kernel keyring description
+ is not required. The specified kernel keyring must already exist at the time of device activation.
+
+ The key part is a string description optionally prefixed by a %key_type:.
+ If no type is specified, the user type key is linked by default. See
+ keyctl1
+ for more information on key descriptions (KEY IDENTIFIERS section).
+
+ Note that the linked volume key is not cleaned up automatically when the device is detached.
+
+
+
+
diff --git a/meson.build b/meson.build
index 4ebe46dd23..c031db78a7 100644
--- a/meson.build
+++ b/meson.build
@@ -1260,7 +1260,8 @@ foreach ident : ['crypt_set_metadata_size',
'crypt_token_max',
'crypt_reencrypt_init_by_passphrase',
'crypt_reencrypt',
- 'crypt_set_data_offset']
+ 'crypt_set_data_offset',
+ 'crypt_set_keyring_to_link']
have_ident = have and cc.has_function(
ident,
prefix : '#include ',
diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c
index 1a02730037..a10b49dda0 100644
--- a/src/cryptsetup/cryptsetup.c
+++ b/src/cryptsetup/cryptsetup.c
@@ -105,6 +105,9 @@ static bool arg_headless = false;
static usec_t arg_token_timeout_usec = 30*USEC_PER_SEC;
static unsigned arg_tpm2_measure_pcr = UINT_MAX; /* This and the following field is about measuring the unlocked volume key to the local TPM */
static char **arg_tpm2_measure_banks = NULL;
+static char *arg_link_keyring = NULL;
+static char *arg_link_key_type = NULL;
+static char *arg_link_key_description = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep);
STATIC_DESTRUCTOR_REGISTER(arg_hash, freep);
@@ -118,6 +121,9 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_banks, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_link_keyring, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_link_key_type, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_link_key_description, freep);
static const char* const passphrase_type_table[_PASSPHRASE_TYPE_MAX] = {
[PASSPHRASE_REGULAR] = "passphrase",
@@ -508,6 +514,56 @@ static int parse_one_option(const char *option) {
if (r < 0)
log_warning_errno(r, "Failed to parse %s, ignoring: %m", option);
+ } else if ((val = startswith(option, "link-volume-key="))) {
+#ifdef HAVE_CRYPT_SET_KEYRING_TO_LINK
+ const char *sep, *c;
+ _cleanup_free_ char *keyring = NULL, *key_type = NULL, *key_description = NULL;
+
+ /* Stick with cryptsetup --link-vk-to-keyring format
+ * ::%:,
+ * where % is optional and defaults to 'user'.
+ */
+ if (!(sep = strstr(val, "::")))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse link-volume-key= option value: %m");
+
+ /* cryptsetup (cli) supports passed in various formats:
+ * - well-known keyrings prefixed with '@' (@u user, @s session, etc)
+ * - text descriptions prefixed with "%:" or "%keyring:".
+ * - text desription with no prefix.
+ * - numeric keyring id (ignored in current patch set). */
+ if (*val == '@' || *val == '%')
+ keyring = strndup(val, sep - val);
+ else
+ /* add type prefix if missing (crypt_set_keyring_to_link() expects it) */
+ keyring = strnappend("%:", val, sep - val);
+ if (!keyring)
+ return log_oom();
+
+ sep += 2;
+
+ /* % is optional (and defaults to 'user') */
+ if (*sep == '%') {
+ /* must be separated by colon */
+ if (!(c = strchr(sep, ':')))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse link-volume-key= option value: %m");
+
+ key_type = strndup(sep + 1, c - sep - 1);
+ if (!key_type)
+ return log_oom();
+
+ sep = c + 1;
+ }
+
+ key_description = strdup(sep);
+ if (!key_description)
+ return log_oom();
+
+ free_and_replace(arg_link_keyring, keyring);
+ free_and_replace(arg_link_key_type, key_type);
+ free_and_replace(arg_link_key_description, key_description);
+#else
+ log_error("Build lacks libcryptsetup support for linking volume keys in user specified kernel keyrings upon device activation, ignoring: %s", option);
+#endif
} else if (!streq(option, "x-initrd.attach"))
log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option);
@@ -2287,6 +2343,15 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to load LUKS superblock on device %s: %m", crypt_get_device_name(cd));
+/* since cryptsetup 2.7.0 (Jan 2024) */
+#if HAVE_CRYPT_SET_KEYRING_TO_LINK
+ if (arg_link_key_description) {
+ r = crypt_set_keyring_to_link(cd, arg_link_key_description, NULL, arg_link_key_type, arg_link_keyring);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set keyring or key description to link volume key in, ignoring: %m");
+ }
+#endif
+
if (arg_header) {
r = crypt_set_data_device(cd, source);
if (r < 0)