mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
Add state disk volume mounter
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
4b156be15e
commit
0e2025b67c
@ -26,6 +26,14 @@ endif()
|
||||
set(NITRO_CFLAGS '-I${CMAKE_BINARY_DIR}/nitro/${CARGOTARGET} -I${CMAKE_BINARY_DIR}/nitro/${CARGOTARGET}/headers')
|
||||
set(NITRO_LDFLAGS '${CMAKE_BINARY_DIR}/nitro/${CARGOTARGET}/libnitro.a ${RUST_STATICLIB_LDFLAGS}')
|
||||
|
||||
#
|
||||
# core-os disk-mapper
|
||||
#
|
||||
add_custom_target(disk-mapper
|
||||
go build -o ${CMAKE_BINARY_DIR}/disk-mapper -ldflags "-s -w"
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/state/cmd
|
||||
)
|
||||
|
||||
#
|
||||
# coordinator
|
||||
#
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
// This function WILL cause a system crash!
|
||||
// DO NOT call it in any code that may be covered by automatic or manual tests.
|
||||
func KernelPanic(err error) {
|
||||
fmt.Println(err)
|
||||
fmt.Fprint(os.Stderr, err)
|
||||
_ = os.WriteFile("/proc/sys/kernel/sysrq", []byte("1"), 0o644)
|
||||
_ = os.WriteFile("/proc/sysrq-trigger", []byte("c"), 0o644)
|
||||
panic(err)
|
||||
|
13
state/README.md
Normal file
13
state/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# State
|
||||
|
||||
Files and source code for mounting persistent state disks
|
||||
|
||||
## Testing
|
||||
|
||||
Integration test is available in `state/test/integration_test.go`.
|
||||
The integration test requires root privileges since it uses dm-crypt.
|
||||
Build and run the test:
|
||||
```bash
|
||||
go test -c ./state/test/
|
||||
sudo ./test.test
|
||||
```
|
51
state/cmd/main.go
Normal file
51
state/cmd/main.go
Normal file
@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/utils"
|
||||
"github.com/edgelesssys/constellation/state/mapper"
|
||||
)
|
||||
|
||||
const (
|
||||
keyPath = "/run/cryptsetup-keys.d"
|
||||
keyFile = "state.key"
|
||||
)
|
||||
|
||||
var csp = flag.String("csp", "", "Cloud Service Provider the image is running on")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
diskPath, err := mapper.GetDiskPath(*csp)
|
||||
if err != nil {
|
||||
utils.KernelPanic(err)
|
||||
}
|
||||
|
||||
mapper, err := mapper.New(diskPath)
|
||||
if err != nil {
|
||||
utils.KernelPanic(err)
|
||||
}
|
||||
defer mapper.Close()
|
||||
|
||||
// generate and save temporary passphrase
|
||||
if err := os.MkdirAll(keyPath, os.ModePerm); err != nil {
|
||||
utils.KernelPanic(err)
|
||||
}
|
||||
passphrase := make([]byte, 32)
|
||||
if _, err := rand.Read(passphrase); err != nil {
|
||||
utils.KernelPanic(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(keyPath, keyFile), passphrase, 0o400); err != nil {
|
||||
utils.KernelPanic(err)
|
||||
}
|
||||
|
||||
if err := mapper.FormatDisk(string(passphrase)); err != nil {
|
||||
utils.KernelPanic(err)
|
||||
}
|
||||
if err := mapper.MapDisk("state", string(passphrase)); err != nil {
|
||||
utils.KernelPanic(err)
|
||||
}
|
||||
}
|
33
state/mapper/cryptdevice.go
Normal file
33
state/mapper/cryptdevice.go
Normal file
@ -0,0 +1,33 @@
|
||||
package mapper
|
||||
|
||||
import cryptsetup "github.com/martinjungblut/go-cryptsetup"
|
||||
|
||||
type cryptDevice interface {
|
||||
// ActivateByPassphrase activates a device by using a passphrase from a specific keyslot.
|
||||
// Returns nil on success, or an error otherwise.
|
||||
// C equivalent: crypt_activate_by_passphrase
|
||||
ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error
|
||||
// Deactivate deactivates a device.
|
||||
// Returns nil on success, or an error otherwise.
|
||||
// C equivalent: crypt_deactivate
|
||||
Deactivate(deviceName string) error
|
||||
// Format formats a Device, using a specific device type, and type-independent parameters.
|
||||
// Returns nil on success, or an error otherwise.
|
||||
// C equivalent: crypt_format
|
||||
Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error
|
||||
// Free releases crypt device context and used memory.
|
||||
// C equivalent: crypt_free
|
||||
Free() bool
|
||||
// Load loads crypt device parameters from the on-disk header.
|
||||
// Returns nil on success, or an error otherwise.
|
||||
// C equivalent: crypt_load
|
||||
Load(cryptsetup.DeviceType) error
|
||||
// KeyslotAddByVolumeKey adds a key slot using a volume key to perform the required security check.
|
||||
// Returns nil on success, or an error otherwise.
|
||||
// C equivalent: crypt_keyslot_add_by_volume_key
|
||||
KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error
|
||||
// KeyslotChangeByPassphrase changes a defined a key slot using a previously added passphrase to perform the required security check.
|
||||
// Returns nil on success, or an error otherwise.
|
||||
// C equivalent: crypt_keyslot_change_by_passphrase
|
||||
KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error
|
||||
}
|
106
state/mapper/mapper.go
Normal file
106
state/mapper/mapper.go
Normal file
@ -0,0 +1,106 @@
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
cryptsetup "github.com/martinjungblut/go-cryptsetup"
|
||||
)
|
||||
|
||||
const (
|
||||
gcpStateDiskPath = "/dev/disk/by-id/google-state-disk"
|
||||
azureStateDiskPath = "/dev/disk/azure/scsi1/lun0"
|
||||
fallBackPath = "/dev/disk/by-id/state-disk"
|
||||
)
|
||||
|
||||
// Mapper handles actions for formating and mapping crypt devices.
|
||||
type Mapper struct {
|
||||
device cryptDevice
|
||||
}
|
||||
|
||||
// New creates a new crypt device for the device at path.
|
||||
func New(path string) (*Mapper, error) {
|
||||
device, err := cryptsetup.Init(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing crypt device for disk %q: %w", path, err)
|
||||
}
|
||||
return &Mapper{device: device}, nil
|
||||
}
|
||||
|
||||
// Close closes and frees memory allocated for the crypt device.
|
||||
func (m *Mapper) Close() error {
|
||||
if m.device.Free() {
|
||||
return nil
|
||||
}
|
||||
return errors.New("unable to close crypt device")
|
||||
}
|
||||
|
||||
// FormatDisk formats the disk and adds passphrase in keyslot 0.
|
||||
func (m *Mapper) FormatDisk(passphrase string) error {
|
||||
luksParams := cryptsetup.LUKS2{
|
||||
SectorSize: 4096,
|
||||
PBKDFType: &cryptsetup.PbkdfType{
|
||||
// Use low memory recommendation from https://datatracker.ietf.org/doc/html/rfc9106#section-7
|
||||
Type: "argon2id",
|
||||
TimeMs: 2000,
|
||||
Iterations: 3,
|
||||
ParallelThreads: 4,
|
||||
MaxMemoryKb: 65536, // ~64MiB
|
||||
},
|
||||
}
|
||||
|
||||
genericParams := cryptsetup.GenericParams{
|
||||
Cipher: "aes",
|
||||
CipherMode: "xts-plain64",
|
||||
VolumeKeySize: 64,
|
||||
}
|
||||
|
||||
if err := m.device.Format(luksParams, genericParams); err != nil {
|
||||
return fmt.Errorf("formating disk: %w", err)
|
||||
}
|
||||
|
||||
if err := m.device.KeyslotAddByVolumeKey(0, "", passphrase); err != nil {
|
||||
return fmt.Errorf("adding keyslot: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MapDisk maps a crypt device to /dev/mapper/target using the provided passphrase.
|
||||
func (m *Mapper) MapDisk(target, passphrase string) error {
|
||||
if err := m.device.ActivateByPassphrase(target, 0, passphrase, 0); err != nil {
|
||||
return fmt.Errorf("mapping disk as %q: %w", target, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmapDisk removes the mapping of target.
|
||||
func (m *Mapper) UnmapDisk(target string) error {
|
||||
return m.device.Deactivate(target)
|
||||
}
|
||||
|
||||
// GetDiskPath returns the device path of the data disk by cloud provider.
|
||||
//
|
||||
// For GCP a symlink to the disk is expected at /dev/disk/by-id/google-state-disk
|
||||
// For Azure a symlink to the disk is expected at /dev/disk/azure/scsi1/lun0
|
||||
// If no symlink can be found at the given path, or if no known cloud provider is supplied,
|
||||
// we instead return the device path of the os-disk stateful partition at /dev/disk/by-partlabel/stateful.
|
||||
func GetDiskPath(csp string) (string, error) {
|
||||
var diskPath string
|
||||
var err error
|
||||
|
||||
switch csp {
|
||||
case "gcp":
|
||||
diskPath, err = filepath.EvalSymlinks(gcpStateDiskPath)
|
||||
case "azure":
|
||||
diskPath, err = filepath.EvalSymlinks(azureStateDiskPath)
|
||||
default:
|
||||
diskPath = fallBackPath
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return filepath.EvalSymlinks(fallBackPath)
|
||||
}
|
||||
return diskPath, nil
|
||||
}
|
57
state/test/integration_test.go
Normal file
57
state/test/integration_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
// go:build integration
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/state/mapper"
|
||||
"github.com/martinjungblut/go-cryptsetup"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
devicePath = "testDevice"
|
||||
mappedDevice = "mappedDevice"
|
||||
)
|
||||
|
||||
func setup() error {
|
||||
return exec.Command("/bin/dd", "if=/dev/zero", fmt.Sprintf("of=%s", devicePath), "bs=64M", "count=1").Run()
|
||||
}
|
||||
|
||||
func teardown() error {
|
||||
return exec.Command("/bin/rm", "-f", devicePath).Run()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getuid() != 0 {
|
||||
fmt.Printf("This test suite requires root privileges, as libcrypsetup uses the kernel's device mapper.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
result := m.Run()
|
||||
os.Exit(result)
|
||||
}
|
||||
|
||||
func TestMapper(t *testing.T) {
|
||||
cryptsetup.SetDebugLevel(cryptsetup.CRYPT_LOG_VERBOSE)
|
||||
cryptsetup.SetLogCallback(func(level int, message string) { fmt.Println(message) })
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
require.NoError(setup(), "failed to setup test disk")
|
||||
defer func() { require.NoError(teardown(), "failed to delete test disk") }()
|
||||
|
||||
mapper, err := mapper.New(devicePath)
|
||||
require.NoError(err, "failed to initialize crypt device")
|
||||
defer func() { require.NoError(mapper.Close(), "failed to close crypt device") }()
|
||||
|
||||
passphrase := "unit-test"
|
||||
require.NoError(mapper.FormatDisk(passphrase), "failed to format disk")
|
||||
require.NoError(mapper.MapDisk(mappedDevice, passphrase), "failed to map disk")
|
||||
|
||||
require.NoError(mapper.UnmapDisk(mappedDevice), "failed to remove disk mapping")
|
||||
assert.Error(mapper.MapDisk(mappedDevice, "invalid-passphrase"), "was able to map disk with incorrect passphrase")
|
||||
}
|
Loading…
Reference in New Issue
Block a user