cmd/core_plugin/metadatasshkey/metadatasshkey.go (136 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.
// Package metadatasshkey provides a module for setting up user accounts from
// ssh keys in instance and project metadata.
package metadatasshkey
import (
"context"
"fmt"
"reflect"
"strings"
"sync"
"sync/atomic"
"github.com/GoogleCloudPlatform/galog"
"github.com/GoogleCloudPlatform/google-guest-agent/cmd/core_plugin/manager"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/accounts"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/events"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/ssh"
)
// userKeyMap is a map of a username to the user's SSH keys.
type userKeyMap map[string][]string
var (
// A map of username to group to add new metadata ssh key users to.
supplementalGroups = make(map[string]*accounts.Group)
// onetimePlatformSetupFinished indicates that platform specific one-time
// system configuration has been performed successfully. On windows, this means
// starting SSHd, on linux this means creating groups and configuring sudo
// access.
onetimePlatformSetupFinished atomic.Bool
// metadataSSHKeyMu is a mutex protecting management of ssh keys. Do not
// write keys to disk or modify the following variables without holding it.
metadataSSHKeyMu sync.Mutex
// lastUserKeyMap is the last seen set of valid user keys in metadata.
lastUserKeyMap = make(userKeyMap)
// lastEnabled is the last seen value of whether metadata ssh key was
// enabled.
lastEnabled bool
)
// ensureGroupExists will check if a group exists, and create it locally if it
// doesn't.
func ensureGroupExists(ctx context.Context, gname string) error {
_, err := accounts.FindGroup(ctx, gname)
if err == nil {
return nil
}
galog.V(1).Infof("Group %s does not exist (lookup returned %v), creating.", gname, err)
return accounts.CreateGroup(ctx, gname)
}
// NewModule constructs a core_plugin module.
func NewModule(context.Context) *manager.Module {
return &manager.Module{
ID: "metadatasshkey",
Enabled: &cfg.Retrieve().Daemons.AccountsDaemon,
Description: "metadatasshkey creates local accounts from ssh keys stored in instance and project metadata",
Setup: moduleSetup,
}
}
func moduleSetup(ctx context.Context, data any) error {
desc, ok := data.(*metadata.Descriptor)
if !ok {
return fmt.Errorf("expected metadata descriptor data in moduleSetup call")
}
for _, err := range metadataSSHKeySetup(ctx, cfg.Retrieve(), desc) {
galog.Errorf("error setting initial metadatasshkey configuration: %v", err)
}
sub := events.EventSubscriber{Name: "metadatasshkey", Callback: handleMetadataChange}
events.FetchManager().Subscribe(metadata.LongpollEvent, sub)
return nil
}
func handleMetadataChange(ctx context.Context, evType string, data any, evData *events.EventData) bool {
desc, ok := evData.Data.(*metadata.Descriptor)
if !ok {
galog.Errorf("Event's data is not a metadata descriptor: %+v.", evData.Data)
return false
}
if evData.Error != nil {
galog.Debugf("Metadata event watcher reported error: %s, skiping.", evData.Error)
return true
}
for _, err := range metadataSSHKeySetup(ctx, cfg.Retrieve(), desc) {
galog.Errorf("error setting new metadatasshkey configuration after metadata change: %v", err)
}
return true
}
// metadataSSHKeySetup performs necessary configuration to setup metadata ssh
// key system requirements, create/remove users, and write ssh keys as
// necessary.
func metadataSSHKeySetup(ctx context.Context, config *cfg.Sections, desc *metadata.Descriptor) []error {
metadataSSHKeyMu.Lock()
defer metadataSSHKeyMu.Unlock()
if !metadataChanged(config, desc, lastUserKeyMap, lastEnabled) {
galog.V(2).Debugf("Metadata ssh key has no difference from enablement or keys on disk, nothing to do.")
return nil
}
enabled := enableMetadataSSHKey(config, desc)
lastEnabled = enabled
if !enabled {
galog.V(2).Infof("Accounts management is disabled or oslogin is enabled, disabling metadata ssh key.")
return deprovisionUnusedUsers(ctx, config, make(userKeyMap))
}
var errs []error
if !onetimePlatformSetupFinished.Load() {
if errs = setPlatformConfiguration(ctx, config, desc); len(errs) == 0 {
onetimePlatformSetupFinished.Store(true)
}
}
errs = append(errs, addSystemUsers(ctx, config, desc)...)
return errs
}
// addSystemUsers will create users on the local system and add keys from
// metadata to their account. Calling this function will update lastValidKeys.
func addSystemUsers(ctx context.Context, config *cfg.Sections, desc *metadata.Descriptor) []error {
newKeys := findValidKeys(desc)
var errs []error
lastUserKeyMap = newKeys
for username, keys := range newKeys {
userAccount, err := ensureUserExists(ctx, username)
if err != nil {
errs = append(errs, fmt.Errorf("giving up on ssh keys for %s, failed to find or create user: %v", username, err))
delete(newKeys, username)
continue
}
if err := updateSSHKeys(ctx, userAccount, keys); err != nil {
errs = append(errs, fmt.Errorf("failed to update SSH keys for %s: %v", userAccount.Username, err))
}
}
for _, err := range deprovisionUnusedUsers(ctx, config, newKeys) {
errs = append(errs, fmt.Errorf("error removing unused users: %v", err))
}
return errs
}
// metadataChanged reports whether the state of metadata ssh key enablement or
// keys have changed and should be reconfigured.
func metadataChanged(config *cfg.Sections, desc *metadata.Descriptor, lastValidKeys userKeyMap, lastEnabled bool) bool {
return enableMetadataSSHKey(config, desc) != lastEnabled || !reflect.DeepEqual(findValidKeys(desc), lastValidKeys)
}
func findValidKeys(desc *metadata.Descriptor) userKeyMap {
keyMap := make(userKeyMap)
keyList := desc.Instance().Attributes().SSHKeys()
if !desc.Instance().Attributes().BlockProjectKeys() {
keyList = append(keyList, desc.Project().Attributes().SSHKeys()...)
}
for _, key := range keyList {
key := strings.TrimSpace(key)
if key == "" {
continue
}
username, keycontent, err := ssh.GetUserKey(key)
if err != nil {
galog.Errorf("Incorrectly formatted key %q in metadata: %v.", key, err)
continue
}
if err := ssh.ValidateUserKey(username, keycontent); err != nil {
galog.Errorf("Invalid user %q or key %q in metadata: %v.", username, keycontent, err)
continue
}
keyMap[username] = append(keyMap[username], keycontent)
}
return keyMap
}