From d35e31286671dcc61c9a23157455981ad533c89c Mon Sep 17 00:00:00 2001 From: Piyush Jena Date: Mon, 31 Aug 2026 05:05:20 +0000 Subject: [PATCH 1/4] apiserver: support ephemeral-encryption-keys in ephemeral-storage The encrypt_ephemeral_device() function was implemented for encrypted-storage feature. In that case, the device was encrypted only if it was un-encrypted. With ephemeral-encryption-keys, we opt to encrypt every time and with plain-mode encryption with dm-crypt instead of LUKS. Signed-off-by: Piyush Jena --- .../apiserver/src/server/ephemeral_storage.rs | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/sources/api/apiserver/src/server/ephemeral_storage.rs b/sources/api/apiserver/src/server/ephemeral_storage.rs index be7f80ab0..9f6c5dcef 100644 --- a/sources/api/apiserver/src/server/ephemeral_storage.rs +++ b/sources/api/apiserver/src/server/ephemeral_storage.rs @@ -590,6 +590,16 @@ fn should_encrypt() -> Result { Ok(features.encrypted_storage) } +/// Returns true when ephemeral-encryption-keys image feature is enabled. +fn ephemeral_encryption_keys_enabled() -> Result { + let features = bottlerocket_image_features::parse_image_features().map_err(|e| { + error::Error::LoadImageFeatures { + message: e.to_string(), + } + })?; + Ok(features.ephemeral_encryption_keys) +} + /// Encrypt ephemeral device using rottweiler fn encrypt_ephemeral_device(device: &str) -> Result { info!("encrypting ephemeral device {device:?}"); @@ -604,36 +614,60 @@ fn encrypt_ephemeral_device(device: &str) -> Result { std::os::unix::fs::symlink(device, EPHEMERAL_DATA_LINK) .context(error::DiskSymlinkFailureSnafu {})?; - let is_encrypted = - run_rottweiler(&["check", "block-device", EPHEMERAL_DATA_LINK, "encrypted"])? - .status - .success(); - - if !is_encrypted { + if ephemeral_encryption_keys_enabled()? { + // No format or encrypted-check needed: a fresh key makes all prior contents unreadable. run_rottweiler_checked( &["generate-key", EPHEMERAL_STORAGE_KEY_ID], EPHEMERAL_DATA_LINK, )?; + run_rottweiler_checked( &[ - "encrypt", + "encrypt-and-attach", "block-device", EPHEMERAL_DATA_LINK, EPHEMERAL_STORAGE_KEY_ID, ], EPHEMERAL_DATA_LINK, )?; - } - run_rottweiler_checked( - &[ - "attach", - "block-device", + run_rottweiler_checked( + &["delete-key", EPHEMERAL_STORAGE_KEY_ID], + EPHEMERAL_DATA_LINK, + )?; + } else { + let is_encrypted = + run_rottweiler(&["check", "block-device", EPHEMERAL_DATA_LINK, "encrypted"])? + .status + .success(); + + if !is_encrypted { + run_rottweiler_checked( + &["generate-key", EPHEMERAL_STORAGE_KEY_ID], + EPHEMERAL_DATA_LINK, + )?; + + run_rottweiler_checked( + &[ + "encrypt", + "block-device", + EPHEMERAL_DATA_LINK, + EPHEMERAL_STORAGE_KEY_ID, + ], + EPHEMERAL_DATA_LINK, + )?; + } + + run_rottweiler_checked( + &[ + "attach", + "block-device", + EPHEMERAL_DATA_LINK, + EPHEMERAL_STORAGE_KEY_ID, + ], EPHEMERAL_DATA_LINK, - EPHEMERAL_STORAGE_KEY_ID, - ], - EPHEMERAL_DATA_LINK, - )?; + )?; + } Ok(EPHEMERAL_MAPPER_DEVICE.to_string()) } From adb584a1c97d74f56f2933f3dea6f4cad8358391 Mon Sep 17 00:00:00 2001 From: Piyush Jena Date: Mon, 31 Aug 2026 05:17:09 +0000 Subject: [PATCH 2/4] rottweiler: add support for plain mode encryption With ephemeral-encryption-keys feature, block devices are encrypted with plain-mode dm-crypt instead of LUKS. Sub commands have been added to encrypt and attach block devices with plain-mode encryption in 1 go instead of separate functions. Signed-off-by: Piyush Jena --- sources/rottweiler/README.md | 5 ++- sources/rottweiler/src/block_device.rs | 13 ++++++ sources/rottweiler/src/key.rs | 44 +++++++++---------- sources/rottweiler/src/main.rs | 45 ++++++++++++++++--- sources/rottweiler/src/system.rs | 60 ++++++++++++++++++++++++-- 5 files changed, 132 insertions(+), 35 deletions(-) diff --git a/sources/rottweiler/README.md b/sources/rottweiler/README.md index 91063afa6..931c5932c 100644 --- a/sources/rottweiler/README.md +++ b/sources/rottweiler/README.md @@ -7,7 +7,7 @@ Current version: 0.1.0 *rottweiler* is Bottlerocket's storage encryption helper. It provides a unified interface for encrypting and managing encrypted storage resources including: -- Block devices (using LUKS) +- Block devices (using LUKS or plain mode encryption) - Directories (using fscrypt) - TPM PCR measurements @@ -19,9 +19,10 @@ interface for encrypting and managing encrypted storage resources including: #### Block Device Operations - `encrypt block-device ` - Encrypt a block device using LUKS +- `encrypt-and-attach block-device ` - Encrypt (plain mode) and attach a block device in one step - `attach block-device ` - Attach an encrypted block device - `detach block-device ` - Detach an encrypted block device -- `resize block-device ` - Resize a LUKS block device +- `resize block-device ` - Resize an encrypted block device to match the underlying device size - `check block-device encrypted|unencrypted` - Check block device encryption state #### Directory Operations diff --git a/sources/rottweiler/src/block_device.rs b/sources/rottweiler/src/block_device.rs index acbc0b4cb..0d791283b 100644 --- a/sources/rottweiler/src/block_device.rs +++ b/sources/rottweiler/src/block_device.rs @@ -17,6 +17,19 @@ pub fn encrypt(path: PathBuf, key_id: String) -> Result<()> { system::cryptsetup_luks_format(device, &key_bytes) } +/// Open a block device as a plain-mode dm-crypt mapper named after the device filename. +pub fn encrypt_and_attach(path: PathBuf, key_id: String) -> Result<()> { + let volume_name = filename(&path)?; + + let source_device = path + .to_str() + .with_whatever_context(|| format!("path is not valid UTF-8: '{}'", path.display()))?; + + let key_bytes = key::load(key_id)?; + + system::cryptsetup_plain_format(volume_name, source_device, &key_bytes) +} + /// Attach (unlock) an encrypted block device, creating a device mapper entry pub fn attach(path: PathBuf, key_id: String) -> Result<()> { let volume_name = filename(&path)?; diff --git a/sources/rottweiler/src/key.rs b/sources/rottweiler/src/key.rs index 1cccca2e9..c70de1449 100644 --- a/sources/rottweiler/src/key.rs +++ b/sources/rottweiler/src/key.rs @@ -13,20 +13,12 @@ const KEYSTORE_PERMANENT: &str = "/.bottlerocket/keystore"; const KEYSTORE_EPHEMERAL: &str = "/run/rottweiler"; const KEY_SIZE: usize = 64; -/// Checks if ephemeral encryption keys feature is enabled via image features -fn ephemeral_encryption_keys_enabled() -> Result { - let features = bottlerocket_image_features::parse_image_features() - .with_whatever_context(|_| "failed to load image features")?; - Ok(features.ephemeral_encryption_keys) -} - -fn keystore_dir() -> PathBuf { - let is_ephemeral_encryption_keys_enabled = ephemeral_encryption_keys_enabled().unwrap_or(false); +fn keystore_dir() -> Result { // If ephemeral encryption keys are enabled, we store the keys in tmpfs rather than the disk - if is_ephemeral_encryption_keys_enabled { - PathBuf::from(KEYSTORE_EPHEMERAL) + if system::ephemeral_encryption_keys_enabled()? { + Ok(PathBuf::from(KEYSTORE_EPHEMERAL)) } else { - PathBuf::from(KEYSTORE_PERMANENT) + Ok(PathBuf::from(KEYSTORE_PERMANENT)) } } @@ -42,10 +34,22 @@ fn validate_key_id(key_id: &str) -> Result<()> { Ok(()) } +fn random_bytes() -> Result>> { + let mut random_bytes = Zeroizing::new(vec![0u8; KEY_SIZE]); + let mut random = fs::File::open(DEV_RANDOM) + .with_whatever_context(|_| format!("failed to open {}", DEV_RANDOM))?; + + random + .read_exact(&mut random_bytes) + .with_whatever_context(|_| "failed to read random bytes")?; + + Ok(random_bytes) +} + /// Generate a random encryption key and encrypt it with TPM2 PCRs 7+14 pub fn generate(key_id: String) -> Result<()> { validate_key_id(&key_id)?; - let keystore = keystore_dir(); + let keystore = keystore_dir()?; let key_path = keystore.join(&key_id); // Skip generation if key already exists @@ -53,15 +57,7 @@ pub fn generate(key_id: String) -> Result<()> { return Ok(()); } - let mut random_bytes = Zeroizing::new(vec![0u8; KEY_SIZE]); - - let mut random = fs::File::open(DEV_RANDOM) - .with_whatever_context(|_| format!("failed to open {}", DEV_RANDOM))?; - - random - .read_exact(&mut random_bytes) - .with_whatever_context(|_| "failed to read random bytes")?; - + let random_bytes = random_bytes()?; let encrypted = system::systemd_creds_encrypt(&key_id, &random_bytes)?; fs::create_dir_all(&keystore).with_whatever_context(|_| { @@ -80,7 +76,7 @@ pub fn generate(key_id: String) -> Result<()> { /// Delete a sealed key from the keystore. pub fn delete(key_id: String) -> Result<()> { validate_key_id(&key_id)?; - let key_path = keystore_dir().join(&key_id); + let key_path = keystore_dir()?.join(&key_id); // Skip deletion if key doesn't exists if !key_path.exists() { @@ -98,7 +94,7 @@ pub fn delete(key_id: String) -> Result<()> { /// Load and decrypt a TPM2-encrypted key from the keystore pub fn load(key_id: String) -> Result>> { validate_key_id(&key_id)?; - let key_path = keystore_dir().join(&key_id); + let key_path = keystore_dir()?.join(&key_id); let encrypted = fs::read(&key_path) .with_whatever_context(|_| format!("failed to read key from '{}'", key_path.display()))?; diff --git a/sources/rottweiler/src/main.rs b/sources/rottweiler/src/main.rs index 689a1d25d..bc5b27964 100644 --- a/sources/rottweiler/src/main.rs +++ b/sources/rottweiler/src/main.rs @@ -4,7 +4,7 @@ *rottweiler* is Bottlerocket's storage encryption helper. It provides a unified interface for encrypting and managing encrypted storage resources including: -- Block devices (using LUKS) +- Block devices (using LUKS or plain mode encryption) - Directories (using fscrypt) - TPM PCR measurements @@ -16,9 +16,10 @@ interface for encrypting and managing encrypted storage resources including: ### Block Device Operations - `encrypt block-device ` - Encrypt a block device using LUKS +- `encrypt-and-attach block-device ` - Encrypt (plain mode) and attach a block device in one step - `attach block-device ` - Attach an encrypted block device - `detach block-device ` - Detach an encrypted block device -- `resize block-device ` - Resize a LUKS block device +- `resize block-device ` - Resize an encrypted block device to match the underlying device size - `check block-device encrypted|unencrypted` - Check block device encryption state ### Directory Operations @@ -64,9 +65,10 @@ async fn main() -> Result<()> { if args.len() >= 3 { let verb = args.get(1).map(|s| s.as_str()); let resource_pos = match verb { - Some("encrypt" | "attach" | "detach" | "resize" | "lock" | "unlock" | "check") => { - Some(2) - } + Some( + "encrypt" | "encrypt-and-attach" | "attach" | "detach" | "resize" | "lock" + | "unlock" | "check", + ) => Some(2), _ => None, }; if let Some(pos) = resource_pos @@ -100,6 +102,11 @@ async fn main() -> Result<()> { EncryptResource::BlockDevice(cmd) => block_device::encrypt(cmd.path, cmd.key_id), EncryptResource::Directory(cmd) => directory::encrypt(cmd.path, cmd.key_id), }, + Command::EncryptAndAttach(cmd) => match cmd.resource { + EncryptAndAttachResource::BlockDevice(cmd) => { + block_device::encrypt_and_attach(cmd.path, cmd.key_id) + } + }, Command::Attach(cmd) => match cmd.resource { AttachResource::BlockDevice(cmd) => block_device::attach(cmd.path, cmd.key_id), }, @@ -200,6 +207,7 @@ enum Command { GenerateKey(GenerateKeyCmd), DeleteKey(DeleteKeyCmd), Encrypt(EncryptCmd), + EncryptAndAttach(EncryptAndAttachCmd), Attach(AttachCmd), Detach(DetachCmd), Resize(ResizeCmd), @@ -262,6 +270,31 @@ struct EncryptDirectoryCmd { key_id: String, } +#[derive(FromArgs)] +#[argh(subcommand, name = "encrypt-and-attach")] +/// Encrypt using dm-crypt in plain mode and attach a resource +struct EncryptAndAttachCmd { + #[argh(subcommand)] + resource: EncryptAndAttachResource, +} + +#[derive(FromArgs)] +#[argh(subcommand)] +enum EncryptAndAttachResource { + BlockDevice(EncryptAndAttachBlockDeviceCmd), +} + +#[derive(FromArgs)] +#[argh(subcommand, name = "block-device")] +/// Encrypt using dm-crypt in plain mode and attach a block device +struct EncryptAndAttachBlockDeviceCmd { + #[argh(positional)] + path: PathBuf, + + #[argh(positional)] + key_id: String, +} + #[derive(FromArgs)] #[argh(subcommand, name = "attach")] /// Attach an encrypted resource @@ -325,7 +358,7 @@ enum ResizeResource { #[derive(FromArgs)] #[argh(subcommand, name = "block-device")] -/// Resize a LUKS block device +/// Resize an encrypted block device struct ResizeBlockDeviceCmd { #[argh(positional)] path: PathBuf, diff --git a/sources/rottweiler/src/system.rs b/sources/rottweiler/src/system.rs index 9fea17cbb..17564b9c6 100644 --- a/sources/rottweiler/src/system.rs +++ b/sources/rottweiler/src/system.rs @@ -11,6 +11,9 @@ const SYSTEMD_CRYPTSETUP: &str = "/usr/lib/systemd/systemd-cryptsetup"; const CRYPTSETUP: &str = "/usr/sbin/cryptsetup"; const APICLIENT: &str = "/usr/bin/apiclient"; const TPM2_PCREXTEND: &str = "/usr/bin/tpm2_pcrextend"; +// Size in bytes of the plain-mode dm-crypt key, and the same value in bits +const PLAIN_KEY_BYTES: usize = 64; +const PLAIN_KEY_SIZE_BITS: usize = 64 * 8; /// Encrypt data using systemd-creds with TPM2 PCRs pub fn systemd_creds_encrypt(name: &str, plaintext: &[u8]) -> Result>> { @@ -88,16 +91,60 @@ pub fn cryptsetup_luks_format(device: &str, key_data: &[u8]) -> Result<()> { Ok(()) } -/// Resize a LUKS device using the provided key -pub fn cryptsetup_resize(volume_name: &str, key_data: &[u8]) -> Result<()> { +/// Open a device as a headerless plain-mode dm-crypt mapper using the provided key. +pub fn cryptsetup_plain_format(volume_name: &str, device: &str, key_data: &[u8]) -> Result<()> { + // A wrong-length key would be silently zero-padded or truncated by cryptsetup. + ensure_whatever!( + key_data.len() == PLAIN_KEY_BYTES, + "plain-mode dm-crypt key must be exactly {} bytes, got {}", + PLAIN_KEY_BYTES, + key_data.len() + ); + + let keyfile_size_arg = format!("--keyfile-size={}", key_data.len()); execute( CRYPTSETUP, - &["resize", "--key-file=-", volume_name], + &[ + "open", + "--type", + "plain", + "--cipher", + "aes-xts-plain64", + "--key-size", + &PLAIN_KEY_SIZE_BITS.to_string(), + "--hash", + "plain", + // Without `--key-file=-`, cryptsetup reads stdin as a passphrase and stops at 0x0A; + // `--keyfile-size` then makes it read exactly N bytes regardless of content. + "--key-file=-", + &keyfile_size_arg, + device, + volume_name, + ], Some(key_data), )?; Ok(()) } +/// Resize a block device to match the underlying device size. +pub fn cryptsetup_resize(volume_name: &str, key_data: &[u8]) -> Result<()> { + if ephemeral_encryption_keys_enabled()? { + // In case of plain-mode dm-crypt the cryptsetup-resize command opts to read the + // key from kernel keyring if available. Because of this, the child process + // running cryptsetup-resize may finish before the parent process can put the + // key_data in the STDIN buffer causing the parent process rottweiler to fail + // with errno EPIPE. This is the reason we are omitting --key-file. + execute(CRYPTSETUP, &["resize", volume_name], None)?; + } else { + execute( + CRYPTSETUP, + &["resize", "--key-file=-", volume_name], + Some(key_data), + )?; + } + Ok(()) +} + /// Execute a command with optional stdin input and return stdout fn execute(cmd: &str, args: &[&str], input: Option<&[u8]>) -> Result>> { let mut child = Command::new(cmd) @@ -162,6 +209,13 @@ fn get_tpm2_pcrs() -> Result { }) } +/// Checks if the ephemeral encryption keys feature is enabled via image features. +pub(crate) fn ephemeral_encryption_keys_enabled() -> Result { + let features = bottlerocket_image_features::parse_image_features() + .with_whatever_context(|_| "failed to load image features")?; + Ok(features.ephemeral_encryption_keys) +} + /// Get canonicalized settings from apiclient, excluding seed and hostname pub fn apiclient_get_settings() -> Result>> { execute( From 78a85de9917b05975ee6de8994e17d5d2045f63a Mon Sep 17 00:00:00 2001 From: Piyush Jena Date: Mon, 31 Aug 2026 05:42:53 +0000 Subject: [PATCH 3/4] release: provide ephemeral-encryption-keys as a sub-package This includes a redesign of how ephemeral-encryption-keys works. It has been ensured that the lifetime of a key is just the service it was generated in. To achieve that we make use of encrypt-and-attach subcommand in rottweiler to encrypt block devices with plan-mode encryption using dm-crypt instead of LUKS. This redesign caused us to drop older drop-ins, required for LUKS based ephemeral-encryption- keys feature. Because the newly introduced services had overlap with existing ones like prepare-local-fs.service, encrypt-datastore.service, and repart-local.service, drop-ins have been added to skip those steps. The keys are created into a tmpfs keystore /run/rottweiler and deleted right after use. Signed-off-by: Piyush Jena --- .../release/bottlerocket-mount-encrypted.conf | 8 -- .../release/bottlerocket-mount-ephemeral.conf | 6 ++ .../release/encrypt-datastore-ephemeral.conf | 4 - .../encrypt-datastore-private-luks.conf | 7 -- .../release/encrypt-local-fs-ephemeral.conf | 4 - packages/release/encrypt-private-fs.service | 27 ------ .../release/encrypt-unlock-datastore.service | 28 ++++++ .../release/encrypt-unlock-local-fs.service | 43 ++++++++++ ...vice => encrypt-unlock-private-fs.service} | 14 +-- .../release/prepare-local-fs-ephemeral.conf | 7 ++ packages/release/prepare-private-fs.service | 4 +- packages/release/release.spec | 85 +++++++++---------- packages/release/repart-local-ephemeral.conf | 8 +- packages/release/run-rottweiler.mount | 3 +- .../release/unlock-datastore-ephemeral.conf | 5 -- 15 files changed, 142 insertions(+), 111 deletions(-) delete mode 100644 packages/release/bottlerocket-mount-encrypted.conf create mode 100644 packages/release/bottlerocket-mount-ephemeral.conf delete mode 100644 packages/release/encrypt-datastore-ephemeral.conf delete mode 100644 packages/release/encrypt-datastore-private-luks.conf delete mode 100644 packages/release/encrypt-local-fs-ephemeral.conf delete mode 100644 packages/release/encrypt-private-fs.service create mode 100644 packages/release/encrypt-unlock-datastore.service create mode 100644 packages/release/encrypt-unlock-local-fs.service rename packages/release/{unlock-private-fs.service => encrypt-unlock-private-fs.service} (63%) create mode 100644 packages/release/prepare-local-fs-ephemeral.conf delete mode 100644 packages/release/unlock-datastore-ephemeral.conf diff --git a/packages/release/bottlerocket-mount-encrypted.conf b/packages/release/bottlerocket-mount-encrypted.conf deleted file mode 100644 index 0ad386b13..000000000 --- a/packages/release/bottlerocket-mount-encrypted.conf +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] -# mount the LUKS mapper instead of the raw partition and only after the -# partition has been encrypted, unlocked and formatted. -After=unlock-private-fs.service prepare-private-fs.service -Requires=prepare-private-fs.service - -[Mount] -What=/dev/mapper/BOTTLEROCKET-PRIVATE diff --git a/packages/release/bottlerocket-mount-ephemeral.conf b/packages/release/bottlerocket-mount-ephemeral.conf new file mode 100644 index 000000000..fda2ae0a7 --- /dev/null +++ b/packages/release/bottlerocket-mount-ephemeral.conf @@ -0,0 +1,6 @@ +[Unit] +After=encrypt-unlock-private-fs.service prepare-private-fs.service +Requires=prepare-private-fs.service + +[Mount] +What=/dev/mapper/BOTTLEROCKET-PRIVATE diff --git a/packages/release/encrypt-datastore-ephemeral.conf b/packages/release/encrypt-datastore-ephemeral.conf deleted file mode 100644 index 6a6c10f73..000000000 --- a/packages/release/encrypt-datastore-ephemeral.conf +++ /dev/null @@ -1,4 +0,0 @@ -[Service] -# Drop the "only if unencrypted" guard so the datastore directory is -# re-encrypted with a freshly generated key on every boot. -ExecCondition= diff --git a/packages/release/encrypt-datastore-private-luks.conf b/packages/release/encrypt-datastore-private-luks.conf deleted file mode 100644 index 87e7aae87..000000000 --- a/packages/release/encrypt-datastore-private-luks.conf +++ /dev/null @@ -1,7 +0,0 @@ -[Service] -# In case of ephemeral-encryption-keys, we format the BOTTLEROCKET-PRIVATE with -# `encrypt` feature flag. So, we drop the tune2fs call, which is used to enable -# the flag and generate the key and encrypt the datastore. -ExecStart= -ExecStart=/usr/bin/rottweiler generate-key datastore -ExecStart=/usr/bin/rottweiler encrypt directory ${DATASTORE_DIR} datastore diff --git a/packages/release/encrypt-local-fs-ephemeral.conf b/packages/release/encrypt-local-fs-ephemeral.conf deleted file mode 100644 index c03edac5c..000000000 --- a/packages/release/encrypt-local-fs-ephemeral.conf +++ /dev/null @@ -1,4 +0,0 @@ -[Service] -# Drop the "only if unencrypted" guard so the Data partition is -# re-encrypted with a freshly generated key on every boot. -ExecCondition= diff --git a/packages/release/encrypt-private-fs.service b/packages/release/encrypt-private-fs.service deleted file mode 100644 index b285cae03..000000000 --- a/packages/release/encrypt-private-fs.service +++ /dev/null @@ -1,27 +0,0 @@ -[Unit] -Description=Encrypt Private Filesystem (/.bottlerocket) -DefaultDependencies=no -Conflicts=umount.target -Wants=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device -After=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device tpm2.target cryptsetup-pre.target -Before=unlock-private-fs.service -ConditionSecurity=tpm2 -RefuseManualStart=true -RefuseManualStop=true - -[Service] -Type=oneshot -Environment=BOTTLEROCKET_PRIVATE=/dev/disk/by-partlabel/BOTTLEROCKET-PRIVATE -EnvironmentFile=/usr/share/bottlerocket/image-features.env - -# Generate the per-boot key into the /run keystore and LUKS2-format PRIVATE. -ExecStart=/usr/bin/rottweiler generate-key bottlerocket-private -ExecStart=/usr/bin/rottweiler encrypt block-device ${BOTTLEROCKET_PRIVATE} bottlerocket-private - -UMask=0077 -RemainAfterExit=true -StandardOutput=journal+console -StandardError=inherit - -[Install] -RequiredBy=unlock-private-fs.service diff --git a/packages/release/encrypt-unlock-datastore.service b/packages/release/encrypt-unlock-datastore.service new file mode 100644 index 000000000..0e5776f19 --- /dev/null +++ b/packages/release/encrypt-unlock-datastore.service @@ -0,0 +1,28 @@ +[Unit] +Description=Encrypt and Unlock Datastore (/.bottlerocket/datastore) +DefaultDependencies=no +Before=migrator.service storewolf.service +After=tpm2.target +ConditionSecurity=tpm2 +# /run/rottweiler is a tmpfs keystore; fscrypt keys must not persist to disk. +RequiresMountsFor=/.bottlerocket /run/rottweiler +RefuseManualStart=true +RefuseManualStop=true + +[Service] +Type=oneshot +Environment=DATASTORE_DIR=/.bottlerocket/datastore +EnvironmentFile=/usr/share/bottlerocket/image-features.env + +ExecStart=/usr/bin/rottweiler generate-key datastore +ExecStart=/usr/bin/rottweiler encrypt directory ${DATASTORE_DIR} datastore +ExecStart=/usr/bin/rottweiler unlock directory ${DATASTORE_DIR} datastore +ExecStartPost=/usr/bin/rottweiler delete-key datastore + +UMask=0077 +RemainAfterExit=true +StandardOutput=journal+console +StandardError=inherit + +[Install] +RequiredBy=migrator.service storewolf.service diff --git a/packages/release/encrypt-unlock-local-fs.service b/packages/release/encrypt-unlock-local-fs.service new file mode 100644 index 000000000..2eefa3662 --- /dev/null +++ b/packages/release/encrypt-unlock-local-fs.service @@ -0,0 +1,43 @@ +[Unit] +Description=Encrypt and Unlock Local Filesystem (/local) +DefaultDependencies=no +Conflicts=umount.target +IgnoreOnIsolate=true +Wants=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dDATA.device blockdev@dev-mapper-BOTTLEROCKET\x2dDATA.target +After=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dDATA.device cryptsetup-pre.target systemd-udevd-kernel.socket +After=tpm2.target +ConditionSecurity=tpm2 +Before=cryptsetup.target blockdev@dev-mapper-BOTTLEROCKET\x2dDATA.target +RequiresMountsFor=/run/rottweiler +RefuseManualStart=true +RefuseManualStop=true + +[Service] +Type=oneshot +Environment=BOTTLEROCKET_DATA=/dev/disk/by-partlabel/BOTTLEROCKET-DATA +Environment=DATA_PARTITION_BLOCK_DEVICE=/dev/mapper/BOTTLEROCKET-DATA +EnvironmentFile=/usr/share/bottlerocket/image-features.env + +ExecStart=/usr/bin/rottweiler generate-key bottlerocket-data +ExecStart=/usr/bin/rottweiler encrypt-and-attach block-device ${BOTTLEROCKET_DATA} bottlerocket-data +ExecStart=/usr/lib/systemd/systemd-makefs ${DATA_PARTITION_FILESYSTEM} ${DATA_PARTITION_BLOCK_DEVICE} + +# Mask repart-data-* so they don't block waiting for a partition this service already owns. +ExecStart=/usr/bin/systemctl stop repart-data-preferred repart-data-fallback --no-block +ExecStart=/usr/bin/ln -s /dev/null /etc/systemd/system/repart-data-preferred.service +ExecStart=/usr/bin/ln -s /dev/null /etc/systemd/system/repart-data-fallback.service + +# Pass the device directly so systemd-repart resizes regardless of which disk / lives on. +ExecStart=-/usr/bin/systemd-repart --dry-run=no ${BOTTLEROCKET_DATA} +ExecStart=/usr/bin/rottweiler resize block-device /dev/disk/by-partlabel/BOTTLEROCKET-DATA bottlerocket-data +ExecStartPost=/usr/bin/rottweiler delete-key bottlerocket-data + +ExecStop=/usr/bin/rottweiler detach block-device ${BOTTLEROCKET_DATA} + +UMask=0077 +RemainAfterExit=yes +StandardOutput=journal+console +StandardError=inherit + +[Install] +RequiredBy=local-fs.target diff --git a/packages/release/unlock-private-fs.service b/packages/release/encrypt-unlock-private-fs.service similarity index 63% rename from packages/release/unlock-private-fs.service rename to packages/release/encrypt-unlock-private-fs.service index cd33b98c9..7f10c7da1 100644 --- a/packages/release/unlock-private-fs.service +++ b/packages/release/encrypt-unlock-private-fs.service @@ -1,12 +1,14 @@ [Unit] -Description=Unlock Private Filesystem (/.bottlerocket) +Description=Encrypt and Unlock Private Filesystem (/.bottlerocket) DefaultDependencies=no Conflicts=umount.target IgnoreOnIsolate=true -After=cryptsetup-pre.target systemd-udevd-kernel.socket dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device Wants=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device blockdev@dev-mapper-BOTTLEROCKET\x2dPRIVATE.target -Before=cryptsetup.target blockdev@dev-mapper-BOTTLEROCKET\x2dPRIVATE.target +After=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device cryptsetup-pre.target systemd-udevd-kernel.socket +After=tpm2.target ConditionSecurity=tpm2 +Before=cryptsetup.target blockdev@dev-mapper-BOTTLEROCKET\x2dPRIVATE.target +RequiresMountsFor=/run/rottweiler RefuseManualStart=true RefuseManualStop=true @@ -15,12 +17,12 @@ Type=oneshot Environment=BOTTLEROCKET_PRIVATE=/dev/disk/by-partlabel/BOTTLEROCKET-PRIVATE EnvironmentFile=/usr/share/bottlerocket/image-features.env -ExecStart=/usr/bin/rottweiler attach block-device ${BOTTLEROCKET_PRIVATE} bottlerocket-private -# Delete the private-partition encryption key from the tmpfs keystore after it has -# been used to unlock the volume, so a fresh key is generated on the next boot. +ExecStart=/usr/bin/rottweiler generate-key bottlerocket-private +ExecStart=/usr/bin/rottweiler encrypt-and-attach block-device ${BOTTLEROCKET_PRIVATE} bottlerocket-private ExecStartPost=/usr/bin/rottweiler delete-key bottlerocket-private ExecStop=/usr/bin/rottweiler detach block-device ${BOTTLEROCKET_PRIVATE} +UMask=0077 RemainAfterExit=yes StandardOutput=journal StandardError=journal+console diff --git a/packages/release/prepare-local-fs-ephemeral.conf b/packages/release/prepare-local-fs-ephemeral.conf new file mode 100644 index 000000000..009a22594 --- /dev/null +++ b/packages/release/prepare-local-fs-ephemeral.conf @@ -0,0 +1,7 @@ +[Unit] +BindsTo=encrypt-unlock-local-fs.service +After=encrypt-unlock-local-fs.service + +[Service] +ExecStart= +ExecStart=/usr/bin/true diff --git a/packages/release/prepare-private-fs.service b/packages/release/prepare-private-fs.service index a92a95060..81bbacb0e 100644 --- a/packages/release/prepare-private-fs.service +++ b/packages/release/prepare-private-fs.service @@ -3,8 +3,8 @@ Description=Prepare Private Filesystem (/.bottlerocket) DefaultDependencies=no Conflicts=umount.target Wants=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device -After=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device unlock-private-fs.service -BindsTo=unlock-private-fs.service +After=dev-disk-by\x2dpartlabel-BOTTLEROCKET\x2dPRIVATE.device encrypt-unlock-private-fs.service +BindsTo=encrypt-unlock-private-fs.service Before=\x2ebottlerocket.mount RefuseManualStart=true RefuseManualStop=true diff --git a/packages/release/release.spec b/packages/release/release.spec index 14a89007b..c80070bd7 100644 --- a/packages/release/release.spec +++ b/packages/release/release.spec @@ -142,23 +142,21 @@ Source1608: measure-settings.service Source1609: measure-cmdline.service Source1610: measure-user-data.service +# Ephemeral encryption keys combines encrypt+unlock in one unit +Source1611: encrypt-unlock-local-fs.service +Source1612: encrypt-unlock-private-fs.service +Source1613: encrypt-unlock-datastore.service + # TPM2-related drop-ins. Source1650: prepare-local-fs-encrypted.conf Source1651: local-mount-encrypted.conf Source1652: repart-local-encrypted.conf -# Ephemeral (per-boot) encryption key drop-ins. -Source1661: encrypt-datastore-ephemeral.conf -Source1662: unlock-datastore-ephemeral.conf -Source1663: encrypt-local-fs-ephemeral.conf -Source1664: repart-local-ephemeral.conf +Source1661: prepare-local-fs-ephemeral.conf +Source1662: repart-local-ephemeral.conf -# Full private-partition LUKS encryption units (ephemeral-encryption-keys). -Source1670: encrypt-private-fs.service -Source1671: unlock-private-fs.service Source1672: prepare-private-fs.service -Source1673: bottlerocket-mount-encrypted.conf -Source1674: encrypt-datastore-private-luks.conf +Source1673: bottlerocket-mount-ephemeral.conf # Runtime FIPS activation sources. # These are always installed but only activate when fips=1 is on the kernel command line. @@ -222,6 +220,8 @@ Requires: %{_cross_os}pciutils Requires: (%{_cross_os}rdma-core if %{_cross_os}variant-platform(aws)) Requires: %{_cross_os}shim Requires: (%{name}-crypt if %{_cross_os}image-feature(encrypted-storage)) +Requires: ((%{name}-crypt-luks or %{_cross_os}image-feature(ephemeral-encryption-keys)) if %{_cross_os}image-feature(encrypted-storage)) +Requires: (%{name}-ephemeral-crypt if %{_cross_os}image-feature(ephemeral-encryption-keys)) %description managed @@ -243,6 +243,15 @@ Requires: %{_cross_os}rottweiler %description crypt %{summary}. +%package crypt-luks +Summary: Bottlerocket release, with LUKS block-device encryption +Requires: (%{_cross_os}image-feature(encrypted-storage) and %{name}-crypt) +Conflicts: %{_cross_os}image-feature(ephemeral-encryption-keys) +Requires: %{_cross_os}rottweiler + +%description crypt-luks +%{summary}. + %package ephemeral-crypt Summary: Bottlerocket release, with ephemeral encryption keys Requires: (%{_cross_os}image-feature(ephemeral-encryption-keys) and %{name}-crypt) @@ -427,36 +436,25 @@ install -d %{buildroot}%{_cross_datadir}/bottlerocket install -p -m 0644 %{S:1702} %{buildroot}%{_cross_datadir}/bottlerocket/fips-go.env install -d %{buildroot}%{_cross_unitdir}/prepare-local-fs.service.d +install -d %{buildroot}%{_cross_unitdir}/local.mount.d +install -d %{buildroot}%{_cross_unitdir}/repart-local.service.d + install -p -m 0644 %{S:1752} \ %{buildroot}%{_cross_unitdir}/prepare-local-fs.service.d/10-managed.conf install -p -m 0644 %{S:1650} %{buildroot}%{_cross_unitdir}/prepare-local-fs.service.d/10-encrypted.conf - -install -d %{buildroot}%{_cross_unitdir}/local.mount.d install -p -m 0644 %{S:1651} %{buildroot}%{_cross_unitdir}/local.mount.d/10-encrypted.conf - -install -d %{buildroot}%{_cross_unitdir}/repart-local.service.d install -p -m 0644 %{S:1652} %{buildroot}%{_cross_unitdir}/repart-local.service.d/10-encrypted.conf -install -d %{buildroot}%{_cross_unitdir}/encrypt-datastore.service.d -install -p -m 0644 %{S:1661} %{buildroot}%{_cross_unitdir}/encrypt-datastore.service.d/20-ephemeral.conf +install -p -m 0644 %{S:1651} %{buildroot}%{_cross_unitdir}/local.mount.d/10-ephemeral.conf +install -p -m 0644 %{S:1661} %{buildroot}%{_cross_unitdir}/prepare-local-fs.service.d/20-ephemeral.conf +install -p -m 0644 %{S:1662} %{buildroot}%{_cross_unitdir}/repart-local.service.d/10-ephemeral.conf -install -d %{buildroot}%{_cross_unitdir}/unlock-datastore.service.d -install -p -m 0644 %{S:1662} %{buildroot}%{_cross_unitdir}/unlock-datastore.service.d/20-ephemeral.conf - -install -d %{buildroot}%{_cross_unitdir}/encrypt-local-fs.service.d -install -p -m 0644 %{S:1663} %{buildroot}%{_cross_unitdir}/encrypt-local-fs.service.d/20-ephemeral.conf - -install -d %{buildroot}%{_cross_unitdir}/repart-local.service.d -install -p -m 0644 %{S:1664} %{buildroot}%{_cross_unitdir}/repart-local.service.d/20-ephemeral.conf - -# Full private-partition LUKS encryption units (ephemeral-encryption-keys). -install -p -m 0644 %{S:1670} %{S:1671} %{S:1672} %{buildroot}%{_cross_unitdir} +install -p -m 0644 %{S:1611} %{S:1612} %{S:1613} %{buildroot}%{_cross_unitdir} +install -p -m 0644 %{S:1672} %{buildroot}%{_cross_unitdir} BOTTLEROCKET_PATH=$(systemd-escape --path /.bottlerocket) install -d %{buildroot}%{_cross_unitdir}/${BOTTLEROCKET_PATH}.mount.d -install -p -m 0644 %{S:1673} %{buildroot}%{_cross_unitdir}/${BOTTLEROCKET_PATH}.mount.d/10-encrypted.conf - -install -p -m 0644 %{S:1674} %{buildroot}%{_cross_unitdir}/encrypt-datastore.service.d/30-private-luks.conf +install -p -m 0644 %{S:1673} %{buildroot}%{_cross_unitdir}/${BOTTLEROCKET_PATH}.mount.d/20-ephemeral.conf ln -s preconfigured.target %{buildroot}%{_cross_unitdir}/default.target @@ -492,10 +490,12 @@ install -p -m 0644 %{S:1753} %{buildroot}%{_cross_tmpfilesdir}/release-managed.c %{_cross_unitdir}/activate-multi-user.service %{_cross_unitdir}/prepare-opt.service %{_cross_unitdir}/prepare-var.service +%dir %{_cross_unitdir}/repart-local.service.d %{_cross_unitdir}/repart-local.service %{_cross_unitdir}/var.mount %{_cross_unitdir}/opt.mount %{_cross_unitdir}/mnt.mount +%dir %{_cross_unitdir}/local.mount.d %{_cross_unitdir}/local.mount %{_cross_unitdir}/*-licenses.mount %{_cross_unitdir}/*-modules.mount @@ -505,6 +505,7 @@ install -p -m 0644 %{S:1753} %{buildroot}%{_cross_tmpfilesdir}/release-managed.c %{_cross_unitdir}/mask-local-var.service %dir %{_cross_unitdir}/network-pre.target.d %{_cross_unitdir}/network-pre.target.d/00-dbus-dep.conf +%dir %{_cross_unitdir}/prepare-local-fs.service.d %{_cross_unitdir}/prepare-local-fs.service %{_cross_unitdir}/service.d/00-aws-config.conf %{_cross_unitdir}/service.d/10-requires-tmp.conf @@ -591,10 +592,6 @@ install -p -m 0644 %{S:1753} %{buildroot}%{_cross_tmpfilesdir}/release-managed.c %{_cross_bootconfigdir}/10-fips.conf %files crypt -%{_cross_unitdir}/encrypt-datastore.service -%dir %{_cross_unitdir}/encrypt-datastore.service.d -%{_cross_unitdir}/encrypt-local-fs.service -%dir %{_cross_unitdir}/encrypt-local-fs.service.d %{_cross_unitdir}/measure-cmdline.service %{_cross_unitdir}/measure-settings.service %{_cross_unitdir}/measure-user-data.service @@ -602,23 +599,25 @@ install -p -m 0644 %{S:1753} %{buildroot}%{_cross_tmpfilesdir}/release-managed.c %{_cross_unitdir}/systemd-pcrphase-multi-user.service %{_cross_unitdir}/systemd-pcrphase-preconfigured.service %{_cross_unitdir}/systemd-pcrphase-sysinit.service + +%files crypt-luks +%{_cross_unitdir}/encrypt-datastore.service %{_cross_unitdir}/unlock-datastore.service -%dir %{_cross_unitdir}/unlock-datastore.service.d +%{_cross_unitdir}/encrypt-local-fs.service %{_cross_unitdir}/unlock-local-fs.service %{_cross_unitdir}/local.mount.d/10-encrypted.conf %{_cross_unitdir}/prepare-local-fs.service.d/10-encrypted.conf %{_cross_unitdir}/repart-local.service.d/10-encrypted.conf %files ephemeral-crypt -%{_cross_unitdir}/encrypt-datastore.service.d/20-ephemeral.conf -%{_cross_unitdir}/encrypt-datastore.service.d/30-private-luks.conf -%{_cross_unitdir}/unlock-datastore.service.d/20-ephemeral.conf -%{_cross_unitdir}/encrypt-local-fs.service.d/20-ephemeral.conf -%{_cross_unitdir}/repart-local.service.d/20-ephemeral.conf -%{_cross_unitdir}/encrypt-private-fs.service -%{_cross_unitdir}/unlock-private-fs.service +%{_cross_unitdir}/encrypt-unlock-local-fs.service +%{_cross_unitdir}/encrypt-unlock-private-fs.service +%{_cross_unitdir}/encrypt-unlock-datastore.service +%{_cross_unitdir}/local.mount.d/10-ephemeral.conf +%{_cross_unitdir}/prepare-local-fs.service.d/20-ephemeral.conf +%{_cross_unitdir}/repart-local.service.d/10-ephemeral.conf %{_cross_unitdir}/prepare-private-fs.service -%{_cross_unitdir}/\x2ebottlerocket.mount.d/10-encrypted.conf +%{_cross_unitdir}/\x2ebottlerocket.mount.d/20-ephemeral.conf %{_cross_unitdir}/run-rottweiler.mount %files swap diff --git a/packages/release/repart-local-ephemeral.conf b/packages/release/repart-local-ephemeral.conf index 689620961..1c787332a 100644 --- a/packages/release/repart-local-ephemeral.conf +++ b/packages/release/repart-local-ephemeral.conf @@ -1,5 +1,5 @@ [Service] -EnvironmentFile=/usr/share/bottlerocket/image-features.env -# Delete the sealed data-partition encryption key after it has been used for the -# time in the boot, forcing format and fresh key generation in the next boot. -ExecStartPost=/usr/bin/rottweiler delete-key bottlerocket-data +# With ephemeral-encryption-keys, the DATA partition grow and dm-crypt mapper resize +# happen in encrypt-unlock-local-fs.service; override repart with a no-op. +ExecStart= +ExecStart=/usr/bin/true diff --git a/packages/release/run-rottweiler.mount b/packages/release/run-rottweiler.mount index 4b4e3e5ec..31ae916c0 100644 --- a/packages/release/run-rottweiler.mount +++ b/packages/release/run-rottweiler.mount @@ -4,12 +4,13 @@ DefaultDependencies=no Conflicts=umount.target After=selinux-policy-files.service Wants=selinux-policy-files.service -Before=encrypt-datastore.service encrypt-local-fs.service encrypt-private-fs.service +Before=encrypt-unlock-datastore.service encrypt-unlock-private-fs.service encrypt-unlock-local-fs.service [Mount] What=tmpfs Where=/run/rottweiler Type=tmpfs +# noswap prevents the keystore from reaching persistent swap Options=nosuid,nodev,noexec,noatime,noswap,context=system_u:object_r:private_t:s0,mode=0700 [Install] diff --git a/packages/release/unlock-datastore-ephemeral.conf b/packages/release/unlock-datastore-ephemeral.conf deleted file mode 100644 index d082a79b8..000000000 --- a/packages/release/unlock-datastore-ephemeral.conf +++ /dev/null @@ -1,5 +0,0 @@ -[Service] -# Delete the key used to encrypt the datastore after it has been used for the final -# time in the boot, forcing new key generation and a complete rebuild of the datastore -# in the subsequent boot. -ExecStartPost=/usr/bin/rottweiler delete-key datastore From 49019411454188f71662009b623af7d183c2cfb6 Mon Sep 17 00:00:00 2001 From: Piyush Jena Date: Mon, 31 Aug 2026 04:59:36 +0000 Subject: [PATCH 4/4] docs: add documentation for ephemeral-encryption-keys Signed-off-by: Piyush Jena --- docs/ENCRYPTED_STORAGE.md | 148 +++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/docs/ENCRYPTED_STORAGE.md b/docs/ENCRYPTED_STORAGE.md index 4ea8c13ef..db92aa285 100644 --- a/docs/ENCRYPTED_STORAGE.md +++ b/docs/ENCRYPTED_STORAGE.md @@ -14,6 +14,9 @@ Bottlerocket's encrypted storage feature provides: All encryption keys are sealed to TPM2 PCRs, ensuring data can only be decrypted when the system boots in a trusted state. +> The LUKS2 flow above applies to `encrypted-storage` variants. Variants that also enable `ephemeral-encryption-keys` use plain-mode dm-crypt with a per-boot key instead; +fscrypt datastore encryption is unchanged. See [Ephemeral Encryption Keys](#ephemeral-encryption-keys). + ## Architecture ### Components @@ -208,6 +211,129 @@ This ensures encrypted data can only be accessed when booting a trusted configur - Before: `migrator.service`, `storewolf.service` - Required by: `migrator.service`, `storewolf.service` +## Ephemeral Encryption Keys + +Variants with the `ephemeral-encryption-keys` image feature (in addition to `encrypted-storage`) replace the LUKS2 flow +with plain-mode dm-crypt for `BOTTLEROCKET-DATA`, `BOTTLEROCKET-PRIVATE`, and the `EPHEMERAL-DATA` device: + +- Each key is 64 bytes from `/dev/random`, TPM2-sealed into the `/run/rottweiler` tmpfs keystore to ensure it can be +decrypted only if the system is in a trusted state, and deleted (`ExecStartPost=rottweiler delete-key`) within the +service that opened the device +- No key or data persists: every boot generates a new key, making prior contents unreadable, and recreates the filesystem +- The datastore still uses `fscrypt`, via a combined `encrypt-unlock-datastore.service` + +### Boot Flow (Every Boot) + +``` +1. encrypt-unlock-local-fs.service + - Generates a per-boot key, opens BOTTLEROCKET-DATA as /dev/mapper/BOTTLEROCKET-DATA (plain mode) + - Creates the filesystem, grows the partition, resizes the mapper + - Deletes the key + ↓ +2. prepare-local-fs.service + - No-op: filesystem already created in step 1 + ↓ +3. local.mount + - Mounts /dev/mapper/BOTTLEROCKET-DATA to /local + ↓ +4. repart-local.service + - systemd-repart grow is masked (done in step 1) + - systemd-growfs /local grows the filesystem to fill the resized mapper +``` + +``` +1. encrypt-unlock-private-fs.service + - Generates a per-boot key, opens BOTTLEROCKET-PRIVATE as /dev/mapper/BOTTLEROCKET-PRIVATE (plain mode) + - Deletes the key + ↓ +2. prepare-private-fs.service + - Creates filesystem on /dev/mapper/BOTTLEROCKET-PRIVATE (mkfs.ext4 -O encrypt) + ↓ +3. .bottlerocket.mount + - Mounts /dev/mapper/BOTTLEROCKET-PRIVATE to /.bottlerocket + ↓ +4. encrypt-unlock-datastore.service + - Seals a key into /run/rottweiler, sets fscrypt policy, unlocks /.bottlerocket/datastore + - Deletes the key +``` + +### Services + +#### encrypt-unlock-local-fs.service + +**Purpose:** Encrypt and open the BOTTLEROCKET-DATA partition on every boot. + +**Key behaviors:** +- Generates a per-boot key, opens the plain-mode mapper, and creates the filesystem before the partition grow, so the +unit waits only on the raw device node, not the by-partlabel symlink recreated by resize +- Grows the partition (`systemd-repart`) and resizes the mapper (`rottweiler resize block-device`) +- Deletes the key (`ExecStartPost=rottweiler delete-key`) +- Detaches on shutdown (`ExecStop`) + +**Dependencies:** +- After: `dev-disk-by-partlabel-BOTTLEROCKET-DATA.device`, `cryptsetup-pre.target`, `systemd-udevd-kernel.socket`, `tpm2.target` +- Before: `cryptsetup.target`, `blockdev@dev-mapper-BOTTLEROCKET-DATA.target` +- Required by: `local-fs.target` + +#### encrypt-unlock-private-fs.service + +**Purpose:** Encrypt and open the BOTTLEROCKET-PRIVATE partition on every boot. + +**Key behaviors:** +- Generates a per-boot key and opens the plain-mode mapper +- Deletes the key (`ExecStartPost=rottweiler delete-key`) +- `RequiresMountsFor=/run/rottweiler` so the key lands on the tmpfs keystore, not a plain `/run` +- Detaches on shutdown (`ExecStop`) + +**Dependencies:** +- After: `dev-disk-by-partlabel-BOTTLEROCKET-PRIVATE.device`, `cryptsetup-pre.target`, `systemd-udevd-kernel.socket`, `tpm2.target` +- Before: `cryptsetup.target`, `blockdev@dev-mapper-BOTTLEROCKET-PRIVATE.target` +- Required by: `prepare-private-fs.service` + +#### encrypt-unlock-datastore.service + +**Purpose:** Encrypt and unlock `/.bottlerocket/datastore` on every boot, replacing the `encrypt-datastore.service` / `unlock-datastore.service` pair. + +**Key behaviors:** +- Seals a per-boot key into the tmpfs keystore, sets the fscrypt policy, and unlocks the directory +- Deletes the key (`ExecStartPost=rottweiler delete-key`) +- `RequiresMountsFor=/.bottlerocket /run/rottweiler` + +**Dependencies:** +- Before: `migrator.service`, `storewolf.service` +- Required by: `migrator.service`, `storewolf.service` + +#### prepare-local-fs.service (modified) + +**Drop-in:** `prepare-local-fs-ephemeral.conf` + +**Changes:** +- No-op: filesystem is created by `encrypt-unlock-local-fs.service` +- `BindsTo` `encrypt-unlock-local-fs.service` + +#### local.mount (modified) + +**Drop-in:** `local.mount.d/10-ephemeral.conf` (reuses the `local-mount-encrypted.conf` source; the mapper name is identical in both modes) + +**Changes:** +- Mounts `/dev/mapper/BOTTLEROCKET-DATA` instead of raw partition + +#### .bottlerocket.mount (modified) + +**Drop-in:** `bottlerocket-mount-ephemeral.conf` + +**Changes:** +- Mounts `/dev/mapper/BOTTLEROCKET-PRIVATE` instead of raw partition +- Requires `prepare-private-fs.service` + +#### repart-local.service (modified) + +**Drop-in:** `repart-local-ephemeral.conf` + +**Changes:** +- Masks the base `systemd-repart` grow (done in `encrypt-unlock-local-fs.service`) +- Keeps `systemd-growfs /local` to grow the filesystem to the resized mapper + ## TPM2 Measurements Bottlerocket extends TPM2 PCRs at various boot stages to establish a cryptographic chain of trust. @@ -299,6 +425,22 @@ cryptsetup luksFormat \ This matches systemd's behavior and avoids unnecessary key stretching. +### Plain Mode Formatting + +With `ephemeral-encryption-keys`, block devices are opened in plain mode. No header is written, +so encryption and attach are a single operation, and `--hash plain` uses the key bytes verbatim: + +```bash +cryptsetup open \ + --type plain \ + --cipher aes-xts-plain64 \ + --key-size 512 \ + --hash plain \ + --key-file=- \ + --keyfile-size=64 \ + +``` + ### fscrypt Configuration Directories are encrypted with: @@ -433,8 +575,12 @@ rottweiler check directory /.bottlerocket/datastore encrypted ### Source Code - `sources/rottweiler/` - Storage encryption helper implementation -- `packages/release/release.spec` - Service packaging +- `sources/bottlerocket-image-features/` - Parses `encrypted-storage` and `ephemeral-encryption-keys` image features +- `sources/api/apiserver/src/server/ephemeral_storage.rs` - Encrypts `EPHEMERAL-DATA` for `apiclient ephemeral-storage init` +- `packages/release/release.spec` - Service packaging; units split into `release-crypt` (mode-independent), `release-crypt-luks` (LUKS mode), and `release-ephemeral-crypt` (plain mode) - `packages/release/encrypt-*.service` - Encryption services - `packages/release/unlock-*.service` - Unlocking services +- `packages/release/encrypt-unlock-*.service` - Combined plain-mode encryption and unlock services +- `packages/release/run-rottweiler.mount` - temporary tmpfs keystore at `/run/rottweiler` - `packages/release/measure-*.service` - Measurement services - `packages/release/systemd-pcrphase-*.service` - Boot phase measurements