From f41dcd79de1b884b4584de333acbeb8f348329bf Mon Sep 17 00:00:00 2001 From: Stephen Lowrie Date: Tue, 9 Jun 2020 23:40:40 -0500 Subject: [PATCH] internal: add LUKS support --- internal/distro/distro.go | 9 + internal/exec/stages/disks/disks.go | 7 +- internal/exec/stages/disks/luks.go | 217 ++++++++++++++++++ internal/exec/stages/files/files.go | 4 + .../exec/stages/files/filesystemEntries.go | 87 ++++++- internal/exec/util/luks.go | 20 ++ 6 files changed, 331 insertions(+), 13 deletions(-) create mode 100644 internal/exec/stages/disks/luks.go create mode 100644 internal/exec/util/luks.go diff --git a/internal/distro/distro.go b/internal/distro/distro.go index 9c21a1506b..946a4ff197 100644 --- a/internal/distro/distro.go +++ b/internal/distro/distro.go @@ -56,6 +56,11 @@ var ( chccwdevCmd = "chccwdev" cioIgnoreCmd = "cio_ignore" + // LUKS programs + clevisCmd = "clevis" + cryptsetupCmd = "cryptsetup" + ddCmd = "dd" + // Flags selinuxRelabel = "true" blackboxTesting = "false" @@ -94,6 +99,10 @@ func VmurCmd() string { return vmurCmd } func ChccwdevCmd() string { return chccwdevCmd } func CioIgnoreCmd() string { return cioIgnoreCmd } +func ClevisCmd() string { return clevisCmd } +func CryptsetupCmd() string { return cryptsetupCmd } +func DdCmd() string { return ddCmd } + func SelinuxRelabel() bool { return bakedStringToBool(selinuxRelabel) && !BlackboxTesting() } func BlackboxTesting() bool { return bakedStringToBool(blackboxTesting) } func WriteAuthorizedKeysFragment() bool { diff --git a/internal/exec/stages/disks/disks.go b/internal/exec/stages/disks/disks.go index a953fe67cd..6d29a7c493 100644 --- a/internal/exec/stages/disks/disks.go +++ b/internal/exec/stages/disks/disks.go @@ -71,7 +71,8 @@ func (s stage) Run(config types.Config) error { // do the udevadm settle and can just return here. if len(config.Storage.Disks) == 0 && len(config.Storage.Raid) == 0 && - len(config.Storage.Filesystems) == 0 { + len(config.Storage.Filesystems) == 0 && + len(config.Storage.Luks) == 0 { return nil } @@ -83,6 +84,10 @@ func (s stage) Run(config types.Config) error { return fmt.Errorf("failed to create raids: %v", err) } + if err := s.createLuks(config); err != nil { + return fmt.Errorf("failed to create luks: %v", err) + } + if err := s.createFilesystems(config); err != nil { return fmt.Errorf("failed to create filesystems: %v", err) } diff --git a/internal/exec/stages/disks/luks.go b/internal/exec/stages/disks/luks.go new file mode 100644 index 0000000000..bc5cb4fd95 --- /dev/null +++ b/internal/exec/stages/disks/luks.go @@ -0,0 +1,217 @@ +// Copyright 2020 Red Hat +// +// 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. + +// The storage stage is responsible for partitioning disks, creating RAID +// arrays, formatting partitions, writing files, writing systemd units, and +// writing network units. +// createRaids creates the raid arrays described in config.Storage.Raid. + +package disks + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2_experimental/types" + "github.com/coreos/ignition/v2/internal/distro" + execUtil "github.com/coreos/ignition/v2/internal/exec/util" +) + +// https://github.com/latchset/clevis/blob/master/src/pins/tang/clevis-encrypt-tang.1.adoc#config +type Tang struct { + URL string `json:"url"` + Thumbprint string `json:"thp,omitempty"` +} + +// https://github.com/latchset/clevis/blob/master/README.md#pin-shamir-secret-sharing +type Pin struct { + Tpm bool `json:"tpm"` + Tang []Tang `json:"tang,omitempty"` +} + +func (p Pin) MarshalJSON() ([]byte, error) { + if p.Tpm { + return json.Marshal(&struct { + Tang []Tang `json:"tang,omitempty"` + Tpm struct{} `json:"tpm2"` + }{ + Tang: p.Tang, + Tpm: struct{}{}, + }) + } + return json.Marshal(&struct { + Tang []Tang `json:"tang"` + }{ + Tang: p.Tang, + }) +} + +type Clevis struct { + Pins Pin `json:"pins"` + Threshold int `json:"t"` +} + +// Initially tested generating keyfiles via dd'ing to a file from /dev/urandom +// however while cryptsetup had no problem with these keyfiles clevis seemed to +// die on them while keyfiles generated via openssl rand -hex would work... +func randHex(length int) (string, error) { + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +func (s *stage) createLuks(config types.Config) error { + if len(config.Storage.Luks) == 0 { + return nil + } + + for _, luks := range config.Storage.Luks { + // TODO: allow Ignition generated KeyFiles for + // non-clevis devices that can be persisted. + // TODO: create devices in parallel. + // track whether Ignition creates the KeyFile + // so that it can be removed creation + var ignitionCreatedKeyFile bool + // create keyfile inside of tmpfs, it will be copied to the + // sysroot by the files stage + os.MkdirAll(execUtil.LuksInitramfsKeyFilePath, 0644) + keyFilePath := filepath.Join(execUtil.LuksInitramfsKeyFilePath, luks.Name) + if luks.KeyFile == nil || *luks.KeyFile == "" { + // create a keyfile + key, err := randHex(4096) + if err != nil { + return fmt.Errorf("generating keyfile: %v", err) + } + if err := ioutil.WriteFile(keyFilePath, []byte(key), 0400); err != nil { + return fmt.Errorf("creating keyfile: %v", err) + } + ignitionCreatedKeyFile = true + } else { + if err := ioutil.WriteFile(keyFilePath, []byte(*luks.KeyFile), 0400); err != nil { + return fmt.Errorf("writing keyfile: %v", err) + } + } + + args := []string{ + "luksFormat", + "--type", "luks2", + "--key-file", keyFilePath, + } + + if !util.NilOrEmpty(luks.Hash) { + args = append(args, "--hash", *luks.Hash) + } + + if !util.NilOrEmpty(luks.Label) { + args = append(args, "--label", *luks.Label) + } + + if !util.NilOrEmpty(luks.UUID) { + args = append(args, "--uuid", *luks.UUID) + } + + if !util.NilOrEmpty(luks.Cipher) { + args = append(args, "--cipher", *luks.Cipher) + } + + if len(luks.Options) > 0 { + // golang's a really great language... + for _, option := range luks.Options { + args = append(args, string(option)) + } + } + + args = append(args, luks.Device) + + if _, err := s.Logger.LogCmd( + exec.Command(distro.CryptsetupCmd(), args...), + "creating %q", luks.Name, + ); err != nil { + return fmt.Errorf("cryptsetup failed: %v", err) + } + + // open the device + if _, err := s.Logger.LogCmd( + exec.Command(distro.CryptsetupCmd(), "luksOpen", luks.Device, luks.Name, "--key-file", keyFilePath), + "opening luks device %v", luks.Name, + ); err != nil { + return fmt.Errorf("opening luks device: %v", err) + } + + if luks.Clevis != nil { + c := Clevis{ + Pins: Pin{}, + } + if luks.Clevis.Threshold == nil { + c.Threshold = 1 + } else { + c.Threshold = *luks.Clevis.Threshold + } + for _, tang := range luks.Clevis.Tang { + c.Pins.Tang = append(c.Pins.Tang, Tang{ + URL: tang.URL, + Thumbprint: tang.Thumbprint, + }) + } + if luks.Clevis.Tpm2 != nil { + c.Pins.Tpm = *luks.Clevis.Tpm2 + } + clevisJson, err := json.Marshal(c) + if err != nil { + return fmt.Errorf("creating clevis json: %v", err) + } + if _, err := s.Logger.LogCmd( + exec.Command(distro.ClevisCmd(), "luks", "bind", "-f", "-k", keyFilePath, "-d", luks.Device, "sss", string(clevisJson)), "Clevis bind", + ); err != nil { + return fmt.Errorf("binding clevis device: %v", err) + } + + // close & re-open Clevis devices to make sure that we can unlock them + if _, err := s.Logger.LogCmd( + exec.Command(distro.CryptsetupCmd(), "luksClose", luks.Name), + "closing clevis luks device %v", luks.Name, + ); err != nil { + return fmt.Errorf("closing luks device: %v", err) + } + if _, err := s.Logger.LogCmd( + exec.Command(distro.ClevisCmd(), "luks", "unlock", "-d", luks.Device, "-n", luks.Name), + "opening clevis luks device %s", luks.Name, + ); err != nil { + return fmt.Errorf("opening luks device %s: %v", luks.Name, err) + } + } + + // assume the user does not want a key file, remove it + if ignitionCreatedKeyFile { + if _, err := s.Logger.LogCmd( + exec.Command(distro.CryptsetupCmd(), "luksRemoveKey", luks.Device, keyFilePath), + "removing key file for %v", luks.Name, + ); err != nil { + return fmt.Errorf("removing key file: %v", err) + } + os.Remove(keyFilePath) + } + } + + return nil +} diff --git a/internal/exec/stages/files/files.go b/internal/exec/stages/files/files.go index 4ba7a68a45..5b0eceebfa 100644 --- a/internal/exec/stages/files/files.go +++ b/internal/exec/stages/files/files.go @@ -81,6 +81,10 @@ func (s stage) Run(config types.Config) error { return fmt.Errorf("failed to create units: %v", err) } + if err := s.createCrypttabEntries(config); err != nil { + return fmt.Errorf("creating crypttab entries: %v", err) + } + if err := s.relabelFiles(); err != nil { return fmt.Errorf("failed to handle relabeling: %v", err) } diff --git a/internal/exec/stages/files/filesystemEntries.go b/internal/exec/stages/files/filesystemEntries.go index 1f53e93a47..76100da59b 100644 --- a/internal/exec/stages/files/filesystemEntries.go +++ b/internal/exec/stages/files/filesystemEntries.go @@ -16,16 +16,90 @@ package files import ( "fmt" + "io/ioutil" "os" + "os/exec" "path/filepath" "sort" "strings" "github.com/coreos/ignition/v2/config/v3_2_experimental/types" + "github.com/coreos/ignition/v2/internal/distro" "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" + + "github.com/vincent-petithory/dataurl" ) +// createCrypttabEntries creates entries inside of /etc/crypttab for LUKS volumes, +// as well as copying keyfiles to the sysroot. +func (s *stage) createCrypttabEntries(config types.Config) error { + if len(config.Storage.Luks) == 0 { + return nil + } + + s.Logger.PushPrefix("createCrypttabEntries") + defer s.Logger.PopPrefix() + + crypttab := fileEntry{ + types.Node{ + Path: "/sysroot/etc/crypttab", + }, + types.FileEmbedded1{}, + } + keyfiles := []fileEntry{} + for _, luks := range config.Storage.Luks { + out, err := exec.Command(distro.CryptsetupCmd(), "luksUUID", luks.Device).CombinedOutput() + if err != nil { + return fmt.Errorf("gathering luks uuid: %s: %v", out, err) + } + uuid := strings.TrimSpace(string(out)) + netdev := "" + if len(luks.Clevis.Tang) > 0 { + netdev = ",_netdev" + } + keyfile := "none" + if luks.Clevis == nil { + keyfile = filepath.Join(util.LuksRealRootKeyFilePath, luks.Name) + + // Copy keyfile from /run to sysroot + contents, err := ioutil.ReadFile(filepath.Join(util.LuksInitramfsKeyFilePath, luks.Name)) + if err != nil { + return fmt.Errorf("reading keyfile for %s: %v", luks.Name, err) + } + contentsUri := dataurl.EncodeBytes(contents) + keyfiles = append(keyfiles, fileEntry{ + types.Node{ + Path: filepath.Join("/sysroot", keyfile), + }, + types.FileEmbedded1{ + Contents: types.Resource{ + Source: &contentsUri, + }, + }, + }) + } + uri := dataurl.EncodeBytes([]byte(fmt.Sprintf("%s UUID=%s %s luks%s\n", luks.Name, uuid, keyfile, netdev))) + crypttab.Append = append(crypttab.Append, types.Resource{ + Source: &uri, + }) + } + if err := crypttab.create(s.Logger, s.Util); err != nil { + return fmt.Errorf("adding luks devices to crypttab: %v", err) + } + for _, file := range keyfiles { + if err := file.create(s.Logger, s.Util); err != nil { + return fmt.Errorf("copying keyfile: %v", err) + } + } + // delete the entire keyfiles folder in /run/ so that the keyfiles are stored on + // only the root device which can be encrypted + if err := os.RemoveAll(util.LuksInitramfsKeyFilePath); err != nil { + return fmt.Errorf("removing initramfs keyfiles: %v", err) + } + return nil +} + // createFilesystemsEntries creates the files described in config.Storage.{Files,Directories}. func (s *stage) createFilesystemsEntries(config types.Config) error { s.Logger.PushPrefix("createFilesystemsFiles") @@ -225,7 +299,6 @@ func (s stage) getOrderedCreationList(config types.Config) ([]filesystemEntry, e entries = append(entries, fileEntry(f)) } - hardlinks := []filesystemEntry{} for _, l := range config.Storage.Links { path, err := s.JoinPath(l.Path) if err != nil { @@ -237,20 +310,10 @@ func (s stage) getOrderedCreationList(config types.Config) ([]filesystemEntry, e } paths[path] = l.Path l.Path = path - if l.Hard != nil && *l.Hard { - hardlinks = append(hardlinks, linkEntry(l)) - } else { - entries = append(entries, linkEntry(l)) - } - + entries = append(entries, linkEntry(l)) } sort.Slice(entries, func(i, j int) bool { return util.Depth(entries[i].node().Path) < util.Depth(entries[j].node().Path) }) - // Append all the hard links to the list after sorting. This allows - // Ignition to create hard links to files that are deeper than the hard - // link. For reference: https://github.com/coreos/ignition/issues/800 - entries = append(entries, hardlinks...) - return entries, nil } diff --git a/internal/exec/util/luks.go b/internal/exec/util/luks.go new file mode 100644 index 0000000000..affd4957b5 --- /dev/null +++ b/internal/exec/util/luks.go @@ -0,0 +1,20 @@ +// Copyright 2020 Red Hat, Inc. +// +// 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 + +var ( + LuksInitramfsKeyFilePath = "/run/ignition/luks-keyfiles/" + LuksRealRootKeyFilePath = "/etc/luks/" +)