From 3ec9bdfb013c54bb0a937b07da07454e2ba43454 Mon Sep 17 00:00:00 2001 From: Adphi Date: Thu, 23 Feb 2023 15:02:12 +0100 Subject: [PATCH] luks: implements support for Alpine Signed-off-by: Adphi --- Dockerfile | 1 + builder.go | 115 +++++++++++++++++++------ builder_test.go | 2 +- cmd/d2vm/build.go | 5 ++ cmd/d2vm/convert.go | 5 ++ cmd/d2vm/flags.go | 2 + convert.go | 4 +- convert_options.go | 8 ++ dockerfile.go | 5 +- docs/content/reference/d2vm_build.md | 1 + docs/content/reference/d2vm_convert.md | 1 + templates/alpine.Dockerfile | 9 ++ templates/debian.Dockerfile | 3 + templates/ubuntu.Dockerfile | 3 + 14 files changed, 133 insertions(+), 31 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3825643..aa6f175 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,7 @@ RUN apt-get update && \ mount \ tar \ extlinux \ + cryptsetup \ qemu-utils && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/builder.go b/builder.go index 3ce7600..c26a8e4 100644 --- a/builder.go +++ b/builder.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/c2h5oh/datasize" + "github.com/google/uuid" "github.com/sirupsen/logrus" "go.uber.org/multierr" @@ -129,19 +130,34 @@ type builder struct { mbrPath string - loDevice string - bootPart string - rootPart string - bootUUID string - rootUUID string + loDevice string + bootPart string + rootPart string + cryptPart string + cryptRoot string + mappedCryptRoot string + bootUUID string + rootUUID string + cryptUUID string + + luksPassword string cmdLineExtra string } -func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64) (Builder, error) { +func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64, luksPassword string) (Builder, error) { if err := checkDependencies(); err != nil { return nil, err } + if luksPassword != "" { + // TODO(adphi): remove this check when we support luks encryption on other distros + if osRelease.ID != ReleaseAlpine { + return nil, fmt.Errorf("luks encryption is only supported on alpine") + } + if !splitBoot { + return nil, fmt.Errorf("luks encryption requires split boot") + } + } f := strings.ToLower(format) valid := false for _, v := range formats { @@ -202,6 +218,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, cmdLineExtra: cmdLineExtra, splitBoot: splitBoot, bootSize: bootSize, + luksPassword: luksPassword, } if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil { return nil, err @@ -302,12 +319,44 @@ func (b *builder) mountImg(ctx context.Context) error { } else { b.rootPart = b.bootPart } - logrus.Infof("creating raw image file system") - if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil { - return err - } - if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil { - return err + if b.isLuksEnabled() { + logrus.Infof("encrypting root partition") + f, err := os.CreateTemp("", "key") + if err != nil { + return err + } + defer f.Close() + defer os.Remove(f.Name()) + if _, err := f.WriteString(b.luksPassword); err != nil { + return err + } + // cryptsetup luksFormat --batch-mode --verify-passphrase --type luks2 $ROOT_DEVICE $KEY_FILE + if err := exec.Run(ctx, "cryptsetup", "luksFormat", "--batch-mode", "--type", "luks2", b.rootPart, f.Name()); err != nil { + return err + } + b.cryptRoot = fmt.Sprintf("d2vm-%s-root", uuid.New().String()) + // cryptsetup open -d $KEY_FILE $ROOT_DEVICE $ROOT_LABEL + if err := exec.Run(ctx, "cryptsetup", "open", "--key-file", f.Name(), b.rootPart, b.cryptRoot); err != nil { + return err + } + b.cryptPart = b.rootPart + b.rootPart = "/dev/mapper/root" + b.mappedCryptRoot = filepath.Join("/dev/mapper", b.cryptRoot) + logrus.Infof("creating raw image file system") + if err := exec.Run(ctx, "mkfs.ext4", b.mappedCryptRoot); err != nil { + return err + } + if err := exec.Run(ctx, "mount", b.mappedCryptRoot, b.mntPoint); err != nil { + return err + } + } else { + logrus.Infof("creating raw image file system") + if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil { + return err + } + if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil { + return err + } } if !b.splitBoot { return nil @@ -328,20 +377,17 @@ func (b *builder) unmountImg(ctx context.Context) error { logrus.Infof("unmounting raw image") var merr error if b.splitBoot { - if err := exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")); err != nil { - merr = multierr.Append(merr, err) - } + merr = multierr.Append(merr, exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot"))) } - if err := exec.Run(ctx, "umount", b.mntPoint); err != nil { - merr = multierr.Append(merr, err) + merr = multierr.Append(merr, exec.Run(ctx, "umount", b.mntPoint)) + if b.isLuksEnabled() { + merr = multierr.Append(merr, exec.Run(ctx, "cryptsetup", "close", b.cryptRoot)) } - if err := exec.Run(ctx, "kpartx", "-d", b.loDevice); err != nil { - merr = multierr.Append(merr, err) - } - if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil { - merr = multierr.Append(merr, err) - } - return merr + return multierr.Combine( + merr, + exec.Run(ctx, "kpartx", "-d", b.loDevice), + exec.Run(ctx, "losetup", "-d", b.loDevice), + ) } func (b *builder) copyRootFS(ctx context.Context) error { @@ -369,6 +415,12 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { if err != nil { return err } + if b.isLuksEnabled() { + b.cryptUUID, err = diskUUID(ctx, b.cryptPart) + if err != nil { + return err + } + } fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot ext4 errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID) } else { b.bootUUID = b.rootUUID @@ -449,7 +501,14 @@ func (b *builder) installKernel(ctx context.Context) error { if err != nil { return err } - if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.rootUUID, b.cmdLineExtra), perm); err != nil { + var cfg string + if b.isLuksEnabled() { + cfg = fmt.Sprintf(sysconfig, b.rootUUID, fmt.Sprintf("%s root=/dev/mapper/root cryptdm=root", b.cmdLineExtra)) + cfg = strings.Replace(cfg, "root=UUID="+b.rootUUID, "cryptroot=UUID="+b.cryptUUID, 1) + } else { + cfg = fmt.Sprintf(sysconfig, b.rootUUID, b.cmdLineExtra) + } + if err := b.chWriteFile("/boot/syslinux.cfg", cfg, perm); err != nil { return err } return nil @@ -483,6 +542,10 @@ func (b *builder) chPath(path string) string { return fmt.Sprintf("%s%s", b.mntPoint, path) } +func (b *builder) isLuksEnabled() bool { + return b.luksPassword != "" +} + func (b *builder) Close() error { return b.img.Close() } @@ -498,7 +561,7 @@ func block(path string, size uint64) error { func checkDependencies() error { var merr error - for _, v := range []string{"mount", "blkid", "tar", "losetup", "parted", "partprobe", "qemu-img", "extlinux", "dd", "mkfs"} { + for _, v := range []string{"mount", "blkid", "tar", "losetup", "parted", "kpartx", "qemu-img", "extlinux", "dd", "mkfs.ext4", "cryptsetup"} { if _, err := exec2.LookPath(v); err != nil { merr = multierr.Append(merr, err) } diff --git a/builder_test.go b/builder_test.go index a317e06..571a46c 100644 --- a/builder_test.go +++ b/builder_test.go @@ -42,7 +42,7 @@ func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, init sys, err := sysconfig(r) require.NoError(t, err) assert.Equal(t, sysconf, sys) - d, err := NewDockerfile(r, img, "root", "") + d, err := NewDockerfile(r, img, "root", "", false) require.NoError(t, err) logrus.Infof("docker image based on %s", d.Release.Name) p := filepath.Join(tmpPath, docker.FormatImgName(img)) diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index aa75295..4d2498c 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -79,6 +79,10 @@ var ( } return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...) } + if luksPassword != "" && !splitBoot { + logrus.Warnf("luks password is set: enabling split boot") + splitBoot = true + } size, err := parseSize(size) if err != nil { return err @@ -109,6 +113,7 @@ var ( d2vm.WithRaw(raw), d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), + d2vm.WithLuksPassword(luksPassword), ); err != nil { return err } diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index b231699..1022a4d 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -51,6 +51,10 @@ var ( } return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...) } + if luksPassword != "" && !splitBoot { + logrus.Warnf("luks password is set: enabling split boot") + splitBoot = true + } img := args[0] tag := "latest" if parts := strings.Split(img, ":"); len(parts) > 1 { @@ -97,6 +101,7 @@ var ( d2vm.WithRaw(raw), d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), + d2vm.WithLuksPassword(luksPassword), ); err != nil { return err } diff --git a/cmd/d2vm/flags.go b/cmd/d2vm/flags.go index d64a900..2377a1e 100644 --- a/cmd/d2vm/flags.go +++ b/cmd/d2vm/flags.go @@ -35,6 +35,7 @@ var ( networkManager string splitBoot bool bootSize uint64 + luksPassword string ) func buildFlags() *pflag.FlagSet { @@ -50,5 +51,6 @@ func buildFlags() *pflag.FlagSet { flags.BoolVar(&push, "push", false, "Push the container disk image to the registry") flags.BoolVar(&splitBoot, "split-boot", false, "Split the boot partition from the root partition") flags.Uint64Var(&bootSize, "boot-size", 100, "Size of the boot partition in MB") + flags.StringVar(&luksPassword, "luks-password", "", "Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted") return flags } diff --git a/convert.go b/convert.go index b7f3893..a7321ee 100644 --- a/convert.go +++ b/convert.go @@ -46,7 +46,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error { return err } if !o.raw { - d, err := NewDockerfile(r, img, o.password, o.networkManager) + d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "") if err != nil { return err } @@ -79,7 +79,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error { if format == "" { format = "raw" } - b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootSize) + b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootSize, o.luksPassword) if err != nil { return err } diff --git a/convert_options.go b/convert_options.go index 4bc4533..a5b68ab 100644 --- a/convert_options.go +++ b/convert_options.go @@ -26,6 +26,8 @@ type convertOptions struct { splitBoot bool bootSize uint64 + + luksPassword string } func WithSize(size uint64) ConvertOption { @@ -75,3 +77,9 @@ func WithBootSize(bootSize uint64) ConvertOption { o.bootSize = bootSize } } + +func WithLuksPassword(password string) ConvertOption { + return func(o *convertOptions) { + o.luksPassword = password + } +} diff --git a/dockerfile.go b/dockerfile.go index a2d60fe..8bfa4df 100644 --- a/dockerfile.go +++ b/dockerfile.go @@ -64,6 +64,7 @@ type Dockerfile struct { Password string Release OSRelease NetworkManager NetworkManager + Luks bool tmpl *template.Template } @@ -71,8 +72,8 @@ func (d Dockerfile) Render(w io.Writer) error { return d.tmpl.Execute(w, d) } -func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager) (Dockerfile, error) { - d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager} +func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks bool) (Dockerfile, error) { + d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks} var net NetworkManager switch release.ID { case ReleaseDebian: diff --git a/docs/content/reference/d2vm_build.md b/docs/content/reference/d2vm_build.md index 34d3241..6c93e67 100644 --- a/docs/content/reference/d2vm_build.md +++ b/docs/content/reference/d2vm_build.md @@ -15,6 +15,7 @@ d2vm build [context directory] [flags] -f, --file string Name of the Dockerfile --force Override output qcow2 image -h, --help help for build + --luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted --network-manager string Network manager to use for the image: none, netplan, ifupdown -o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2") -p, --password string Optional root user password diff --git a/docs/content/reference/d2vm_convert.md b/docs/content/reference/d2vm_convert.md index 7624b30..49449da 100644 --- a/docs/content/reference/d2vm_convert.md +++ b/docs/content/reference/d2vm_convert.md @@ -13,6 +13,7 @@ d2vm convert [docker image] [flags] --boot-size uint Size of the boot partition in MB (default 100) --force Override output qcow2 image -h, --help help for convert + --luks-password string Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted --network-manager string Network manager to use for the image: none, netplan, ifupdown -o, --output string The output image, the extension determine the image format, raw will be used if none. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2") -p, --password string Optional root user password diff --git a/templates/alpine.Dockerfile b/templates/alpine.Dockerfile index 7f48e20..22f1d4d 100644 --- a/templates/alpine.Dockerfile +++ b/templates/alpine.Dockerfile @@ -6,6 +6,9 @@ RUN apk update --no-cache && \ apk add \ util-linux \ linux-virt \ +{{- if .Luks }} + cryptsetup \ +{{- end }} {{- if ge .Release.VersionID "3.17" }} busybox-openrc \ busybox-mdev-openrc \ @@ -29,3 +32,9 @@ allow-hotplug eth0\n\ iface eth0 inet dhcp\n\ ' > /etc/network/interfaces {{ end }} + +{{- if .Luks }} +RUN source /etc/mkinitfs/mkinitfs.conf && \ + echo "features=\"${features} cryptsetup\"" > /etc/mkinitfs/mkinitfs.conf && \ + mkinitfs $(ls /lib/modules) +{{- end }} diff --git a/templates/debian.Dockerfile b/templates/debian.Dockerfile index 62ccb1d..37d4fa5 100644 --- a/templates/debian.Dockerfile +++ b/templates/debian.Dockerfile @@ -4,6 +4,9 @@ USER root RUN apt-get -y update && \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ +{{- if .Luks }} + cryptsetup \ +{{- end }} linux-image-amd64 RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ diff --git a/templates/ubuntu.Dockerfile b/templates/ubuntu.Dockerfile index 3998a19..1397ab3 100644 --- a/templates/ubuntu.Dockerfile +++ b/templates/ubuntu.Dockerfile @@ -6,6 +6,9 @@ RUN apt-get update -y && \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ linux-image-virtual \ initramfs-tools \ +{{- if .Luks }} + cryptsetup \ +{{- end }} systemd-sysv \ systemd \ dbus \