From a3daf281926282e20f37337377b40f6ac4169295 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sat, 25 Apr 2020 17:35:37 +0200 Subject: [PATCH 1/5] Add support for LUKS to volumes --- README.md | 14 + cmd/do-csi-plugin/Dockerfile | 1 + .../csi-digitalocean-v1.3.0-luks.yaml | 532 ++++++++++++++++++ driver/controller.go | 65 ++- driver/luks_util.go | 389 +++++++++++++ driver/mounter.go | 152 ++++- driver/node.go | 51 +- 7 files changed, 1154 insertions(+), 50 deletions(-) create mode 100644 deploy/kubernetes/releases/csi-digitalocean-v1.3.0-luks.yaml create mode 100644 driver/luks_util.go diff --git a/README.md b/README.md index 58c39d5d8..5ec25215d 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 ff2d38105..705d7c37f 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-v1.3.0-luks.yaml b/deploy/kubernetes/releases/csi-digitalocean-v1.3.0-luks.yaml new file mode 100644 index 000000000..33eb21c7a --- /dev/null +++ b/deploy/kubernetes/releases/csi-digitalocean-v1.3.0-luks.yaml @@ -0,0 +1,532 @@ +# Copyright 2020 DigitalOcean +# +# 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. + +# Configuration to deploy release version of the CSI DigitalOcean +# plugin (https://github.com/digitalocean/csi-digitalocean) compatible with +# Kubernetes >=v1.14+ +# +# example usage: kubectl create -f +# + + +# Install the CSI Driver. This simplifies driver discovery and enables us to +# customize Kubernetes behavior +# https://kubernetes-csi.github.io/docs/csi-driver-object.html +apiVersion: storage.k8s.io/v1beta1 +kind: CSIDriver +metadata: + name: dobs.csi.digitalocean.com +spec: + attachRequired: true + podInfoOnMount: true + +--- + +############################################## +########### ############ +########### Snapshot CRDs ############ +########### ############ +############################################## +# +# The following CRD's are created by the csi-snapshotter, however it +# complicates installing a driver, because we're not able to install a custom +# VolumeSnapshotClass until the csi-snapshotter sidecar is up and running. We +# pulled out the CRD's and put them here to simplify the installation for the +# users. Make sure these are up to date with the original ones whenever we +# release a new version: https://github.com/kubernetes-csi/external-snapshotter/blob/master/cmd/csi-snapshotter/create_crd.go + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: volumesnapshotclasses.snapshot.storage.k8s.io +spec: + group: snapshot.storage.k8s.io + names: + kind: VolumeSnapshotClass + plural: volumesnapshotclasses + scope: Cluster + version: v1alpha1 + subresources: + status: {} + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: volumesnapshotcontents.snapshot.storage.k8s.io +spec: + group: snapshot.storage.k8s.io + names: + kind: VolumeSnapshotContent + plural: volumesnapshotcontents + scope: Cluster + version: v1alpha1 + +--- + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: volumesnapshots.snapshot.storage.k8s.io +spec: + group: snapshot.storage.k8s.io + names: + kind: VolumeSnapshot + plural: volumesnapshots + scope: Namespaced + version: v1alpha1 + subresources: + status: {} + +--- + +kind: VolumeSnapshotClass +apiVersion: snapshot.storage.k8s.io/v1alpha1 +metadata: + name: do-block-storage + namespace: kube-system + annotations: + snapshot.storage.kubernetes.io/is-default-class: "true" +snapshotter: dobs.csi.digitalocean.com + +--- + +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: do-block-storage + namespace: kube-system + 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 +--- + +############################################## +########### ############ +########### Controller plugin ############ +########### ############ +############################################## + +kind: StatefulSet +apiVersion: apps/v1 +metadata: + name: csi-do-controller + namespace: kube-system +spec: + serviceName: "csi-do" + selector: + matchLabels: + app: csi-do-controller + replicas: 1 + template: + metadata: + labels: + app: csi-do-controller + role: csi-do + spec: + priorityClassName: system-cluster-critical + serviceAccount: csi-do-controller-sa + containers: + - name: csi-provisioner + image: quay.io/k8scsi/csi-provisioner:v1.4.0 + args: + - "--csi-address=$(ADDRESS)" + - "--v=5" + env: + - name: ADDRESS + value: /var/lib/csi/sockets/pluginproxy/csi.sock + imagePullPolicy: "IfNotPresent" + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + - name: csi-attacher + image: quay.io/k8scsi/csi-attacher:v2.0.0 + args: + - "--v=5" + - "--csi-address=$(ADDRESS)" + env: + - name: ADDRESS + value: /var/lib/csi/sockets/pluginproxy/csi.sock + imagePullPolicy: "IfNotPresent" + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + - name: csi-snapshotter + image: quay.io/k8scsi/csi-snapshotter:v1.2.2 + args: + - "--csi-address=$(ADDRESS)" + env: + - name: ADDRESS + value: /var/lib/csi/sockets/pluginproxy/csi.sock + imagePullPolicy: IfNotPresent + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + - name: csi-resizer + image: quay.io/k8scsi/csi-resizer:v0.3.0 + args: + - "--v=5" + - "--csi-address=$(ADDRESS)" + - "--csiTimeout=30s" + env: + - name: ADDRESS + value: /var/lib/csi/sockets/pluginproxy/csi.sock + imagePullPolicy: "IfNotPresent" + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + - name: csi-do-plugin + image: digitalocean/do-csi-plugin:v1.3.0-luks + args : + - "--endpoint=$(CSI_ENDPOINT)" + - "--token=$(DIGITALOCEAN_ACCESS_TOKEN)" + - "--url=$(DIGITALOCEAN_API_URL)" + env: + - name: CSI_ENDPOINT + value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock + - name: DIGITALOCEAN_API_URL + value: https://api.digitalocean.com/ + - name: DIGITALOCEAN_ACCESS_TOKEN + valueFrom: + secretKeyRef: + name: digitalocean + key: access-token + imagePullPolicy: "Always" + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + volumes: + - name: socket-dir + emptyDir: {} +--- + +kind: ServiceAccount +apiVersion: v1 +metadata: + name: csi-do-controller-sa + namespace: kube-system + +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-provisioner-role +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "create", "delete"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: ["storage.k8s.io"] + resources: ["csinodes"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["list", "watch", "create", "update", "patch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshots"] + verbs: ["get", "list"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotcontents"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list", "watch"] + +--- + +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-provisioner-binding +subjects: + - kind: ServiceAccount + name: csi-do-controller-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: csi-do-provisioner-role + apiGroup: rbac.authorization.k8s.io + +--- +# Attacher must be able to work with PVs, nodes and VolumeAttachments +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-attacher-role +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list", "watch"] + - apiGroups: ["storage.k8s.io"] + resources: ["csinodes"] + verbs: ["get", "list", "watch"] + - apiGroups: ["storage.k8s.io"] + resources: ["volumeattachments"] + verbs: ["get", "list", "watch", "update", "patch"] + +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-attacher-binding +subjects: + - kind: ServiceAccount + name: csi-do-controller-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: csi-do-attacher-role + apiGroup: rbac.authorization.k8s.io + +--- + +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-snapshotter-role +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotcontents"] + verbs: ["create", "get", "list", "watch", "update", "delete"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshots"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshots/status"] + verbs: ["update"] + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["create", "list", "watch", "delete", "get", "update"] + +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-snapshotter-binding +subjects: + - kind: ServiceAccount + name: csi-do-controller-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: csi-do-snapshotter-role + apiGroup: rbac.authorization.k8s.io + +--- + +# Resizer must be able to work with PVCs, PVs, SCs. +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-resizer-role +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims/status"] + verbs: ["update", "patch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["list", "watch", "create", "update", "patch"] + +--- + +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-resizer-binding +subjects: + - kind: ServiceAccount + name: csi-do-controller-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: csi-do-resizer-role + apiGroup: rbac.authorization.k8s.io + +--- + +######################################## +########### ############ +########### Node plugin ############ +########### ############ +######################################## + +kind: DaemonSet +apiVersion: apps/v1 +metadata: + name: csi-do-node + namespace: kube-system +spec: + selector: + matchLabels: + app: csi-do-node + template: + metadata: + labels: + app: csi-do-node + role: csi-do + spec: + priorityClassName: system-node-critical + serviceAccount: csi-do-node-sa + hostNetwork: true + containers: + - name: csi-node-driver-registrar + image: quay.io/k8scsi/csi-node-driver-registrar:v1.1.0 + args: + - "--v=5" + - "--csi-address=$(ADDRESS)" + - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)" + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "rm -rf /registration/dobs.csi.digitalocean.com /registration/dobs.csi.digitalocean.com-reg.sock"] + env: + - name: ADDRESS + value: /csi/csi.sock + - name: DRIVER_REG_SOCK_PATH + value: /var/lib/kubelet/plugins/dobs.csi.digitalocean.com/csi.sock + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + volumeMounts: + - name: plugin-dir + mountPath: /csi/ + - name: registration-dir + mountPath: /registration/ + - name: csi-do-plugin + image: digitalocean/do-csi-plugin:v1.3.0-luks + args : + - "--endpoint=$(CSI_ENDPOINT)" + - "--url=$(DIGITALOCEAN_API_URL)" + env: + - name: CSI_ENDPOINT + value: unix:///csi/csi.sock + - name: DIGITALOCEAN_API_URL + value: https://api.digitalocean.com/ + imagePullPolicy: "Always" + securityContext: + privileged: true + capabilities: + add: ["SYS_ADMIN"] + allowPrivilegeEscalation: true + volumeMounts: + - name: plugin-dir + mountPath: /csi + - name: pods-mount-dir + mountPath: /var/lib/kubelet + # needed so that any mounts setup inside this container are + # propagated back to the host machine. + mountPropagation: "Bidirectional" + - name: device-dir + mountPath: /dev + - name: tmpfs + mountPath: /tmp + volumes: + - name: registration-dir + hostPath: + path: /var/lib/kubelet/plugins_registry/ + type: DirectoryOrCreate + - name: plugin-dir + hostPath: + path: /var/lib/kubelet/plugins/dobs.csi.digitalocean.com + type: DirectoryOrCreate + - name: pods-mount-dir + hostPath: + path: /var/lib/kubelet + type: Directory + - name: device-dir + hostPath: + path: /dev + # 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 + namespace: kube-system + +--- + +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-node-driver-registrar-role + namespace: kube-system +rules: + - apiGroups: [""] + resources: ["events"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + +--- + +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-do-node-driver-registrar-binding +subjects: + - kind: ServiceAccount + name: csi-do-node-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: csi-do-node-driver-registrar-role + apiGroup: rbac.authorization.k8s.io diff --git a/driver/controller.go b/driver/controller.go index 0734ff6d6..f0d372438 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/luks_util.go b/driver/luks_util.go new file mode 100644 index 000000000..896c95dd4 --- /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 + // cypher 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/mounter.go b/driver/mounter.go index b5cb72183..1ce82db07 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 758b8a2e0..e2efd071e 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, publishContext, 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, @@ -271,6 +292,16 @@ func (d *Driver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublish return nil, err } + if mounted { + log.Info("unmounting the target path") + err := d.mounter.Unmount(req.TargetPath, luksContext) + if err != nil { + return nil, err + } + } else { + log.Info("target path is already unmounted") + } + log.Info("unmounting volume is finished") return &csi.NodeUnpublishVolumeResponse{}, nil } @@ -470,7 +501,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 +529,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 +539,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 +565,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 { From 7218f432e4108ad32bb35efe60edf8ee5ae7d9a3 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sun, 19 Jul 2020 11:29:41 +0200 Subject: [PATCH 2/5] Fix test fakes --- driver/driver_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/driver/driver_test.go b/driver/driver_test.go index e7583b5eb..df786d154 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 From 610e4e9ddc4782bc6810a5cc5be4f9c4505d4e82 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sun, 19 Jul 2020 12:58:57 +0200 Subject: [PATCH 3/5] Add create-volume-test for LUKS --- driver/luks_util.go | 2 +- driver/luks_util_test.go | 110 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 driver/luks_util_test.go diff --git a/driver/luks_util.go b/driver/luks_util.go index 896c95dd4..5ba64ecd0 100644 --- a/driver/luks_util.go +++ b/driver/luks_util.go @@ -33,7 +33,7 @@ const ( LuksEncryptedAttribute = DefaultDriverName + "/luks-encrypted" // LuksCipherAttribute is used to pass the information about the luks encryption - // cypher to `NodeStageVolume` + // cipher to `NodeStageVolume` LuksCipherAttribute = DefaultDriverName + "/luks-cipher" // LuksKeySizeAttribute is used to pass the information about the luks key size diff --git a/driver/luks_util_test.go b/driver/luks_util_test.go new file mode 100644 index 000000000..c43cbeb12 --- /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) + } + }) + } +} From c59016eacc8916e921a5b68c9144f527da5a83cf Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sat, 20 Feb 2021 16:44:45 +0100 Subject: [PATCH 4/5] Change PublishContext to VolumeContext --- driver/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/node.go b/driver/node.go index e2efd071e..0139dcbff 100644 --- a/driver/node.go +++ b/driver/node.go @@ -106,7 +106,7 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe source := getDeviceByIDPath(volumeName) - luksContext := getLuksContext(req.Secrets, publishContext, VolumeLifecycleNodeStageVolume) + luksContext := getLuksContext(req.Secrets, req.VolumeContext, VolumeLifecycleNodeStageVolume) target := req.StagingTargetPath From 795fd5c609febf374ca2179b2f855cf159990082 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Fri, 18 Mar 2022 14:47:54 +0100 Subject: [PATCH 5/5] Merge upstream changes for node.unmount --- .../csi-digitalocean-v1.3.0-luks.yaml | 532 ------------------ .../csi-digitalocean-v4.0.0/driver.yaml | 54 +- driver/node.go | 12 +- 3 files changed, 34 insertions(+), 564 deletions(-) delete mode 100644 deploy/kubernetes/releases/csi-digitalocean-v1.3.0-luks.yaml diff --git a/deploy/kubernetes/releases/csi-digitalocean-v1.3.0-luks.yaml b/deploy/kubernetes/releases/csi-digitalocean-v1.3.0-luks.yaml deleted file mode 100644 index 33eb21c7a..000000000 --- a/deploy/kubernetes/releases/csi-digitalocean-v1.3.0-luks.yaml +++ /dev/null @@ -1,532 +0,0 @@ -# Copyright 2020 DigitalOcean -# -# 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. - -# Configuration to deploy release version of the CSI DigitalOcean -# plugin (https://github.com/digitalocean/csi-digitalocean) compatible with -# Kubernetes >=v1.14+ -# -# example usage: kubectl create -f -# - - -# Install the CSI Driver. This simplifies driver discovery and enables us to -# customize Kubernetes behavior -# https://kubernetes-csi.github.io/docs/csi-driver-object.html -apiVersion: storage.k8s.io/v1beta1 -kind: CSIDriver -metadata: - name: dobs.csi.digitalocean.com -spec: - attachRequired: true - podInfoOnMount: true - ---- - -############################################## -########### ############ -########### Snapshot CRDs ############ -########### ############ -############################################## -# -# The following CRD's are created by the csi-snapshotter, however it -# complicates installing a driver, because we're not able to install a custom -# VolumeSnapshotClass until the csi-snapshotter sidecar is up and running. We -# pulled out the CRD's and put them here to simplify the installation for the -# users. Make sure these are up to date with the original ones whenever we -# release a new version: https://github.com/kubernetes-csi/external-snapshotter/blob/master/cmd/csi-snapshotter/create_crd.go - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: volumesnapshotclasses.snapshot.storage.k8s.io -spec: - group: snapshot.storage.k8s.io - names: - kind: VolumeSnapshotClass - plural: volumesnapshotclasses - scope: Cluster - version: v1alpha1 - subresources: - status: {} - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: volumesnapshotcontents.snapshot.storage.k8s.io -spec: - group: snapshot.storage.k8s.io - names: - kind: VolumeSnapshotContent - plural: volumesnapshotcontents - scope: Cluster - version: v1alpha1 - ---- - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: volumesnapshots.snapshot.storage.k8s.io -spec: - group: snapshot.storage.k8s.io - names: - kind: VolumeSnapshot - plural: volumesnapshots - scope: Namespaced - version: v1alpha1 - subresources: - status: {} - ---- - -kind: VolumeSnapshotClass -apiVersion: snapshot.storage.k8s.io/v1alpha1 -metadata: - name: do-block-storage - namespace: kube-system - annotations: - snapshot.storage.kubernetes.io/is-default-class: "true" -snapshotter: dobs.csi.digitalocean.com - ---- - -kind: StorageClass -apiVersion: storage.k8s.io/v1 -metadata: - name: do-block-storage - namespace: kube-system - 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 ---- - -############################################## -########### ############ -########### Controller plugin ############ -########### ############ -############################################## - -kind: StatefulSet -apiVersion: apps/v1 -metadata: - name: csi-do-controller - namespace: kube-system -spec: - serviceName: "csi-do" - selector: - matchLabels: - app: csi-do-controller - replicas: 1 - template: - metadata: - labels: - app: csi-do-controller - role: csi-do - spec: - priorityClassName: system-cluster-critical - serviceAccount: csi-do-controller-sa - containers: - - name: csi-provisioner - image: quay.io/k8scsi/csi-provisioner:v1.4.0 - args: - - "--csi-address=$(ADDRESS)" - - "--v=5" - env: - - name: ADDRESS - value: /var/lib/csi/sockets/pluginproxy/csi.sock - imagePullPolicy: "IfNotPresent" - volumeMounts: - - name: socket-dir - mountPath: /var/lib/csi/sockets/pluginproxy/ - - name: csi-attacher - image: quay.io/k8scsi/csi-attacher:v2.0.0 - args: - - "--v=5" - - "--csi-address=$(ADDRESS)" - env: - - name: ADDRESS - value: /var/lib/csi/sockets/pluginproxy/csi.sock - imagePullPolicy: "IfNotPresent" - volumeMounts: - - name: socket-dir - mountPath: /var/lib/csi/sockets/pluginproxy/ - - name: csi-snapshotter - image: quay.io/k8scsi/csi-snapshotter:v1.2.2 - args: - - "--csi-address=$(ADDRESS)" - env: - - name: ADDRESS - value: /var/lib/csi/sockets/pluginproxy/csi.sock - imagePullPolicy: IfNotPresent - volumeMounts: - - name: socket-dir - mountPath: /var/lib/csi/sockets/pluginproxy/ - - name: csi-resizer - image: quay.io/k8scsi/csi-resizer:v0.3.0 - args: - - "--v=5" - - "--csi-address=$(ADDRESS)" - - "--csiTimeout=30s" - env: - - name: ADDRESS - value: /var/lib/csi/sockets/pluginproxy/csi.sock - imagePullPolicy: "IfNotPresent" - volumeMounts: - - name: socket-dir - mountPath: /var/lib/csi/sockets/pluginproxy/ - - name: csi-do-plugin - image: digitalocean/do-csi-plugin:v1.3.0-luks - args : - - "--endpoint=$(CSI_ENDPOINT)" - - "--token=$(DIGITALOCEAN_ACCESS_TOKEN)" - - "--url=$(DIGITALOCEAN_API_URL)" - env: - - name: CSI_ENDPOINT - value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock - - name: DIGITALOCEAN_API_URL - value: https://api.digitalocean.com/ - - name: DIGITALOCEAN_ACCESS_TOKEN - valueFrom: - secretKeyRef: - name: digitalocean - key: access-token - imagePullPolicy: "Always" - volumeMounts: - - name: socket-dir - mountPath: /var/lib/csi/sockets/pluginproxy/ - volumes: - - name: socket-dir - emptyDir: {} ---- - -kind: ServiceAccount -apiVersion: v1 -metadata: - name: csi-do-controller-sa - namespace: kube-system - ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-provisioner-role -rules: - - apiGroups: [""] - resources: ["secrets"] - verbs: ["get", "list"] - - apiGroups: [""] - resources: ["persistentvolumes"] - verbs: ["get", "list", "watch", "create", "delete"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list", "watch", "update"] - - apiGroups: ["storage.k8s.io"] - resources: ["storageclasses"] - verbs: ["get", "list", "watch"] - - apiGroups: ["storage.k8s.io"] - resources: ["csinodes"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["events"] - verbs: ["list", "watch", "create", "update", "patch"] - - apiGroups: ["snapshot.storage.k8s.io"] - resources: ["volumesnapshots"] - verbs: ["get", "list"] - - apiGroups: ["snapshot.storage.k8s.io"] - resources: ["volumesnapshotcontents"] - verbs: ["get", "list"] - - apiGroups: [""] - resources: ["nodes"] - verbs: ["get", "list", "watch"] - ---- - -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-provisioner-binding -subjects: - - kind: ServiceAccount - name: csi-do-controller-sa - namespace: kube-system -roleRef: - kind: ClusterRole - name: csi-do-provisioner-role - apiGroup: rbac.authorization.k8s.io - ---- -# Attacher must be able to work with PVs, nodes and VolumeAttachments -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-attacher-role -rules: - - apiGroups: [""] - resources: ["persistentvolumes"] - verbs: ["get", "list", "watch", "update", "patch"] - - apiGroups: [""] - resources: ["nodes"] - verbs: ["get", "list", "watch"] - - apiGroups: ["storage.k8s.io"] - resources: ["csinodes"] - verbs: ["get", "list", "watch"] - - apiGroups: ["storage.k8s.io"] - resources: ["volumeattachments"] - verbs: ["get", "list", "watch", "update", "patch"] - ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-attacher-binding -subjects: - - kind: ServiceAccount - name: csi-do-controller-sa - namespace: kube-system -roleRef: - kind: ClusterRole - name: csi-do-attacher-role - apiGroup: rbac.authorization.k8s.io - ---- - -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-snapshotter-role -rules: - - apiGroups: [""] - resources: ["persistentvolumes"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list", "watch", "update"] - - apiGroups: ["storage.k8s.io"] - resources: ["storageclasses"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["events"] - verbs: ["list", "watch", "create", "update", "patch"] - - apiGroups: [""] - resources: ["secrets"] - verbs: ["get", "list"] - - apiGroups: ["snapshot.storage.k8s.io"] - resources: ["volumesnapshotclasses"] - verbs: ["get", "list", "watch"] - - apiGroups: ["snapshot.storage.k8s.io"] - resources: ["volumesnapshotcontents"] - verbs: ["create", "get", "list", "watch", "update", "delete"] - - apiGroups: ["snapshot.storage.k8s.io"] - resources: ["volumesnapshots"] - verbs: ["get", "list", "watch", "update"] - - apiGroups: ["snapshot.storage.k8s.io"] - resources: ["volumesnapshots/status"] - verbs: ["update"] - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: ["create", "list", "watch", "delete", "get", "update"] - ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-snapshotter-binding -subjects: - - kind: ServiceAccount - name: csi-do-controller-sa - namespace: kube-system -roleRef: - kind: ClusterRole - name: csi-do-snapshotter-role - apiGroup: rbac.authorization.k8s.io - ---- - -# Resizer must be able to work with PVCs, PVs, SCs. -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-resizer-role -rules: - - apiGroups: [""] - resources: ["persistentvolumes"] - verbs: ["get", "list", "watch", "update", "patch"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["persistentvolumeclaims/status"] - verbs: ["update", "patch"] - - apiGroups: [""] - resources: ["events"] - verbs: ["list", "watch", "create", "update", "patch"] - ---- - -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-resizer-binding -subjects: - - kind: ServiceAccount - name: csi-do-controller-sa - namespace: kube-system -roleRef: - kind: ClusterRole - name: csi-do-resizer-role - apiGroup: rbac.authorization.k8s.io - ---- - -######################################## -########### ############ -########### Node plugin ############ -########### ############ -######################################## - -kind: DaemonSet -apiVersion: apps/v1 -metadata: - name: csi-do-node - namespace: kube-system -spec: - selector: - matchLabels: - app: csi-do-node - template: - metadata: - labels: - app: csi-do-node - role: csi-do - spec: - priorityClassName: system-node-critical - serviceAccount: csi-do-node-sa - hostNetwork: true - containers: - - name: csi-node-driver-registrar - image: quay.io/k8scsi/csi-node-driver-registrar:v1.1.0 - args: - - "--v=5" - - "--csi-address=$(ADDRESS)" - - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)" - lifecycle: - preStop: - exec: - command: ["/bin/sh", "-c", "rm -rf /registration/dobs.csi.digitalocean.com /registration/dobs.csi.digitalocean.com-reg.sock"] - env: - - name: ADDRESS - value: /csi/csi.sock - - name: DRIVER_REG_SOCK_PATH - value: /var/lib/kubelet/plugins/dobs.csi.digitalocean.com/csi.sock - - name: KUBE_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - volumeMounts: - - name: plugin-dir - mountPath: /csi/ - - name: registration-dir - mountPath: /registration/ - - name: csi-do-plugin - image: digitalocean/do-csi-plugin:v1.3.0-luks - args : - - "--endpoint=$(CSI_ENDPOINT)" - - "--url=$(DIGITALOCEAN_API_URL)" - env: - - name: CSI_ENDPOINT - value: unix:///csi/csi.sock - - name: DIGITALOCEAN_API_URL - value: https://api.digitalocean.com/ - imagePullPolicy: "Always" - securityContext: - privileged: true - capabilities: - add: ["SYS_ADMIN"] - allowPrivilegeEscalation: true - volumeMounts: - - name: plugin-dir - mountPath: /csi - - name: pods-mount-dir - mountPath: /var/lib/kubelet - # needed so that any mounts setup inside this container are - # propagated back to the host machine. - mountPropagation: "Bidirectional" - - name: device-dir - mountPath: /dev - - name: tmpfs - mountPath: /tmp - volumes: - - name: registration-dir - hostPath: - path: /var/lib/kubelet/plugins_registry/ - type: DirectoryOrCreate - - name: plugin-dir - hostPath: - path: /var/lib/kubelet/plugins/dobs.csi.digitalocean.com - type: DirectoryOrCreate - - name: pods-mount-dir - hostPath: - path: /var/lib/kubelet - type: Directory - - name: device-dir - hostPath: - path: /dev - # 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 - namespace: kube-system - ---- - -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-node-driver-registrar-role - namespace: kube-system -rules: - - apiGroups: [""] - resources: ["events"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - ---- - -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: csi-do-node-driver-registrar-binding -subjects: - - kind: ServiceAccount - name: csi-do-node-sa - namespace: kube-system -roleRef: - kind: ClusterRole - name: csi-do-node-driver-registrar-role - apiGroup: rbac.authorization.k8s.io 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 c047ddad8..c6163b4eb 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/node.go b/driver/node.go index 0139dcbff..b13da5e77 100644 --- a/driver/node.go +++ b/driver/node.go @@ -287,21 +287,11 @@ 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 } - if mounted { - log.Info("unmounting the target path") - err := d.mounter.Unmount(req.TargetPath, luksContext) - if err != nil { - return nil, err - } - } else { - log.Info("target path is already unmounted") - } - log.Info("unmounting volume is finished") return &csi.NodeUnpublishVolumeResponse{}, nil }