mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-12-23 05:20:19 +00:00
7e6feecc25
The new StoreCryptoPassphrase() method makes it possible to store an unencrypted passphrase newly encrypted in the DEKStore. Cloning volumes will use this, as the passphrase from the original volume will need to get copied as part of the metadata for the volume. Signed-off-by: Niels de Vos <ndevos@redhat.com>
306 lines
10 KiB
Go
306 lines
10 KiB
Go
/*
|
|
Copyright 2019 The Ceph-CSI Authors.
|
|
|
|
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 util
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
"crypto/rand"
|
|
)
|
|
|
|
const (
|
|
mapperFilePrefix = "luks-rbd-"
|
|
mapperFilePathPrefix = "/dev/mapper"
|
|
|
|
// kmsConfigPath is the location of the vault config file
|
|
kmsConfigPath = "/etc/ceph-csi-encryption-kms-config/config.json"
|
|
|
|
// Passphrase size - 20 bytes is 160 bits to satisfy:
|
|
// https://tools.ietf.org/html/rfc6749#section-10.10
|
|
encryptionPassphraseSize = 20
|
|
)
|
|
|
|
var (
|
|
// ErrDEKStoreNotFound is an error that is returned when the DEKStore
|
|
// has not been configured for the volumeID in the KMS instance.
|
|
ErrDEKStoreNotFound = errors.New("DEKStore not found")
|
|
|
|
// ErrDEKStoreNeeded is an indication that gets returned with
|
|
// NewVolumeEncryption when the KMS does not include support for the
|
|
// DEKStore interface.
|
|
ErrDEKStoreNeeded = errors.New("DEKStore required, use " +
|
|
"VolumeEncryption.SetDEKStore()")
|
|
)
|
|
|
|
type VolumeEncryption struct {
|
|
KMS EncryptionKMS
|
|
|
|
// dekStore that will be used, this can be the EncryptionKMS or a
|
|
// different object implementing the DEKStore interface.
|
|
dekStore DEKStore
|
|
|
|
id string
|
|
}
|
|
|
|
// NewVolumeEncryption creates a new instance of VolumeEncryption and
|
|
// configures the DEKStore. If the KMS does not provide a DEKStore interface,
|
|
// the VolumeEncryption will be created *and* a ErrDEKStoreNeeded is returned.
|
|
// Callers that receive a ErrDEKStoreNeeded error, should use
|
|
// VolumeEncryption.SetDEKStore() to configure an alternative storage for the
|
|
// DEKs.
|
|
func NewVolumeEncryption(id string, kms EncryptionKMS) (*VolumeEncryption, error) {
|
|
kmsID := id
|
|
if kmsID == "" {
|
|
// if kmsID is not set, encryption is enabled, and the type is
|
|
// SecretsKMS
|
|
kmsID = defaultKMSType
|
|
}
|
|
|
|
ve := &VolumeEncryption{
|
|
id: kmsID,
|
|
KMS: kms,
|
|
}
|
|
|
|
if kms.requiresDEKStore() == DEKStoreIntegrated {
|
|
dekStore, ok := kms.(DEKStore)
|
|
if !ok {
|
|
return nil, fmt.Errorf("KMS %T does not implement the "+
|
|
"DEKStore interface", kms)
|
|
}
|
|
|
|
ve.dekStore = dekStore
|
|
return ve, nil
|
|
}
|
|
|
|
return ve, ErrDEKStoreNeeded
|
|
}
|
|
|
|
// SetDEKStore sets the DEKStore for this VolumeEncryption instance. It will be
|
|
// used when StoreNewCryptoPassphrase() or RemoveDEK() is called.
|
|
func (ve *VolumeEncryption) SetDEKStore(dekStore DEKStore) {
|
|
ve.dekStore = dekStore
|
|
}
|
|
|
|
// Destroy frees any resources that the VolumeEncryption instance allocated.
|
|
func (ve *VolumeEncryption) Destroy() {
|
|
ve.KMS.Destroy()
|
|
}
|
|
|
|
// RemoveDEK deletes the DEK for a particular volumeID from the DEKStore linked
|
|
// with this VolumeEncryption instance.
|
|
func (ve *VolumeEncryption) RemoveDEK(volumeID string) error {
|
|
if ve.dekStore == nil {
|
|
return ErrDEKStoreNotFound
|
|
}
|
|
|
|
return ve.dekStore.RemoveDEK(volumeID)
|
|
}
|
|
|
|
func (ve *VolumeEncryption) GetID() string {
|
|
return ve.id
|
|
}
|
|
|
|
// EncryptionKMS provides external Key Management System for encryption
|
|
// passphrases storage.
|
|
type EncryptionKMS interface {
|
|
Destroy()
|
|
|
|
// requiresDEKStore returns the DEKStoreType that is needed to be
|
|
// configure for the KMS. Nothing needs to be done when this function
|
|
// returns DEKStoreIntegrated, otherwise you will need to configure an
|
|
// alternative storage for the DEKs.
|
|
requiresDEKStore() DEKStoreType
|
|
|
|
// EncryptDEK provides a way for a KMS to encrypt a DEK. In case the
|
|
// encryption is done transparently inside the KMS service, the
|
|
// function can return an unencrypted value.
|
|
EncryptDEK(volumeID, plainDEK string) (string, error)
|
|
|
|
// DecryptDEK provides a way for a KMS to decrypt a DEK. In case the
|
|
// encryption is done transparently inside the KMS service, the
|
|
// function does not need to do anything except return the encyptedDEK
|
|
// as it was received.
|
|
DecryptDEK(volumeID, encyptedDEK string) (string, error)
|
|
}
|
|
|
|
// DEKStoreType describes what DEKStore needs to be configured when using a
|
|
// particular KMS. A KMS might support different DEKStores depending on its
|
|
// configuration.
|
|
type DEKStoreType string
|
|
|
|
const (
|
|
// DEKStoreIntegrated indicates that the KMS itself supports storing
|
|
// DEKs.
|
|
DEKStoreIntegrated = DEKStoreType("")
|
|
// DEKStoreMetadata indicates that the KMS should be configured to
|
|
// store the DEK in the metadata of the volume.
|
|
DEKStoreMetadata = DEKStoreType("metadata")
|
|
)
|
|
|
|
// DEKStore allows KMS instances to implement a modular backend for DEK
|
|
// storage. This can be used to store the DEK in a different location, in case
|
|
// the KMS can not store passphrases for volumes.
|
|
type DEKStore interface {
|
|
// StoreDEK saves the DEK in the configured store.
|
|
StoreDEK(volumeID string, dek string) error
|
|
// FetchDEK reads the DEK from the configured store and returns it.
|
|
FetchDEK(volumeID string) (string, error)
|
|
// RemoveDEK deletes the DEK from the configured store.
|
|
RemoveDEK(volumeID string) error
|
|
}
|
|
|
|
// integratedDEK is a DEKStore that can not be configured. Either the KMS does
|
|
// not use a DEK, or the DEK is stored in the KMS without additional
|
|
// configuration options.
|
|
type integratedDEK struct{}
|
|
|
|
func (i integratedDEK) requiresDEKStore() DEKStoreType {
|
|
return DEKStoreIntegrated
|
|
}
|
|
|
|
func (i integratedDEK) EncryptDEK(volumeID, plainDEK string) (string, error) {
|
|
return plainDEK, nil
|
|
}
|
|
|
|
func (i integratedDEK) DecryptDEK(volumeID, encyptedDEK string) (string, error) {
|
|
return encyptedDEK, nil
|
|
}
|
|
|
|
// StoreCryptoPassphrase takes an unencrypted passphrase, encrypts it and saves
|
|
// it in the DEKStore.
|
|
func (ve *VolumeEncryption) StoreCryptoPassphrase(volumeID, passphrase string) error {
|
|
encryptedPassphrase, err := ve.KMS.EncryptDEK(volumeID, passphrase)
|
|
if err != nil {
|
|
return fmt.Errorf("failed encrypt the passphrase for %s: %w", volumeID, err)
|
|
}
|
|
|
|
err = ve.dekStore.StoreDEK(volumeID, encryptedPassphrase)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save the passphrase for %s: %w", volumeID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StoreNewCryptoPassphrase generates a new passphrase and saves it in the KMS.
|
|
func (ve *VolumeEncryption) StoreNewCryptoPassphrase(volumeID string) error {
|
|
passphrase, err := generateNewEncryptionPassphrase()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate passphrase for %s: %w", volumeID, err)
|
|
}
|
|
|
|
return ve.StoreCryptoPassphrase(volumeID, passphrase)
|
|
}
|
|
|
|
// GetCryptoPassphrase Retrieves passphrase to encrypt volume.
|
|
func (ve *VolumeEncryption) GetCryptoPassphrase(volumeID string) (string, error) {
|
|
passphrase, err := ve.dekStore.FetchDEK(volumeID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return ve.KMS.DecryptDEK(volumeID, passphrase)
|
|
}
|
|
|
|
// generateNewEncryptionPassphrase generates a random passphrase for encryption.
|
|
func generateNewEncryptionPassphrase() (string, error) {
|
|
bytesPassphrase := make([]byte, encryptionPassphraseSize)
|
|
_, err := rand.Read(bytesPassphrase)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.URLEncoding.EncodeToString(bytesPassphrase), nil
|
|
}
|
|
|
|
// VolumeMapper returns file name and it's path to where encrypted device should be open.
|
|
func VolumeMapper(volumeID string) (mapperFile, mapperFilePath string) {
|
|
mapperFile = mapperFilePrefix + volumeID
|
|
mapperFilePath = path.Join(mapperFilePathPrefix, mapperFile)
|
|
return mapperFile, mapperFilePath
|
|
}
|
|
|
|
// EncryptVolume encrypts provided device with LUKS.
|
|
func EncryptVolume(ctx context.Context, devicePath, passphrase string) error {
|
|
DebugLog(ctx, "Encrypting device %s with LUKS", devicePath)
|
|
if _, _, err := LuksFormat(devicePath, passphrase); err != nil {
|
|
return fmt.Errorf("failed to encrypt device %s with LUKS: %w", devicePath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// OpenEncryptedVolume opens volume so that it can be used by the client.
|
|
func OpenEncryptedVolume(ctx context.Context, devicePath, mapperFile, passphrase string) error {
|
|
DebugLog(ctx, "Opening device %s with LUKS on %s", devicePath, mapperFile)
|
|
_, stderr, err := LuksOpen(devicePath, mapperFile, passphrase)
|
|
if err != nil {
|
|
WarningLog(ctx, "failed to open LUKS device %q: %s", devicePath, stderr)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// CloseEncryptedVolume closes encrypted volume so it can be detached.
|
|
func CloseEncryptedVolume(ctx context.Context, mapperFile string) error {
|
|
DebugLog(ctx, "Closing LUKS device %s", mapperFile)
|
|
_, _, err := LuksClose(mapperFile)
|
|
return err
|
|
}
|
|
|
|
// IsDeviceOpen determines if encrypted device is already open.
|
|
func IsDeviceOpen(ctx context.Context, device string) (bool, error) {
|
|
_, mappedFile, err := DeviceEncryptionStatus(ctx, device)
|
|
return (mappedFile != ""), err
|
|
}
|
|
|
|
// DeviceEncryptionStatus looks to identify if the passed device is a LUKS mapping
|
|
// and if so what the device is and the mapper name as used by LUKS.
|
|
// If not, just returns the original device and an empty string.
|
|
func DeviceEncryptionStatus(ctx context.Context, devicePath string) (mappedDevice, mapper string, err error) {
|
|
if !strings.HasPrefix(devicePath, mapperFilePathPrefix) {
|
|
return devicePath, "", nil
|
|
}
|
|
mapPath := strings.TrimPrefix(devicePath, mapperFilePathPrefix+"/")
|
|
stdout, _, err := LuksStatus(mapPath)
|
|
if err != nil {
|
|
DebugLog(ctx, "device %s is not an active LUKS device: %v", devicePath, err)
|
|
return devicePath, "", nil
|
|
}
|
|
lines := strings.Split(string(stdout), "\n")
|
|
if len(lines) < 1 {
|
|
return "", "", fmt.Errorf("device encryption status returned no stdout for %s", devicePath)
|
|
}
|
|
if !strings.HasSuffix(lines[0], " is active.") {
|
|
// Implies this is not a LUKS device
|
|
return devicePath, "", nil
|
|
}
|
|
for i := 1; i < len(lines); i++ {
|
|
kv := strings.SplitN(strings.TrimSpace(lines[i]), ":", 2)
|
|
if len(kv) < 1 {
|
|
return "", "", fmt.Errorf("device encryption status output for %s is badly formatted: %s",
|
|
devicePath, lines[i])
|
|
}
|
|
if strings.Compare(kv[0], "device") == 0 {
|
|
return strings.TrimSpace(kv[1]), mapPath, nil
|
|
}
|
|
}
|
|
// Identified as LUKS, but failed to identify a mapped device
|
|
return "", "", fmt.Errorf("mapped device not found in path %s", devicePath)
|
|
}
|