From f830c4e552628d9271620cc6fae421c43309085d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20L=C3=B6nnegren?= Date: Mon, 22 Apr 2024 15:18:09 +0200 Subject: [PATCH] Add persistent disk encryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Signed-off-by: Fredrik Lönnegren --- .github/workflows/build_and_test_x86.yaml | 13 ++- Dockerfile | 1 + cmd/config/config.go | 13 +++ cmd/flags.go | 7 ++ cmd/install.go | 1 + examples/green/Dockerfile | 1 + make/Makefile.test | 4 + pkg/action/mount.go | 20 +++- pkg/constants/constants.go | 4 + pkg/elemental/elemental.go | 21 +++- .../grub-config/etc/elemental/grub.cfg | 6 ++ .../etc/elemental/bootargs.cfg | 5 +- pkg/partitioner/cryptsetup.go | 76 +++++++++++++++ pkg/types/config.go | 96 ++++++++++++++----- pkg/types/grub.go | 4 + pkg/types/sensitive.go | 50 ++++++++++ tests/encryption/installer_encryption_test.go | 76 +++++++++++++++ tests/encryption/tests_suite_test.go | 13 +++ 18 files changed, 380 insertions(+), 31 deletions(-) create mode 100644 pkg/partitioner/cryptsetup.go create mode 100644 pkg/types/sensitive.go create mode 100644 tests/encryption/installer_encryption_test.go create mode 100644 tests/encryption/tests_suite_test.go diff --git a/.github/workflows/build_and_test_x86.yaml b/.github/workflows/build_and_test_x86.yaml index cfd02471da6..fcc32ec955e 100644 --- a/.github/workflows/build_and_test_x86.yaml +++ b/.github/workflows/build_and_test_x86.yaml @@ -164,6 +164,7 @@ jobs: runs-on: ubuntu-latest outputs: tests: ${{ steps.detect.outputs.tests }} + installertests: ${{ steps.detect.outputs.installertests }} steps: - id: detect env: @@ -171,8 +172,10 @@ jobs: 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: @@ -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 @@ -288,12 +295,12 @@ 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 @@ -301,7 +308,7 @@ jobs: 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 diff --git a/Dockerfile b/Dockerfile index 68b00c81771..200a0ecd47c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,6 +57,7 @@ RUN ARCH=$(uname -m); \ gptfdisk \ patterns-microos-selinux \ btrfsprogs \ + cryptsetup \ lvm2 && \ zypper cc -a diff --git a/cmd/config/config.go b/cmd/config/config.go index 285a1f6cf06..3c3fb337709 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -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) + } + } + } } diff --git a/cmd/flags.go b/cmd/flags.go index 6b537cd43bd..cc66ff5ef75 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -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 diff --git a/cmd/install.go b/cmd/install.go index 04b65b767c7..97f49176b7b 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -123,6 +123,7 @@ func NewInstallCmd(root *cobra.Command, addCheckRoot bool) *cobra.Command { addSharedInstallUpgradeFlags(c) addLocalImageFlag(c) addPlatformFlags(c) + addEncryptionFlags(c) return c } diff --git a/examples/green/Dockerfile b/examples/green/Dockerfile index c240ea32851..f7ec83a4695 100644 --- a/examples/green/Dockerfile +++ b/examples/green/Dockerfile @@ -65,6 +65,7 @@ RUN ARCH=$(uname -m); \ btrfsmaintenance \ snapper \ xterm-resize \ + cryptsetup \ ${ADD_PKGS} && \ zypper clean --all diff --git a/make/Makefile.test b/make/Makefile.test index 8acf1d8ebab..b1fa37a99ae 100644 --- a/make/Makefile.test +++ b/make/Makefile.test @@ -49,6 +49,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 diff --git a/pkg/action/mount.go b/pkg/action/mount.go index 1194675a0df..4996f82ccd7 100644 --- a/pkg/action/mount.go +++ b/pkg/action/mount.go @@ -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") @@ -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 diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 8d601d1b54b..e5bd1a1ac43 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -107,6 +107,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" diff --git a/pkg/elemental/elemental.go b/pkg/elemental/elemental.go index cef5628fe78..84df43d9afe 100644 --- a/pkg/elemental/elemental.go +++ b/pkg/elemental/elemental.go @@ -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 } diff --git a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg index 060a5def4ca..87737e03176 100644 --- a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg +++ b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg @@ -54,6 +54,12 @@ insmod gfxterm insmod loopback insmod squash4 +if [ "${encrypted_volumes}" ]; then + insmod luks2 + insmod gcry_rijndael + insmod gcry_sha256 +fi + ## Sets a loopback device volume for a given image function set_loopdevice { set volume="loop" diff --git a/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg b/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg index 82030070752..cf16affc4b5 100644 --- a/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg +++ b/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg @@ -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) @@ -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=/boot/vmlinuz diff --git a/pkg/partitioner/cryptsetup.go b/pkg/partitioner/cryptsetup.go new file mode 100644 index 00000000000..b031a57b35e --- /dev/null +++ b/pkg/partitioner/cryptsetup.go @@ -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") +} diff --git a/pkg/types/config.go b/pkg/types/config.go index 42a174d6f51..49c790d7c3e 100644 --- a/pkg/types/config.go +++ b/pkg/types/config.go @@ -215,20 +215,23 @@ func KeyValuePairFromData(data interface{}) (KeyValuePair, error) { // InstallSpec struct represents all the installation action details type InstallSpec struct { - Target string `yaml:"target,omitempty" mapstructure:"target"` - Firmware string - PartTable string - Partitions ElementalPartitions `yaml:"partitions,omitempty" mapstructure:"partitions"` - ExtraPartitions PartitionList `yaml:"extra-partitions,omitempty" mapstructure:"extra-partitions"` - NoFormat bool `yaml:"no-format,omitempty" mapstructure:"no-format"` - Force bool `yaml:"force,omitempty" mapstructure:"force"` - CloudInit []string `yaml:"cloud-init,omitempty" mapstructure:"cloud-init"` - Iso string `yaml:"iso,omitempty" mapstructure:"iso"` - GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` - System *ImageSource `yaml:"system,omitempty" mapstructure:"system"` - RecoverySystem Image `yaml:"recovery-system,omitempty" mapstructure:"recovery-system"` - DisableBootEntry bool `yaml:"disable-boot-entry,omitempty" mapstructure:"disable-boot-entry"` - SnapshotLabels KeyValuePair `yaml:"snapshot-labels,omitempty" mapstructure:"snapshot-labels"` + Target string `yaml:"target,omitempty" mapstructure:"target"` + Firmware string + PartTable string + Partitions ElementalPartitions `yaml:"partitions,omitempty" mapstructure:"partitions"` + ExtraPartitions PartitionList `yaml:"extra-partitions,omitempty" mapstructure:"extra-partitions"` + NoFormat bool `yaml:"no-format,omitempty" mapstructure:"no-format"` + Force bool `yaml:"force,omitempty" mapstructure:"force"` + CloudInit []string `yaml:"cloud-init,omitempty" mapstructure:"cloud-init"` + Iso string `yaml:"iso,omitempty" mapstructure:"iso"` + GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` + System *ImageSource `yaml:"system,omitempty" mapstructure:"system"` + RecoverySystem Image `yaml:"recovery-system,omitempty" mapstructure:"recovery-system"` + DisableBootEntry bool `yaml:"disable-boot-entry,omitempty" mapstructure:"disable-boot-entry"` + EncryptPersistent bool `yaml:"encrypt-persistent,omitempty" mapstructure:"encrypt-persistent"` + SnapshotLabels KeyValuePair `yaml:"snapshot-labels,omitempty" mapstructure:"snapshot-labels"` + EnrollPassphrases []SensitiveString `yaml:"enroll-passphrase,omitempty" mapstructure:"enroll-passphrase"` + EnrollKeyFiles []string `yaml:"enroll-key-file,omitempty" mapstructure:"enroll-key-file"` } // Sanitize checks the consistency of the struct, returns error @@ -261,6 +264,36 @@ func (i *InstallSpec) Sanitize() error { } } + // Setup disk encryption + if i.EncryptPersistent { + slot := 0 + keySlots := []KeySlot{} + if i.EnrollPassphrases != nil { + for _, passphrase := range i.EnrollPassphrases { + keySlots = append(keySlots, KeySlot{ + Slot: slot, + Passphrase: passphrase, + }) + slot++ + } + } + + if i.EnrollKeyFiles != nil { + for _, keyfile := range i.EnrollKeyFiles { + keySlots = append(keySlots, KeySlot{ + Slot: slot, + KeyFile: keyfile, + }) + slot++ + } + } + + i.Partitions.Persistent.Encryption = &PartitionEncryption{ + MappedDeviceName: constants.PersistentDeviceMapperName, + KeySlots: keySlots, + } + } + if extraPartsSizeCheck > 1 { return fmt.Errorf("more than one extra partition has its size set to 0. Only one partition can have its size set to 0 which means that it will take all the available disk space in the device") } @@ -301,9 +334,10 @@ type VolumeMount struct { // PersistentMounts struct contains settings for which paths to mount as // persistent type PersistentMounts struct { - Mode string `yaml:"mode,omitempty" mapstructure:"mode"` - Paths []string `yaml:"paths,omitempty" mapstructure:"paths"` - Volume VolumeMount `yaml:"volume,omitempty" mapstructure:"volume"` + Mode string `yaml:"mode,omitempty" mapstructure:"mode"` + Paths []string `yaml:"paths,omitempty" mapstructure:"paths"` + Volume VolumeMount `yaml:"volume,omitempty" mapstructure:"volume"` + Encrypted bool `yaml:"encrypted,omitempty" mapstructure:"encrypted"` } // EphemeralMounts contains information about the RW overlay mounted over the @@ -465,17 +499,16 @@ func (u *UpgradeSpec) SanitizeForRecoveryOnly() error { // Partition struct represents a partition with its commonly configurable values, size in MiB type Partition struct { Name string - FilesystemLabel string `yaml:"label,omitempty" mapstructure:"label"` - Size uint `yaml:"size,omitempty" mapstructure:"size"` - FS string `yaml:"fs,omitempty" mapstructure:"fs"` - Flags []string `yaml:"flags,omitempty" mapstructure:"flags"` + FilesystemLabel string `yaml:"label,omitempty" mapstructure:"label"` + Size uint `yaml:"size,omitempty" mapstructure:"size"` + FS string `yaml:"fs,omitempty" mapstructure:"fs"` + Flags []string `yaml:"flags,omitempty" mapstructure:"flags"` + Encryption *PartitionEncryption `yaml:"encryption,omitempty" mapstructure:"encryption"` MountPoint string Path string Disk string } -type PartitionList []*Partition - // ToImage returns an image object that matches the partition. This is helpful if the partition // is managed as an image. func (p Partition) ToImage() *Image { @@ -489,6 +522,23 @@ func (p Partition) ToImage() *Image { } } +// PartitionEncryption contains information needed for encrypting a partition +type PartitionEncryption struct { + // MappedDeviceName is the mapped device usable after encryption, eg cr_root + MappedDeviceName string `yaml:"name,omitempty" mapstructure:"name"` + // KeySlots contains all key-slots to write to the LUKS-header + KeySlots []KeySlot `yaml:"key_slots,omitempty" mapstructure:"key_slots"` +} + +// KeySlot is a key-slot in a LUKS-header +type KeySlot struct { + Slot int + Passphrase SensitiveString + KeyFile string +} + +type PartitionList []*Partition + // GetByName gets a partitions by its name from the PartitionList func (pl PartitionList) GetByName(name string) *Partition { var part *Partition diff --git a/pkg/types/grub.go b/pkg/types/grub.go index 99f58f1bb9e..8eded62316d 100644 --- a/pkg/types/grub.go +++ b/pkg/types/grub.go @@ -29,6 +29,10 @@ func (i InstallSpec) GetGrubLabels() map[string]string { if i.Partitions.Persistent != nil { grubEnv["persistent_label"] = i.Partitions.Persistent.FilesystemLabel + + if i.Partitions.Persistent.Encryption != nil { + grubEnv["encrypted_volumes"] = "persistent" + } } return grubEnv diff --git a/pkg/types/sensitive.go b/pkg/types/sensitive.go new file mode 100644 index 00000000000..49148b15fd2 --- /dev/null +++ b/pkg/types/sensitive.go @@ -0,0 +1,50 @@ +/* +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 types + +import ( + "encoding" + "encoding/json" + "fmt" + + "gopkg.in/yaml.v3" +) + +var ( + _ fmt.Formatter = (*SensitiveString)(nil) + _ json.Marshaler = (*SensitiveString)(nil) + _ yaml.Marshaler = (*SensitiveString)(nil) + _ encoding.TextMarshaler = (*SensitiveString)(nil) +) + +type SensitiveString string + +func (s SensitiveString) Format(f fmt.State, _ rune) { + _, _ = f.Write([]byte("***")) +} + +func (s SensitiveString) MarshalJSON() ([]byte, error) { + return json.Marshal(string("***")) +} + +func (s SensitiveString) MarshalYAML() (any, error) { + return json.Marshal(string("***")) +} + +func (s SensitiveString) MarshalText() (text []byte, err error) { + return []byte("***"), nil +} diff --git a/tests/encryption/installer_encryption_test.go b/tests/encryption/installer_encryption_test.go new file mode 100644 index 00000000000..f6b59c29ebb --- /dev/null +++ b/tests/encryption/installer_encryption_test.go @@ -0,0 +1,76 @@ +package cos_test + +import ( + "fmt" + "math/rand" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + sut "github.com/rancher/elemental-toolkit/v2/tests/vm" +) + +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func generatePassphrase(length int) string { + b := make([]byte, length) + + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + + return string(b) +} + +var _ = Describe("Elemental Installer encryption tests", func() { + var s *sut.SUT + + BeforeEach(func() { + s = sut.NewSUT() + s.EventuallyConnects() + }) + + Context("Using efi", func() { + // EFI variant tests is not able to set the boot order and there is no plan to let the user do so according to virtualbox + // So we need to manually eject/insert the cd to force the boot order. CD inserted -> boot from cd, CD empty -> boot from disk + BeforeEach(func() { + // Assert we are booting from CD before running the tests + By("Making sure we booted from CD") + ExpectWithOffset(1, s.BootFrom()).To(Equal(sut.LiveCD)) + }) + AfterEach(func() { + if CurrentSpecReport().Failed() { + s.GatherAllLogs() + } + }) + + Context("partition layout tests", func() { + Context("with partition layout", func() { + It("performs a standard install", func() { + passphrase := generatePassphrase(10) + By(fmt.Sprintf("Running the elemental install with passphrase '%s'", passphrase)) + out, err := s.Command(s.ElementalCmd("install", "--squash-no-compression", "/dev/vda", "--encrypt-persistent", "--enroll-passphrase", passphrase)) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(ContainSubstring("Mounting disk partitions")) + Expect(out).To(ContainSubstring("Partitioning device...")) + Expect(out).To(ContainSubstring("Running after-install hook")) + + // Mount OEM before changing boot entry + _, err = s.Command("mount /dev/disk/by-partlabel/oem /oem") + Expect(err).ToNot(HaveOccurred()) + + err = s.ChangeBootOnce(sut.Recovery) + Expect(err).ToNot(HaveOccurred()) + s.Reboot() + s.AssertBootedFrom(sut.Recovery) + + By("Mounting cr_persistent") + + // Verify we can mount the persistent partition + _, err = s.Command(fmt.Sprintf("echo %s | cryptsetup open --type luks /dev/disk/by-partlabel/persistent cr_persistent", passphrase)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + }) +}) diff --git a/tests/encryption/tests_suite_test.go b/tests/encryption/tests_suite_test.go new file mode 100644 index 00000000000..ba042c6b82b --- /dev/null +++ b/tests/encryption/tests_suite_test.go @@ -0,0 +1,13 @@ +package cos_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestTests(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Elemental Installer test Suite") +}