luks: implements support for Alpine

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
Adphi 2023-02-23 15:02:12 +01:00 committed by Adphi
parent cab7d8badd
commit 3ec9bdfb01
14 changed files with 133 additions and 31 deletions

View File

@ -38,6 +38,7 @@ RUN apt-get update && \
mount \ mount \
tar \ tar \
extlinux \ extlinux \
cryptsetup \
qemu-utils && \ qemu-utils && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

View File

@ -23,6 +23,7 @@ import (
"strings" "strings"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
"github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"go.uber.org/multierr" "go.uber.org/multierr"
@ -129,19 +130,34 @@ type builder struct {
mbrPath string mbrPath string
loDevice string loDevice string
bootPart string bootPart string
rootPart string rootPart string
bootUUID string cryptPart string
rootUUID string cryptRoot string
mappedCryptRoot string
bootUUID string
rootUUID string
cryptUUID string
luksPassword string
cmdLineExtra 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 { if err := checkDependencies(); err != nil {
return nil, err 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) f := strings.ToLower(format)
valid := false valid := false
for _, v := range formats { for _, v := range formats {
@ -202,6 +218,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
cmdLineExtra: cmdLineExtra, cmdLineExtra: cmdLineExtra,
splitBoot: splitBoot, splitBoot: splitBoot,
bootSize: bootSize, bootSize: bootSize,
luksPassword: luksPassword,
} }
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil { if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
return nil, err return nil, err
@ -302,12 +319,44 @@ func (b *builder) mountImg(ctx context.Context) error {
} else { } else {
b.rootPart = b.bootPart b.rootPart = b.bootPart
} }
logrus.Infof("creating raw image file system") if b.isLuksEnabled() {
if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil { logrus.Infof("encrypting root partition")
return err f, err := os.CreateTemp("", "key")
} if err != nil {
if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil { return err
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 { if !b.splitBoot {
return nil return nil
@ -328,20 +377,17 @@ func (b *builder) unmountImg(ctx context.Context) error {
logrus.Infof("unmounting raw image") logrus.Infof("unmounting raw image")
var merr error var merr error
if b.splitBoot { if b.splitBoot {
if err := exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")); err != nil { merr = multierr.Append(merr, exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")))
merr = multierr.Append(merr, err)
}
} }
if err := exec.Run(ctx, "umount", b.mntPoint); err != nil { merr = multierr.Append(merr, exec.Run(ctx, "umount", b.mntPoint))
merr = multierr.Append(merr, err) 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 { return multierr.Combine(
merr = multierr.Append(merr, err) merr,
} exec.Run(ctx, "kpartx", "-d", b.loDevice),
if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil { exec.Run(ctx, "losetup", "-d", b.loDevice),
merr = multierr.Append(merr, err) )
}
return merr
} }
func (b *builder) copyRootFS(ctx context.Context) error { func (b *builder) copyRootFS(ctx context.Context) error {
@ -369,6 +415,12 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) {
if err != nil { if err != nil {
return err 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) 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 { } else {
b.bootUUID = b.rootUUID b.bootUUID = b.rootUUID
@ -449,7 +501,14 @@ func (b *builder) installKernel(ctx context.Context) error {
if err != nil { if err != nil {
return err 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 err
} }
return nil return nil
@ -483,6 +542,10 @@ func (b *builder) chPath(path string) string {
return fmt.Sprintf("%s%s", b.mntPoint, path) return fmt.Sprintf("%s%s", b.mntPoint, path)
} }
func (b *builder) isLuksEnabled() bool {
return b.luksPassword != ""
}
func (b *builder) Close() error { func (b *builder) Close() error {
return b.img.Close() return b.img.Close()
} }
@ -498,7 +561,7 @@ func block(path string, size uint64) error {
func checkDependencies() error { func checkDependencies() error {
var merr 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 { if _, err := exec2.LookPath(v); err != nil {
merr = multierr.Append(merr, err) merr = multierr.Append(merr, err)
} }

View File

@ -42,7 +42,7 @@ func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, init
sys, err := sysconfig(r) sys, err := sysconfig(r)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, sysconf, sys) assert.Equal(t, sysconf, sys)
d, err := NewDockerfile(r, img, "root", "") d, err := NewDockerfile(r, img, "root", "", false)
require.NoError(t, err) require.NoError(t, err)
logrus.Infof("docker image based on %s", d.Release.Name) logrus.Infof("docker image based on %s", d.Release.Name)
p := filepath.Join(tmpPath, docker.FormatImgName(img)) p := filepath.Join(tmpPath, docker.FormatImgName(img))

View File

@ -79,6 +79,10 @@ var (
} }
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, in, out, cmd.Name(), os.Args[2:]...) 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) size, err := parseSize(size)
if err != nil { if err != nil {
return err return err
@ -109,6 +113,7 @@ var (
d2vm.WithRaw(raw), d2vm.WithRaw(raw),
d2vm.WithSplitBoot(splitBoot), d2vm.WithSplitBoot(splitBoot),
d2vm.WithBootSize(bootSize), d2vm.WithBootSize(bootSize),
d2vm.WithLuksPassword(luksPassword),
); err != nil { ); err != nil {
return err return err
} }

View File

@ -51,6 +51,10 @@ var (
} }
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, out, out, cmd.Name(), dargs...) 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] img := args[0]
tag := "latest" tag := "latest"
if parts := strings.Split(img, ":"); len(parts) > 1 { if parts := strings.Split(img, ":"); len(parts) > 1 {
@ -97,6 +101,7 @@ var (
d2vm.WithRaw(raw), d2vm.WithRaw(raw),
d2vm.WithSplitBoot(splitBoot), d2vm.WithSplitBoot(splitBoot),
d2vm.WithBootSize(bootSize), d2vm.WithBootSize(bootSize),
d2vm.WithLuksPassword(luksPassword),
); err != nil { ); err != nil {
return err return err
} }

View File

@ -35,6 +35,7 @@ var (
networkManager string networkManager string
splitBoot bool splitBoot bool
bootSize uint64 bootSize uint64
luksPassword string
) )
func buildFlags() *pflag.FlagSet { 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(&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.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.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 return flags
} }

View File

@ -46,7 +46,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
return err return err
} }
if !o.raw { 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 { if err != nil {
return err return err
} }
@ -79,7 +79,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
if format == "" { if format == "" {
format = "raw" 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 { if err != nil {
return err return err
} }

View File

@ -26,6 +26,8 @@ type convertOptions struct {
splitBoot bool splitBoot bool
bootSize uint64 bootSize uint64
luksPassword string
} }
func WithSize(size uint64) ConvertOption { func WithSize(size uint64) ConvertOption {
@ -75,3 +77,9 @@ func WithBootSize(bootSize uint64) ConvertOption {
o.bootSize = bootSize o.bootSize = bootSize
} }
} }
func WithLuksPassword(password string) ConvertOption {
return func(o *convertOptions) {
o.luksPassword = password
}
}

View File

@ -64,6 +64,7 @@ type Dockerfile struct {
Password string Password string
Release OSRelease Release OSRelease
NetworkManager NetworkManager NetworkManager NetworkManager
Luks bool
tmpl *template.Template tmpl *template.Template
} }
@ -71,8 +72,8 @@ func (d Dockerfile) Render(w io.Writer) error {
return d.tmpl.Execute(w, d) return d.tmpl.Execute(w, d)
} }
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager) (Dockerfile, error) { func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks bool) (Dockerfile, error) {
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager} d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks}
var net NetworkManager var net NetworkManager
switch release.ID { switch release.ID {
case ReleaseDebian: case ReleaseDebian:

View File

@ -15,6 +15,7 @@ d2vm build [context directory] [flags]
-f, --file string Name of the Dockerfile -f, --file string Name of the Dockerfile
--force Override output qcow2 image --force Override output qcow2 image
-h, --help help for build -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 --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") -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 -p, --password string Optional root user password

View File

@ -13,6 +13,7 @@ d2vm convert [docker image] [flags]
--boot-size uint Size of the boot partition in MB (default 100) --boot-size uint Size of the boot partition in MB (default 100)
--force Override output qcow2 image --force Override output qcow2 image
-h, --help help for convert -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 --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") -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 -p, --password string Optional root user password

View File

@ -6,6 +6,9 @@ RUN apk update --no-cache && \
apk add \ apk add \
util-linux \ util-linux \
linux-virt \ linux-virt \
{{- if .Luks }}
cryptsetup \
{{- end }}
{{- if ge .Release.VersionID "3.17" }} {{- if ge .Release.VersionID "3.17" }}
busybox-openrc \ busybox-openrc \
busybox-mdev-openrc \ busybox-mdev-openrc \
@ -29,3 +32,9 @@ allow-hotplug eth0\n\
iface eth0 inet dhcp\n\ iface eth0 inet dhcp\n\
' > /etc/network/interfaces ' > /etc/network/interfaces
{{ end }} {{ end }}
{{- if .Luks }}
RUN source /etc/mkinitfs/mkinitfs.conf && \
echo "features=\"${features} cryptsetup\"" > /etc/mkinitfs/mkinitfs.conf && \
mkinitfs $(ls /lib/modules)
{{- end }}

View File

@ -4,6 +4,9 @@ USER root
RUN apt-get -y update && \ RUN apt-get -y update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
{{- if .Luks }}
cryptsetup \
{{- end }}
linux-image-amd64 linux-image-amd64
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \

View File

@ -6,6 +6,9 @@ RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-virtual \ linux-image-virtual \
initramfs-tools \ initramfs-tools \
{{- if .Luks }}
cryptsetup \
{{- end }}
systemd-sysv \ systemd-sysv \
systemd \ systemd \
dbus \ dbus \