Skip to content

Commit

Permalink
Add persistent disk encryption
Browse files Browse the repository at this point in the history
Add flags to encrypt persistent partition on install:

* encrypt-persistent: flag to enable luks encryption on persistent
  partition.
* enroll-passphrase: string to enroll as passphrase to unlock partition.
* enroll-key-file: key-file to enroll as key to unlock partition.

During install this will invoke cryptsetup to create the LUKS partition
and during mount we use systemd-cryptsetup to attach the partition
before mounting the contained filesystem.

This also introduces some changes in the grub configuration, the
encrypted_volumes variable can be set in grub_oem_env during install to
configure which volumes are actually encrypted.

Using a config-file it is also possible to encrypt any extra-partitions
using the following syntax:

```yaml
install:
  extra-partitions:
    - Name: extra
      size: 100
      fs: ext4
      label: extra
      encryption:
        name: cr_extra
        key_slots:
        - slot: 1
          passphrase: "extrapass"
```

Signed-off-by: Fredrik Lönnegren <[email protected]>
  • Loading branch information
frelon committed Nov 12, 2024
1 parent 5f996b5 commit dd5dbf0
Show file tree
Hide file tree
Showing 18 changed files with 395 additions and 32 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/build_and_test_x86.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,18 @@ jobs:
runs-on: ubuntu-latest
outputs:
tests: ${{ steps.detect.outputs.tests }}
installertests: ${{ steps.detect.outputs.installertests }}
steps:
- id: detect
env:
FLAVOR: ${{ inputs.flavor }}
run: |
if [ "${FLAVOR}" == green ]; then
echo "tests=['test-upgrade', 'test-downgrade', 'test-recovery', 'test-fallback', 'test-fsck', 'test-grubfallback']" >> $GITHUB_OUTPUT
echo "installertests=['test-installer', 'test-encryption']" >> $GITHUB_OUTPUT
else
echo "tests=['test-active']" >> $GITHUB_OUTPUT
echo "installertests=['test-installer']" >> $GITHUB_OUTPUT
fi
tests-matrix:
Expand Down Expand Up @@ -255,6 +258,10 @@ jobs:
- build-iso
- detect
runs-on: ubuntu-latest
strategy:
matrix:
test: ${{ fromJson(needs.detect.outputs.installertests) }}
fail-fast: false
env:
FLAVOR: ${{ inputs.flavor }}
ARCH: x86_64
Expand Down Expand Up @@ -288,20 +295,20 @@ jobs:
sudo udevadm trigger --name-match=kvm
- name: Run installer test
run: |
make ISO=/tmp/elemental-${{ env.FLAVOR }}.${{ env.ARCH}}.iso ELMNTL_TARGETARCH=${{ env.ARCH }} ELMNTL_FIRMWARE=/usr/share/OVMF/OVMF_CODE.fd test-installer
make ISO=/tmp/elemental-${{ env.FLAVOR }}.${{ env.ARCH}}.iso ELMNTL_TARGETARCH=${{ env.ARCH }} ELMNTL_FIRMWARE=/usr/share/OVMF/OVMF_CODE.fd ${{ matrix.test }}
- name: Upload serial console for installer tests
uses: actions/upload-artifact@v4
if: always()
with:
name: serial-${{ env.ARCH }}-${{ env.FLAVOR }}-installer.log
name: serial-${{ env.ARCH }}-${{ env.FLAVOR }}-${{ matrix.test }}.log
path: tests/serial.log
if-no-files-found: error
overwrite: true
- name: Upload qemu stdout for installer tests
uses: actions/upload-artifact@v4
if: failure()
with:
name: vmstdout-${{ env.ARCH }}-${{ env.FLAVOR }}-installer.log
name: vmstdout-${{ env.ARCH }}-${{ env.FLAVOR }}-${{ matrix.test }}.log
path: tests/vmstdout
if-no-files-found: error
overwrite: true
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ RUN ARCH=$(uname -m); \
gptfdisk \
patterns-microos-selinux \
btrfsprogs \
snapper \
snapper \
cryptsetup \
lvm2 && \
zypper cc -a

Expand Down
13 changes: 13 additions & 0 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,19 @@ func applyKernelCmdline(r *types.RunConfig, mount *types.MountSpec) error {
Options: []string{"rw", "defaults"},
})
}
case "elemental.encrypted_volumes":
vols := strings.Split(split[1], ",")

for _, vol := range vols {
switch vol {
case "persistent":
mount.Persistent.Encrypted = true
mount.Persistent.Volume.Device = constants.PersistentDeviceMapperPath
default:
r.Logger.Warnf("Unknown encrypted volume '%s', skipping", vol)
}
}

}
}

Expand Down
7 changes: 7 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ func addPlatformFlags(cmd *cobra.Command) {
cmd.Flags().String("platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), "Platform to build the image for")
}

// addEncryptionFlags adds the disk encryption flag for install command
func addEncryptionFlags(cmd *cobra.Command) {
cmd.Flags().Bool("encrypt-persistent", false, "Encrypt the persistent data partition on install")
cmd.Flags().StringArray("enroll-passphrase", nil, "Clear text password to enroll as key for disk encryption")
cmd.Flags().StringArray("enroll-key-file", nil, "Key-files to enroll as keys for disk encryption")
}

type enum struct {
Allowed []string
Value string
Expand Down
1 change: 1 addition & 0 deletions cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func NewInstallCmd(root *cobra.Command, addCheckRoot bool) *cobra.Command {
addSharedInstallUpgradeFlags(c)
addLocalImageFlag(c)
addPlatformFlags(c)
addEncryptionFlags(c)
return c
}

Expand Down
1 change: 1 addition & 0 deletions examples/green/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ RUN ARCH=$(uname -m); \
btrfsmaintenance \
snapper \
xterm-resize \
cryptsetup \
${ADD_PKGS} && \
zypper clean --all

Expand Down
4 changes: 4 additions & 0 deletions make/Makefile.test
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ test-installer: prepare-installer-test
VM_PID=$$(scripts/run_vm.sh vmpid) go run $(GINKGO) $(GINKGO_ARGS) ./tests/installer
VM_PID=$$(scripts/run_vm.sh vmpid) go run $(GINKGO) $(GINKGO_ARGS) ./tests/smoke

.PHONY: test-encryption
test-encryption: prepare-installer-test
VM_PID=$$(scripts/run_vm.sh vmpid) go run $(GINKGO) $(GINKGO_ARGS) ./tests/encryption

.PHONY: test-smoke
test-smoke: test-active
VM_PID=$$(scripts/run_vm.sh vmpid) go run $(GINKGO) $(GINKGO_ARGS) ./tests/smoke
Expand Down
20 changes: 19 additions & 1 deletion pkg/action/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,18 @@ func RunMount(cfg *types.RunConfig, spec *types.MountSpec) error {
cfg.Logger.Info("Running mount command")

if spec.WriteFstab {
cfg.Logger.Debug("Generating inital sysroot fstab lines")
cfg.Logger.Debug("Generating initial sysroot fstab lines")
fstabData, err = InitialFstabData(cfg.Runner, spec.Sysroot)
if err != nil {
cfg.Logger.Errorf("Error mounting volumes: %s", err.Error())
return err
}
}

cfg.Logger.Debug("Mounting encrypted devices")
if err = MountEncryptedVolumes(cfg, spec); err != nil {
cfg.Logger.Errorf("Error mounting encrypted devices: %s", err.Error())
return err
}

cfg.Logger.Debug("Mounting volumes")
Expand Down Expand Up @@ -95,6 +100,19 @@ func RunMount(cfg *types.RunConfig, spec *types.MountSpec) error {
return nil
}

func MountEncryptedVolumes(cfg *types.RunConfig, spec *types.MountSpec) error {
if !spec.Persistent.Encrypted {
cfg.Logger.Debug("No encrypted devices specified")
return nil
}

data, err := cfg.Runner.Run("systemd-cryptsetup", "attach", "cr_persistent", "/dev/disk/by-partlabel/persistent")
if err != nil {
cfg.Logger.Errorf("Failed unlocking persistent partition: %s\nLogs: %s", err.Error(), string(data))
}
return err
}

func MountVolumes(cfg *types.RunConfig, spec *types.MountSpec) error {
var errs error

Expand Down
4 changes: 4 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ const (
PersistentStateDir = ".state"
RunningStateDir = "/run/initramfs/elemental-state" // TODO: converge this constant with StateDir/RecoveryDir when moving to elemental-rootfs as default rootfs feature.

// Disk encryption constants
PersistentDeviceMapperName = "cr_persistent"
PersistentDeviceMapperPath = "/dev/mapper/" + PersistentDeviceMapperName

// Running mode sentinel files
ActiveMode = "/run/elemental/active_mode"
PassiveMode = "/run/elemental/passive_mode"
Expand Down
21 changes: 18 additions & 3 deletions pkg/elemental/elemental.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,37 @@ func createAndFormatPartition(c types.Config, disk *partitioner.Disk, part *type
if err != nil {
return err
}

mappedDev := partDev
if part.Encryption != nil {
c.Logger.Debugf("Encrypting partition %s into %s, using %v slots", partDev, part.Encryption.MappedDeviceName, len(part.Encryption.KeySlots))
err := partitioner.EncryptDevice(c.Runner, partDev, part.Encryption.MappedDeviceName, part.Encryption.KeySlots)
if err != nil {
c.Logger.Errorf("Failed encrypting %s partition", partDev)
return err
}

mappedDev = fmt.Sprintf("/dev/mapper/%s", part.Encryption.MappedDeviceName)
}

c.Logger.Debugf("Using device %s", mappedDev)

if part.FS != "" {
c.Logger.Debugf("Formatting partition with label %s", part.FilesystemLabel)
err = partitioner.FormatDevice(c.Runner, partDev, part.FS, part.FilesystemLabel)
err = partitioner.FormatDevice(c.Runner, mappedDev, part.FS, part.FilesystemLabel)
if err != nil {
c.Logger.Errorf("Failed formatting partition %s", part.Name)
return err
}
} else {
c.Logger.Debugf("Wipe file system on %s", part.Name)
err = disk.WipeFsOnPartition(partDev)
err = disk.WipeFsOnPartition(mappedDev)
if err != nil {
c.Logger.Errorf("Failed to wipe filesystem of partition %s", partDev)
return err
}
}
part.Path = partDev
part.Path = mappedDev
return nil
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# bootargs.cfg inherits from grub.cfg several context variables:
# 'img' => defines the image path to boot from. Active img is statically defined, does not require a value
# 'mode' => active/passive/recovery, mode to boot
# 'state_label' => label of the state partition filesystem
# 'oem_label' => label of the oem partition filesystem
# 'recovery_label' => label of the recovery partition filesystem
# 'snapshotter' => snapshotter type, assumes loopdevice type if undefined
# 'snap_arg' => kernel args for snapshotter
# 'encrypted_volumes' => comma-separated list of encrypted volume names
#
# In addition bootargs.cfg is responsible of setting the following variables:
# 'kernelcmd' => essential kernel command line parameters (all elemental specific and non elemental specific)
Expand All @@ -20,7 +23,7 @@ else
if [ "${snapshotter}" == "btrfs" ]; then
set snap_arg="elemental.snapshotter=btrfs"
fi
set kernelcmd="console=tty1 console=ttyS0 root=LABEL=${state_label} ${img_arg} ${snap_arg} elemental.mode=${mode} elemental.oemlabel=${oem_label} panic=5 security=selinux fsck.mode=force fsck.repair=yes"
set kernelcmd="console=tty1 console=ttyS0 root=LABEL=${state_label} ${img_arg} ${snap_arg} elemental.mode=${mode} elemental.oemlabel=${oem_label} panic=5 security=selinux fsck.mode=force fsck.repair=yes elemental.encrypted_volumes=${encrypted_volumes}"
fi

set kernel=/${root_subpath}boot/vmlinuz
Expand Down
76 changes: 76 additions & 0 deletions pkg/partitioner/cryptsetup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
Copyright © 2022 - 2024 SUSE LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package partitioner

import (
"errors"
"fmt"
"os/exec"
"strings"

"github.com/rancher/elemental-toolkit/v2/pkg/types"
)

func EncryptDevice(runner types.Runner, device, mappedName string, slots []types.KeySlot) error {
logger := runner.GetLogger()

if len(slots) == 0 {
return fmt.Errorf("Needs at least 1 key-slot to encrypt %s", device)
}

firstSlot := slots[0]

cmd := runner.InitCmd("cryptsetup", "luksFormat", "--key-slot", fmt.Sprintf("%d", firstSlot.Slot), device, "-")
err := unlockCmd(cmd, firstSlot)
if err != nil {
logger.Errorf("Error generating unlock command for device '%s': %s", device, err.Error())
return err
}

stdout, err := runner.RunCmd(cmd)
if err != nil {
logger.Errorf("Error formatting device %s: %s", device, stdout)
return err
}

cmd = runner.InitCmd("cryptsetup", "open", device, mappedName)

if err = unlockCmd(cmd, firstSlot); err != nil {
return err
}

stdout, err = runner.RunCmd(cmd)
if err != nil {
logger.Errorf("Error opening device %s: %s", device, stdout)
}

return err
}

func unlockCmd(cmd *exec.Cmd, slot types.KeySlot) error {
if slot.Passphrase != "" {
cmd.Stdin = strings.NewReader(string(slot.Passphrase))
return nil
}

if slot.KeyFile != "" {
cmd.Args = append(cmd.Args, "--key-file", slot.KeyFile)
return nil
}

return errors.New("Unknown key slot authorization")
}
Loading

0 comments on commit dd5dbf0

Please sign in to comment.