Skip to content

Commit

Permalink
initializer: add cryptsetup subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
jmxnzo committed Jan 27, 2025
1 parent a7f7e30 commit 72e4b70
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 34 deletions.
10 changes: 9 additions & 1 deletion initializer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ import (
"github.com/spf13/cobra"
)

const (
// workloadSecretPath is fixed path to the Contrast workload secret.
workloadSecretPath = "/contrast/secrets/workload-secret-seed"
)

func main() {
if err := execute(); err != nil {
os.Exit(1)
Expand All @@ -48,6 +53,9 @@ func newRootCmd() *cobra.Command {
Version: constants.Version,
}
root.InitDefaultVersionFlag()
root.AddCommand(
NewCryptsetupCmd(),
)
return root
}

Expand Down Expand Up @@ -150,7 +158,7 @@ func run(cmd *cobra.Command, _ []string) (retErr error) {
}

if len(resp.WorkloadSecret) > 0 {
err = os.WriteFile("/contrast/secrets/workload-secret-seed", []byte(hex.EncodeToString(resp.WorkloadSecret)), 0o400)
err = os.WriteFile(workloadSecretPath, []byte(hex.EncodeToString(resp.WorkloadSecret)), 0o400)
if err != nil {
return fmt.Errorf("writing workload-secret-seed: %w", err)
}
Expand Down
144 changes: 144 additions & 0 deletions initializer/mount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package main

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"log/slog"
"os"
"os/exec"
"os/signal"
"syscall"

"github.com/edgelesssys/contrast/internal/logger"
"github.com/edgelesssys/contrast/internal/mount"
"github.com/spf13/cobra"
)

// parsedCryptsetupFlags holds configuration for mounting a LUKS encrypted device.
type parsedCryptsetupFlags struct {
devicePath string
mappingName string
volumeMountPoint string
}

// NewCryptsetupCmd creates a Cobra subcommand that mounts an encrypted LUKS volume.
func NewCryptsetupCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "cryptsetup -d [device-path] -m [mount-point]",
Short: "cryptsetup on block device at [device-path] with decrypted mapper device at [mount-point]",
Long: `Set up an LUKS encrypted VolumeMount on the provided VolumeDevice
located at the specified [device-path] and mount the decrypted mapper
device to the provided [mount-point].
In certain deployments, we require a persistent volume claim configured
as block storage to be encrypted by the initializer binary.
Therefore we expose the defined PVC as a block VolumeDevice to our
initializer container. This allows the initializer to setup the
encryption on the block device located at [device-path] using cryptsetup
with the current workload secret as passphrase.
The mapped decrypted block device can then be shared with other containers
on the pod by setting up a shared VolumeMount on the specified [mount-point],
where the mapper device will be mounted to.`,
RunE: setupEncryptedMount,
}
cmd.Flags().StringP("device-path", "d", "", "path to the volume device to be encrypted")
cmd.Flags().StringP("mount-point", "m", "", "mount point of decrypted mapper device")
must(cmd.MarkFlagRequired("device-path"))
must(cmd.MarkFlagRequired("mount-point"))

return cmd
}

// parseCryptsetupFlags returns struct of type parsedCryptsetupFlags, representing the provided flags to the subcommand, which is then used to setup the encrypted volume mount.
func parseCryptsetupFlags(cmd *cobra.Command) (*parsedCryptsetupFlags, error) {
devicePath, err := cmd.Flags().GetString("device-path")
if err != nil {
return nil, err
}
mountPoint, err := cmd.Flags().GetString("mount-point")
if err != nil {
return nil, err
}
mapperHash := sha256.Sum256([]byte(devicePath + mountPoint))
mappingName := hex.EncodeToString(mapperHash[:8])
return &parsedCryptsetupFlags{
devicePath: devicePath,
mappingName: mappingName,
volumeMountPoint: mountPoint,
}, nil
}

// setupEncryptedMount sets up an encrypted LUKS volume mount using the device path and mount point, provided to the setupEncryptedMount subcommand.
func setupEncryptedMount(cmd *cobra.Command, _ []string) error {
// Register channel waiting for SIGTERM signal
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)
logger, err := logger.Default()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: creating logger: %v\n", err)
return err
}
parsedCryptsetupFlags, err := parseCryptsetupFlags(cmd)
if err != nil {
return fmt.Errorf("parsing setupEncryptedMount flags: %w", err)
}
ctx := cmd.Context()
if !isLuks(ctx, logger, parsedCryptsetupFlags.devicePath) {
// TODO(jmxnzo) check what happens if container is terminated in between formatting
if err := luksFormat(ctx, parsedCryptsetupFlags.devicePath, workloadSecretPath); err != nil {
return err
}
}
if err := openEncryptedDevice(ctx, parsedCryptsetupFlags, workloadSecretPath); err != nil {
return err
}
// The decrypted devices with <name> will always be mapped to /dev/mapper/<name> by default.
if err := mount.SetupMount(ctx, logger, "/dev/mapper/"+parsedCryptsetupFlags.mappingName, parsedCryptsetupFlags.volumeMountPoint); err != nil {
return err
}

if err := os.WriteFile("/done", []byte(""), 0o644); err != nil {
return fmt.Errorf("Creating startup probe done directory:%w", err)
}
// Wait for SIGTERM signal
<-signalChan

return nil
}

// isLuks wraps the cryptsetup isLuks command and returns a bool reflecting if the device is formatted as LUKS.
func isLuks(ctx context.Context, logger *slog.Logger, devName string) bool {
if _, err := exec.CommandContext(ctx, "cryptsetup", "isLuks", "--debug", devName).CombinedOutput(); err != nil {
logger.Info("device", devName, "is not a LUKS device or cannot be accessed", "err", err)
return false
}
return true
}

// luksFormat wraps the luksFormat command.
func luksFormat(ctx context.Context, devName, pathToKey string) error {
if out, err := exec.CommandContext(ctx, "cryptsetup", "luksFormat", "--pbkdf-memory=10240", devName, pathToKey).CombinedOutput(); err != nil {
return fmt.Errorf("cryptsetup luksFormat: %w, output: %q", err, out)
}
return nil
}

// openEncryptedDevice wraps the cryptsetup open command.
func openEncryptedDevice(ctx context.Context, parsedCryptsetupFlags *parsedCryptsetupFlags, pathToKey string) error {
if _, err := exec.CommandContext(ctx, "cryptsetup", "open", parsedCryptsetupFlags.devicePath, parsedCryptsetupFlags.mappingName, "-d", pathToKey).CombinedOutput(); err != nil {
return fmt.Errorf("cryptsetup open %s failed: %w", parsedCryptsetupFlags.devicePath, err)
}
return nil
}

func must(err error) {
if err != nil {
panic(err)
}
}
29 changes: 0 additions & 29 deletions internal/kuberesource/parts.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,32 +553,3 @@ func ServiceMeshProxy() *applycorev1.ContainerApplyConfiguration {
"-l", "debug",
)
}

// CryptsetupInitCommand returns the init command for the cryptsetup
// container to setup an encrypted LUKS mount.
func CryptsetupInitCommand() string {
return `#!/bin/bash
set -e
# device is the path to the block device to be encrypted.
device="/dev/csi0"
# workload_secret_path is the path to the Contrast workload secret.
workload_secret_path="/contrast/secrets/workload-secret-seed"
if ! cryptsetup isLuks "${device}"; then
# cryptsetup derives the encryption key of the master key using PBKDF2 based on
# the workloadSecret as passphrase and a random 32 byte salt (stored in LUKS header)
# which ensures uniqueness of encryption key per disk.
cryptsetup luksFormat --pbkdf-memory=10240 $device "${workload_secret_path}" </dev/null
cryptsetup open "${device}" state -d "${workload_secret_path}"
# Create the ext4 filesystem on the mapper device.
mkfs.ext4 /dev/mapper/state
else
cryptsetup open "${device}" state -d "${workload_secret_path}"
fi
mount /dev/mapper/state /state
touch /done
sleep inf
`
}
4 changes: 2 additions & 2 deletions internal/kuberesource/sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ func VolumeStatefulSet() []any {
Container().
WithName("volume-tester-init").
WithImage("ghcr.io/edgelesssys/contrast/initializer:latest").
WithCommand("/bin/sh", "-c", CryptsetupInitCommand()).
WithArgs("cryptsetup", "--device-path", "/dev/csi0", "--mount-point", "/state").
WithVolumeDevices(
applycorev1.VolumeDevice().
WithName("state").
Expand Down Expand Up @@ -617,7 +617,7 @@ func MySQL() []any {
Container().
WithName("luks-setup").
WithImage("ghcr.io/edgelesssys/contrast/initializer:latest").
WithCommand("/bin/sh", "-c", CryptsetupInitCommand()).
WithArgs("cryptsetup", "--device-path", "/dev/csi0", "--mount-point", "/state").
WithVolumeDevices(
applycorev1.VolumeDevice().
WithName("state").
Expand Down
4 changes: 2 additions & 2 deletions packages/containers.nix
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ let
busybox
cryptsetup
e2fsprogs # mkfs.ext4
mount
util-linux # blkid
openssl
])
++ (with dockerTools; [ caCertificates ]);
config = {
Cmd = [ "${pkgs.contrast.initializer}/bin/initializer" ];
# Use Entrypoint so we can append arguments.
Entrypoint = [ "${pkgs.contrast.initializer}/bin/initializer" ];
Env = [ "PATH=/bin" ]; # This is only here for policy generation.
};
};
Expand Down

0 comments on commit 72e4b70

Please sign in to comment.