Skip to content

Commit

Permalink
Luks backchannel
Browse files Browse the repository at this point in the history
Track and report potentially in use LUKS passphrases for volumes and snapshots

Adds additional Trident REST API
PUT .../volume/{volume}/luksPassphraseNames
for use by the Trident node plugin to inform Trident of a volumes list of potential passphrases.
Adds new field "config.luksPassphraseNames", a list of strings, to volume and snapshot API responses.
  • Loading branch information
ameade authored Jan 20, 2023
1 parent 2d1f538 commit 8e46e8f
Show file tree
Hide file tree
Showing 23 changed files with 1,156 additions and 122 deletions.
53 changes: 53 additions & 0 deletions core/orchestrator_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,34 @@ func (o *TridentOrchestrator) addVolumeFinish(
return externalVol, nil
}

// UpdateVolumeLUKSPassphraseNames updates the LUKS passphrase names stored on a volume in the cache and persistent store
func (o *TridentOrchestrator) UpdateVolume(ctx context.Context, volume string, passphraseNames *[]string) error {
if o.bootstrapError != nil {
return o.bootstrapError
}

o.mutex.Lock()
defer o.mutex.Unlock()
defer o.updateMetrics()
vol, err := o.storeClient.GetVolume(ctx, volume)
if err != nil {
return err
}

// We want to make sure the persistence layer is updated before we update the core copy
// So we have to deep copy the volume from the cache to construct the volume to pass into the store client
newVolume := storage.NewVolume(vol.Config.ConstructClone(), vol.BackendUUID, vol.Pool, vol.Orphaned, vol.State)
if passphraseNames != nil {
newVolume.Config.LUKSPassphraseNames = *passphraseNames
}
err = o.storeClient.UpdateVolume(ctx, newVolume)
if err != nil {
return err
}
o.volumes[volume] = newVolume
return nil
}

func (o *TridentOrchestrator) CloneVolume(
ctx context.Context, volumeConfig *storage.VolumeConfig,
) (externalVol *storage.VolumeExternal, err error) {
Expand Down Expand Up @@ -2030,11 +2058,27 @@ func (o *TridentOrchestrator) cloneVolumeInitial(
// Clear these values as they were copied from the source volume Config
cloneConfig.SubordinateVolumes = make(map[string]interface{})
cloneConfig.ShareSourceVolume = ""
cloneConfig.LUKSPassphraseNames = sourceVolume.Config.LUKSPassphraseNames
// Override this value only if SplitOnClone has been defined in clone volume's config
if volumeConfig.SplitOnClone != "" {
cloneConfig.SplitOnClone = volumeConfig.SplitOnClone
}

// If it's from snapshot, we need the LUKS passphrases value from the snapshot
isLUKS, err := strconv.ParseBool(cloneConfig.LUKSEncryption)
// If the LUKSEncryption is not a bool (or is empty string) assume we are not making a LUKS volume
if err != nil {
isLUKS = false
}
if cloneConfig.CloneSourceSnapshot != "" && isLUKS {
snapshotID := storage.MakeSnapshotID(cloneConfig.CloneSourceVolume, cloneConfig.CloneSourceSnapshot)
sourceSnapshot, found := o.snapshots[snapshotID]
if !found {
return nil, utils.NotFoundError("Could not find source snapshot for volume")
}
cloneConfig.LUKSPassphraseNames = sourceSnapshot.Config.LUKSPassphraseNames
}

// With the introduction of Virtual Pools we will try our best to place the cloned volume in the same
// Virtual Pool. For cases where attributes are not defined in the PVC (source/clone) but instead in the
// backend storage pool, e.g. splitOnClone, we would like the cloned PV to have the same attribute value
Expand Down Expand Up @@ -3771,6 +3815,7 @@ func (o *TridentOrchestrator) CreateSnapshot(
// Complete the snapshot config
snapshotConfig.InternalName = snapshotConfig.Name
snapshotConfig.VolumeInternalName = volume.Config.InternalName
snapshotConfig.LUKSPassphraseNames = volume.Config.LUKSPassphraseNames

// Ensure a snapshot is even possible before creating the transaction
if err := backend.CanSnapshot(ctx, snapshotConfig, volume.Config); err != nil {
Expand Down Expand Up @@ -3803,6 +3848,14 @@ func (o *TridentOrchestrator) CreateSnapshot(
snapshotConfig.Name, snapshotConfig.VolumeName, backend.Name(), err)
}

// Ensure we store all potential LUKS passphrases, there may have been a passphrase rotation during the snapshot process
volume = o.volumes[snapshotConfig.VolumeName]
for _, v := range volume.Config.LUKSPassphraseNames {
if !utils.SliceContainsString(snapshotConfig.LUKSPassphraseNames, v) {
snapshotConfig.LUKSPassphraseNames = append(snapshotConfig.LUKSPassphraseNames, v)
}
}

// Save references to new snapshot
if err = o.storeClient.AddSnapshot(ctx, snapshot); err != nil {
return nil, err
Expand Down
196 changes: 196 additions & 0 deletions core/orchestrator_core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,202 @@ func TestAddStorageClassVolumes(t *testing.T) {
cleanup(t, orchestrator)
}

func TestUpdateVolume_LUKSPassphraseNames(t *testing.T) {
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Positive case: luksPassphraseNames field updated
orchestrator := getOrchestrator(t, false)
vol := &storage.Volume{
Config: &storage.VolumeConfig{Name: "test-vol", LUKSPassphraseNames: []string{}},
BackendUUID: "12345",
}
orchestrator.volumes[vol.Config.Name] = vol
err := orchestrator.storeClient.AddVolume(context.TODO(), vol)
assert.NoError(t, err)
assert.Empty(t, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames)

err = orchestrator.UpdateVolume(context.TODO(), "test-vol", &[]string{"A"})
desiredPassphraseNames := []string{"A"}
assert.NoError(t, err)
assert.Equal(t, desiredPassphraseNames, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames)

storedVol, err := orchestrator.storeClient.GetVolume(context.TODO(), "test-vol")
assert.NoError(t, err)
assert.Equal(t, desiredPassphraseNames, storedVol.Config.LUKSPassphraseNames)

err = orchestrator.storeClient.DeleteVolume(context.TODO(), vol)
assert.NoError(t, err)

// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Positive case: luksPassphraseNames field nil
orchestrator = getOrchestrator(t, false)
vol = &storage.Volume{
Config: &storage.VolumeConfig{Name: "test-vol", LUKSPassphraseNames: []string{}},
BackendUUID: "12345",
}
orchestrator.volumes[vol.Config.Name] = vol
err = orchestrator.storeClient.AddVolume(context.TODO(), vol)
assert.NoError(t, err)
assert.Empty(t, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames)

err = orchestrator.UpdateVolume(context.TODO(), "test-vol", nil)
desiredPassphraseNames = []string{}
assert.NoError(t, err)
assert.Equal(t, desiredPassphraseNames, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames)

storedVol, err = orchestrator.storeClient.GetVolume(context.TODO(), "test-vol")
assert.NoError(t, err)
assert.Equal(t, desiredPassphraseNames, storedVol.Config.LUKSPassphraseNames)

err = orchestrator.storeClient.DeleteVolume(context.TODO(), vol)
assert.NoError(t, err)

// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Negative case: failed to update persistence, volume not found
orchestrator = getOrchestrator(t, false)
vol = &storage.Volume{
Config: &storage.VolumeConfig{Name: "test-vol", LUKSPassphraseNames: []string{}},
BackendUUID: "12345",
}
orchestrator.volumes[vol.Config.Name] = vol

err = orchestrator.UpdateVolume(context.TODO(), "test-vol", &[]string{"A"})
desiredPassphraseNames = []string{}
assert.Error(t, err)
assert.Equal(t, desiredPassphraseNames, orchestrator.volumes[vol.Config.Name].Config.LUKSPassphraseNames)

_, err = orchestrator.storeClient.GetVolume(context.TODO(), "test-vol")
// Not found
assert.Error(t, err)

// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Negative case: bootstrap error
orchestrator = getOrchestrator(t, false)
bootstrapError := fmt.Errorf("my bootstrap error")
orchestrator.bootstrapError = bootstrapError

err = orchestrator.UpdateVolume(context.TODO(), "test-vol", &[]string{"A"})
assert.Error(t, err)
assert.ErrorIs(t, err, bootstrapError)
}

func TestCloneVolume_SnapshotDataSource_LUKS(t *testing.T) {
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Positive case: luksPassphraseNames field updated
// // Setup
mockPools := tu.GetFakePools()
orchestrator := getOrchestrator(t, false)

// Make a backend
poolNames := []string{tu.SlowSnapshots}
pools := make(map[string]*fake.StoragePool, len(poolNames))
for _, poolName := range poolNames {
pools[poolName] = mockPools[poolName]
}
volumes := make([]fake.Volume, 0)
cfg, err := fakedriver.NewFakeStorageDriverConfigJSON("slow-block", "block", pools, volumes)
assert.NoError(t, err)
_, err = orchestrator.AddBackend(ctx(), cfg, "")
assert.NoError(t, err)
defer orchestrator.DeleteBackend(ctx(), "slow-block")

// Make a StorageClass
storageClass := &storageclass.Config{Name: "specific"}
_, err = orchestrator.AddStorageClass(ctx(), storageClass)
defer orchestrator.DeleteStorageClass(ctx(), storageClass.Name)
assert.NoError(t, err)

// Create the original volume
volConfig := tu.GenerateVolumeConfig("block", 1, "specific", config.Block)
volConfig.LUKSEncryption = "true"
volConfig.LUKSPassphraseNames = []string{"A", "B"}
_, err = orchestrator.AddVolume(ctx(), volConfig)
assert.NoError(t, err)
defer orchestrator.DeleteVolume(ctx(), volConfig.Name)

// Create a snapshot
snapshotConfig := generateSnapshotConfig("test-snapshot", volConfig.Name, volConfig.InternalName)
snapshot, err := orchestrator.CreateSnapshot(ctx(), snapshotConfig)
assert.NoError(t, err)
assert.Equal(t, snapshot.Config.LUKSPassphraseNames, []string{"A", "B"})
defer orchestrator.DeleteSnapshot(ctx(), volConfig.Name, snapshotConfig.Name)

// "rotate" the luksPassphraseNames of the volume
err = orchestrator.UpdateVolume(ctx(), volConfig.Name, &[]string{"A"})
assert.NoError(t, err)
vol, err := orchestrator.GetVolume(ctx(), volConfig.Name)
assert.NoError(t, err)
volConfig = vol.Config
assert.Equal(t, vol.Config.LUKSPassphraseNames, []string{"A"})

// Now clone the snapshot and ensure everything looks fine
cloneName := volConfig.Name + "_clone"
cloneConfig := &storage.VolumeConfig{
Name: cloneName,
StorageClass: volConfig.StorageClass,
CloneSourceVolume: volConfig.Name,
CloneSourceSnapshot: snapshotConfig.Name,
VolumeMode: volConfig.VolumeMode,
}
cloneResult, err := orchestrator.CloneVolume(ctx(), cloneConfig)
assert.NoError(t, err)
assert.Equal(t, []string{"A", "B"}, cloneResult.Config.LUKSPassphraseNames)
defer orchestrator.DeleteVolume(ctx(), cloneResult.Config.Name)
}

func TestCloneVolume_VolumeDataSource_LUKS(t *testing.T) {
// // Setup
mockPools := tu.GetFakePools()
orchestrator := getOrchestrator(t, false)

// Create backend
poolNames := []string{tu.SlowSnapshots}
pools := make(map[string]*fake.StoragePool, len(poolNames))
for _, poolName := range poolNames {
pools[poolName] = mockPools[poolName]
}
volumes := make([]fake.Volume, 0)
cfg, err := fakedriver.NewFakeStorageDriverConfigJSON("slow-block", "block", pools, volumes)
assert.NoError(t, err)
_, err = orchestrator.AddBackend(ctx(), cfg, "")
assert.NoError(t, err)
defer orchestrator.DeleteBackend(ctx(), "slow-block")

// Create a StorageClass
storageClass := &storageclass.Config{Name: "specific"}
_, err = orchestrator.AddStorageClass(ctx(), storageClass)
defer orchestrator.DeleteStorageClass(ctx(), storageClass.Name)
assert.NoError(t, err)

// Create the Volume
volConfig := tu.GenerateVolumeConfig("block", 1, "specific", config.Block)
volConfig.LUKSEncryption = "true"
volConfig.LUKSPassphraseNames = []string{"A", "B"}
// Create the source volume
_, err = orchestrator.AddVolume(ctx(), volConfig)
assert.NoError(t, err)
defer orchestrator.DeleteVolume(ctx(), volConfig.Name)

// Create a snapshot
snapshotConfig := generateSnapshotConfig("test-snapshot", volConfig.Name, volConfig.InternalName)
snapshot, err := orchestrator.CreateSnapshot(ctx(), snapshotConfig)
assert.NoError(t, err)
assert.Equal(t, []string{"A", "B"}, snapshot.Config.LUKSPassphraseNames)
defer orchestrator.DeleteSnapshot(ctx(), volConfig.Name, snapshotConfig.Name)

// Now clone the volume and ensure everything looks fine
cloneName := volConfig.Name + "_clone"
cloneConfig := &storage.VolumeConfig{
Name: cloneName,
StorageClass: volConfig.StorageClass,
CloneSourceVolume: volConfig.Name,
VolumeMode: volConfig.VolumeMode,
}
cloneResult, err := orchestrator.CloneVolume(ctx(), cloneConfig)
assert.NoError(t, err)
assert.Equal(t, []string{"A", "B"}, cloneResult.Config.LUKSPassphraseNames)
defer orchestrator.DeleteVolume(ctx(), cloneResult.Config.Name)
}

// This test is modeled after TestAddStorageClassVolumes, but we don't need all the
// tests around storage class deletion, etc.
func TestCloneVolumes(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Orchestrator interface {
RemoveBackendConfigRef(ctx context.Context, backendUUID, configRef string) (err error)

AddVolume(ctx context.Context, volumeConfig *storage.VolumeConfig) (*storage.VolumeExternal, error)
UpdateVolume(ctx context.Context, volume string, passphraseNames *[]string) error
AttachVolume(ctx context.Context, volumeName, mountpoint string, publishInfo *utils.VolumePublishInfo) error
CloneVolume(ctx context.Context, volumeConfig *storage.VolumeConfig) (*storage.VolumeExternal, error)
DetachVolume(ctx context.Context, volumeName, mountpoint string) error
Expand Down
19 changes: 19 additions & 0 deletions frontend/csi/controller_api/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,22 @@ func (c *ControllerRestClient) UpdateVolumePublication(
}
return nil
}

func (c *ControllerRestClient) UpdateVolumeLUKSPassphraseNames(
ctx context.Context, volumeName string, passphraseNames []string,
) error {
operations := passphraseNames
body, err := json.Marshal(operations)
if err != nil {
return fmt.Errorf("could not marshal JSON; %v", err)
}
url := config.VolumeURL + "/" + volumeName + "/luksPassphraseNames"
resp, _, err := c.InvokeAPI(ctx, body, "PUT", url, false, false)
if err != nil {
return fmt.Errorf("could not log into the Trident CSI Controller: %v", err)
}
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not update volume LUKS passphrase names")
}
return nil
}
42 changes: 42 additions & 0 deletions frontend/csi/controller_api/rest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -415,3 +416,44 @@ func TestCreateTLSRestClient(t *testing.T) {
e = os.Remove("data.txt")
assert.NoError(t, e)
}

func TestUpdateVolumeLUKSPassphraseNames(t *testing.T) {
// Positive
controllerRestClient := ControllerRestClient{}
ctx = context.Background()
mockUpdateVolumeLUKSPassphraseNames := func(w http.ResponseWriter, r *http.Request) {
passphraseNames := new([]string)
body, err := io.ReadAll(io.LimitReader(r.Body, config.MaxRESTRequestSize))
assert.NoError(t, err)

err = json.Unmarshal(body, passphraseNames)
assert.NoError(t, err, "Got: ", body)

createResponse(w, "", http.StatusOK)
}

server := getHttpServer(config.VolumeURL+"/"+"test-vol/luksPassphraseNames", mockUpdateVolumeLUKSPassphraseNames)
controllerRestClient.url = server.URL
err := controllerRestClient.UpdateVolumeLUKSPassphraseNames(ctx, "test-vol", []string{"A"})
assert.NoError(t, err)
server.Close()

// Negative: Volume not found
controllerRestClient = ControllerRestClient{}
ctx = context.Background()
mockUpdateVolumeLUKSPassphraseNames = func(w http.ResponseWriter, r *http.Request) {
createResponse(w, "", http.StatusNotFound)
}

server = getHttpServer(config.VolumeURL+"/"+"test-vol/luksPassphraseNames", mockUpdateVolumeLUKSPassphraseNames)
controllerRestClient.url = server.URL
err = controllerRestClient.UpdateVolumeLUKSPassphraseNames(ctx, "test-vol", []string{"A"})
assert.Error(t, err)
server.Close()

// Negative: Cannot connect to trident api
controllerRestClient = ControllerRestClient{}
ctx = context.Background()
err = controllerRestClient.UpdateVolumeLUKSPassphraseNames(ctx, "test-vol", []string{"A"})
assert.Error(t, err)
}
1 change: 1 addition & 0 deletions frontend/csi/controller_api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ type TridentController interface {
DeleteNode(ctx context.Context, name string) error
GetChap(ctx context.Context, volume, node string) (*utils.IscsiChapInfo, error)
UpdateVolumePublication(ctx context.Context, publication *utils.VolumePublicationExternal) error
UpdateVolumeLUKSPassphraseNames(ctx context.Context, volume string, passphraseNames []string) error
}
Loading

0 comments on commit 8e46e8f

Please sign in to comment.