mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-12-24 22:10:23 +00:00
f1f50e0218
Call Mount.Setup with SingleUserWritable constant instead of 0o755, which is silently ignored and causes the /.fscrypt/{policy,protector}/ directories to have mode 000. Signed-off-by: Marcel Lauhoff <marcel.lauhoff@suse.com>
440 lines
12 KiB
Go
440 lines
12 KiB
Go
/*
|
|
Copyright 2022 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 fscrypt
|
|
|
|
/*
|
|
#include <linux/fs.h>
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"time"
|
|
"unsafe"
|
|
|
|
fscryptactions "github.com/google/fscrypt/actions"
|
|
fscryptcrypto "github.com/google/fscrypt/crypto"
|
|
fscryptfilesystem "github.com/google/fscrypt/filesystem"
|
|
fscryptmetadata "github.com/google/fscrypt/metadata"
|
|
"github.com/pkg/xattr"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/ceph/ceph-csi/internal/kms"
|
|
"github.com/ceph/ceph-csi/internal/util"
|
|
"github.com/ceph/ceph-csi/internal/util/log"
|
|
)
|
|
|
|
const (
|
|
FscryptHashingTimeTarget = 1 * time.Second
|
|
FscryptProtectorPrefix = "ceph-csi"
|
|
FscryptSubdir = "ceph-csi-encrypted"
|
|
encryptionPassphraseSize = 64
|
|
)
|
|
|
|
var policyV2Support = []util.KernelVersion{
|
|
{
|
|
Version: 5,
|
|
PatchLevel: 4,
|
|
SubLevel: 0,
|
|
ExtraVersion: 0,
|
|
Distribution: "",
|
|
Backport: false,
|
|
},
|
|
}
|
|
|
|
func AppendEncyptedSubdirectory(dir string) string {
|
|
return path.Join(dir, FscryptSubdir)
|
|
}
|
|
|
|
// getPassphrase returns the passphrase from the configured Ceph CSI KMS to be used as a protector key in fscrypt.
|
|
func getPassphrase(ctx context.Context, encryption util.VolumeEncryption, volID string) (string, error) {
|
|
var (
|
|
passphrase string
|
|
err error
|
|
)
|
|
|
|
switch encryption.KMS.RequiresDEKStore() {
|
|
case kms.DEKStoreIntegrated:
|
|
passphrase, err = encryption.GetCryptoPassphrase(volID)
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: failed to get passphrase from KMS: %v", err)
|
|
|
|
return "", err
|
|
}
|
|
case kms.DEKStoreMetadata:
|
|
passphrase, err = encryption.KMS.GetSecret(volID)
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: failed to GetSecret: %v", err)
|
|
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return passphrase, nil
|
|
}
|
|
|
|
// createKeyFuncFromVolumeEncryption returns an fscrypt key function returning
|
|
// encryption keys form a VolumeEncryption struct.
|
|
func createKeyFuncFromVolumeEncryption(
|
|
ctx context.Context,
|
|
encryption util.VolumeEncryption,
|
|
volID string,
|
|
) (func(fscryptactions.ProtectorInfo, bool) (*fscryptcrypto.Key, error), error) {
|
|
keyFunc := func(info fscryptactions.ProtectorInfo, retry bool) (*fscryptcrypto.Key, error) {
|
|
passphrase, err := getPassphrase(ctx, encryption, volID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key, err := fscryptcrypto.NewBlankKey(encryptionPassphraseSize / 2)
|
|
copy(key.Data(), passphrase)
|
|
|
|
return key, err
|
|
}
|
|
|
|
return keyFunc, nil
|
|
}
|
|
|
|
// fsyncEncryptedDirectory calls sync on dirPath. It is intended to
|
|
// work around the fscrypt library not syncing the directory it sets a
|
|
// policy on.
|
|
// TODO Remove when the fscrypt dependency has https://github.com/google/fscrypt/pull/359
|
|
func fsyncEncryptedDirectory(dirPath string) error {
|
|
dir, err := os.Open(dirPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dir.Close()
|
|
|
|
return dir.Sync()
|
|
}
|
|
|
|
// unlockExisting tries to unlock an already set up fscrypt directory using keys from Ceph CSI.
|
|
func unlockExisting(
|
|
ctx context.Context,
|
|
fscryptContext *fscryptactions.Context,
|
|
encryptedPath string, protectorName string,
|
|
keyFn func(fscryptactions.ProtectorInfo, bool) (*fscryptcrypto.Key, error),
|
|
) error {
|
|
var err error
|
|
|
|
policy, err := fscryptactions.GetPolicyFromPath(fscryptContext, encryptedPath)
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: policy get failed %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
optionFn := func(policyDescriptor string, options []*fscryptactions.ProtectorOption) (int, error) {
|
|
for idx, option := range options {
|
|
if option.Name() == protectorName {
|
|
return idx, nil
|
|
}
|
|
}
|
|
|
|
return 0, &fscryptactions.ErrNotProtected{PolicyDescriptor: policyDescriptor, ProtectorDescriptor: protectorName}
|
|
}
|
|
|
|
if err = policy.Unlock(optionFn, keyFn); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: unlock with protector error: %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
err = policy.Lock()
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: failed to lock policy after use: %v", err)
|
|
}
|
|
}()
|
|
|
|
if err = policy.Provision(); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: provision fail %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
log.DebugLog(ctx, "fscrypt protector unlock: %s %+v", protectorName, policy)
|
|
|
|
return nil
|
|
}
|
|
|
|
func initializeAndUnlock(
|
|
ctx context.Context,
|
|
fscryptContext *fscryptactions.Context,
|
|
encryptedPath string, protectorName string,
|
|
keyFn func(fscryptactions.ProtectorInfo, bool) (*fscryptcrypto.Key, error),
|
|
) error {
|
|
var owner *user.User
|
|
var err error
|
|
|
|
if err = os.Mkdir(encryptedPath, 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
protector, err := fscryptactions.CreateProtector(fscryptContext, protectorName, keyFn, owner)
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: protector name=%s create failed: %v. reverting.", protectorName, err)
|
|
if revertErr := protector.Revert(); revertErr != nil {
|
|
return revertErr
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
if err = protector.Unlock(keyFn); err != nil {
|
|
return err
|
|
}
|
|
log.DebugLog(ctx, "fscrypt protector unlock: %+v", protector)
|
|
|
|
var policy *fscryptactions.Policy
|
|
if policy, err = fscryptactions.CreatePolicy(fscryptContext, protector); err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
err = policy.Lock()
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: failed to lock policy after init: %w")
|
|
err = policy.Revert()
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: failed to revert policy after failed lock: %w")
|
|
}
|
|
}
|
|
}()
|
|
|
|
if err = policy.UnlockWithProtector(protector); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: Failed to unlock policy: %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
if err = policy.Provision(); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: Failed to provision policy: %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
if err = policy.Apply(encryptedPath); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: Failed to apply protector (see also kernel log): %w", err)
|
|
if err = policy.Deprovision(false); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: Policy cleanup response to failing apply failed: %w", err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
if err = fsyncEncryptedDirectory(encryptedPath); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: fsync encrypted dir - to flush kernel policy to disk failed %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getInodeEncryptedAttribute returns the inode's encrypt attribute similar to lsattr(1)
|
|
func getInodeEncryptedAttribute(p string) (bool, error) {
|
|
file, err := os.Open(p)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var attr int
|
|
_, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), unix.FS_IOC_GETFLAGS,
|
|
uintptr(unsafe.Pointer(&attr)))
|
|
if errno != 0 {
|
|
return false, fmt.Errorf("error calling ioctl_iflags: %w", errno)
|
|
}
|
|
|
|
if attr&C.FS_ENCRYPT_FL != 0 {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// IsDirectoryUnlockedFscrypt checks if a directory is an unlocked fscrypted directory.
|
|
func IsDirectoryUnlocked(directoryPath, filesystem string) error {
|
|
if _, err := fscryptmetadata.GetPolicy(directoryPath); err != nil {
|
|
return fmt.Errorf("no fscrypt policy set on directory %q: %w", directoryPath, err)
|
|
}
|
|
|
|
switch filesystem {
|
|
case "ceph":
|
|
_, err := xattr.Get(directoryPath, "ceph.fscrypt.auth")
|
|
if err != nil {
|
|
return fmt.Errorf("error reading ceph.fscrypt.auth xattr on %q: %w", directoryPath, err)
|
|
}
|
|
default:
|
|
encrypted, err := getInodeEncryptedAttribute(directoryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !encrypted {
|
|
return fmt.Errorf("path %s does not have the encrypted inode flag set. Encryption init must have failed",
|
|
directoryPath)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getBestPolicyVersion() (int64, error) {
|
|
// fetch the current running kernel info
|
|
release, err := util.GetKernelVersion()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("fetching current kernel version failed: %w", err)
|
|
}
|
|
|
|
switch {
|
|
case util.CheckKernelSupport(release, policyV2Support):
|
|
return 2, nil
|
|
default:
|
|
return 1, nil
|
|
}
|
|
}
|
|
|
|
// InitializeNode performs once per nodeserver initialization
|
|
// required by the fscrypt library. Creates /etc/fscrypt.conf.
|
|
func InitializeNode(ctx context.Context) error {
|
|
policyVersion, err := getBestPolicyVersion()
|
|
if err != nil {
|
|
return fmt.Errorf("fscrypt node init failed to determine best policy version: %w", err)
|
|
}
|
|
|
|
err = fscryptactions.CreateConfigFile(FscryptHashingTimeTarget, policyVersion)
|
|
if err != nil {
|
|
existsError := &fscryptactions.ErrConfigFileExists{}
|
|
if errors.As(err, &existsError) {
|
|
log.ErrorLog(ctx, "fscrypt: config file %q already exists. Skipping fscrypt node setup",
|
|
existsError.Path)
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("fscrypt node init failed to create node configuration (/etc/fscrypt.conf): %w",
|
|
err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FscryptUnlock unlocks possilby creating fresh fscrypt metadata
|
|
// iff a volume is encrypted. Otherwise return immediately Calling
|
|
// this function requires that InitializeFscrypt ran once on this node.
|
|
func Unlock(
|
|
ctx context.Context,
|
|
volEncryption *util.VolumeEncryption,
|
|
stagingTargetPath string, volID string,
|
|
) error {
|
|
// Fetches keys from KMS. Do this first to catch KMS errors before setting up anything.
|
|
keyFn, err := createKeyFuncFromVolumeEncryption(ctx, *volEncryption, volID)
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: could not create key function: %v", err)
|
|
|
|
return err
|
|
}
|
|
|
|
err = fscryptfilesystem.UpdateMountInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fscryptContext, err := fscryptactions.NewContextFromMountpoint(stagingTargetPath, nil)
|
|
if err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: failed to create context from mountpoint %v: %w", stagingTargetPath, err)
|
|
|
|
return err
|
|
}
|
|
|
|
fscryptContext.Config.UseFsKeyringForV1Policies = true
|
|
|
|
log.DebugLog(ctx, "fscrypt context: %+v", fscryptContext)
|
|
|
|
if err = fscryptContext.Mount.CheckSupport(); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: filesystem mount %s does not support fscrypt", fscryptContext.Mount)
|
|
|
|
return err
|
|
}
|
|
|
|
// A proper set up fscrypy directory requires metadata and a kernel policy:
|
|
|
|
// 1. Do we have a metadata directory (.fscrypt) set up?
|
|
metadataDirExists := false
|
|
if err = fscryptContext.Mount.Setup(fscryptfilesystem.SingleUserWritable); err != nil {
|
|
alreadySetupErr := &fscryptfilesystem.ErrAlreadySetup{}
|
|
if errors.As(err, &alreadySetupErr) {
|
|
log.DebugLog(ctx, "fscrypt: metadata directory in %q already set up", alreadySetupErr.Mount.Path)
|
|
metadataDirExists = true
|
|
} else {
|
|
log.ErrorLog(ctx, "fscrypt: mount setup failed: %v", err)
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
encryptedPath := path.Join(stagingTargetPath, FscryptSubdir)
|
|
kernelPolicyExists := false
|
|
// 2. Ask the kernel if the directory has an fscrypt policy in place.
|
|
if _, err = fscryptmetadata.GetPolicy(encryptedPath); err == nil { // encrypted directory already set up
|
|
kernelPolicyExists = true
|
|
}
|
|
|
|
if metadataDirExists != kernelPolicyExists {
|
|
return fmt.Errorf("fscrypt: unsupported state metadata=%t kernel_policy=%t",
|
|
metadataDirExists, kernelPolicyExists)
|
|
}
|
|
|
|
protectorName := FscryptProtectorPrefix
|
|
|
|
switch volEncryption.KMS.RequiresDEKStore() {
|
|
case kms.DEKStoreMetadata:
|
|
// Metadata style KMS use the KMS secret as a custom
|
|
// passphrase directly in fscrypt, circumenting key
|
|
// derivation on the CSI side to allow users to fall
|
|
// back on the fscrypt commandline tool easily
|
|
fscryptContext.Config.Source = fscryptmetadata.SourceType_custom_passphrase
|
|
case kms.DEKStoreIntegrated:
|
|
fscryptContext.Config.Source = fscryptmetadata.SourceType_raw_key
|
|
}
|
|
|
|
if kernelPolicyExists && metadataDirExists {
|
|
log.DebugLog(ctx, "fscrypt: Encrypted directory already set up, policy exists")
|
|
|
|
return unlockExisting(ctx, fscryptContext, encryptedPath, protectorName, keyFn)
|
|
}
|
|
|
|
if !kernelPolicyExists && !metadataDirExists {
|
|
log.DebugLog(ctx, "fscrypt: Creating new protector and policy")
|
|
if volEncryption.KMS.RequiresDEKStore() == kms.DEKStoreIntegrated {
|
|
if err := volEncryption.StoreNewCryptoPassphrase(volID, encryptionPassphraseSize); err != nil {
|
|
log.ErrorLog(ctx, "fscrypt: store new crypto passphrase failed: %v", err)
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
return initializeAndUnlock(ctx, fscryptContext, encryptedPath, protectorName, keyFn)
|
|
}
|
|
|
|
return fmt.Errorf("unsupported")
|
|
}
|