Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add persistent disk encryption #2219

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those would also make sense on reset command.

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,11 @@
# 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
frelon marked this conversation as resolved.
Show resolved Hide resolved
# '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
# '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 +22,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
Loading