cmd/core_plugin/metadatasshkey/metadatasshkey_linux.go (176 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 metadatasshkey
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/GoogleCloudPlatform/galog"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/accounts"
"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/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)
var (
// googleSudoersConfig is the configuration file used for granting metadata
// ssh key users NOPASSWD sudo permission.
googleSudoersConfig = "/etc/sudoers.d/google_sudoers"
// googleSudoersGroup is the group used for granting metadata ssh key users
// NOPASSWD sudo permission. All users created by metadata ssh key are added
// to this group.
googleSudoersGroup = "google-sudoers"
// execLookPath is the function used to look up the path to an executable.
// This is overridden in tests.
execLookPath = exec.LookPath
// listGoogleUsers is a function to list google users, overridden in tests.
listGoogleUsers = accounts.ListGoogleUsers
// deprovisionUnusedUsers is a function to deprovision unused users,
// overridden in tests.
deprovisionUnusedUsers = defaultDeprovisionUnusedUsers
)
// defaultDeprovisionUnusedUsers removes accounts which were removed from ssh key
// metadata from the local system. Depending on user configuration, the account
// may not be deleted but instead have ssh keys removed.
func defaultDeprovisionUnusedUsers(ctx context.Context, config *cfg.Sections, activeUsers userKeyMap) []error {
googleUsers, err := listGoogleUsers(ctx)
if err != nil {
return []error{fmt.Errorf("could not determine which users are unused, failed to list google users: %w", err)}
}
var errs []error
for _, guser := range googleUsers {
if _, ok := activeUsers[guser]; ok || guser == "" {
continue
}
guserAccount, err := accounts.FindUser(ctx, guser)
if err != nil {
errs = append(errs, fmt.Errorf("not deprovisioning unused user %q, could not find local account: %w", guser, err))
continue
}
if config.Accounts.DeprovisionRemove {
if err := accounts.DelUser(ctx, guserAccount); err != nil {
errs = append(errs, fmt.Errorf("error removing user account %s from system: %w", guser, err))
}
continue
}
if err := updateSSHKeys(ctx, guserAccount, nil); err != nil {
errs = append(errs, fmt.Errorf("failed to remove user %s's ssh keys: %w", guser, err))
continue
}
if err := accounts.RemoveUserFromGroup(ctx, guserAccount, supplementalGroups[googleSudoersGroup]); err != nil {
errs = append(errs, fmt.Errorf("failed to remove user %s from %s: %w", guser, googleSudoersGroup, err))
continue
}
}
return errs
}
// write SSH keys to the user's $HOME/.ssh/authorized_keys file
func updateSSHKeys(ctx context.Context, user *accounts.User, keys []string) error {
gComment := "# Added by Google"
if user.HomeDir == "" {
return fmt.Errorf("user %s has no homedir set", user.Username)
}
if user.Shell == "/sbin/nologin" {
return nil
}
galog.V(2).Debugf("Updating keys for user %s to %v", user.Username, keys)
sshPath := filepath.Join(user.HomeDir, ".ssh")
if !file.Exists(sshPath, file.TypeDir) {
if err := os.Mkdir(sshPath, 0700); err != nil {
return err
}
if err := os.Chown(sshPath, user.UnixUID(), user.UnixGID()); err != nil {
return err
}
}
authorizedKeysPath := filepath.Join(sshPath, "authorized_keys")
// Remove empty file.
if len(keys) == 0 {
os.Remove(authorizedKeysPath)
return nil
}
authorizedKeysContents, err := os.ReadFile(authorizedKeysPath)
if err != nil && !os.IsNotExist(err) {
return err
}
var isGoogle bool
var userKeys []string
for _, key := range strings.Split(string(authorizedKeysContents), "\n") {
if key == "" {
continue
}
if isGoogle {
isGoogle = false
continue
}
if key == gComment {
isGoogle = true
continue
}
userKeys = append(userKeys, key)
}
authorizedKeysOutput := strings.Join(userKeys, "\n")
if len(userKeys) > 0 {
authorizedKeysOutput += "\n"
}
for _, k := range keys {
authorizedKeysOutput += fmt.Sprintf("%s\n%s\n", gComment, k)
}
writeOpts := file.Options{
Perm: 0600,
Owner: &file.GUID{
UID: user.UnixUID(),
GID: user.UnixGID(),
},
}
if err := file.SaferWriteFile(ctx, []byte(authorizedKeysOutput), authorizedKeysPath, writeOpts); err != nil {
return fmt.Errorf("failed to write authorized_keys file: %w", err)
}
return selinuxRestoreCon(ctx, authorizedKeysPath)
}
// selinuxRestoreCon restores selinux context using the restorecon binary.
func selinuxRestoreCon(ctx context.Context, path string) error {
galog.V(2).Debugf("Restoring selinux context for %s", path)
execPath, err := execLookPath("restorecon")
if err != nil {
galog.Debug("restorecon not found, skipping selinux context restore")
return nil
}
opts := run.Options{ExecMode: run.ExecModeSync, OutputType: run.OutputCombined, Name: execPath, Args: []string{path}}
if _, err := run.WithContext(ctx, opts); err != nil {
return fmt.Errorf("failed to restore selinux context: %w", err)
}
return nil
}
// ensureUserExists finds the named user, creating it locally if it doesn't
// exist. Wraps errors from accounts package.
func ensureUserExists(ctx context.Context, username string) (*accounts.User, error) {
u, err := accounts.FindUser(ctx, username)
if err == nil {
return u, nil
}
galog.V(1).Infof("User %s does not exist (lookup returned %v), creating.", username, err)
err = accounts.CreateUser(ctx, &accounts.User{Username: username, GID: "-1", UID: "-1"})
if err != nil {
return nil, fmt.Errorf("failed to create user %s: %w", username, err)
}
u, err = accounts.FindUser(ctx, username)
if err != nil {
return nil, fmt.Errorf("could not find user %s after creation: %w", username, err)
}
for _, group := range supplementalGroups {
if err := accounts.AddUserToGroup(ctx, u, group); err != nil {
galog.Errorf("Failed to add user %s to group %s: %v.", u.Username, group.Name, err)
}
}
return u, nil
}
// setPlatformConfiguration creates supplemental groups and google-sudoers if
// necessary, and writes the google-sudoers sudo configuration file.
func setPlatformConfiguration(ctx context.Context, config *cfg.Sections, _ *metadata.Descriptor) []error {
// If you are adding new configuration behavior, prefer to return early
// rather than compounding errors. The compounded errors here now are present
// to maintain existing behavior. This should be avoided in the future.
var errs []error
configline := fmt.Sprintf("%%%s ALL=(ALL:ALL) NOPASSWD:ALL", googleSudoersGroup)
if err := os.WriteFile(googleSudoersConfig, []byte(fmt.Sprintf("%s\n", configline)), 0440); err != nil {
errs = append(errs, fmt.Errorf("could not write sudo configuration for %s: %v", googleSudoersGroup, err))
}
// Legacy agent continues on error and attempts to add users to groups even
// if they might not exist, this preserves the same behavior while still
// reporting errors back to the caller.
g := &accounts.Group{Name: googleSudoersGroup}
supplementalGroups[g.Name] = g
if err := ensureGroupExists(ctx, googleSudoersGroup); err != nil {
errs = append(errs, fmt.Errorf("could not find or create %s group: %v", googleSudoersGroup, err))
}
for _, gname := range strings.Split(config.Accounts.Groups, ",") {
g := &accounts.Group{Name: gname}
supplementalGroups[g.Name] = g
if err := ensureGroupExists(ctx, gname); err != nil {
errs = append(errs, fmt.Errorf("could not find or create %s group: %v", gname, err))
}
}
return errs
}
// enableMetadataSSHKey reports whether metadata ssh keys should be managed.
func enableMetadataSSHKey(config *cfg.Sections, mdsdesc *metadata.Descriptor) bool {
// Legacy agent documentation refers to metadata ssh key functionality as
// accounts management.
if !config.Daemons.AccountsDaemon {
return false
}
return !mdsdesc.OSLoginEnabled()
}