diff --git a/README.md b/README.md index 58c39d5d..5ec25215 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,20 @@ $ kubectl exec -ti my-csi-app /bin/sh / # ls /data hello-world ``` +## Volume parameters + +This plugin supports the following `StorageClass` parameters: + +For LUKS encryption: + +* `dobs.csi.digitalocean.com/luks-encrypted`: set to the string `"true"` if the volume should be encrypted + with LUKS +* `dobs.csi.digitalocean.com/luks-cipher`: cipher to use; must be supported by the kernel and luks +* `dobs.csi.digitalocean.com/luks-key-size`: key-size to use + +For LUKS encrypted volumes, a secret that contains the LUKS key needs to be referenced through +the `csi.storage.k8s.io/node-stage-secret-name` and `csi.storage.k8s.io/node-stage-secret-namespace` +parameter. See the included `StorageClass` definition. ## Upgrading diff --git a/cmd/do-csi-plugin/Dockerfile b/cmd/do-csi-plugin/Dockerfile index ff2d3810..705d7c37 100644 --- a/cmd/do-csi-plugin/Dockerfile +++ b/cmd/do-csi-plugin/Dockerfile @@ -17,6 +17,7 @@ FROM alpine:3.15 # e2fsprogs-extra is required for resize2fs used for the resize operation # blkid: block device identification tool from util-linux RUN apk add --no-cache ca-certificates \ + cryptsetup \ e2fsprogs \ findmnt \ xfsprogs \ diff --git a/deploy/kubernetes/releases/csi-digitalocean-v4.0.0/driver.yaml b/deploy/kubernetes/releases/csi-digitalocean-v4.0.0/driver.yaml index c047ddad..c6163b4e 100644 --- a/deploy/kubernetes/releases/csi-digitalocean-v4.0.0/driver.yaml +++ b/deploy/kubernetes/releases/csi-digitalocean-v4.0.0/driver.yaml @@ -40,11 +40,17 @@ deletionPolicy: Delete kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: - name: do-block-storage + name: do-block-storage-luks annotations: storageclass.kubernetes.io/is-default-class: "true" provisioner: dobs.csi.digitalocean.com allowVolumeExpansion: true +parameters: + dobs.csi.digitalocean.com/luks-encrypted: "true" + dobs.csi.digitalocean.com/luks-cipher: "aes-xts-plain64" + dobs.csi.digitalocean.com/luks-key-size: "512" + csi.storage.k8s.io/node-stage-secret-namespace: ${pvc.namespace} + csi.storage.k8s.io/node-stage-secret-name: ${pvc.name}-luks-key --- @@ -57,24 +63,24 @@ allowVolumeExpansion: true kind: StatefulSet apiVersion: apps/v1 metadata: - name: csi-do-controller + name: csi-do-controller-luks namespace: kube-system spec: serviceName: "csi-do" selector: matchLabels: - app: csi-do-controller + app: csi-do-controller-luks replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: csi-do-plugin labels: - app: csi-do-controller + app: csi-do-controller-luks role: csi-do spec: priorityClassName: system-cluster-critical - serviceAccount: csi-do-controller-sa + serviceAccount: csi-do-controller-sa-luks containers: - name: csi-provisioner image: k8s.gcr.io/sig-storage/csi-provisioner:v3.0.0 @@ -129,7 +135,7 @@ spec: - name: socket-dir mountPath: /var/lib/csi/sockets/pluginproxy/ - name: csi-do-plugin - image: digitalocean/do-csi-plugin:v4.0.0 + image: edeckers/do-csi-plugin:v4.0.0-luks args : - "--endpoint=$(CSI_ENDPOINT)" - "--token=$(DIGITALOCEAN_ACCESS_TOKEN)" @@ -157,7 +163,7 @@ spec: kind: ServiceAccount apiVersion: v1 metadata: - name: csi-do-controller-sa + name: csi-do-controller-sa-luks namespace: kube-system --- @@ -202,7 +208,7 @@ metadata: name: csi-do-provisioner-binding subjects: - kind: ServiceAccount - name: csi-do-controller-sa + name: csi-do-controller-sa-luks namespace: kube-system roleRef: kind: ClusterRole @@ -239,7 +245,7 @@ metadata: name: csi-do-attacher-binding subjects: - kind: ServiceAccount - name: csi-do-controller-sa + name: csi-do-controller-sa-luks namespace: kube-system roleRef: kind: ClusterRole @@ -275,7 +281,7 @@ metadata: name: csi-do-snapshotter-binding subjects: - kind: ServiceAccount - name: csi-do-controller-sa + name: csi-do-controller-sa-luks namespace: kube-system roleRef: kind: ClusterRole @@ -311,7 +317,7 @@ metadata: name: csi-do-resizer-binding subjects: - kind: ServiceAccount - name: csi-do-controller-sa + name: csi-do-controller-sa-luks namespace: kube-system roleRef: kind: ClusterRole @@ -329,22 +335,22 @@ roleRef: kind: DaemonSet apiVersion: apps/v1 metadata: - name: csi-do-node + name: csi-do-node-luks namespace: kube-system spec: selector: matchLabels: - app: csi-do-node + app: csi-do-node-luks template: metadata: annotations: kubectl.kubernetes.io/default-container: csi-do-plugin labels: - app: csi-do-node + app: csi-do-node-luks role: csi-do spec: priorityClassName: system-node-critical - serviceAccount: csi-do-node-sa + serviceAccount: csi-do-node-luks hostNetwork: true initContainers: # Delete automount udev rule running on all DO droplets. The rule mounts @@ -385,7 +391,7 @@ spec: - name: registration-dir mountPath: /registration/ - name: csi-do-plugin - image: digitalocean/do-csi-plugin:v4.0.0 + image: edeckers/do-csi-plugin:v4.0.0-luks args : - "--endpoint=$(CSI_ENDPOINT)" - "--url=$(DIGITALOCEAN_API_URL)" @@ -410,6 +416,8 @@ spec: mountPropagation: "Bidirectional" - name: device-dir mountPath: /dev + - name: tmpfs + mountPath: /tmp volumes: - name: registration-dir hostPath: @@ -429,12 +437,16 @@ spec: - name: udev-rules-dir hostPath: path: /etc/udev/rules.d/ + # to make sure temporary stored luks keys never touch a disk + - name: tmpfs + emptyDir: + medium: Memory --- apiVersion: v1 kind: ServiceAccount metadata: - name: csi-do-node-sa + name: csi-do-node-sa-luks namespace: kube-system --- @@ -442,7 +454,7 @@ metadata: kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: csi-do-node-driver-registrar-role + name: csi-do-node-luks-driver-registrar-role namespace: kube-system rules: - apiGroups: [""] @@ -454,12 +466,12 @@ rules: kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: csi-do-node-driver-registrar-binding + name: csi-do-node-luks-driver-registrar-binding subjects: - kind: ServiceAccount - name: csi-do-node-sa + name: csi-do-node-sa-luks namespace: kube-system roleRef: kind: ClusterRole - name: csi-do-node-driver-registrar-role + name: csi-do-node-luks-driver-registrar-role apiGroup: rbac.authorization.k8s.io diff --git a/driver/controller.go b/driver/controller.go index 0734ff6d..f0d37243 100644 --- a/driver/controller.go +++ b/driver/controller.go @@ -44,6 +44,12 @@ const ( tiB ) +const ( + // PublishInfoVolumeName is used to pass the volume name from + // `ControllerPublishVolume` to `NodeStageVolume or `NodePublishVolume` + PublishInfoVolumeName = DefaultDriverName + "/volume-name" +) + const ( // minimumVolumeSizeInBytes is used to validate that the user is not trying // to create a volume that is smaller than what we support @@ -112,12 +118,17 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) } volumeName := req.Name + luksEncrypted := "false" + if req.Parameters[LuksEncryptedAttribute] == "true" { + luksEncrypted = "true" + } log := d.log.WithFields(logrus.Fields{ "volume_name": volumeName, "storage_size_giga_bytes": size / giB, "method": "create_volume", "volume_capabilities": req.VolumeCapabilities, + "luks_encrypted": luksEncrypted, }) log.Info("create volume called") @@ -130,6 +141,26 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) return nil, status.Error(codes.Internal, err.Error()) } + csiVolume := csi.Volume{ + AccessibleTopology: []*csi.Topology{ + { + Segments: map[string]string{ + "region": d.region, + }, + }, + }, + CapacityBytes: size, + VolumeContext: map[string]string{ + LuksEncryptedAttribute: luksEncrypted, + PublishInfoVolumeName: volumeName, + }, + } + + if luksEncrypted == "true" { + csiVolume.VolumeContext[LuksCipherAttribute] = req.Parameters[LuksCipherAttribute] + csiVolume.VolumeContext[LuksKeySizeAttribute] = req.Parameters[LuksKeySizeAttribute] + } + // volume already exist, do nothing if len(volumes) != 0 { if len(volumes) > 1 { @@ -142,12 +173,9 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) } log.Info("volume already created") - return &csi.CreateVolumeResponse{ - Volume: &csi.Volume{ - VolumeId: vol.ID, - CapacityBytes: vol.SizeGigaBytes * giB, - }, - }, nil + csiVolume.VolumeId = vol.ID + csiVolume.CapacityBytes = vol.SizeGigaBytes * giB + return &csi.CreateVolumeResponse{Volume: &csiVolume}, nil } volumeReq := &godo.VolumeCreateRequest{ @@ -198,19 +226,8 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) return nil, status.Error(codes.Internal, err.Error()) } - resp := &csi.CreateVolumeResponse{ - Volume: &csi.Volume{ - VolumeId: vol.ID, - CapacityBytes: size, - AccessibleTopology: []*csi.Topology{ - { - Segments: map[string]string{ - "region": d.region, - }, - }, - }, - }, - } + csiVolume.VolumeId = vol.ID + resp := &csi.CreateVolumeResponse{Volume: &csiVolume} // external-provisioner expects a content source to be returned if the PVC // specified a data source, which corresponds to us having received a @@ -327,6 +344,9 @@ func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.Controlle return &csi.ControllerPublishVolumeResponse{ PublishContext: map[string]string{ d.publishInfoVolumeName: vol.Name, + LuksEncryptedAttribute: req.VolumeContext[LuksEncryptedAttribute], + LuksCipherAttribute: req.VolumeContext[LuksCipherAttribute], + LuksKeySizeAttribute: req.VolumeContext[LuksKeySizeAttribute], }, }, nil } @@ -352,6 +372,9 @@ func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.Controlle return &csi.ControllerPublishVolumeResponse{ PublishContext: map[string]string{ d.publishInfoVolumeName: vol.Name, + LuksEncryptedAttribute: req.VolumeContext[LuksEncryptedAttribute], + LuksCipherAttribute: req.VolumeContext[LuksCipherAttribute], + LuksKeySizeAttribute: req.VolumeContext[LuksKeySizeAttribute], }, }, nil } @@ -383,6 +406,9 @@ func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.Controlle return &csi.ControllerPublishVolumeResponse{ PublishContext: map[string]string{ d.publishInfoVolumeName: vol.Name, + LuksEncryptedAttribute: req.VolumeContext[LuksEncryptedAttribute], + LuksCipherAttribute: req.VolumeContext[LuksCipherAttribute], + LuksKeySizeAttribute: req.VolumeContext[LuksKeySizeAttribute], }, }, nil } @@ -808,6 +834,7 @@ func (d *Driver) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsReques untypedSnapshots, nextToken, err := listResources(ctx, log, startingToken, req.MaxEntries, func(ctx context.Context, listOpts *godo.ListOptions) ([]interface{}, *godo.Response, error) { snapshots, resp, err := d.snapshots.ListVolume(ctx, listOpts) + if err != nil { return nil, resp, err } diff --git a/driver/driver_test.go b/driver/driver_test.go index e7583b5e..df786d15 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -514,16 +514,16 @@ type fakeMounter struct { mounted map[string]string } -func (f *fakeMounter) Format(source string, fsType string) error { +func (f *fakeMounter) Format(source string, fsType string, context LuksContext) error { return nil } -func (f *fakeMounter) Mount(source string, target string, fsType string, options ...string) error { +func (f *fakeMounter) Mount(source string, target string, fsType string, context LuksContext, options ...string) error { f.mounted[target] = source return nil } -func (f *fakeMounter) Unmount(target string) error { +func (f *fakeMounter) Unmount(target string, context LuksContext) error { delete(f.mounted, target) return nil } @@ -536,9 +536,10 @@ func (f *fakeMounter) GetDeviceName(_ mount.Interface, mountPath string) (string return "", nil } -func (f *fakeMounter) IsFormatted(source string) (bool, error) { +func (f *fakeMounter) IsFormatted(source string, context LuksContext) (bool, error) { return true, nil } + func (f *fakeMounter) IsMounted(target string) (bool, error) { _, ok := f.mounted[target] return ok, nil diff --git a/driver/luks_util.go b/driver/luks_util.go new file mode 100644 index 00000000..5ba64ecd --- /dev/null +++ b/driver/luks_util.go @@ -0,0 +1,389 @@ +/* +Copyright 2019 linkyard ag +Copyright cloudscale.ch + +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 driver + +import ( + "errors" + "fmt" + "github.com/sirupsen/logrus" + "io/ioutil" + "os" + "os/exec" + "strings" +) + +const ( + // LuksEncryptedAttribute is used to pass the information if the volume should be + // encrypted with luks to `NodeStageVolume` + LuksEncryptedAttribute = DefaultDriverName + "/luks-encrypted" + + // LuksCipherAttribute is used to pass the information about the luks encryption + // cipher to `NodeStageVolume` + LuksCipherAttribute = DefaultDriverName + "/luks-cipher" + + // LuksKeySizeAttribute is used to pass the information about the luks key size + // to `NodeStageVolume` + LuksKeySizeAttribute = DefaultDriverName + "/luks-key-size" + + // LuksKeyAttribute is the key of the luks key used in the map of secrets passed from the CO + LuksKeyAttribute = "luksKey" +) + +type VolumeLifecycle string + +const ( + VolumeLifecycleNodeStageVolume VolumeLifecycle = "NodeStageVolume" + VolumeLifecycleNodePublishVolume VolumeLifecycle = "NodePublishVolume" + VolumeLifecycleNodeUnstageVolume VolumeLifecycle = "NodeUnstageVolume" + VolumeLifecycleNodeUnpublishVolume VolumeLifecycle = "NodeUnpublishVolume" +) + +type LuksContext struct { + EncryptionEnabled bool + EncryptionKey string + EncryptionCipher string + EncryptionKeySize string + VolumeName string + VolumeLifecycle VolumeLifecycle +} + +func (ctx *LuksContext) validate() error { + if !ctx.EncryptionEnabled { + return nil + } + + var appendFn = func(x string, xs string) string { + if xs != "" { + xs += "; " + } + xs += x + return xs + } + + errorMsg := "" + if ctx.VolumeName == "" { + errorMsg = appendFn("no volume name provided", errorMsg) + } + if ctx.EncryptionKey == "" { + errorMsg = appendFn("no encryption key provided", errorMsg) + } + if ctx.EncryptionCipher == "" { + errorMsg = appendFn("no encryption cipher provided", errorMsg) + } + if ctx.EncryptionKeySize == "" { + errorMsg = appendFn("no encryption key size provided", errorMsg) + } + if errorMsg == "" { + return nil + } + return errors.New(errorMsg) +} + +func getLuksContext(secrets map[string]string, context map[string]string, lifecycle VolumeLifecycle) LuksContext { + if context[LuksEncryptedAttribute] != "true" { + return LuksContext{ + EncryptionEnabled: false, + VolumeLifecycle: lifecycle, + } + } + + luksKey := secrets[LuksKeyAttribute] + luksCipher := context[LuksCipherAttribute] + luksKeySize := context[LuksKeySizeAttribute] + volumeName := context[PublishInfoVolumeName] + + return LuksContext{ + EncryptionEnabled: true, + EncryptionKey: luksKey, + EncryptionCipher: luksCipher, + EncryptionKeySize: luksKeySize, + VolumeName: volumeName, + VolumeLifecycle: lifecycle, + } +} + +func luksFormat(source string, mkfsCmd string, mkfsArgs []string, ctx LuksContext, log *logrus.Entry) error { + cryptsetupCmd, err := getCryptsetupCmd() + if err != nil { + return err + } + filename, err := writeLuksKey(ctx.EncryptionKey, log) + if err != nil { + return err + } + + defer func() { + e := os.Remove(filename) + if e != nil { + log.Errorf("cannot delete temporary file %s: %s", filename, e.Error()) + } + }() + + // initialize the luks partition + cryptsetupArgs := []string{ + "-v", + "--batch-mode", + "--cipher", ctx.EncryptionCipher, + "--key-size", ctx.EncryptionKeySize, + "--key-file", filename, + "luksFormat", source, + } + + log.WithFields(logrus.Fields{ + "cmd": cryptsetupCmd, + "args": cryptsetupArgs, + }).Info("executing cryptsetup luksFormat command") + + out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("cryptsetup luksFormat failed: %v cmd: '%s %s' output: %q", + err, cryptsetupCmd, strings.Join(cryptsetupArgs, " "), string(out)) + } + + // format the disk with the desired filesystem + + // open the luks partition and set up a mapping + err = luksOpen(source, filename, ctx, log) + if err != nil { + return fmt.Errorf("cryptsetup luksOpen failed: %v cmd: '%s %s' output: %q", + err, cryptsetupCmd, strings.Join(cryptsetupArgs, " "), string(out)) + } + + defer func() { + e := luksClose(ctx.VolumeName, log) + if e != nil { + log.Errorf("cannot close luks device: %s", e.Error()) + } + }() + + // replace the source volume with the mapped one in the arguments to mkfs + mkfsNewArgs := make([]string, len(mkfsArgs)) + for i, elem := range mkfsArgs { + if elem != source { + mkfsNewArgs[i] = elem + } else { + mkfsArgs[i] = "/dev/mapper/" + ctx.VolumeName + } + } + + log.WithFields(logrus.Fields{ + "cmd": mkfsCmd, + "args": mkfsArgs, + }).Info("executing format command") + + out, err = exec.Command(mkfsCmd, mkfsArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("formatting disk failed: %v cmd: '%s %s' output: %q", + err, mkfsCmd, strings.Join(mkfsArgs, " "), string(out)) + } + + return nil +} + +// prepares a luks-encrypted volume for mounting and returns the path of the mapped volume +func luksPrepareMount(source string, ctx LuksContext, log *logrus.Entry) (string, error) { + filename, err := writeLuksKey(ctx.EncryptionKey, log) + if err != nil { + return "", err + } + defer func() { + e := os.Remove(filename) + if e != nil { + log.Errorf("cannot delete temporary file %s: %s", filename, e.Error()) + } + }() + + err = luksOpen(source, filename, ctx, log) + if err != nil { + return "", err + } + return "/dev/mapper/" + ctx.VolumeName, nil +} + +func luksClose(volume string, log *logrus.Entry) error { + cryptsetupCmd, err := getCryptsetupCmd() + if err != nil { + return err + } + cryptsetupArgs := []string{"--batch-mode", "close", volume} + + log.WithFields(logrus.Fields{ + "cmd": cryptsetupCmd, + "args": cryptsetupArgs, + }).Info("executing cryptsetup close command") + + out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("removing luks mapping failed: %v cmd: '%s %s' output: %q", + err, cryptsetupCmd, strings.Join(cryptsetupArgs, " "), string(out)) + } + return nil +} + +// checks if the given volume is formatted by checking if it is a luks volume and +// if the luks volume, once opened, contains a filesystem +func isLuksVolumeFormatted(volume string, ctx LuksContext, log *logrus.Entry) (bool, error) { + isLuks, err := isLuks(volume) + if err != nil { + return false, err + } + if !isLuks { + return false, nil + } + + filename, err := writeLuksKey(ctx.EncryptionKey, log) + if err != nil { + return false, err + } + defer func() { + e := os.Remove(filename) + if e != nil { + log.Errorf("cannot delete temporary file %s: %s", filename, e.Error()) + } + }() + + err = luksOpen(volume, filename, ctx, log) + if err != nil { + return false, err + } + defer func() { + e := luksClose(ctx.VolumeName, log) + if e != nil { + log.Errorf("cannot close luks device: %s", e.Error()) + } + }() + + return isVolumeFormatted(volume, log) +} + +func luksOpen(volume string, keyFile string, ctx LuksContext, log *logrus.Entry) error { + // check if the luks volume is already open + if _, err := os.Stat("/dev/mapper/" + ctx.VolumeName); !os.IsNotExist(err) { + log.WithFields(logrus.Fields{ + "volume": volume, + }).Info("luks volume is already open") + return nil + } + + cryptsetupCmd, err := getCryptsetupCmd() + if err != nil { + return err + } + cryptsetupArgs := []string{ + "--batch-mode", + "luksOpen", + "--key-file", keyFile, + volume, ctx.VolumeName, + } + log.WithFields(logrus.Fields{ + "cmd": cryptsetupCmd, + "args": cryptsetupArgs, + }).Info("executing cryptsetup luksOpen command") + out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("cryptsetup luksOpen failed: %v cmd: '%s %s' output: %q", + err, cryptsetupCmd, strings.Join(cryptsetupArgs, " "), string(out)) + } + return nil +} + +// runs cryptsetup isLuks for a given volume +func isLuks(volume string) (bool, error) { + cryptsetupCmd, err := getCryptsetupCmd() + if err != nil { + return false, err + } + cryptsetupArgs := []string{"--batch-mode", "isLuks", volume} + + // cryptsetup isLuks exits with code 0 if the target is a luks volume; otherwise it returns + // a non-zero exit code which exec.Command interprets as an error + _, err = exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() + if err != nil { + return false, nil + } + return true, nil +} + +// check is a given mapping under /dev/mapper is a luks volume +func isLuksMapping(volume string) (bool, string, error) { + if strings.HasPrefix(volume, "/dev/mapper/") { + mappingName := volume[len("/dev/mapper/"):] + cryptsetupCmd, err := getCryptsetupCmd() + if err != nil { + return false, mappingName, err + } + cryptsetupArgs := []string{"status", mappingName} + + out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() + if err != nil { + return false, mappingName, nil + } + for _, statusLine := range strings.Split(string(out), "\n") { + if strings.Contains(statusLine, "type:") { + if strings.Contains(strings.ToLower(statusLine), "luks") { + return true, mappingName, nil + } + return false, mappingName, nil + } + } + + } + return false, "", nil +} + +func getCryptsetupCmd() (string, error) { + cryptsetupCmd := "cryptsetup" + _, err := exec.LookPath(cryptsetupCmd) + if err != nil { + if err == exec.ErrNotFound { + return "", fmt.Errorf("%q executable not found in $PATH", cryptsetupCmd) + } + return "", err + } + return cryptsetupCmd, nil +} + +// writes the given luks encryption key to a temporary file and returns the name of the temporary +// file +func writeLuksKey(key string, log *logrus.Entry) (string, error) { + if !checkTmpFs("/tmp") { + return "", errors.New("temporary directory /tmp is not a tmpfs volume; refusing to write luks key to a volume backed by a disk") + } + tmpFile, err := ioutil.TempFile("/tmp", "luks-") + if err != nil { + return "", err + } + _, err = tmpFile.WriteString(key) + if err != nil { + log.WithField("tmp_file", tmpFile.Name()).Warnf("Unable to write luks key file: %s", err.Error()) + return "", err + } + return tmpFile.Name(), nil +} + +// makes sure that the given directory is a tmpfs +func checkTmpFs(dir string) bool { + out, err := exec.Command("sh", "-c", "df -T "+dir+" | tail -n1 | awk '{print $2}'").CombinedOutput() + if err != nil { + return false + } + if len(out) == 0 { + return false + } + return strings.TrimSpace(string(out)) == "tmpfs" +} diff --git a/driver/luks_util_test.go b/driver/luks_util_test.go new file mode 100644 index 00000000..c43cbeb1 --- /dev/null +++ b/driver/luks_util_test.go @@ -0,0 +1,110 @@ +package driver + +import ( + "context" + "errors" + "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/sirupsen/logrus" + "strings" + "testing" +) + +type TestPodVolume struct { + ClaimName string + SizeGB int + StorageClass string + LuksKey string +} + +type TestPodDescriptor struct { + Kind string + Name string + Volumes []TestPodVolume +} + +type DiskInfo struct { + PVCName string `json:"pvcName"` + DeviceName string `json:"deviceName"` + DeviceSize int `json:"deviceSize"` + Filesystem string `json:"filesystem"` + FilesystemUUID string `json:"filesystemUUID"` + FilesystemSize int `json:"filesystemSize"` + DeviceSource string `json:"deviceSource"` + Luks string `json:"luks,omitempty"` + Cipher string `json:"cipher,omitempty"` + Keysize int `json:"keysize,omitempty"` +} + +// creates a kubernetes pod from the given TestPodDescriptor +func TestCreateLuksVolume(t *testing.T) { + tests := []struct { + name string + listVolumesErr error + getSnapshotErr error + }{ + { + name: "listing volumes failing", + listVolumesErr: errors.New("failed to list volumes"), + }, + { + name: "fetching snapshot failing", + getSnapshotErr: errors.New("failed to get snapshot"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + d := &Driver{ + storage: &fakeStorageDriver{ + listVolumesErr: test.listVolumesErr, + }, + snapshots: &fakeSnapshotsDriver{ + getSnapshotErr: test.getSnapshotErr, + }, + log: logrus.New().WithField("test_enabed", true), + } + + _, err := d.CreateVolume(context.Background(), &csi.CreateVolumeRequest{ + Name: "name", + Parameters: map[string]string{ + LuksCipherAttribute: "cipher", + LuksEncryptedAttribute: "true", + LuksKeyAttribute: "key", + LuksKeySizeAttribute: "23234", + }, + VolumeCapabilities: []*csi.VolumeCapability{ + &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + }, + VolumeContentSource: &csi.VolumeContentSource{ + Type: &csi.VolumeContentSource_Snapshot{ + Snapshot: &csi.VolumeContentSource_SnapshotSource{ + SnapshotId: "snapshotId", + }, + }, + }, + }) + + var wantErr error + switch { + case test.listVolumesErr != nil: + wantErr = test.listVolumesErr + case test.getSnapshotErr != nil: + wantErr = test.getSnapshotErr + } + + if wantErr == nil && err != nil { + t.Errorf("got error %q, want none", err) + } + if wantErr != nil && !strings.Contains(err.Error(), wantErr.Error()) { + t.Errorf("want error %q to include %q", err, wantErr) + } + }) + } +} diff --git a/driver/mounter.go b/driver/mounter.go index b5cb7218..1ce82db0 100644 --- a/driver/mounter.go +++ b/driver/mounter.go @@ -59,17 +59,17 @@ const ( // more than just mounting functionality by now. type Mounter interface { // Format formats the source with the given filesystem type - Format(source, fsType string) error + Format(source, fsType string, luksContext LuksContext) error // Mount mounts source to target with the given fstype and options. - Mount(source, target, fsType string, options ...string) error + Mount(source, target, fsType string, luksContext LuksContext, options ...string) error // Unmount unmounts the given target - Unmount(target string) error + Unmount(target string, luksContext LuksContext) error // IsFormatted checks whether the source device is formatted or not. It // returns true if the source device is already formatted. - IsFormatted(source string) (bool, error) + IsFormatted(source string, luksContext LuksContext) (bool, error) // IsMounted checks whether the target path is a correct mount (i.e: // propagated). It returns true if it's mounted. An error is returned in @@ -107,7 +107,7 @@ func newMounter(log *logrus.Entry) *mounter { } } -func (m *mounter) Format(source, fsType string) error { +func (m *mounter) Format(source, fsType string, luksContext LuksContext) error { mkfsCmd := fmt.Sprintf("mkfs.%s", fsType) _, err := exec.LookPath(mkfsCmd) @@ -133,21 +133,35 @@ func (m *mounter) Format(source, fsType string) error { mkfsArgs = []string{"-F", source} } - m.log.WithFields(logrus.Fields{ - "cmd": mkfsCmd, - "args": mkfsArgs, - }).Info("executing format command") + if !luksContext.EncryptionEnabled { + m.log.WithFields(logrus.Fields{ + "cmd": mkfsCmd, + "args": mkfsArgs, + }).Info("executing format command") - out, err := exec.Command(mkfsCmd, mkfsArgs...).CombinedOutput() - if err != nil { - return fmt.Errorf("formatting disk failed: %v cmd: '%s %s' output: %q", - err, mkfsCmd, strings.Join(mkfsArgs, " "), string(out)) - } + out, err := exec.Command(mkfsCmd, mkfsArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("formatting disk failed: %v cmd: '%s %s' output: %q", + err, mkfsCmd, strings.Join(mkfsArgs, " "), string(out)) + } - return nil + return nil + } else { + err := luksContext.validate() + if err != nil { + return err + } + + err = luksFormat(source, mkfsCmd, mkfsArgs, luksContext, m.log) + if err != nil { + return err + } + + return nil + } } -func (m *mounter) Mount(source, target, fsType string, opts ...string) error { +func (m *mounter) Mount(source, target, fsType string, luksContext LuksContext, opts ...string) error { mountCmd := "mount" mountArgs := []string{} @@ -187,7 +201,21 @@ func (m *mounter) Mount(source, target, fsType string, opts ...string) error { mountArgs = append(mountArgs, "-o", strings.Join(opts, ",")) } - mountArgs = append(mountArgs, source) + if luksContext.EncryptionEnabled && luksContext.VolumeLifecycle == VolumeLifecycleNodeStageVolume { + luksSource, err := luksPrepareMount(source, luksContext, m.log) + if err != nil { + m.log.WithFields(logrus.Fields{ + "error": err.Error(), + "volume": luksContext.VolumeName, + }).Error("failed to prepare luks volume for mounting") + return err + } + + mountArgs = append(mountArgs, luksSource) + } else { + mountArgs = append(mountArgs, source) + } + mountArgs = append(mountArgs, target) m.log.WithFields(logrus.Fields{ @@ -204,11 +232,93 @@ func (m *mounter) Mount(source, target, fsType string, opts ...string) error { return nil } -func (m *mounter) Unmount(target string) error { - return mount.CleanupMountPoint(target, m.kMounter, true) +func (m *mounter) Unmount(target string, luksContext LuksContext) error { + if target == "" { + return errors.New("target is not specified for unmounting the volume") + } + + // if this is the unmount call after the mount-bind has been removed, + // a luks volume needs to be closed after unmounting; get the source + // of the mount to check if that is a luks volume + mountSources, err := getMountSources(target) + if err != nil { + return err + } + + if len(mountSources) == 0 { + return fmt.Errorf("unable to determine mount sources of target %s", target) + } + + umountCmd := "umount" + umountArgs := []string{target} + + m.log.WithFields(logrus.Fields{ + "cmd": umountCmd, + "args": umountArgs, + }).Info("executing umount command") + + out, err := exec.Command(umountCmd, umountArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("unmounting failed: %v cmd: '%s %s' output: %q", + err, umountCmd, target, string(out)) + } + + merror := mount.CleanupMountPoint(target, m.kMounter, true) + + // if this is the unstaging process, check if the source is a luks volume and close it + if luksContext.VolumeLifecycle == VolumeLifecycleNodeUnstageVolume { + for _, source := range mountSources { + isLuksMapping, mappingName, err := isLuksMapping(source) + if err != nil { + return err + } + if isLuksMapping { + err := luksClose(mappingName, m.log) + if err != nil { + return err + } + } + } + } + + return merror +} + +// gets the mount sources of a mountpoint +func getMountSources(target string) ([]string, error) { + _, err := exec.LookPath("findmnt") + if err != nil { + if err == exec.ErrNotFound { + return nil, fmt.Errorf("%q executable not found in $PATH", "findmnt") + } + return nil, err + } + out, err := exec.Command("sh", "-c", fmt.Sprintf("findmnt -o SOURCE -n -M %s", target)).CombinedOutput() + if err != nil { + // findmnt exits with non zero exit status if it couldn't find anything + if strings.TrimSpace(string(out)) == "" { + return nil, nil + } + return nil, fmt.Errorf("checking mounted failed: %v cmd: %q output: %q", + err, "findmnt", string(out)) + } + return strings.Split(string(out), "\n"), nil } -func (m *mounter) IsFormatted(source string) (bool, error) { +func (m *mounter) IsFormatted(source string, luksContext LuksContext) (bool, error) { + if !luksContext.EncryptionEnabled { + return isVolumeFormatted(source, m.log) + } + + formatted, err := isLuksVolumeFormatted(source, luksContext, m.log) + if err != nil { + return false, err + } + + return formatted, nil +} + +func isVolumeFormatted(source string, log *logrus.Entry) (bool, error) { if source == "" { return false, errors.New("source is not specified") } @@ -224,7 +334,7 @@ func (m *mounter) IsFormatted(source string) (bool, error) { blkidArgs := []string{source} - m.log.WithFields(logrus.Fields{ + log.WithFields(logrus.Fields{ "cmd": blkidCmd, "args": blkidArgs, }).Info("checking if source is formatted") diff --git a/driver/node.go b/driver/node.go index 758b8a2e..b13da5e7 100644 --- a/driver/node.go +++ b/driver/node.go @@ -85,6 +85,11 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe }) log.Info("node stage volume called") + publishContext := req.GetPublishContext() + if publishContext == nil { + return nil, status.Error(codes.InvalidArgument, "PublishContext must be provided") + } + volumeName := "" if volName, ok := req.GetPublishContext()[d.publishInfoVolumeName]; !ok { return nil, status.Error(codes.InvalidArgument, "Could not find the volume by name") @@ -100,6 +105,9 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe } source := getDeviceByIDPath(volumeName) + + luksContext := getLuksContext(req.Secrets, req.VolumeContext, VolumeLifecycleNodeStageVolume) + target := req.StagingTargetPath mnt := req.VolumeCapability.GetMount() @@ -118,6 +126,7 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe "source": source, "fs_type": fsType, "mount_options": options, + "luks_encrypted": luksContext.EncryptionEnabled, }) var noFormat bool @@ -130,14 +139,14 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe if noFormat { log.Info("skipping formatting the source device") } else { - formatted, err := d.mounter.IsFormatted(source) + formatted, err := d.mounter.IsFormatted(source, luksContext) if err != nil { return nil, err } if !formatted { log.Info("formatting the volume for staging") - if err := d.mounter.Format(source, fsType); err != nil { + if err := d.mounter.Format(source, fsType, luksContext); err != nil { return nil, status.Error(codes.Internal, err.Error()) } } else { @@ -153,7 +162,7 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe } if !mounted { - if err := d.mounter.Mount(source, target, fsType, options...); err != nil { + if err := d.mounter.Mount(source, target, fsType, luksContext, options...); err != nil { return nil, status.Error(codes.Internal, err.Error()) } } else { @@ -174,6 +183,8 @@ func (d *Driver) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolu return nil, status.Error(codes.InvalidArgument, "NodeUnstageVolume Staging Target Path must be provided") } + luksContext := LuksContext{VolumeLifecycle: VolumeLifecycleNodeUnstageVolume} + log := d.log.WithFields(logrus.Fields{ "volume_id": req.VolumeId, "staging_target_path": req.StagingTargetPath, @@ -188,7 +199,7 @@ func (d *Driver) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolu if mounted { log.Info("unmounting the staging target path") - err := d.mounter.Unmount(req.StagingTargetPath) + err := d.mounter.Unmount(req.StagingTargetPath, luksContext) if err != nil { return nil, err } @@ -218,11 +229,19 @@ func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolu return nil, status.Error(codes.InvalidArgument, "NodePublishVolume Volume Capability must be provided") } + publishContext := req.GetPublishContext() + if publishContext == nil { + return nil, status.Error(codes.InvalidArgument, "PublishContext must be provided") + } + + luksContext := getLuksContext(req.Secrets, publishContext, VolumeLifecycleNodePublishVolume) + log := d.log.WithFields(logrus.Fields{ "volume_id": req.VolumeId, "staging_target_path": req.StagingTargetPath, "target_path": req.TargetPath, "method": "node_publish_volume", + "luks_encrypted": luksContext.EncryptionEnabled, }) log.Info("node publish volume called") @@ -234,9 +253,9 @@ func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolu var err error switch req.GetVolumeCapability().GetAccessType().(type) { case *csi.VolumeCapability_Block: - err = d.nodePublishVolumeForBlock(req, options, log) + err = d.nodePublishVolumeForBlock(req, luksContext, options, log) case *csi.VolumeCapability_Mount: - err = d.nodePublishVolumeForFileSystem(req, options, log) + err = d.nodePublishVolumeForFileSystem(req, luksContext, options, log) default: return nil, status.Error(codes.InvalidArgument, "Unknown access type") } @@ -259,6 +278,8 @@ func (d *Driver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublish return nil, status.Error(codes.InvalidArgument, "NodeUnpublishVolume Target Path must be provided") } + luksContext := LuksContext{VolumeLifecycle: VolumeLifecycleNodeUnpublishVolume} + log := d.log.WithFields(logrus.Fields{ "volume_id": req.VolumeId, "target_path": req.TargetPath, @@ -266,7 +287,7 @@ func (d *Driver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublish }) log.Info("node unpublish volume called") - err := d.mounter.Unmount(req.TargetPath) + err := d.mounter.Unmount(req.TargetPath, luksContext) if err != nil { return nil, err } @@ -470,7 +491,7 @@ func (d *Driver) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolume return &csi.NodeExpandVolumeResponse{}, nil } -func (d *Driver) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeRequest, mountOptions []string, log *logrus.Entry) error { +func (d *Driver) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeRequest, luksContext LuksContext, mountOptions []string, log *logrus.Entry) error { source := req.StagingTargetPath target := req.TargetPath @@ -498,7 +519,7 @@ func (d *Driver) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeReques if !mounted { log.Info("mounting the volume") - if err := d.mounter.Mount(source, target, fsType, mountOptions...); err != nil { + if err := d.mounter.Mount(source, target, fsType, luksContext, mountOptions...); err != nil { return status.Error(codes.Internal, err.Error()) } } else { @@ -508,7 +529,7 @@ func (d *Driver) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeReques return nil } -func (d *Driver) nodePublishVolumeForBlock(req *csi.NodePublishVolumeRequest, mountOptions []string, log *logrus.Entry) error { +func (d *Driver) nodePublishVolumeForBlock(req *csi.NodePublishVolumeRequest, luksContext LuksContext, mountOptions []string, log *logrus.Entry) error { volumeName, ok := req.GetPublishContext()[d.publishInfoVolumeName] if !ok { return status.Error(codes.InvalidArgument, fmt.Sprintf("Could not find the volume name from the publish context %q", d.publishInfoVolumeName)) @@ -534,7 +555,7 @@ func (d *Driver) nodePublishVolumeForBlock(req *csi.NodePublishVolumeRequest, mo if !mounted { log.Info("mounting the volume") - if err := d.mounter.Mount(source, target, "", mountOptions...); err != nil { + if err := d.mounter.Mount(source, target, "", luksContext, mountOptions...); err != nil { return status.Errorf(codes.Internal, err.Error()) } } else {