cmd/core_plugin/firstboot/firstboot_linux.go (133 lines of code) (raw):

// Copyright 2024 Google LLC // // 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. //go:build linux package firstboot import ( "context" "fmt" "os" "os/exec" "path" "strings" "github.com/GoogleCloudPlatform/galog" "github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg" "github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata" "github.com/GoogleCloudPlatform/google-guest-agent/internal/run" "github.com/go-ini/ini" ) const ( // hostKeyFilePrefix is the common/known prefix of the host key file name. hostKeyFilePrefix = "ssh_host_" // hostKeyFileSuffix is the common/known suffix of the host key file name. hostKeyFileSuffix = "_key" ) var ( // botoConfigFile is the path to the boto config file. botoConfigFile = "/etc/boto.cfg" ) // platformSetup runs the actual firstboot setup for linux. func platformSetup(ctx context.Context, projectID string, config *cfg.Sections) error { // Generate host SSH keys and upload them to guest attributes. if err := writeSSHKeys(ctx, config.InstanceSetup); err != nil { return err } // Write the boto config file. if err := writeBotoConfig(projectID); err != nil { return err } return nil } // writeBotoConfig overwrites the boto config file with the provided project id, // sets the default_api_version to 2, and sets the service_account to default. func writeBotoConfig(projectID string) error { templatePath := botoConfigFile + ".template" botoCfg, err := ini.LooseLoad(botoConfigFile, templatePath) if err != nil { return fmt.Errorf("failed to load boto config: %w", err) } botoCfg.Section("GSUtil").Key("default_project_id").SetValue(projectID) botoCfg.Section("GSUtil").Key("default_api_version").SetValue("2") botoCfg.Section("GoogleCompute").Key("service_account").SetValue("default") if err := botoCfg.SaveTo(botoConfigFile); err != nil { return fmt.Errorf("failed to save boto config: %w", err) } return nil } // writeSSHKeys generates host SSH keys and uploads them to guest attributes. func writeSSHKeys(ctx context.Context, instanceSetup *cfg.InstanceSetup) error { if instanceSetup == nil { galog.V(2).Debug("No instance setup config, skipping SSH key generation") return nil } hostKeyDir := instanceSetup.HostKeyDir dir, err := os.Open(hostKeyDir) if err != nil { return fmt.Errorf("failed to open host key dir: %w", err) } defer dir.Close() files, err := dir.Readdirnames(0) if err != nil { return fmt.Errorf("failed to read host key dir: %w", err) } keytypes := make(map[string]bool) // Find keys present on disk, and deduce their type from filename. for _, file := range files { if !hostKeyFile(file) { galog.V(2).Debugf("Skipping file %q, not a key file", file) continue } keytype := file keytype = strings.TrimPrefix(keytype, hostKeyFilePrefix) keytype = strings.TrimSuffix(keytype, hostKeyFileSuffix) keytypes[keytype] = true } // List keys we should generate, according to the config. configKeys := instanceSetup.HostKeyTypes for _, keytype := range strings.Split(configKeys, ",") { keytypes[keytype] = true } client := metadata.New() // Generate new keys and upload to guest attributes. for keytype := range keytypes { keyfile := path.Join(hostKeyDir, fmt.Sprintf("%s_%s_%s", hostKeyFilePrefix, keytype, hostKeyFileSuffix)) pubKeyFile := keyfile + ".pub" tmpKeyFile := keyfile + ".temp" tmpPubKeyFile := keyfile + ".temp.pub" cmd := []string{"ssh-keygen", "-t", keytype, "-f", tmpKeyFile, "-N", "", "-q"} opts := run.Options{Name: cmd[0], Args: cmd[1:], OutputType: run.OutputNone} if _, err := run.WithContext(ctx, opts); err != nil { galog.Warnf("Failed to generate SSH host key %q: %v", keyfile, err) continue } if err := os.Chmod(tmpKeyFile, 0600); err != nil { galog.Errorf("Failed to chmod SSH host key %q: %v", tmpKeyFile, err) continue } if err := os.Chmod(tmpPubKeyFile, 0644); err != nil { galog.Errorf("Failed to chmod SSH host key %q: %v", tmpPubKeyFile, err) continue } if err := os.Rename(tmpKeyFile, keyfile); err != nil { galog.Errorf("Failed to overwrite %q: %v", keyfile, err) continue } if err := os.Rename(tmpPubKeyFile, pubKeyFile); err != nil { galog.Errorf("Failed to overwrite %q: %v", keyfile+".pub", err) continue } pubKey, err := os.ReadFile(pubKeyFile) if err != nil { galog.Errorf("Can't read %s public key: %v", keytype, err) continue } vals := strings.Split(string(pubKey), " ") if len(vals) < 2 { galog.Warnf("Generated key(%q) is malformed, not uploading", keytype) continue } if err := client.WriteGuestAttributes(ctx, "hostkeys/"+vals[0], vals[1]); err != nil { galog.Errorf("Failed to upload %s key to guest attributes: %v", keytype, err) } } _, err = exec.LookPath("restorecon") if err != nil { galog.Infof("restorecon not found, skipping SELinux context restoration") return nil } cmd := []string{"restorecon", "-FR", hostKeyDir} opts := run.Options{Name: cmd[0], Args: cmd[1:], OutputType: run.OutputNone} if _, err := run.WithContext(ctx, opts); err != nil { return fmt.Errorf("failed to restore SELinux context for: %s, %w", hostKeyDir, err) } return nil } // hostKeyFile returns true if the file name matches the pattern of a host key // file. func hostKeyFile(fName string) bool { return strings.HasPrefix(fName, hostKeyFilePrefix) && strings.HasSuffix(fName, hostKeyFileSuffix) && len(fName) > len(hostKeyFilePrefix+hostKeyFileSuffix) }