mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-02 01:26:43 +00:00
We should not default to use the underlying OS files and binaries for grub installs as we may be on a takeover scenario in which the underlying OS does not provide the needed files. This moves the grub binary and files to be provided by the source rootfs and moves the grubOptions hook to use the go libraries directly instead of shelling out to another binary Signed-off-by: Itxaka <itxaka@kairos.io>
263 lines
8.6 KiB
Go
263 lines
8.6 KiB
Go
package hook
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/config"
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
|
internalutils "github.com/kairos-io/kairos-agent/v2/pkg/utils"
|
|
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
|
|
"github.com/kairos-io/kairos-sdk/machine"
|
|
"github.com/kairos-io/kairos-sdk/utils"
|
|
kcrypt "github.com/kairos-io/kcrypt/pkg/lib"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// Finish is a hook that runs after the install process.
|
|
// It is used to encrypt partitions and run the BundlePostInstall, CustomMounts and CopyLogs hooks
|
|
type Finish struct{}
|
|
|
|
func (k Finish) Run(c config.Config, spec v1.Spec) error {
|
|
var err error
|
|
if len(c.Install.Encrypt) != 0 || internalutils.IsUki() {
|
|
c.Logger.Logger.Info().Msg("Running encrypt hook")
|
|
|
|
if internalutils.IsUki() {
|
|
err = EncryptUKI(c, spec)
|
|
} else {
|
|
err = Encrypt(c, spec)
|
|
}
|
|
|
|
// They return with partitions unlocked so make sure to lock them before we end
|
|
defer lockPartitions(c)
|
|
|
|
if err != nil {
|
|
c.Logger.Logger.Error().Err(err).Msg("could not encrypt partitions")
|
|
return err
|
|
}
|
|
c.Logger.Logger.Info().Msg("Finished encrypt hook")
|
|
}
|
|
|
|
// Now that we have everything encrypted and ready to mount if needed
|
|
err = GrubPostInstallOptions{}.Run(c, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = BundlePostInstall{}.Run(c, spec)
|
|
if err != nil {
|
|
c.Logger.Logger.Warn().Err(err).Msg("could not copy run bundles post install")
|
|
if c.FailOnBundleErrors {
|
|
return err
|
|
}
|
|
}
|
|
err = CustomMounts{}.Run(c, spec)
|
|
if err != nil {
|
|
c.Logger.Logger.Warn().Err(err).Msg("could not create custom mounts")
|
|
}
|
|
err = CopyLogs{}.Run(c, spec)
|
|
if err != nil {
|
|
c.Logger.Logger.Warn().Err(err).Msg("could not copy logs")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Encrypt is a hook that encrypts partitions using kcrypt for non uki.
|
|
// It will unmount OEM and PERSISTENT and return with them unmounted
|
|
func Encrypt(c config.Config, _ v1.Spec) error {
|
|
// Start with unmounted partitions
|
|
_ = machine.Umount(constants.OEMDir) //nolint:errcheck
|
|
_ = machine.Umount(constants.PersistentDir) //nolint:errcheck
|
|
|
|
// Config passed during install ends up here, so we need to read it, try to mount it
|
|
_ = machine.Mount("COS_OEM", "/oem")
|
|
defer func() {
|
|
err := syscall.Unmount(constants.OEMPath, 0)
|
|
if err != nil {
|
|
c.Logger.Warnf("could not unmount Oem partition: %s", err)
|
|
}
|
|
}()
|
|
|
|
for _, p := range c.Install.Encrypt {
|
|
_, err := kcrypt.Luksify(p, c.Logger)
|
|
if err != nil {
|
|
c.Logger.Errorf("could not encrypt partition: %s", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
_ = kcrypt.UnlockAllWithLogger(false, c.Logger)
|
|
|
|
for _, p := range c.Install.Encrypt {
|
|
for i := 0; i < 10; i++ {
|
|
c.Logger.Infof("Waiting for unlocked partition %s to appear", p)
|
|
_, _ = utils.SH("sync")
|
|
part, _ := utils.SH(fmt.Sprintf("blkid -L %s", p))
|
|
if part == "" {
|
|
c.Logger.Infof("Partition %s not found, waiting %d seconds before retrying", p, i)
|
|
time.Sleep(time.Duration(i) * time.Second)
|
|
// Retry the unlock as well, because maybe the partition was not refreshed on time for unlock to unlock it
|
|
// So no matter how many tries we do, it will still be locked and will never appear
|
|
err := kcrypt.UnlockAllWithLogger(false, c.Logger)
|
|
if err != nil {
|
|
c.Logger.Debugf("UnlockAll returned: %s", err)
|
|
}
|
|
if i == 9 {
|
|
c.Logger.Errorf("Partition %s not unlocked/found after 10 retries", p)
|
|
return fmt.Errorf("partition %s not unlocked/found after 10 retries", p)
|
|
}
|
|
continue
|
|
}
|
|
c.Logger.Infof("Partition found, continuing")
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
// EncryptUKI encrypts the partitions using kcrypt in uki mode
|
|
// It will unmount OEM and PERSISTENT and return with them unmounted
|
|
func EncryptUKI(c config.Config, spec v1.Spec) error {
|
|
// pre-check for systemd version, we need something higher or equal to 252
|
|
run, err := utils.SH("systemctl --version | head -1 | awk '{ print $2}'")
|
|
systemdVersion := strings.TrimSpace(string(run))
|
|
if err != nil {
|
|
c.Logger.Errorf("could not get systemd version: %s", err)
|
|
c.Logger.Errorf("could not get systemd version: %s", run)
|
|
return err
|
|
}
|
|
if systemdVersion == "" {
|
|
c.Logger.Errorf("could not get systemd version: %s", err)
|
|
return err
|
|
}
|
|
// Extract the numeric portion of the version string using a regular expression
|
|
re := regexp.MustCompile(`\d+`)
|
|
matches := re.FindString(systemdVersion)
|
|
if matches == "" {
|
|
return fmt.Errorf("could not extract numeric part from systemd version: %s", systemdVersion)
|
|
}
|
|
// Change systemdVersion to int value
|
|
systemdVersionInt, err := strconv.Atoi(systemdVersion)
|
|
if err != nil {
|
|
c.Logger.Errorf("could not convert systemd version to int: %s", err)
|
|
return err
|
|
}
|
|
// If systemd version is less than 252 return
|
|
if systemdVersionInt < 252 {
|
|
c.Logger.Infof("systemd version is %s, we need 252 or higher for encrypting partitions", systemdVersion)
|
|
return fmt.Errorf("systemd version is %s, we need 252 or higher for encrypting partitions", systemdVersion)
|
|
}
|
|
|
|
// Check for a TPM 2.0 device as its needed to encrypt
|
|
// Exposed by the kernel to userspace as /dev/tpmrm0 since kernel 4.12
|
|
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fdc915f7f71939ad5a3dda3389b8d2d7a7c5ee66
|
|
_, err = os.Stat("/dev/tpmrm0")
|
|
if err != nil {
|
|
c.Logger.Warnf("Skipping partition encryption, could not find TPM 2.0 device at /dev/tpmrm0")
|
|
return fmt.Errorf("Skipping partition encryption, could not find TPM 2.0 device at /dev/tpmrm0")
|
|
}
|
|
|
|
// We always encrypt OEM and PERSISTENT under UKI
|
|
// If mounted, unmount it
|
|
_ = machine.Umount(constants.OEMDir) //nolint:errcheck
|
|
_ = machine.Umount(constants.PersistentDir) //nolint:errcheck
|
|
|
|
// Backup oem as we already copied files on there and on luksify it will be wiped
|
|
err = machine.Mount(constants.OEMLabel, constants.OEMDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpDir, err := fsutils.TempDir(c.Fs, "", "oem-backup-xxxx")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove everything when we finish
|
|
defer c.Fs.RemoveAll(tmpDir) //nolint:errcheck
|
|
|
|
err = internalutils.SyncData(c.Logger, c.Runner, c.Fs, constants.OEMDir, tmpDir, []string{}...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = machine.Umount(constants.OEMDir) //nolint:errcheck
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, p := range append([]string{constants.OEMLabel, constants.PersistentLabel}, c.Install.Encrypt...) {
|
|
c.Logger.Infof("Encrypting %s", p)
|
|
_ = os.Setenv("SYSTEMD_LOG_LEVEL", "debug")
|
|
err = kcrypt.LuksifyMeasurements(p, c.BindPublicPCRs, c.BindPCRs, c.Logger)
|
|
_ = os.Unsetenv("SYSTEMD_LOG_LEVEL")
|
|
if err != nil {
|
|
c.Logger.Errorf("could not encrypt partition: %s", err)
|
|
return err
|
|
}
|
|
c.Logger.Infof("Done encrypting %s", p)
|
|
}
|
|
|
|
_, _ = utils.SH("sync")
|
|
|
|
_ = os.Setenv("SYSTEMD_LOG_LEVEL", "debug")
|
|
|
|
err = kcrypt.UnlockAllWithLogger(true, c.Logger)
|
|
|
|
_ = os.Unsetenv("SYSTEMD_LOG_LEVEL")
|
|
if err != nil {
|
|
lockPartitions(c)
|
|
c.Logger.Errorf("could not unlock partitions: %s", err)
|
|
return err
|
|
}
|
|
|
|
// Here it can take the oem partition a bit of time to appear after unlocking so we need to retry a couple of time with some waiting
|
|
// retry + backoff
|
|
// Check all encrypted partitions are unlocked
|
|
for _, p := range []string{constants.OEMLabel, constants.PersistentLabel} {
|
|
for i := 0; i < 10; i++ {
|
|
c.Logger.Infof("Waiting for unlocked partition %s to appear", p)
|
|
_, _ = utils.SH("sync")
|
|
part, _ := utils.SH(fmt.Sprintf("blkid -L %s", p))
|
|
if part == "" {
|
|
c.Logger.Infof("Partition %s not found, waiting %d seconds before retrying", p, i)
|
|
time.Sleep(time.Duration(i) * time.Second)
|
|
// Retry the unlock as well, because maybe the partition was not refreshed on time for unlock to unlock it
|
|
// So no matter how many tries we do, it will still be locked and will never appear
|
|
err := kcrypt.UnlockAllWithLogger(true, c.Logger)
|
|
if err != nil {
|
|
c.Logger.Debugf("UnlockAll returned: %s", err)
|
|
}
|
|
if i == 9 {
|
|
c.Logger.Errorf("Partition %s not unlocked/found after 10 retries", p)
|
|
return fmt.Errorf("partition %s not unlocked/found after 10 retries", p)
|
|
}
|
|
continue
|
|
}
|
|
c.Logger.Infof("Partition found, continuing")
|
|
break
|
|
}
|
|
}
|
|
|
|
// Mount the unlocked oem partition
|
|
err = machine.Mount(constants.OEMLabel, constants.OEMDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Copy back the contents of the oem partition that we saved before encrypting
|
|
err = internalutils.SyncData(c.Logger, c.Runner, c.Fs, tmpDir, constants.OEMDir, []string{}...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Unmount the oem partition and leave everything unmounted
|
|
err = machine.Umount(constants.OEMDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|