Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 147 additions & 1 deletion docs/ENCRYPTED_STORAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Comment thread
piyush-jena marked this conversation as resolved.

```bash
cryptsetup open \
--type plain \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash plain \
--key-file=- \
--keyfile-size=64 \
<device> <name>
```

### fscrypt Configuration

Directories are encrypted with:
Expand Down Expand Up @@ -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
8 changes: 0 additions & 8 deletions packages/release/bottlerocket-mount-encrypted.conf

This file was deleted.

6 changes: 6 additions & 0 deletions packages/release/bottlerocket-mount-ephemeral.conf
Original file line number Diff line number Diff line change
@@ -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
4 changes: 0 additions & 4 deletions packages/release/encrypt-datastore-ephemeral.conf

This file was deleted.

7 changes: 0 additions & 7 deletions packages/release/encrypt-datastore-private-luks.conf

This file was deleted.

4 changes: 0 additions & 4 deletions packages/release/encrypt-local-fs-ephemeral.conf

This file was deleted.

27 changes: 0 additions & 27 deletions packages/release/encrypt-private-fs.service

This file was deleted.

28 changes: 28 additions & 0 deletions packages/release/encrypt-unlock-datastore.service
Original file line number Diff line number Diff line change
@@ -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
Comment thread
piyush-jena marked this conversation as resolved.

[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
43 changes: 43 additions & 0 deletions packages/release/encrypt-unlock-local-fs.service
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions packages/release/prepare-local-fs-ephemeral.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[Unit]
BindsTo=encrypt-unlock-local-fs.service
After=encrypt-unlock-local-fs.service

[Service]
ExecStart=
ExecStart=/usr/bin/true
4 changes: 2 additions & 2 deletions packages/release/prepare-private-fs.service
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading