diff --git a/go.mod b/go.mod index 8e016982..c53369f1 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/jaypipes/ghw v0.20.0 github.com/joho/godotenv v1.5.1 github.com/kairos-io/go-nodepair v0.3.0 - github.com/kairos-io/kairos-sdk v0.12.1 + github.com/kairos-io/kairos-sdk v0.12.1-0.20251106060709-87c4533e1d90 github.com/labstack/echo/v4 v4.13.4 github.com/mitchellh/mapstructure v1.5.0 github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5 @@ -109,12 +109,18 @@ require ( github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/certificate-transparency-go v1.1.4 // indirect + github.com/google/go-attestation v0.5.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-tpm v0.9.6 // indirect + github.com/google/go-tpm-tools v0.4.4 // indirect + github.com/google/go-tspi v0.3.0 // indirect github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -124,6 +130,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jezek/xgb v1.1.0 // indirect github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect + github.com/kairos-io/tpm-helpers v0.0.0-20251103104631-9d4298b86881 // indirect github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 // indirect github.com/kendru/darwin/go/depgraph v0.0.0-20230809052043-4d1c7e9d1767 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect diff --git a/go.sum b/go.sum index 05ce8fd7..b3bae9be 100644 --- a/go.sum +++ b/go.sum @@ -232,6 +232,11 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY= +github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ= +github.com/google/go-attestation v0.5.1 h1:jqtOrLk5MNdliTKjPbIPrAaRKJaKW+0LIU2n/brJYms= +github.com/google/go-attestation v0.5.1/go.mod h1:KqGatdUhg5kPFkokyzSBDxwSCFyRgIgtRkMp6c3lOBQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -243,12 +248,26 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98= +github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/go-github/v77 v77.0.0 h1:9DsKKbZqil5y/4Z9mNpZDQnpli6PJbqipSuuNdcbjwI= github.com/google/go-github/v77 v77.0.0/go.mod h1:c8VmGXRUmaZUqbctUcGEDWYnMrtzZfJhDSylEf1wfmA= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-sev-guest v0.9.3 h1:GOJ+EipURdeWFl/YYdgcCxyPeMgQUWlI056iFkBD8UU= +github.com/google/go-sev-guest v0.9.3/go.mod h1:hc1R4R6f8+NcJwITs0L90fYWTsBpd1Ix+Gur15sqHDs= +github.com/google/go-tdx-guest v0.3.1 h1:gl0KvjdsD4RrJzyLefDOvFOUH3NAJri/3qvaL5m83Iw= +github.com/google/go-tdx-guest v0.3.1/go.mod h1:/rc3d7rnPykOPuY8U9saMyEps0PZDThLk/RygXm04nE= +github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= +github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= +github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= +github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= +github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= +github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= +github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -260,6 +279,9 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -297,8 +319,10 @@ github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c= github.com/kairos-io/go-nodepair v0.3.0 h1:JIMBAtbNhIAsx89aP61mQDGMuGFoIQH/woK2tMDYD6k= github.com/kairos-io/go-nodepair v0.3.0/go.mod h1:7i905W/KmR9DAcMSVJr/Wdb84E5Yyu9YLgj7chwX1xs= -github.com/kairos-io/kairos-sdk v0.12.1 h1:gqAvBgXCSvf44VgoM5n+0ezVglcgqgdOhwSo1WbBAnA= -github.com/kairos-io/kairos-sdk v0.12.1/go.mod h1:IA8+4pheh0sCcfev6jjGOXTJ/AZ3pPJz2xeanD9pjFI= +github.com/kairos-io/kairos-sdk v0.12.1-0.20251106060709-87c4533e1d90 h1:J6N3v51GMO4dUJXHuJlGQWHByGI7ZJZws8eIOIgqDkI= +github.com/kairos-io/kairos-sdk v0.12.1-0.20251106060709-87c4533e1d90/go.mod h1:39AhCp9dUQBfdkOQMTCegTo2Q5GUJR1//gPXo3r+9AA= +github.com/kairos-io/tpm-helpers v0.0.0-20251103104631-9d4298b86881 h1:teAOOtMYhlPz7Q0DXTih8ZOVgPctyUz86PplTi1K29g= +github.com/kairos-io/tpm-helpers v0.0.0-20251103104631-9d4298b86881/go.mod h1:zOtMRZ4zGpy+qL6W1N0LrczM/9T0sdoaJ1K5PgfchAs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 h1:YOp8St+CM/AQ9Vp4XYm4272E77MptJDHkwypQHIRl9Q= @@ -418,6 +442,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs= diff --git a/internal/agent/hooks/encrypt.go b/internal/agent/hooks/encrypt.go new file mode 100644 index 00000000..a83adc28 --- /dev/null +++ b/internal/agent/hooks/encrypt.go @@ -0,0 +1,361 @@ +package hook + +import ( + "context" + "fmt" + "os/exec" + "slices" + "strings" + "time" + + "github.com/kairos-io/kairos-agent/v2/pkg/config" + "github.com/kairos-io/kairos-agent/v2/pkg/constants" + 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/kcrypt" + "github.com/kairos-io/kairos-sdk/machine" + "github.com/kairos-io/kairos-sdk/types" + "github.com/kairos-io/kairos-sdk/utils" +) + +// Encrypt is the unified encryption method that works for both UKI and non-UKI modes +func Encrypt(c config.Config) error { + c.Logger.Logger.Info().Msg("Starting unified encryption flow") + + partitions := determinePartitionsToEncrypt(c) + if len(partitions) == 0 { + c.Logger.Logger.Info().Msg("No partitions to encrypt") + return nil + } + c.Logger.Logger.Info().Strs("partitions", partitions).Msg("Partitions to encrypt") + + // Get the appropriate encryptor based on configuration + // IMPORTANT: Do this BEFORE unmounting partitions so the encryptor can scan + // for kcrypt configuration in /run/cos/oem + encryptor, err := kcrypt.GetEncryptor(c.Logger) + if err != nil { + return fmt.Errorf("failed to get encryptor: %w", err) + } + c.Logger.Logger.Info().Str("method", encryptor.Name()).Msg("Using encryption method") + + c.Logger.Logger.Info().Msg("Settling udev") + if err := udevAdmSettle(c.Logger, 15*time.Second); err != nil { + return fmt.Errorf("ERROR settling udev: %w", err) + } + + // Backup OEM if it's in the list (before unmounting!) + var oemBackupPath string + var cleanupBackup func() + needsOEMBackup := slices.Contains(partitions, constants.OEMLabel) + if needsOEMBackup { + var err error + oemBackupPath, cleanupBackup, err = backupOEMIfNeeded(c) + if err != nil { + return fmt.Errorf("failed to backup OEM: %w", err) + } + defer cleanupBackup() + } + + // Prepare partitions (unmount them) + if err := preparePartitionsForEncryption(c, partitions); err != nil { + return fmt.Errorf("failed to prepare partitions: %w", err) + } + + if err := encryptor.Encrypt(partitions); err != nil { + return fmt.Errorf("failed to encrypt partitions: %w", err) + } + + // The Unlock method will wait for partitions to be ready before returning + if err := encryptor.Unlock(partitions); err != nil { + // Lock partitions on failure (cleanup) + lockPartitions(c.Logger) + return fmt.Errorf("failed to unlock partitions: %w", err) + } + + // Restore OEM if needed + if needsOEMBackup { + if err := restoreOEM(c, oemBackupPath); err != nil { + return fmt.Errorf("failed to restore OEM: %w", err) + } + } + + c.Logger.Logger.Info().Msg("Finished unified encryption flow") + return nil +} + +// Helper methods for unified encryption flow + +// determinePartitionsToEncrypt returns the list of partitions to encrypt based on mode +func determinePartitionsToEncrypt(c config.Config) []string { + // If user has specified partitions, respect their preference + if len(c.Install.Encrypt) > 0 { + return c.Install.Encrypt + } + + // No user-specified partitions + if internalutils.IsUki() { + // UKI mode: encrypt OEM and PERSISTENT by default + return []string{constants.OEMLabel, constants.PersistentLabel} + } + + // Non-UKI mode with no user-specified partitions: don't encrypt anything + return []string{} +} + +// preparePartitionsForEncryption unmounts all partitions that will be encrypted +func preparePartitionsForEncryption(c config.Config, partitions []string) error { + for _, p := range partitions { + c.Logger.Logger.Info().Str("partition", p).Msg("Preparing to encrypt partition") + + // Unmount the partition before encrypting it + // Find the device path for this partition label + devPath, err := utils.SH(fmt.Sprintf("blkid -L %s", p)) + if err != nil { + c.Logger.Logger.Warn().Str("label", p).Err(err).Msg("Could not find device for label") + } else { + devPath = strings.TrimSpace(devPath) + c.Logger.Logger.Info().Str("device", devPath).Str("label", p).Msg("Found device for label") + + // Find all mount points for this device and unmount them + mountPoints, _ := utils.SH(fmt.Sprintf("findmnt -n -o TARGET -S %s", devPath)) + if mountPoints != "" { + for _, mp := range strings.Split(strings.TrimSpace(mountPoints), "\n") { + if mp != "" { + c.Logger.Logger.Info().Str("device", devPath).Str("mountpoint", mp).Msg("Unmounting partition") + if err := machine.Umount(mp); err != nil { + c.Logger.Logger.Warn().Str("mountpoint", mp).Err(err).Msg("Could not unmount") + } + } + } + } + } + } + return nil +} + +// backupOEMIfNeeded backs up the OEM partition contents before encryption +func backupOEMIfNeeded(c config.Config) (backupPath string, cleanup func(), err error) { + c.Logger.Logger.Info().Msg("Backing up OEM partition before encryption") + + // Check if OEM is already mounted + _, err = utils.SH(fmt.Sprintf("findmnt %s", constants.OEMDir)) + oemAlreadyMounted := (err == nil) + + // Mount OEM partition if not already mounted + if !oemAlreadyMounted { + c.Logger.Logger.Info().Msg("Mounting OEM partition for backup") + err = machine.Mount(constants.OEMLabel, constants.OEMDir) + if err != nil { + return "", nil, fmt.Errorf("failed to mount OEM for backup: %w", err) + } + } else { + c.Logger.Logger.Info().Msg("OEM already mounted, using existing mount") + } + + // Create temporary directory for backup + tmpDir, err := fsutils.TempDir(c.Fs, "", "oem-backup-xxxx") + if err != nil { + if !oemAlreadyMounted { + machine.Umount(constants.OEMDir) //nolint:errcheck + } + return "", nil, fmt.Errorf("failed to create temp dir for OEM backup: %w", err) + } + + // Sync OEM data to temp directory + err = internalutils.SyncData(c.Logger, c.Runner, c.Fs, constants.OEMDir, tmpDir, []string{}...) + if err != nil { + c.Fs.RemoveAll(tmpDir) //nolint:errcheck + if !oemAlreadyMounted { + machine.Umount(constants.OEMDir) //nolint:errcheck + } + return "", nil, fmt.Errorf("failed to sync OEM data: %w", err) + } + + // Unmount OEM (it will be unmounted again by preparePartitionsForEncryption, but that's ok) + err = machine.Umount(constants.OEMDir) //nolint:errcheck + if err != nil { + c.Fs.RemoveAll(tmpDir) //nolint:errcheck + return "", nil, fmt.Errorf("failed to unmount OEM after backup: %w", err) + } + + // Return cleanup function that removes the temp directory + cleanup = func() { + c.Logger.Logger.Info().Str("path", tmpDir).Msg("Cleaning up OEM backup") + c.Fs.RemoveAll(tmpDir) //nolint:errcheck + } + + c.Logger.Logger.Info().Str("backup_path", tmpDir).Msg("OEM backup completed") + return tmpDir, cleanup, nil +} + +// findMapperDeviceForPartition finds an unlocked dm-crypt mapper device for a partition label. +// It first finds the underlying partition device using the label, then checks if a mapper +// device exists for that partition in dmsetup. +// Returns the full path to the mapper device (e.g., /dev/mapper/vda2). +func findMapperDeviceForPartition(c config.Config, label string) (string, error) { + // First, find the underlying encrypted partition device by its label + // This will return the LUKS container device (e.g., /dev/vda2) + partitionPath, err := utils.SH(fmt.Sprintf("blkid -L %s", label)) + if err != nil { + return "", fmt.Errorf("failed to find partition with label %s: %w", label, err) + } + partitionPath = strings.TrimSpace(partitionPath) + c.Logger.Logger.Debug().Str("partition", partitionPath).Str("label", label).Msg("Found encrypted partition") + + // Extract the base device name (e.g., "vda2" from "/dev/vda2" or "/dev/mapper/vda2") + // After encryption, blkid might return the mapper device instead of the underlying device + baseName := strings.TrimPrefix(partitionPath, "/dev/mapper/") + baseName = strings.TrimPrefix(baseName, "/dev/") + mapperPath := fmt.Sprintf("/dev/mapper/%s", baseName) + + // Get list of active encrypted mapper devices to verify it's unlocked + dmOutput, err := utils.SH("dmsetup ls --target crypt") + if err != nil { + return "", fmt.Errorf("failed to list dm-crypt devices: %w", err) + } + + c.Logger.Logger.Info(). + Str("dmsetup_output", strings.TrimSpace(dmOutput)). + Str("looking_for", baseName). + Str("partition_path", partitionPath). + Str("label", label). + Msg("Checking dmsetup for mapper device") + + // Check if our mapper device is in the list of active devices + mapperExists := false + lines := strings.Split(strings.TrimSpace(dmOutput), "\n") + for _, line := range lines { + if line == "" || strings.Contains(line, "No devices found") { + continue + } + + // Extract mapper name (first field before whitespace) + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + mapperName := fields[0] + + c.Logger.Logger.Info(). + Str("line", line). + Str("mapper_name", mapperName). + Str("looking_for", baseName). + Bool("matches", mapperName == baseName). + Msg("Comparing mapper names") + + if mapperName == baseName { + mapperExists = true + c.Logger.Logger.Info().Str("mapper", mapperPath).Str("partition", baseName).Msg("Found unlocked mapper device") + break + } + } + + if !mapperExists { + return "", fmt.Errorf("partition %s (label: %s) is not unlocked - mapper device %s not found in dmsetup", partitionPath, label, baseName) + } + + return mapperPath, nil +} + +// restoreOEM restores the OEM partition contents after encryption. +// This function assumes: +// - COS_OEM partition is encrypted +// - COS_OEM has been unlocked (mapper device exists in dmsetup) +// - We need to find the mapper device path to mount and restore data +func restoreOEM(c config.Config, backupPath string) error { + c.Logger.Logger.Info().Str("backup_path", backupPath).Msg("Restoring OEM partition from backup") + + // Find the unlocked mapper device for COS_OEM + // We find the underlying partition by label, then check if it's unlocked in dmsetup + devicePath, err := findMapperDeviceForPartition(c, constants.OEMLabel) + if err != nil { + return fmt.Errorf("failed to find unlocked OEM mapper device: %w", err) + } + + c.Logger.Logger.Info().Str("device", devicePath).Msg("Found unlocked OEM mapper device") + + c.Logger.Logger.Info().Str("device", devicePath).Msg("Waiting for udev to create device node") + if err := udevAdmSettle(c.Logger, 15*time.Second); err != nil { + return fmt.Errorf("failed settling udev: %w", err) + } + + // Create mount point if it doesn't exist + if err := fsutils.MkdirAll(c.Fs, constants.OEMDir, 0755); err != nil { + return fmt.Errorf("failed to create mount point: %w", err) + } + + // Mount the unlocked device + c.Logger.Logger.Info().Str("device", devicePath).Str("mountpoint", constants.OEMDir).Msg("Mounting OEM partition") + + // First check what filesystem is on the device + fsType, _ := utils.SH(fmt.Sprintf("blkid -s TYPE -o value %s", devicePath)) + c.Logger.Logger.Info().Str("device", devicePath).Str("fs_type", strings.TrimSpace(fsType)).Msg("Filesystem type on mapper device") + + mountOut, err := utils.SH(fmt.Sprintf("mount %s %s", devicePath, constants.OEMDir)) + if err != nil { + c.Logger.Logger.Error().Str("mount_output", mountOut).Str("device", devicePath).Msg("Mount failed") + return fmt.Errorf("failed to mount OEM for restore: %w (output: %s)", err, mountOut) + } + + // Copy back the contents of the OEM partition that we saved before encrypting + err = internalutils.SyncData(c.Logger, c.Runner, c.Fs, backupPath, constants.OEMDir, []string{}...) + if err != nil { + machine.Umount(constants.OEMDir) //nolint:errcheck + return fmt.Errorf("failed to restore OEM data: %w", err) + } + + // Unmount the OEM partition and leave everything unmounted + err = machine.Umount(constants.OEMDir) + if err != nil { + return fmt.Errorf("failed to unmount OEM after restore: %w", err) + } + + c.Logger.Logger.Info().Msg("OEM partition restored successfully") + return nil +} + +// udevAdmSettle triggers udev events, waits for them to complete, +// and adds basic debugging / diagnostics around the device state. +func udevAdmSettle(logger types.KairosLogger, timeout time.Duration) error { + logger.Logger.Info().Msg("Triggering udev events") + + // Trigger subsystems and devices (this replays all udev rules) + triggerCmds := [][]string{ + {"udevadm", "trigger", "--action=add", "--type=subsystems"}, + {"udevadm", "trigger", "--action=add", "--type=devices"}, + } + + for _, args := range triggerCmds { + cmd := exec.Command(args[0], args[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s failed: %v (output: %s)", args, err, string(output)) + } + } + + logger.Logger.Info().Msg("Flushing filesystem buffers (sync)") + if err := exec.Command("sync").Run(); err != nil { + logger.Logger.Warn().Err(err).Msg("sync failed") + } + + logger.Logger.Info().Dur("timeout", timeout).Msg("Waiting for udev to settle") + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, "udevadm", "settle") + output, err := cmd.CombinedOutput() + + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("udevadm settle timed out after %s", timeout) + } + if err != nil { + return fmt.Errorf("udevadm settle failed: %v (output: %s)", err, string(output)) + } + + logger.Logger.Info().Msg("udevadm settle completed successfully") + + // give the kernel a moment to release device locks. + time.Sleep(2 * time.Second) + + return nil +} diff --git a/internal/agent/hooks/finish.go b/internal/agent/hooks/finish.go index 6aa0ff6f..62bee383 100644 --- a/internal/agent/hooks/finish.go +++ b/internal/agent/hooks/finish.go @@ -1,21 +1,8 @@ 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/kcrypt" - "github.com/kairos-io/kairos-sdk/machine" - "github.com/kairos-io/kairos-sdk/utils" - "os" - "regexp" - "strconv" - "strings" - "syscall" - "time" ) // Finish is a hook that runs after the install process. @@ -24,23 +11,13 @@ 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") + // Run encryption (handles both UKI and non-UKI, returns early if nothing to encrypt) + err = Encrypt(c) + defer lockPartitions(c.Logger) // partitions are unlocked, make sure to lock them before we end + if err != nil { + c.Logger.Logger.Error().Err(err).Msg("could not encrypt partitions") + return err } // Now that we have everything encrypted and ready to mount if needed @@ -66,198 +43,3 @@ func (k Finish) Run(c config.Config, spec v1.Spec) error { } 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.Encrypt(p, c.Logger) - if err != nil { - c.Logger.Errorf("could not encrypt partition: %s", err) - return err - } - } - - _ = kcrypt.UnlockAll(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.UnlockAll(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.EncryptWithPcrs(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.UnlockAll(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.UnlockAll(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 -} diff --git a/internal/agent/hooks/gruboptions.go b/internal/agent/hooks/gruboptions.go index a73b619f..420a2131 100644 --- a/internal/agent/hooks/gruboptions.go +++ b/internal/agent/hooks/gruboptions.go @@ -1,24 +1,75 @@ package hook import ( + "fmt" + "strings" + + "path/filepath" + "github.com/kairos-io/kairos-agent/v2/pkg/config" cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" "github.com/kairos-io/kairos-agent/v2/pkg/utils" + "github.com/kairos-io/kairos-sdk/collector" "github.com/kairos-io/kairos-sdk/machine" - "path/filepath" ) // GrubPostInstallOptions is a hook that runs after the install process to add grub options. type GrubPostInstallOptions struct{} func (b GrubPostInstallOptions) Run(c config.Config, _ v1.Spec) error { - if len(c.Install.GrubOptions) == 0 { + if utils.IsUki() { + c.Logger.Logger.Info().Msg("Skipping GrubPostInstallOptions hook in uki mode") + return nil + } + + // Combine regular grub options with extracted kcrypt options + grubOpts := make(map[string]string) + + // Copy existing grub options + for k, v := range c.Install.GrubOptions { + grubOpts[k] = v + } + + // Check if COS_OEM is in the list of encrypted partitions + oemEncrypted := false + if len(c.Install.Encrypt) > 0 { + for _, part := range c.Install.Encrypt { + if part == cnst.OEMLabel { + oemEncrypted = true + break + } + } + } + + // Extract and add kcrypt.challenger settings to cmdline if COS_OEM is encrypted + // This solves the chicken-egg problem where kcrypt config is on the encrypted OEM partition + // Only works in non-UKI case. For UKI, it's up to the user to add the challenger + // server url in the cmdline when creating the signed artifact (cmdline is also signed) + // TODO: There are more things written in the OEM partition to make boot assessment + // and next-boot selection work. These are not handled at all right now. We should + // fix those before we officially support OEM encryption. + if oemEncrypted { + c.Logger.Logger.Info().Msg("COS_OEM is encrypted, extracting kcrypt.challenger config to cmdline") + kcryptCmdline := extractKcryptCmdline(&c) + if kcryptCmdline != "" { + // Append to extra_cmdline + if existing, ok := grubOpts["extra_cmdline"]; ok { + grubOpts["extra_cmdline"] = existing + " " + kcryptCmdline + } else { + grubOpts["extra_cmdline"] = kcryptCmdline + } + c.Logger.Logger.Info().Str("kcrypt_cmdline", kcryptCmdline).Msg("Added kcrypt config to cmdline") + } + } + + if len(grubOpts) == 0 { return nil } + c.Logger.Logger.Info().Msg("Running GrubOptions hook") - c.Logger.Debugf("Setting grub options: %s", c.Install.GrubOptions) - err := grubOptions(c, c.Install.GrubOptions) + c.Logger.Debugf("Setting grub options: %s", grubOpts) + err := grubOptions(c, grubOpts, oemEncrypted) if err != nil { return err } @@ -35,7 +86,8 @@ func (b GrubFirstBootOptions) Run(c config.Config, _ v1.Spec) error { } c.Logger.Logger.Info().Msg("Running GrubOptions hook") c.Logger.Debugf("Setting grub options: %s", c.GrubOptions) - err := grubOptions(c, c.GrubOptions) + // At first boot, we don't know if OEM is encrypted, so write to both as a fallback + err := grubOptions(c, c.GrubOptions, false) if err != nil { return err } @@ -44,22 +96,112 @@ func (b GrubFirstBootOptions) Run(c config.Config, _ v1.Spec) error { } // grubOptions sets the grub options in the grubenv file -// It mounts the OEM partition if not already mounted -// If its mounted but RO, it remounts it as RW -func grubOptions(c config.Config, opts map[string]string) error { - _ = machine.Umount(cnst.OEMDir) - _ = machine.Umount(cnst.OEMPath) - - c.Logger.Logger.Debug().Msg("Mounting OEM partition") - _ = machine.Mount(cnst.OEMLabel, cnst.OEMPath) +// It ALWAYS writes to STATE partition (grubenv) as that's what GRUB reads +// It optionally writes to OEM partition (grub_oem_env) only if OEM is not encrypted +func grubOptions(c config.Config, opts map[string]string, oemEncrypted bool) error { + var firstErr error + + // Always write to STATE partition (grubenv) - this is what GRUB reads during boot + _ = machine.Umount(cnst.StateDir) + c.Logger.Logger.Debug().Msg("Mounting STATE partition") + _ = machine.Mount(cnst.StateLabel, cnst.StateDir) defer func() { - c.Logger.Logger.Debug().Msg("Unmounting OEM partition") - _ = machine.Umount(cnst.OEMPath) + c.Logger.Logger.Debug().Msg("Unmounting STATE partition") + _ = machine.Umount(cnst.StateDir) }() - err := utils.SetPersistentVariables(filepath.Join(cnst.OEMPath, "grubenv"), opts, &c) + err := utils.SetPersistentVariables(filepath.Join(cnst.StateDir, cnst.GrubEnv), opts, &c) if err != nil { - c.Logger.Logger.Error().Err(err).Str("grubfile", filepath.Join(cnst.OEMPath, "grubenv")).Msg("Failed to set grub options") + c.Logger.Logger.Error().Err(err).Str("grubfile", filepath.Join(cnst.StateDir, cnst.GrubEnv)).Msg("Failed to set grub options in STATE") + firstErr = err + } else { + c.Logger.Logger.Info().Str("grubfile", filepath.Join(cnst.StateDir, cnst.GrubEnv)).Msg("Successfully set grub options in STATE") } - return err + + // Only write to OEM if it's not encrypted (GRUB can't read it if encrypted anyway) + if !oemEncrypted { + _ = machine.Umount(cnst.OEMDir) + _ = machine.Umount(cnst.OEMPath) + + c.Logger.Logger.Debug().Msg("Mounting OEM partition") + _ = machine.Mount(cnst.OEMLabel, cnst.OEMPath) + + err = utils.SetPersistentVariables(filepath.Join(cnst.OEMPath, cnst.GrubOEMEnv), opts, &c) + if err != nil { + c.Logger.Logger.Warn().Err(err).Str("grubfile", filepath.Join(cnst.OEMPath, cnst.GrubOEMEnv)).Msg("Failed to set grub options in OEM (non-critical)") + } else { + c.Logger.Logger.Info().Str("grubfile", filepath.Join(cnst.OEMPath, cnst.GrubOEMEnv)).Msg("Successfully set grub options in OEM") + } + + _ = machine.Umount(cnst.OEMPath) + } else { + c.Logger.Logger.Info().Msg("Skipping OEM grubenv write as OEM partition is encrypted") + } + + return firstErr +} + +// extractKcryptCmdline extracts kcrypt.challenger config from the Kairos config and +// formats it as kernel command line arguments for use in grub. +// This allows kcrypt-challenger to access KMS settings even when COS_OEM is encrypted. +func extractKcryptCmdline(c *config.Config) string { + var cmdlineArgs []string + + // Access the generic config values map to get kcrypt settings + if c.Config.Values == nil { + return "" + } + + kcryptVal, hasKcrypt := c.Config.Values["kcrypt"] + if !hasKcrypt { + return "" + } + + // Type assert to access nested structure + kcryptMap, ok := kcryptVal.(collector.ConfigValues) + if !ok { + c.Logger.Logger.Debug().Msg("kcrypt config is not in expected format") + return "" + } + + challengerVal, hasChallengerKey := kcryptMap["challenger"] + if !hasChallengerKey { + return "" + } + + challengerMap, ok := challengerVal.(collector.ConfigValues) + if !ok { + c.Logger.Logger.Debug().Msg("kcrypt.challenger config is not in expected format") + return "" + } + + // Extract individual settings and add as cmdline parameters + // Using kcrypt.challenger.* prefix to match the expected config structure + + if server, ok := challengerMap["challenger_server"].(string); ok && server != "" { + // URL encode any special characters in the server URL + cmdlineArgs = append(cmdlineArgs, fmt.Sprintf("kcrypt.challenger.challenger_server=%s", server)) + } + + if mdns, ok := challengerMap["mdns"].(bool); ok && mdns { + cmdlineArgs = append(cmdlineArgs, "kcrypt.challenger.mdns=true") + } + + if cert, ok := challengerMap["certificate"].(string); ok && cert != "" { + cmdlineArgs = append(cmdlineArgs, fmt.Sprintf("kcrypt.challenger.certificate=%s", cert)) + } + + if nvIndex, ok := challengerMap["nv_index"].(string); ok && nvIndex != "" { + cmdlineArgs = append(cmdlineArgs, fmt.Sprintf("kcrypt.challenger.nv_index=%s", nvIndex)) + } + + if cIndex, ok := challengerMap["c_index"].(string); ok && cIndex != "" { + cmdlineArgs = append(cmdlineArgs, fmt.Sprintf("kcrypt.challenger.c_index=%s", cIndex)) + } + + if tpmDevice, ok := challengerMap["tpm_device"].(string); ok && tpmDevice != "" { + cmdlineArgs = append(cmdlineArgs, fmt.Sprintf("kcrypt.challenger.tpm_device=%s", tpmDevice)) + } + + return strings.Join(cmdlineArgs, " ") } diff --git a/internal/agent/hooks/hook.go b/internal/agent/hooks/hook.go index b4afaa5d..804e7103 100644 --- a/internal/agent/hooks/hook.go +++ b/internal/agent/hooks/hook.go @@ -2,10 +2,12 @@ package hook import ( "fmt" + "strings" + "github.com/kairos-io/kairos-agent/v2/pkg/config" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" + "github.com/kairos-io/kairos-sdk/types" "github.com/kairos-io/kairos-sdk/utils" - "strings" ) type Interface interface { @@ -60,16 +62,37 @@ func Run(c config.Config, spec v1.Spec, hooks ...Interface) error { } // lockPartitions will try to close all the partitions that are unencrypted. -func lockPartitions(c config.Config) { - for _, p := range c.Install.Encrypt { - _, _ = utils.SH("udevadm trigger --type=all || udevadm trigger") - c.Logger.Debugf("Closing unencrypted /dev/disk/by-label/%s", p) - out, err := utils.SH(fmt.Sprintf("cryptsetup close /dev/disk/by-label/%s", p)) +func lockPartitions(log types.KairosLogger) { + _, _ = utils.SH("udevadm trigger --type=all || udevadm trigger") + + // Get list of active mapper devices + dmOutput, err := utils.SH("dmsetup ls --target crypt") + if err != nil { + log.Debugf("could not list dm devices: %v", err) + return + } + + // Parse dmsetup output (format: "vda2 (252:1)") + lines := strings.Split(strings.TrimSpace(dmOutput), "\n") + for _, line := range lines { + if line == "" || strings.Contains(line, "No devices found") { + continue + } + + // Extract mapper name (first field before whitespace) + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + mapperName := fields[0] + + log.Debugf("Closing encrypted device: %s", mapperName) + out, err := utils.SH(fmt.Sprintf("cryptsetup close %s", mapperName)) // There is a known error with cryptsetup that it can't close the device because of a semaphore // doesnt seem to affect anything as the device is closed as expected so we ignore it if it matches the // output of the error if err != nil && !strings.Contains(out, "incorrect semaphore state") { - c.Logger.Debugf("could not close /dev/disk/by-label/%s: %s", p, out) + log.Debugf("could not close %s: %s", mapperName, out) } } } diff --git a/pkg/action/install.go b/pkg/action/install.go index 46c57fde..1e9d3a74 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -204,11 +204,11 @@ func (i InstallAction) Run() (err error) { // Create extra dirs in rootfs as afterwards this will be impossible due to RO system createExtraDirsInRootfs(i.cfg, i.spec.ExtraDirsRootfs, i.spec.Active.MountPoint) - // Copy cloud-init if any - err = e.CopyCloudConfig(i.spec.CloudInit) - if err != nil { + // Copy cloud-config if any + if err = e.CopyCloudConfig(i.spec.CloudInit); err != nil { return err } + // Install grub grub := utils.NewGrub(i.cfg) err = grub.Install(