diff --git a/.drone.jsonnet b/.drone.jsonnet index d0d48c264..bdb106a30 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -358,16 +358,24 @@ local integration_cilium = Step("e2e-cilium-1.8.5", target="e2e-qemu", privilege "SHORT_INTEGRATION_TEST": "yes", "CUSTOM_CNI_URL": "https://raw.githubusercontent.com/cilium/cilium/v1.8.5/install/kubernetes/quick-install.yaml", "REGISTRY": local_registry, + "CLUSTER_CIDR": 2, }); local integration_uefi = Step("e2e-uefi", target="e2e-qemu", privileged=true, depends_on=[integration_cilium], environment={ "SHORT_INTEGRATION_TEST": "yes", "WITH_UEFI": "true", + "CLUSTER_CIDR": 3, "REGISTRY": local_registry, }); local integration_disk_image = Step("e2e-disk-image", target="e2e-qemu", privileged=true, depends_on=[integration_uefi], environment={ "SHORT_INTEGRATION_TEST": "yes", "USE_DISK_IMAGE": "true", "REGISTRY": local_registry, + "CLUSTER_CIDR": 4, +}); +local integration_disk_encryption = Step("e2e-encrypted", target="e2e-qemu", privileged=true, depends_on=[integration_disk_image], environment={ + "WITH_DISK_ENCRYPTION": "true", + "REGISTRY": local_registry, + "CLUSTER_CIDR": 5, }); local push_edge = { @@ -403,13 +411,13 @@ local integration_pipelines = [ Pipeline('integration-qemu', default_pipeline_steps + [integration_qemu, push_edge]) + integration_trigger(['integration-qemu']), Pipeline('integration-provision-0', default_pipeline_steps + [integration_provision_tests_prepare, integration_provision_tests_track_0]) + integration_trigger(['integration-provision', 'integration-provision-0']), Pipeline('integration-provision-1', default_pipeline_steps + [integration_provision_tests_prepare, integration_provision_tests_track_1]) + integration_trigger(['integration-provision', 'integration-provision-1']), - Pipeline('integration-misc', default_pipeline_steps + [integration_cilium, integration_uefi, integration_disk_image]) + integration_trigger(['integration-misc']), + Pipeline('integration-misc', default_pipeline_steps + [integration_cilium, integration_uefi, integration_disk_image, integration_disk_encryption]) + integration_trigger(['integration-misc']), // cron pipelines, triggered on schedule events Pipeline('cron-integration-qemu', default_pipeline_steps + [integration_qemu, push_edge]) + cron_trigger(['thrice-daily', 'nightly']), Pipeline('cron-integration-provision-0', default_pipeline_steps + [integration_provision_tests_prepare, integration_provision_tests_track_0]) + cron_trigger(['thrice-daily', 'nightly']), Pipeline('cron-integration-provision-1', default_pipeline_steps + [integration_provision_tests_prepare, integration_provision_tests_track_1]) + cron_trigger(['thrice-daily', 'nightly']), - Pipeline('cron-integration-misc', default_pipeline_steps + [integration_cilium, integration_uefi, integration_disk_image]) + cron_trigger(['thrice-daily', 'nightly']), + Pipeline('cron-integration-misc', default_pipeline_steps + [integration_cilium, integration_uefi, integration_disk_image, integration_disk_encryption]) + cron_trigger(['thrice-daily', 'nightly']), ]; diff --git a/Dockerfile b/Dockerfile index be7244860..5a84bfe5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,8 @@ FROM ghcr.io/talos-systems/dosfstools:${PKGS} AS pkg-dosfstools FROM ghcr.io/talos-systems/eudev:${PKGS} AS pkg-eudev FROM ghcr.io/talos-systems/grub:${PKGS} AS pkg-grub FROM ghcr.io/talos-systems/iptables:${PKGS} AS pkg-iptables +FROM ghcr.io/talos-systems/libjson-c:${PKGS} AS pkg-libjson-c +FROM ghcr.io/talos-systems/libpopt:${PKGS} AS pkg-libpopt FROM ghcr.io/talos-systems/libressl:${PKGS} AS pkg-libressl FROM ghcr.io/talos-systems/libseccomp:${PKGS} AS pkg-libseccomp FROM ghcr.io/talos-systems/linux-firmware:${PKGS} AS pkg-linux-firmware @@ -358,6 +360,8 @@ COPY --from=pkg-containerd / /rootfs COPY --from=pkg-dosfstools / /rootfs COPY --from=pkg-eudev / /rootfs COPY --from=pkg-iptables / /rootfs +COPY --from=pkg-libjson-c / /rootfs +COPY --from=pkg-libpopt / /rootfs COPY --from=pkg-libressl / /rootfs COPY --from=pkg-libseccomp / /rootfs COPY --from=pkg-linux-firmware /lib/firmware/bnx2 /rootfs/lib/firmware/bnx2 diff --git a/cmd/installer/pkg/install/manifest.go b/cmd/installer/pkg/install/manifest.go index a361e9ad6..700ee65b5 100644 --- a/cmd/installer/pkg/install/manifest.go +++ b/cmd/installer/pkg/install/manifest.go @@ -510,7 +510,6 @@ func (m *Manifest) zeroDevice(device Device) (err error) { func (t *Target) Partition(pt *gpt.GPT, pos int, bd *blockdevice.BlockDevice) (err error) { if t.Skip { part := pt.Partitions().FindByName(t.Label) - if part != nil { log.Printf("skipped %s (%s) size %d blocks", t.PartitionName, t.Label, part.Length()) } diff --git a/cmd/talosctl/cmd/mgmt/cluster/create.go b/cmd/talosctl/cmd/mgmt/cluster/create.go index 5aed3a300..b157a7137 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create.go @@ -20,6 +20,7 @@ import ( humanize "github.com/dustin/go-humanize" "github.com/spf13/cobra" + "github.com/talos-systems/go-blockdevice/blockdevice/encryption" talosnet "github.com/talos-systems/net" "k8s.io/client-go/tools/clientcmd" @@ -42,51 +43,52 @@ import ( ) var ( - talosconfig string - nodeImage string - nodeInstallImage string - registryMirrors []string - registryInsecure []string - kubernetesVersion string - nodeVmlinuzPath string - nodeInitramfsPath string - nodeISOPath string - nodeDiskImagePath string - applyConfigEnabled bool - bootloaderEnabled bool - uefiEnabled bool - configDebug bool - networkCIDR string - networkMTU int - networkIPv4 bool - networkIPv6 bool - wireguardCIDR string - nameservers []string - dnsDomain string - workers int - masters int - clusterCpus string - clusterMemory int - clusterDiskSize int - clusterDisks []string - targetArch string - clusterWait bool - clusterWaitTimeout time.Duration - forceInitNodeAsEndpoint bool - forceEndpoint string - inputDir string - cniBinPath []string - cniConfDir string - cniCacheDir string - cniBundleURL string - ports string - dockerHostIP string - withInitNode bool - customCNIUrl string - crashdumpOnFailure bool - skipKubeconfig bool - skipInjectingConfig bool - talosVersion string + talosconfig string + nodeImage string + nodeInstallImage string + registryMirrors []string + registryInsecure []string + kubernetesVersion string + nodeVmlinuzPath string + nodeInitramfsPath string + nodeISOPath string + nodeDiskImagePath string + applyConfigEnabled bool + bootloaderEnabled bool + uefiEnabled bool + configDebug bool + networkCIDR string + networkMTU int + networkIPv4 bool + networkIPv6 bool + wireguardCIDR string + nameservers []string + dnsDomain string + workers int + masters int + clusterCpus string + clusterMemory int + clusterDiskSize int + clusterDisks []string + targetArch string + clusterWait bool + clusterWaitTimeout time.Duration + forceInitNodeAsEndpoint bool + forceEndpoint string + inputDir string + cniBinPath []string + cniConfDir string + cniCacheDir string + cniBundleURL string + ports string + dockerHostIP string + withInitNode bool + customCNIUrl string + crashdumpOnFailure bool + skipKubeconfig bool + skipInjectingConfig bool + talosVersion string + encryptEphemeralPartition bool ) // createCmd represents the cluster up command. @@ -304,6 +306,22 @@ func create(ctx context.Context) (err error) { genOptions = append(genOptions, generate.WithVersionContract(versionContract)) } + if encryptEphemeralPartition { + genOptions = append(genOptions, generate.WithSystemDiskEncryption( + &v1alpha1.SystemDiskEncryptionConfig{ + EphemeralPartition: &v1alpha1.EncryptionConfig{ + EncryptionProvider: encryption.LUKS2, + EncryptionKeys: []*v1alpha1.EncryptionKey{ + { + KeyNodeID: &v1alpha1.EncryptionKeyNodeID{}, + KeySlot: 0, + }, + }, + }, + }, + )) + } + defaultInternalLB, defaultEndpoint := provisioner.GetLoadBalancers(request.Network) if defaultInternalLB == "" { @@ -701,6 +719,7 @@ func init() { createCmd.Flags().BoolVar(&crashdumpOnFailure, "crashdump", false, "print debug crashdump to stderr when cluster startup fails") createCmd.Flags().BoolVar(&skipKubeconfig, "skip-kubeconfig", false, "skip merging kubeconfig from the created cluster") createCmd.Flags().BoolVar(&skipInjectingConfig, "skip-injecting-config", false, "skip injecting config from embedded metadata server, write config files to current directory") + createCmd.Flags().BoolVar(&encryptEphemeralPartition, "encrypt-ephemeral", false, "enable ephemeral partition encryption") createCmd.Flags().StringVar(&talosVersion, "talos-version", "", "the desired Talos version to generate config for (if not set, defaults to image version)") Cmd.AddCommand(createCmd) } diff --git a/hack/test/e2e-qemu.sh b/hack/test/e2e-qemu.sh index a93cf4879..83c8a3a5d 100755 --- a/hack/test/e2e-qemu.sh +++ b/hack/test/e2e-qemu.sh @@ -8,6 +8,7 @@ source ./hack/test/e2e.sh PROVISIONER=qemu CLUSTER_NAME=e2e-${PROVISIONER} +CLUSTER_CIDR=${CLUSTER_CIDR:-1} case "${CI:-false}" in true) @@ -45,6 +46,15 @@ case "${USE_DISK_IMAGE:-false}" in ;; esac +case "${WITH_DISK_ENCRYPTION:-false}" in + false) + DISK_ENCRYPTION_FLAG="" + ;; + *) + DISK_ENCRYPTION_FLAG="--encrypt-ephemeral" + ;; +esac + function create_cluster { build_registry_mirrors @@ -55,7 +65,7 @@ function create_cluster { --mtu 1450 \ --memory 2048 \ --cpus 2.0 \ - --cidr 172.20.1.0/24 \ + --cidr 172.20.${CLUSTER_CIDR}.0/24 \ --user-disk /var/lib/extra:100MB \ --user-disk /var/lib/p1:100MB:/var/lib/p2:100MB \ --install-image ${REGISTRY:-ghcr.io}/talos-systems/installer:${INSTALLER_TAG} \ @@ -63,11 +73,12 @@ function create_cluster { --cni-bundle-url ${ARTIFACTS}/talosctl-cni-bundle-'${ARCH}'.tar.gz \ --crashdump \ ${DISK_IMAGE_FLAG} \ + ${DISK_ENCRYPTION_FLAG} \ ${REGISTRY_MIRROR_FLAGS} \ ${QEMU_FLAGS} \ ${CUSTOM_CNI_FLAG} - "${TALOSCTL}" config node 172.20.1.2 + "${TALOSCTL}" config node 172.20.${CLUSTER_CIDR}.2 } function destroy_cluster() { diff --git a/internal/integration/api/apply-config.go b/internal/integration/api/apply-config.go index 351e9cfaa..784a06b0a 100644 --- a/internal/integration/api/apply-config.go +++ b/internal/integration/api/apply-config.go @@ -23,6 +23,7 @@ import ( "github.com/talos-systems/talos/pkg/machinery/client" "github.com/talos-systems/talos/pkg/machinery/config" "github.com/talos-systems/talos/pkg/machinery/config/configloader" + "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine" "github.com/talos-systems/talos/pkg/machinery/constants" ) @@ -179,6 +180,129 @@ func (suite *ApplyConfigSuite) TestApplyOnReboot() { suite.Require().NoError(err, "failed to apply deferred configuration (node %q): %w", node) } +// TestApplyConfigRotateEncryptionSecrets verify key rotation by sequential apply config calls. +func (suite *ApplyConfigSuite) TestApplyConfigRotateEncryptionSecrets() { + suite.T().Skip("skip: this test is not stable yet") + + node := suite.RandomDiscoveredNode(machine.TypeJoin) + suite.ClearConnectionRefused(suite.ctx, node) + + nodeCtx := client.WithNodes(suite.ctx, node) + provider, err := suite.readConfigFromNode(nodeCtx) + + suite.Assert().NoError(err) + + encryption := provider.Machine().SystemDiskEncryption().Get(constants.EphemeralPartitionLabel) + + if encryption == nil { + suite.T().Skip("skipped in not encrypted mode") + } + + suite.WaitForBootDone(suite.ctx) + + cfg, ok := encryption.(*v1alpha1.EncryptionConfig) + suite.Assert().True(ok) + + keySets := [][]*v1alpha1.EncryptionKey{ + { + { + KeyNodeID: &v1alpha1.EncryptionKeyNodeID{}, + KeySlot: 0, + }, + { + KeyStatic: &v1alpha1.EncryptionKeyStatic{ + KeyData: "AlO93jayutOpsDxDS=-", + }, + KeySlot: 1, + }, + }, + { + { + KeyStatic: &v1alpha1.EncryptionKeyStatic{ + KeyData: "AlO93jayutOpsDxDS=-", + }, + KeySlot: 1, + }, + }, + { + { + KeyNodeID: &v1alpha1.EncryptionKeyNodeID{}, + KeySlot: 0, + }, + { + KeyStatic: &v1alpha1.EncryptionKeyStatic{ + KeyData: "AlO93jayutOpsDxDS=-", + }, + KeySlot: 1, + }, + }, + { + { + KeyNodeID: &v1alpha1.EncryptionKeyNodeID{}, + KeySlot: 0, + }, + { + KeyStatic: &v1alpha1.EncryptionKeyStatic{ + KeyData: "1js4nfhvneJJsak=GVN4Inf5gh", + }, + KeySlot: 1, + }, + }, + } + + for _, keys := range keySets { + cfg.EncryptionKeys = keys + + data, err := provider.Bytes() + suite.Require().NoError(err) + + suite.AssertRebooted(suite.ctx, node, func(nodeCtx context.Context) error { + _, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{ + Data: data, + }) + if err != nil { + // It is expected that the connection will EOF here, so just log the error + suite.Assert().Nilf("failed to apply configuration (node %q): %w", node, err) + } + + return nil + }, assertRebootedRebootTimeout) + + // Verify configuration change + var newProvider config.Provider + + suite.Require().Nilf(retry.Constant(time.Minute, retry.WithUnits(time.Second)).Retry(func() error { + newProvider, err = suite.readConfigFromNode(nodeCtx) + if err != nil { + return retry.ExpectedError(err) + } + + return nil + }), "failed to read updated configuration from node %q: %w", node, err) + + e := newProvider.Machine().SystemDiskEncryption().Get(constants.EphemeralPartitionLabel) + + for i, k := range e.Keys() { + if keys[i].KeyStatic == nil { + suite.Require().Nil(k.Static()) + } else { + suite.Require().Equal(keys[i].Static().Key(), k.Static().Key()) + } + + if keys[i].KeyNodeID == nil { + suite.Require().Nil(k.NodeID()) + } else { + suite.Require().NotNil(keys[i].NodeID()) + } + + suite.Require().Equal(keys[i].Slot(), k.Slot()) + suite.Require().Equal(keys[i].Slot(), k.Slot()) + } + + suite.WaitForBootDone(suite.ctx) + } +} + func (suite *ApplyConfigSuite) readConfigFromNode(nodeCtx context.Context) (config.Provider, error) { // Load the current node machine config cfgData := new(bytes.Buffer) diff --git a/internal/pkg/encryption/encryption.go b/internal/pkg/encryption/encryption.go new file mode 100644 index 000000000..e404c9988 --- /dev/null +++ b/internal/pkg/encryption/encryption.go @@ -0,0 +1,254 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package encryption provides modules for the partition encryption handling. +package encryption + +import ( + "fmt" + "log" + "sort" + "strconv" + + "github.com/talos-systems/go-blockdevice/blockdevice" + "github.com/talos-systems/go-blockdevice/blockdevice/encryption" + "github.com/talos-systems/go-blockdevice/blockdevice/encryption/luks" + "github.com/talos-systems/go-blockdevice/blockdevice/partition/gpt" + + "github.com/talos-systems/talos/internal/pkg/encryption/keys" + "github.com/talos-systems/talos/pkg/machinery/config" +) + +// NewHandler creates new Handler. +func NewHandler(device *blockdevice.BlockDevice, partition *gpt.Partition, encryptionConfig config.Encryption) (*Handler, error) { + keys, err := getKeys(encryptionConfig, partition) + if err != nil { + return nil, err + } + + var provider encryption.Provider + + switch encryptionConfig.Kind() { + case encryption.LUKS2: + cipher, err := luks.ParseCipherKind(encryptionConfig.Cipher()) + if err != nil { + return nil, err + } + + provider = luks.New( + cipher, + ) + default: + return nil, fmt.Errorf("unknown encryption kind %s", encryptionConfig.Kind()) + } + + return &Handler{ + device: device, + partition: partition, + encryptionConfig: encryptionConfig, + keys: keys, + encryptionProvider: provider, + }, nil +} + +// Handler reads encryption config, creates appropriate +// encryption provider, handles encrypted partition open and close. +type Handler struct { + device *blockdevice.BlockDevice + partition *gpt.Partition + encryptionConfig config.Encryption + keys []*encryption.Key + encryptionProvider encryption.Provider + encryptedPath string +} + +// Open encrypted partition. +// nolint:gocyclo +func (h *Handler) Open() (string, error) { + partPath, err := h.partition.Path() + if err != nil { + return "", err + } + + sb, err := h.partition.SuperBlock() + if err != nil { + return "", err + } + + var path string + + // encrypt if partition is not encrypted and empty + if sb == nil { + err = h.formatAndEncrypt(partPath) + if err != nil { + return "", err + } + } else if sb.Type() != h.encryptionConfig.Kind() { + return "", fmt.Errorf("failed to encrypt the partition %s, because it is not empty", partPath) + } + + var k *encryption.Key + + for _, k = range h.keys { + path, err = h.encryptionProvider.Open(partPath, k) + if err != nil { + if err == encryption.ErrEncryptionKeyRejected { + continue + } + + return "", err + } + + break + } + + if path == "" { + return "", fmt.Errorf("failed to open encrypted device %s, no key matched", partPath) + } + + log.Printf("mapped encrypted partition %s -> %s", partPath, path) + + if err = h.syncKeys(k, partPath); err != nil { + return "", err + } + + h.encryptedPath = path + + return path, nil +} + +// Close encrypted partition. +func (h *Handler) Close() error { + if h.encryptedPath == "" { + return nil + } + + if err := h.encryptionProvider.Close(h.encryptedPath); err != nil { + return err + } + + log.Printf("closed encrypted partition %s", h.encryptedPath) + + return nil +} + +func (h *Handler) formatAndEncrypt(path string) error { + log.Printf("encrypting the partition %s (%s)", path, h.partition.Name) + + if len(h.keys) == 0 { + return fmt.Errorf("no encryption keys found") + } + + key := h.keys[0] + + err := h.encryptionProvider.Encrypt(path, key) + if err != nil { + return err + } + + for _, extraKey := range h.keys[1:] { + if err = h.encryptionProvider.AddKey(path, key, extraKey); err != nil { + return err + } + } + + return nil +} + +//nolint:gocyclo +func (h *Handler) syncKeys(k *encryption.Key, path string) error { + keyslots, err := h.encryptionProvider.ReadKeyslots(path) + if err != nil { + return err + } + + visited := map[string]bool{} + + for _, key := range h.keys { + slot := fmt.Sprintf("%d", key.Slot) + visited[slot] = true + // no need to update the key which we already detected as unchanged + if k.Slot == key.Slot { + continue + } + + // keyslot exists + if _, ok := keyslots.Keyslots[slot]; ok { + if err = h.updateKey(k, key, path); err != nil { + return err + } + + log.Printf("updated encryption key at slot %d", key.Slot) + } else { + // keyslot does not exist so just add the key + if err = h.encryptionProvider.AddKey(path, k, key); err != nil { + return err + } + + log.Printf("added encryption key to slot %d", key.Slot) + } + } + + // cleanup deleted key slots + for slot := range keyslots.Keyslots { + if !visited[slot] { + s, err := strconv.ParseInt(slot, 10, 64) + if err != nil { + return err + } + + if err = h.encryptionProvider.RemoveKey(path, int(s), k); err != nil { + return err + } + + log.Printf("removed key at slot %d", k.Slot) + } + } + + return nil +} + +func (h *Handler) updateKey(existingKey, newKey *encryption.Key, path string) error { + if valid, err := h.encryptionProvider.CheckKey(path, newKey); err != nil { + return err + } else if !valid { + // re-add the key to the slot + err = h.encryptionProvider.RemoveKey(path, newKey.Slot, existingKey) + if err != nil { + return fmt.Errorf("failed to drop old key during key update %w", err) + } + + err = h.encryptionProvider.AddKey(path, existingKey, newKey) + if err != nil { + return fmt.Errorf("failed to add new key during key update %w", err) + } + + return err + } + + return nil +} + +func getKeys(encryptionConfig config.Encryption, partition *gpt.Partition) ([]*encryption.Key, error) { + encryptionKeys := make([]*encryption.Key, len(encryptionConfig.Keys())) + + for i, cfg := range encryptionConfig.Keys() { + handler, err := keys.NewHandler(cfg) + if err != nil { + return nil, err + } + + k, err := handler.GetKey(keys.WithPartitionLabel(partition.Name)) + if err != nil { + return nil, err + } + + encryptionKeys[i] = encryption.NewKey(cfg.Slot(), k) + } + + //nolint:scopelint + sort.Slice(encryptionKeys, func(i, j int) bool { return encryptionKeys[i].Slot < encryptionKeys[j].Slot }) + + return encryptionKeys, nil +} diff --git a/internal/pkg/encryption/encryption_test.go b/internal/pkg/encryption/encryption_test.go new file mode 100644 index 000000000..c777097ce --- /dev/null +++ b/internal/pkg/encryption/encryption_test.go @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package encryption_test + +import "testing" + +func TestEmpty(t *testing.T) { + // added for accurate coverage estimation + // + // please remove it once any unit-test is added + // for this package +} diff --git a/internal/pkg/encryption/keys/keys.go b/internal/pkg/encryption/keys/keys.go new file mode 100644 index 000000000..e44a2c8de --- /dev/null +++ b/internal/pkg/encryption/keys/keys.go @@ -0,0 +1,34 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package keys contains various encryption KeyHandler implementations. +package keys + +import ( + "fmt" + + "github.com/talos-systems/talos/pkg/machinery/config" +) + +// NewHandler creates a new key handler depending on key handler kind. +func NewHandler(key config.EncryptionKey) (Handler, error) { + switch { + case key.Static() != nil: + k := key.Static().Key() + if k == nil { + return nil, fmt.Errorf("static key must have key data defined") + } + + return NewStaticKeyHandler(k) + case key.NodeID() != nil: + return NewNodeIDKeyHandler() + } + + return nil, fmt.Errorf("failed to create key handler: malformed config") +} + +// Handler represents an interface for fetching encryption keys. +type Handler interface { + GetKey(options ...KeyOption) ([]byte, error) +} diff --git a/internal/pkg/encryption/keys/nodeid.go b/internal/pkg/encryption/keys/nodeid.go new file mode 100644 index 000000000..997b85f63 --- /dev/null +++ b/internal/pkg/encryption/keys/nodeid.go @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package keys + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/talos-systems/go-smbios/smbios" +) + +// NodeIDKeyHandler generates the key based on current node information +// and provided template string. +type NodeIDKeyHandler struct { +} + +// NewNodeIDKeyHandler creates new NodeIDKeyHandler. +func NewNodeIDKeyHandler() (*NodeIDKeyHandler, error) { + return &NodeIDKeyHandler{}, nil +} + +// GetKey implements KeyHandler interface. +func (h *NodeIDKeyHandler) GetKey(options ...KeyOption) ([]byte, error) { + opts, err := NewDefaultOptions(options) + if err != nil { + return nil, err + } + + s, err := smbios.New() + if err != nil { + return nil, err + } + + machineUUID, err := s.SystemInformation().UUID() + if err != nil { + return nil, err + } + + if machineUUID == uuid.Nil { + return nil, fmt.Errorf("machine UUID is not populated %s", machineUUID) + } + + id := machineUUID.String() + + // primitive entropy check + counts := map[rune]int{} + for _, s := range id { + counts[s]++ + if counts[s] > len(id)/2 { + return nil, fmt.Errorf("machine UUID %s entropy check failed", machineUUID) + } + } + + return []byte(id + opts.PartitionLabel), nil +} diff --git a/internal/pkg/encryption/keys/options.go b/internal/pkg/encryption/keys/options.go new file mode 100644 index 000000000..5d16f7c65 --- /dev/null +++ b/internal/pkg/encryption/keys/options.go @@ -0,0 +1,36 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package keys + +// KeyOption represents key option callback used in KeyHandler.GetKey func. +type KeyOption func(o *KeyOptions) error + +// KeyOptions set of options to be used in KeyHandler.GetKey func. +type KeyOptions struct { + PartitionLabel string +} + +// WithPartitionLabel passes the partition label in to GetKey function. +func WithPartitionLabel(label string) KeyOption { + return func(o *KeyOptions) error { + o.PartitionLabel = label + + return nil + } +} + +// NewDefaultOptions creates new KeyOptions. +func NewDefaultOptions(options []KeyOption) (*KeyOptions, error) { + var opts KeyOptions + + for _, o := range options { + err := o(&opts) + if err != nil { + return nil, err + } + } + + return &opts, nil +} diff --git a/internal/pkg/encryption/keys/static.go b/internal/pkg/encryption/keys/static.go new file mode 100644 index 000000000..f17800c33 --- /dev/null +++ b/internal/pkg/encryption/keys/static.go @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package keys + +// StaticKeyHandler just handles the static key value all the time. +type StaticKeyHandler struct { + key []byte +} + +// NewStaticKeyHandler creates new EphemeralKeyHandler. +func NewStaticKeyHandler(key []byte) (*StaticKeyHandler, error) { + return &StaticKeyHandler{ + key: key, + }, nil +} + +// GetKey implements KeyHandler interface. +func (h *StaticKeyHandler) GetKey(options ...KeyOption) ([]byte, error) { + return h.key, nil +} diff --git a/internal/pkg/mount/options.go b/internal/pkg/mount/options.go index f55ece25d..38a237e16 100644 --- a/internal/pkg/mount/options.go +++ b/internal/pkg/mount/options.go @@ -4,6 +4,8 @@ package mount +import "github.com/talos-systems/talos/pkg/machinery/config" + const ( // ReadOnly is a flag for setting the mount point as readonly. ReadOnly Flags = 1 << iota @@ -29,6 +31,7 @@ type Options struct { MountFlags Flags PreMountHooks []Hook PostUnmountHooks []Hook + Encryption config.Encryption } // Option is the functional option func. @@ -72,6 +75,13 @@ func WithPostUnmountHooks(hooks ...Hook) Option { } } +// WithEncryptionConfig partition encryption configuration. +func WithEncryptionConfig(cfg config.Encryption) Option { + return func(args *Options) { + args.Encryption = cfg + } +} + // Hook represents pre/post mount hook. type Hook func(p *Point) error diff --git a/internal/pkg/mount/system.go b/internal/pkg/mount/system.go index 35fe2bad1..1a968a393 100644 --- a/internal/pkg/mount/system.go +++ b/internal/pkg/mount/system.go @@ -14,7 +14,9 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/disk" + "github.com/talos-systems/talos/internal/pkg/encryption" "github.com/talos-systems/talos/internal/pkg/partition" + "github.com/talos-systems/talos/pkg/machinery/config" "github.com/talos-systems/talos/pkg/machinery/constants" ) @@ -89,8 +91,46 @@ func SystemMountPointForLabel(device *blockdevice.BlockDevice, label string, opt return nil, err } + o := NewDefaultOptions(opts...) + preMountHooks := []Hook{} + if o.Encryption != nil { + encryptionHandler, err := encryption.NewHandler( + device, + part, + o.Encryption, + ) + if err != nil { + return nil, err + } + + preMountHooks = append(preMountHooks, + func(p *Point) error { + var ( + err error + path string + ) + + if path, err = encryptionHandler.Open(); err != nil { + return err + } + + p.source = path + + return nil + }, + ) + + opts = append(opts, + WithPostUnmountHooks( + func(p *Point) error { + return encryptionHandler.Close() + }, + ), + ) + } + // Format the partition if it does not have any filesystem preMountHooks = append(preMountHooks, func(p *Point) error { sb, err := filesystem.Probe(p.source) @@ -132,6 +172,16 @@ func SystemPartitionMount(r runtime.Runtime, label string, opts ...Option) (err return fmt.Errorf("failed to find device with partition labeled %s", label) } + var encryptionConfig config.Encryption + + if r.Config() != nil && r.Config().Machine() != nil { + encryptionConfig = r.Config().Machine().SystemDiskEncryption().Get(label) + } + + if encryptionConfig != nil { + opts = append(opts, WithEncryptionConfig(encryptionConfig)) + } + mountpoint, err := SystemMountPointForLabel(device.BlockDevice, label, opts...) if err != nil { return err diff --git a/pkg/machinery/config/provider.go b/pkg/machinery/config/provider.go index 180fd60b0..ff31fd385 100644 --- a/pkg/machinery/config/provider.go +++ b/pkg/machinery/config/provider.go @@ -45,6 +45,7 @@ type MachineConfig interface { Kubelet() Kubelet Sysctls() map[string]string Registries() Registries + SystemDiskEncryption() SystemDiskEncryption } // Disk represents the options available for partitioning, formatting, and @@ -342,3 +343,31 @@ type CoreDNS interface { type AdminKubeconfig interface { CertLifetime() time.Duration } + +// EncryptionKey defines settings for the partition encryption key handling. +type EncryptionKey interface { + Static() EncryptionKeyStatic + NodeID() EncryptionKeyNodeID + Slot() int +} + +// EncryptionKeyStatic ephemeral encryption key. +type EncryptionKeyStatic interface { + Key() []byte +} + +// EncryptionKeyNodeID deterministically generated encryption key. +type EncryptionKeyNodeID interface { +} + +// Encryption defines settings for the partition encryption. +type Encryption interface { + Kind() string + Cipher() string + Keys() []EncryptionKey +} + +// SystemDiskEncryption accumulates settings for all system partitions encryption. +type SystemDiskEncryption interface { + Get(label string) Encryption +} diff --git a/pkg/machinery/config/types/v1alpha1/bundle/bundle.go b/pkg/machinery/config/types/v1alpha1/bundle/bundle.go index 3733e4ad2..75f760b58 100644 --- a/pkg/machinery/config/types/v1alpha1/bundle/bundle.go +++ b/pkg/machinery/config/types/v1alpha1/bundle/bundle.go @@ -19,7 +19,7 @@ import ( "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine" ) -// NewConfigBundle returns a new bundle +// NewConfigBundle returns a new bundle. // nolint: gocyclo func NewConfigBundle(opts ...Option) (*v1alpha1.ConfigBundle, error) { options := DefaultOptions() diff --git a/pkg/machinery/config/types/v1alpha1/generate/generate.go b/pkg/machinery/config/types/v1alpha1/generate/generate.go index 61621e711..1de57ed83 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/generate.go +++ b/pkg/machinery/config/types/v1alpha1/generate/generate.go @@ -80,9 +80,10 @@ type Input struct { NetworkConfig *v1alpha1.NetworkConfig CNIConfig *v1alpha1.CNIConfig - RegistryMirrors map[string]*v1alpha1.RegistryMirrorConfig - RegistryConfig map[string]*v1alpha1.RegistryConfig - MachineDisks []*v1alpha1.MachineDisk + RegistryMirrors map[string]*v1alpha1.RegistryMirrorConfig + RegistryConfig map[string]*v1alpha1.RegistryConfig + MachineDisks []*v1alpha1.MachineDisk + SystemDiskEncryptionConfig *v1alpha1.SystemDiskEncryptionConfig Debug bool Persist bool @@ -454,28 +455,29 @@ func NewInput(clustername, endpoint, kubernetesVersion string, secrets *SecretsB } input = &Input{ - Certs: secrets.Certs, - ControlPlaneEndpoint: endpoint, - PodNet: []string{podNet}, - ServiceNet: []string{serviceNet}, - ServiceDomain: options.DNSDomain, - ClusterName: clustername, - KubernetesVersion: kubernetesVersion, - Secrets: secrets.Secrets, - TrustdInfo: secrets.TrustdInfo, - AdditionalSubjectAltNames: additionalSubjectAltNames, - AdditionalMachineCertSANs: additionalMachineCertSANs, - InstallDisk: options.InstallDisk, - InstallImage: options.InstallImage, - InstallExtraKernelArgs: options.InstallExtraKernelArgs, - NetworkConfig: options.NetworkConfig, - CNIConfig: options.CNIConfig, - RegistryMirrors: options.RegistryMirrors, - RegistryConfig: options.RegistryConfig, - Debug: options.Debug, - Persist: options.Persist, - AllowSchedulingOnMasters: options.AllowSchedulingOnMasters, - MachineDisks: options.MachineDisks, + Certs: secrets.Certs, + ControlPlaneEndpoint: endpoint, + PodNet: []string{podNet}, + ServiceNet: []string{serviceNet}, + ServiceDomain: options.DNSDomain, + ClusterName: clustername, + KubernetesVersion: kubernetesVersion, + Secrets: secrets.Secrets, + TrustdInfo: secrets.TrustdInfo, + AdditionalSubjectAltNames: additionalSubjectAltNames, + AdditionalMachineCertSANs: additionalMachineCertSANs, + InstallDisk: options.InstallDisk, + InstallImage: options.InstallImage, + InstallExtraKernelArgs: options.InstallExtraKernelArgs, + NetworkConfig: options.NetworkConfig, + CNIConfig: options.CNIConfig, + RegistryMirrors: options.RegistryMirrors, + RegistryConfig: options.RegistryConfig, + Debug: options.Debug, + Persist: options.Persist, + AllowSchedulingOnMasters: options.AllowSchedulingOnMasters, + MachineDisks: options.MachineDisks, + SystemDiskEncryptionConfig: options.SystemDiskEncryptionConfig, } return input, nil diff --git a/pkg/machinery/config/types/v1alpha1/generate/init.go b/pkg/machinery/config/types/v1alpha1/generate/init.go index ee8e73e76..c615c02b6 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/init.go +++ b/pkg/machinery/config/types/v1alpha1/generate/init.go @@ -38,7 +38,8 @@ func initUd(in *Input) (*v1alpha1.Config, error) { RegistryMirrors: in.RegistryMirrors, RegistryConfig: in.RegistryConfig, }, - MachineDisks: in.MachineDisks, + MachineDisks: in.MachineDisks, + MachineSystemDiskEncryption: in.SystemDiskEncryptionConfig, } certSANs := in.GetAPIServerSANs() diff --git a/pkg/machinery/config/types/v1alpha1/generate/join.go b/pkg/machinery/config/types/v1alpha1/generate/join.go index 78940ebe2..4216b2a20 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/join.go +++ b/pkg/machinery/config/types/v1alpha1/generate/join.go @@ -39,7 +39,8 @@ func workerUd(in *Input) (*v1alpha1.Config, error) { RegistryMirrors: in.RegistryMirrors, RegistryConfig: in.RegistryConfig, }, - MachineDisks: in.MachineDisks, + MachineDisks: in.MachineDisks, + MachineSystemDiskEncryption: in.SystemDiskEncryptionConfig, } controlPlaneURL, err := url.Parse(in.ControlPlaneEndpoint) diff --git a/pkg/machinery/config/types/v1alpha1/generate/options.go b/pkg/machinery/config/types/v1alpha1/generate/options.go index d0f6503b8..10f6e28c3 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/options.go +++ b/pkg/machinery/config/types/v1alpha1/generate/options.go @@ -163,23 +163,33 @@ func WithVersionContract(versionContract *config.VersionContract) GenOption { } } +// WithSystemDiskEncryption specifies encryption settings for the system disk partitions. +func WithSystemDiskEncryption(cfg *v1alpha1.SystemDiskEncryptionConfig) GenOption { + return func(o *GenOptions) error { + o.SystemDiskEncryptionConfig = cfg + + return nil + } +} + // GenOptions describes generate parameters. type GenOptions struct { - EndpointList []string - InstallDisk string - InstallImage string - InstallExtraKernelArgs []string - AdditionalSubjectAltNames []string - NetworkConfig *v1alpha1.NetworkConfig - CNIConfig *v1alpha1.CNIConfig - RegistryMirrors map[string]*v1alpha1.RegistryMirrorConfig - RegistryConfig map[string]*v1alpha1.RegistryConfig - DNSDomain string - Debug bool - Persist bool - AllowSchedulingOnMasters bool - MachineDisks []*v1alpha1.MachineDisk - VersionContract *config.VersionContract + EndpointList []string + InstallDisk string + InstallImage string + InstallExtraKernelArgs []string + AdditionalSubjectAltNames []string + NetworkConfig *v1alpha1.NetworkConfig + CNIConfig *v1alpha1.CNIConfig + RegistryMirrors map[string]*v1alpha1.RegistryMirrorConfig + RegistryConfig map[string]*v1alpha1.RegistryConfig + DNSDomain string + Debug bool + Persist bool + AllowSchedulingOnMasters bool + MachineDisks []*v1alpha1.MachineDisk + VersionContract *config.VersionContract + SystemDiskEncryptionConfig *v1alpha1.SystemDiskEncryptionConfig } // DefaultGenOptions returns default options. diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go index feac51bcb..c85a0800f 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go @@ -233,6 +233,15 @@ func (m *MachineConfig) Registries() config.Registries { return &m.MachineRegistries } +// SystemDiskEncryption implements the config.Provider interface. +func (m *MachineConfig) SystemDiskEncryption() config.SystemDiskEncryption { + if m.MachineSystemDiskEncryption == nil { + return &SystemDiskEncryptionConfig{} + } + + return m.MachineSystemDiskEncryption +} + // Image implements the config.Provider interface. func (k *KubeletConfig) Image() string { image := k.KubeletImage @@ -1179,3 +1188,65 @@ func (p *DiskPartition) Size() uint64 { func (p *DiskPartition) MountPoint() string { return p.DiskMountPoint } + +// Kind implements the config.Provider interface. +func (e *EncryptionConfig) Kind() string { + return e.EncryptionProvider +} + +// Cipher implements the config.Provider interface. +func (e *EncryptionConfig) Cipher() string { + return e.EncryptionCipher +} + +// Keys implements the config.Provider interface. +func (e *EncryptionConfig) Keys() []config.EncryptionKey { + keys := make([]config.EncryptionKey, len(e.EncryptionKeys)) + + for i, key := range e.EncryptionKeys { + keys[i] = key + } + + return keys +} + +// Static implements the config.Provider interface. +func (e *EncryptionKey) Static() config.EncryptionKeyStatic { + if e.KeyStatic == nil { + return nil + } + + return e.KeyStatic +} + +// NodeID implements the config.Provider interface. +func (e *EncryptionKey) NodeID() config.EncryptionKeyNodeID { + if e.KeyNodeID == nil { + return nil + } + + return e.KeyNodeID +} + +// Slot implements the config.Provider interface. +func (e *EncryptionKey) Slot() int { + return e.KeySlot +} + +// Key implements the config.Provider interface. +func (e *EncryptionKeyStatic) Key() []byte { + return []byte(e.KeyData) +} + +// Get implements the config.Provider interface. +func (e *SystemDiskEncryptionConfig) Get(label string) config.Encryption { + if label == constants.EphemeralPartitionLabel { + if e.EphemeralPartition == nil { + return nil + } + + return e.EphemeralPartition + } + + return nil +} diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go index bbcff464a..24706899f 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go @@ -198,6 +198,18 @@ var ( "net.ipv4.ip_forward": "0", } + machineSystemDiskEncryptionExample = &SystemDiskEncryptionConfig{ + EphemeralPartition: &EncryptionConfig{ + EncryptionProvider: "luks2", + EncryptionKeys: []*EncryptionKey{ + { + KeyNodeID: &EncryptionKeyNodeID{}, + KeySlot: 0, + }, + }, + }, + } + clusterConfigExample = struct { ControlPlane *ControlPlaneConfig `yaml:"controlPlane"` ClusterName string `yaml:"clusterName"` @@ -533,6 +545,12 @@ type MachineConfig struct { // examples: // - value: machineConfigRegistriesExample MachineRegistries RegistriesConfig `yaml:"registries,omitempty"` + // description: | + // Machine system disk encryption configuration. + // Defines each system partition encryption parameters. + // examples: + // - value: machineSystemDiskEncryptionExample + MachineSystemDiskEncryption *SystemDiskEncryptionConfig `yaml:"systemDiskEncryption,omitempty"` } // ClusterConfig represents the cluster-wide config values. @@ -1051,6 +1069,45 @@ type DiskPartition struct { DiskMountPoint string `yaml:"mountpoint,omitempty"` } +// EncryptionConfig represents partition encryption settings. +type EncryptionConfig struct { + // description: > + // Encryption provider to use for the encryption. + // examples: + // - value: '"luks2"' + EncryptionProvider string `yaml:"provider"` + // description: > + // Defines the encryption keys generation and storage method. + EncryptionKeys []*EncryptionKey `yaml:"keys"` + // description: > + // Cipher kind to use for the encryption. + // Depends on the encryption provider. + EncryptionCipher string `yaml:"cipher,omitempty"` +} + +// EncryptionKey represents configuration for disk encryption key. +type EncryptionKey struct { + // description: > + // Key which value is stored in the configuration file. + KeyStatic *EncryptionKeyStatic `yaml:"static,omitempty"` + // description: > + // Deterministically generated key from the node UUID and PartitionLabel. + KeyNodeID *EncryptionKeyNodeID `yaml:"nodeID,omitempty"` + // description: > + // Key slot number for luks2 encryption. + KeySlot int `yaml:"slot"` +} + +// EncryptionKeyStatic represents throw away key type. +type EncryptionKeyStatic struct { + // description: > + // Defines the static passphrase value. + KeyData string `yaml:"passphrase,omitempty"` +} + +// EncryptionKeyNodeID represents deterministically generated key from the node UUID and PartitionLabel. +type EncryptionKeyNodeID struct{} + // Env represents a set of environment variables. type Env = map[string]string @@ -1396,3 +1453,10 @@ type RegistryTLSConfig struct { // Skip TLS server certificate verification (not recommended). TLSInsecureSkipVerify bool `yaml:"insecureSkipVerify,omitempty"` } + +// SystemDiskEncryptionConfig specifies system disk partitions encryption settings. +type SystemDiskEncryptionConfig struct { + // description: | + // Ephemeral partition encryption. + EphemeralPartition *EncryptionConfig `yaml:"ephemeral,omitempty"` +} diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go index ca1fa47a8..07d549834 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go @@ -11,41 +11,46 @@ import ( ) var ( - ConfigDoc encoder.Doc - MachineConfigDoc encoder.Doc - ClusterConfigDoc encoder.Doc - KubeletConfigDoc encoder.Doc - NetworkConfigDoc encoder.Doc - InstallConfigDoc encoder.Doc - TimeConfigDoc encoder.Doc - RegistriesConfigDoc encoder.Doc - PodCheckpointerDoc encoder.Doc - CoreDNSDoc encoder.Doc - EndpointDoc encoder.Doc - ControlPlaneConfigDoc encoder.Doc - APIServerConfigDoc encoder.Doc - ControllerManagerConfigDoc encoder.Doc - ProxyConfigDoc encoder.Doc - SchedulerConfigDoc encoder.Doc - EtcdConfigDoc encoder.Doc - ClusterNetworkConfigDoc encoder.Doc - CNIConfigDoc encoder.Doc - AdminKubeconfigConfigDoc encoder.Doc - MachineDiskDoc encoder.Doc - DiskPartitionDoc encoder.Doc - MachineFileDoc encoder.Doc - ExtraHostDoc encoder.Doc - DeviceDoc encoder.Doc - DHCPOptionsDoc encoder.Doc - DeviceWireguardConfigDoc encoder.Doc - DeviceWireguardPeerDoc encoder.Doc - BondDoc encoder.Doc - VlanDoc encoder.Doc - RouteDoc encoder.Doc - RegistryMirrorConfigDoc encoder.Doc - RegistryConfigDoc encoder.Doc - RegistryAuthConfigDoc encoder.Doc - RegistryTLSConfigDoc encoder.Doc + ConfigDoc encoder.Doc + MachineConfigDoc encoder.Doc + ClusterConfigDoc encoder.Doc + KubeletConfigDoc encoder.Doc + NetworkConfigDoc encoder.Doc + InstallConfigDoc encoder.Doc + TimeConfigDoc encoder.Doc + RegistriesConfigDoc encoder.Doc + PodCheckpointerDoc encoder.Doc + CoreDNSDoc encoder.Doc + EndpointDoc encoder.Doc + ControlPlaneConfigDoc encoder.Doc + APIServerConfigDoc encoder.Doc + ControllerManagerConfigDoc encoder.Doc + ProxyConfigDoc encoder.Doc + SchedulerConfigDoc encoder.Doc + EtcdConfigDoc encoder.Doc + ClusterNetworkConfigDoc encoder.Doc + CNIConfigDoc encoder.Doc + AdminKubeconfigConfigDoc encoder.Doc + MachineDiskDoc encoder.Doc + DiskPartitionDoc encoder.Doc + EncryptionConfigDoc encoder.Doc + EncryptionKeyDoc encoder.Doc + EncryptionKeyStaticDoc encoder.Doc + EncryptionKeyNodeIDDoc encoder.Doc + MachineFileDoc encoder.Doc + ExtraHostDoc encoder.Doc + DeviceDoc encoder.Doc + DHCPOptionsDoc encoder.Doc + DeviceWireguardConfigDoc encoder.Doc + DeviceWireguardPeerDoc encoder.Doc + BondDoc encoder.Doc + VlanDoc encoder.Doc + RouteDoc encoder.Doc + RegistryMirrorConfigDoc encoder.Doc + RegistryConfigDoc encoder.Doc + RegistryAuthConfigDoc encoder.Doc + RegistryTLSConfigDoc encoder.Doc + SystemDiskEncryptionConfigDoc encoder.Doc ) func init() { @@ -107,7 +112,7 @@ func init() { FieldName: "machine", }, } - MachineConfigDoc.Fields = make([]encoder.Doc, 13) + MachineConfigDoc.Fields = make([]encoder.Doc, 14) MachineConfigDoc.Fields[0].Name = "type" MachineConfigDoc.Fields[0].Type = "string" MachineConfigDoc.Fields[0].Note = "" @@ -213,6 +218,13 @@ func init() { MachineConfigDoc.Fields[12].Comments[encoder.LineComment] = "Used to configure the machine's container image registry mirrors." MachineConfigDoc.Fields[12].AddExample("", machineConfigRegistriesExample) + MachineConfigDoc.Fields[13].Name = "systemDiskEncryption" + MachineConfigDoc.Fields[13].Type = "SystemDiskEncryptionConfig" + MachineConfigDoc.Fields[13].Note = "" + MachineConfigDoc.Fields[13].Description = "Machine system disk encryption configuration.\nDefines each system partition encryption parameters." + MachineConfigDoc.Fields[13].Comments[encoder.LineComment] = "Machine system disk encryption configuration." + + MachineConfigDoc.Fields[13].AddExample("", machineSystemDiskEncryptionExample) ClusterConfigDoc.Type = "ClusterConfig" ClusterConfigDoc.Comments[encoder.LineComment] = "ClusterConfig represents the cluster-wide config values." @@ -912,6 +924,87 @@ func init() { DiskPartitionDoc.Fields[1].Description = "Where to mount the partition." DiskPartitionDoc.Fields[1].Comments[encoder.LineComment] = "Where to mount the partition." + EncryptionConfigDoc.Type = "EncryptionConfig" + EncryptionConfigDoc.Comments[encoder.LineComment] = "EncryptionConfig represents partition encryption settings." + EncryptionConfigDoc.Description = "EncryptionConfig represents partition encryption settings." + EncryptionConfigDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "SystemDiskEncryptionConfig", + FieldName: "ephemeral", + }, + } + EncryptionConfigDoc.Fields = make([]encoder.Doc, 3) + EncryptionConfigDoc.Fields[0].Name = "provider" + EncryptionConfigDoc.Fields[0].Type = "string" + EncryptionConfigDoc.Fields[0].Note = "" + EncryptionConfigDoc.Fields[0].Description = "Encryption provider to use for the encryption." + EncryptionConfigDoc.Fields[0].Comments[encoder.LineComment] = "Encryption provider to use for the encryption." + + EncryptionConfigDoc.Fields[0].AddExample("", "luks2") + EncryptionConfigDoc.Fields[1].Name = "keys" + EncryptionConfigDoc.Fields[1].Type = "[]EncryptionKey" + EncryptionConfigDoc.Fields[1].Note = "" + EncryptionConfigDoc.Fields[1].Description = "Defines the encryption keys generation and storage method." + EncryptionConfigDoc.Fields[1].Comments[encoder.LineComment] = "Defines the encryption keys generation and storage method." + EncryptionConfigDoc.Fields[2].Name = "cipher" + EncryptionConfigDoc.Fields[2].Type = "string" + EncryptionConfigDoc.Fields[2].Note = "" + EncryptionConfigDoc.Fields[2].Description = "Cipher kind to use for the encryption. Depends on the encryption provider." + EncryptionConfigDoc.Fields[2].Comments[encoder.LineComment] = "Cipher kind to use for the encryption. Depends on the encryption provider." + + EncryptionKeyDoc.Type = "EncryptionKey" + EncryptionKeyDoc.Comments[encoder.LineComment] = "EncryptionKey represents configuration for disk encryption key." + EncryptionKeyDoc.Description = "EncryptionKey represents configuration for disk encryption key." + EncryptionKeyDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "EncryptionConfig", + FieldName: "keys", + }, + } + EncryptionKeyDoc.Fields = make([]encoder.Doc, 3) + EncryptionKeyDoc.Fields[0].Name = "static" + EncryptionKeyDoc.Fields[0].Type = "EncryptionKeyStatic" + EncryptionKeyDoc.Fields[0].Note = "" + EncryptionKeyDoc.Fields[0].Description = "Key which value is stored in the configuration file." + EncryptionKeyDoc.Fields[0].Comments[encoder.LineComment] = "Key which value is stored in the configuration file." + EncryptionKeyDoc.Fields[1].Name = "nodeID" + EncryptionKeyDoc.Fields[1].Type = "EncryptionKeyNodeID" + EncryptionKeyDoc.Fields[1].Note = "" + EncryptionKeyDoc.Fields[1].Description = "Deterministically generated key from the node UUID and PartitionLabel." + EncryptionKeyDoc.Fields[1].Comments[encoder.LineComment] = "Deterministically generated key from the node UUID and PartitionLabel." + EncryptionKeyDoc.Fields[2].Name = "slot" + EncryptionKeyDoc.Fields[2].Type = "int" + EncryptionKeyDoc.Fields[2].Note = "" + EncryptionKeyDoc.Fields[2].Description = "Key slot number for luks2 encryption." + EncryptionKeyDoc.Fields[2].Comments[encoder.LineComment] = "Key slot number for luks2 encryption." + + EncryptionKeyStaticDoc.Type = "EncryptionKeyStatic" + EncryptionKeyStaticDoc.Comments[encoder.LineComment] = "EncryptionKeyStatic represents throw away key type." + EncryptionKeyStaticDoc.Description = "EncryptionKeyStatic represents throw away key type." + EncryptionKeyStaticDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "EncryptionKey", + FieldName: "static", + }, + } + EncryptionKeyStaticDoc.Fields = make([]encoder.Doc, 1) + EncryptionKeyStaticDoc.Fields[0].Name = "passphrase" + EncryptionKeyStaticDoc.Fields[0].Type = "string" + EncryptionKeyStaticDoc.Fields[0].Note = "" + EncryptionKeyStaticDoc.Fields[0].Description = "Defines the static passphrase value." + EncryptionKeyStaticDoc.Fields[0].Comments[encoder.LineComment] = "Defines the static passphrase value." + + EncryptionKeyNodeIDDoc.Type = "EncryptionKeyNodeID" + EncryptionKeyNodeIDDoc.Comments[encoder.LineComment] = "EncryptionKeyNodeID represents deterministically generated key from the node UUID and PartitionLabel." + EncryptionKeyNodeIDDoc.Description = "EncryptionKeyNodeID represents deterministically generated key from the node UUID and PartitionLabel." + EncryptionKeyNodeIDDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "EncryptionKey", + FieldName: "nodeID", + }, + } + EncryptionKeyNodeIDDoc.Fields = make([]encoder.Doc, 0) + MachineFileDoc.Type = "MachineFile" MachineFileDoc.Comments[encoder.LineComment] = "MachineFile represents a file to write to disk." MachineFileDoc.Description = "MachineFile represents a file to write to disk." @@ -1473,6 +1566,24 @@ func init() { RegistryTLSConfigDoc.Fields[2].Note = "" RegistryTLSConfigDoc.Fields[2].Description = "Skip TLS server certificate verification (not recommended)." RegistryTLSConfigDoc.Fields[2].Comments[encoder.LineComment] = "Skip TLS server certificate verification (not recommended)." + + SystemDiskEncryptionConfigDoc.Type = "SystemDiskEncryptionConfig" + SystemDiskEncryptionConfigDoc.Comments[encoder.LineComment] = "SystemDiskEncryptionConfig specifies system disk partitions encryption settings." + SystemDiskEncryptionConfigDoc.Description = "SystemDiskEncryptionConfig specifies system disk partitions encryption settings." + + SystemDiskEncryptionConfigDoc.AddExample("", machineSystemDiskEncryptionExample) + SystemDiskEncryptionConfigDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "MachineConfig", + FieldName: "systemDiskEncryption", + }, + } + SystemDiskEncryptionConfigDoc.Fields = make([]encoder.Doc, 1) + SystemDiskEncryptionConfigDoc.Fields[0].Name = "ephemeral" + SystemDiskEncryptionConfigDoc.Fields[0].Type = "EncryptionConfig" + SystemDiskEncryptionConfigDoc.Fields[0].Note = "" + SystemDiskEncryptionConfigDoc.Fields[0].Description = "Ephemeral partition encryption." + SystemDiskEncryptionConfigDoc.Fields[0].Comments[encoder.LineComment] = "Ephemeral partition encryption." } func (_ Config) Doc() *encoder.Doc { @@ -1563,6 +1674,22 @@ func (_ DiskPartition) Doc() *encoder.Doc { return &DiskPartitionDoc } +func (_ EncryptionConfig) Doc() *encoder.Doc { + return &EncryptionConfigDoc +} + +func (_ EncryptionKey) Doc() *encoder.Doc { + return &EncryptionKeyDoc +} + +func (_ EncryptionKeyStatic) Doc() *encoder.Doc { + return &EncryptionKeyStaticDoc +} + +func (_ EncryptionKeyNodeID) Doc() *encoder.Doc { + return &EncryptionKeyNodeIDDoc +} + func (_ MachineFile) Doc() *encoder.Doc { return &MachineFileDoc } @@ -1615,6 +1742,10 @@ func (_ RegistryTLSConfig) Doc() *encoder.Doc { return &RegistryTLSConfigDoc } +func (_ SystemDiskEncryptionConfig) Doc() *encoder.Doc { + return &SystemDiskEncryptionConfigDoc +} + // GetConfigurationDoc returns documentation for the file ./v1alpha1_types_doc.go. func GetConfigurationDoc() *encoder.FileDoc { return &encoder.FileDoc{ @@ -1643,6 +1774,10 @@ func GetConfigurationDoc() *encoder.FileDoc { &AdminKubeconfigConfigDoc, &MachineDiskDoc, &DiskPartitionDoc, + &EncryptionConfigDoc, + &EncryptionKeyDoc, + &EncryptionKeyStaticDoc, + &EncryptionKeyNodeIDDoc, &MachineFileDoc, &ExtraHostDoc, &DeviceDoc, @@ -1656,6 +1791,7 @@ func GetConfigurationDoc() *encoder.FileDoc { &RegistryConfigDoc, &RegistryAuthConfigDoc, &RegistryTLSConfigDoc, + &SystemDiskEncryptionConfigDoc, }, } } diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go index ef14c07d6..5b04f82f0 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go @@ -119,6 +119,28 @@ func (c *Config) Validate(mode config.RuntimeMode) error { result = multierror.Append(result, fmt.Errorf("%q is not a valid DNS name", c.ClusterConfig.ClusterNetwork.DNSDomain)) } + for _, label := range []string{constants.EphemeralPartitionLabel} { + encryptionConfig := c.MachineConfig.SystemDiskEncryption().Get(label) + if encryptionConfig != nil { + if len(encryptionConfig.Keys()) == 0 { + result = multierror.Append(result, fmt.Errorf("no encryption keys provided for the ephemeral partition encryption")) + } + + slotsInUse := map[int]bool{} + for _, key := range encryptionConfig.Keys() { + if slotsInUse[key.Slot()] { + result = multierror.Append(result, fmt.Errorf("encryption key slot %d is already in use", key.Slot())) + } + + slotsInUse[key.Slot()] = true + + if key.NodeID() == nil && key.Static() == nil { + result = multierror.Append(result, fmt.Errorf("encryption key at slot %d doesn't have any settings", key.Slot())) + } + } + } + } + return result.ErrorOrNil() } diff --git a/website/content/docs/v0.9/Reference/cli.md b/website/content/docs/v0.9/Reference/cli.md index 42d48e770..e37866930 100644 --- a/website/content/docs/v0.9/Reference/cli.md +++ b/website/content/docs/v0.9/Reference/cli.md @@ -88,6 +88,7 @@ talosctl cluster create [flags] --disk-image-path string disk image to use --dns-domain string the dns domain to use for cluster (default "cluster.local") --docker-host-ip string Host IP to forward exposed ports to (Docker provisioner only) (default "0.0.0.0") + --encrypt-ephemeral enable ephemeral partition encryption --endpoint string use endpoint instead of provider defaults -p, --exposed-ports string Comma-separated list of ports/protocols to expose on init node. Ex -p :/ (Docker provisioner only) -h, --help help for create diff --git a/website/content/docs/v0.9/Reference/configuration.md b/website/content/docs/v0.9/Reference/configuration.md index ed111948e..d1d8f9a6b 100644 --- a/website/content/docs/v0.9/Reference/configuration.md +++ b/website/content/docs/v0.9/Reference/configuration.md @@ -658,6 +658,38 @@ registries:
+
+ +systemDiskEncryption SystemDiskEncryptionConfig + +
+
+ +Machine system disk encryption configuration. +Defines each system partition encryption parameters. + + + +Examples: + + +``` yaml +systemDiskEncryption: + # Ephemeral partition encryption. + ephemeral: + provider: luks2 # Encryption provider to use for the encryption. + # Defines the encryption keys generation and storage method. + keys: + - # Deterministically generated key from the node UUID and PartitionLabel. + nodeID: {} + slot: 0 # Key slot number for luks2 encryption. +``` + + +
+ +
+ @@ -2693,6 +2725,167 @@ Where to mount the partition. +## EncryptionConfig +EncryptionConfig represents partition encryption settings. + +Appears in: + + +- SystemDiskEncryptionConfig.ephemeral + + + +
+ +
+ +provider string + +
+
+ +Encryption provider to use for the encryption. + + + +Examples: + + +``` yaml +provider: luks2 +``` + + +
+ +
+ +
+ +keys []EncryptionKey + +
+
+ +Defines the encryption keys generation and storage method. + +
+ +
+ +
+ +cipher string + +
+
+ +Cipher kind to use for the encryption. Depends on the encryption provider. + +
+ +
+ + + + + +## EncryptionKey +EncryptionKey represents configuration for disk encryption key. + +Appears in: + + +- EncryptionConfig.keys + + + +
+ +
+ +static EncryptionKeyStatic + +
+
+ +Key which value is stored in the configuration file. + +
+ +
+ +
+ +nodeID EncryptionKeyNodeID + +
+
+ +Deterministically generated key from the node UUID and PartitionLabel. + +
+ +
+ +
+ +slot int + +
+
+ +Key slot number for luks2 encryption. + +
+ +
+ + + + + +## EncryptionKeyStatic +EncryptionKeyStatic represents throw away key type. + +Appears in: + + +- EncryptionKey.static + + + +
+ +
+ +passphrase string + +
+
+ +Defines the static passphrase value. + +
+ +
+ + + + + +## EncryptionKeyNodeID +EncryptionKeyNodeID represents deterministically generated key from the node UUID and PartitionLabel. + +Appears in: + + +- EncryptionKey.nodeID + + + + + ## MachineFile MachineFile represents a file to write to disk. @@ -4199,3 +4392,42 @@ Skip TLS server certificate verification (not recommended). + +## SystemDiskEncryptionConfig +SystemDiskEncryptionConfig specifies system disk partitions encryption settings. + +Appears in: + + +- MachineConfig.systemDiskEncryption + + +``` yaml +# Ephemeral partition encryption. +ephemeral: + provider: luks2 # Encryption provider to use for the encryption. + # Defines the encryption keys generation and storage method. + keys: + - # Deterministically generated key from the node UUID and PartitionLabel. + nodeID: {} + slot: 0 # Key slot number for luks2 encryption. +``` + +
+ +
+ +ephemeral EncryptionConfig + +
+
+ +Ephemeral partition encryption. + +
+ +
+ + + +