cmd/core_plugin/metadatasshkey/metadatasshkey_windows.go (123 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 windows
package metadatasshkey
import (
"context"
"fmt"
"strconv"
"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/reg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/run"
)
const (
// Minimum major version of the use of AuthorizedKeysCommand.
minSSHMajorVersion = 8
// Minimum minor version of the use of AuthorizedKeysCommand.
minSSHMinorVersion = 6
// The registry key where the sshd service is kept. Used to look up the path
// of the binary, which is check against the minimum version.
sshdRegKey = `SYSTEM\CurrentControlSet\Services\sshd`
)
// deprovisionUnusedUsers 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 deprovisionUnusedUsers(context.Context, *cfg.Sections, userKeyMap) []error {
galog.V(2).Info("Metadata ssh key called deprovisionUnusedUsers() but users are never removed on windows. Not doing anything.")
return nil
}
func updateSSHKeys(context.Context, *accounts.User, []string) error {
galog.V(2).Info("Metadata ssh key called updateSSHKey() but all keys on windows come from authorized keys command. Not doing anything.")
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)
pwd, err := accounts.GeneratePassword(20)
if err != nil {
return nil, fmt.Errorf("could not generate password for new user %s: %v", username, err)
}
u = &accounts.User{
Name: username,
Password: pwd,
}
err = accounts.CreateUser(ctx, u)
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.Name, group.Name, err)
}
}
return u, nil
}
// enableMetadataSSHKey reports whether metadata ssh keys should be managed.
func enableMetadataSSHKey(config *cfg.Sections, mdsdesc *metadata.Descriptor) bool {
if config.AccountManager != nil {
return !config.AccountManager.Disable && mdsdesc.WindowsSSHEnabled()
}
return !mdsdesc.AccountManagerDisabled() && mdsdesc.WindowsSSHEnabled()
}
// setPlatformConfiguration adds the local Administrators group as a
// supplemental group for new users, and logs a warning if sshd is not running.
func setPlatformConfiguration(ctx context.Context, config *cfg.Sections, desc *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.
supplementalGroups[accounts.AdminGroup.Name] = accounts.AdminGroup
major, minor, err := sshdVersion(ctx)
if err != nil {
galog.Warnf("Could not determine if openssh version is compatible: could not find version: %v.", err)
} else if major < minSSHMajorVersion || (major == minSSHMajorVersion && minor < minSSHMinorVersion) {
// We warn users about incompatibilities but this is only actionable for
// the user, nothing the guest agent can do about it.
galog.Warnf("Detected openssh version may be incompatible with enable_windows_ssh. Found version %d.%d, need version %d.%d.\nSee the windows ssh documentation for instructions on enabling ssh: https://cloud.google.com/compute/docs/connect/windows-ssh.", major, minor, minSSHMajorVersion, minSSHMinorVersion)
}
galog.V(2).Debug("Not configuring SSH, configuration is done by google-compute-engine-ssh googet package, not the agent.")
opts := run.Options{
OutputType: run.OutputStdout,
Name: "sc",
Args: []string{"query", "sshd"},
ExecMode: run.ExecModeSync,
}
if out, err := run.WithContext(ctx, opts); err == nil && !strings.Contains(out.Output, "RUNNING") {
opts := run.Options{
OutputType: run.OutputCombined,
Name: "powershell",
Args: []string{"-c", "Start-Service -Name sshd"},
ExecMode: run.ExecModeSync,
}
if _, err := run.WithContext(ctx, opts); err != nil {
return []error{fmt.Errorf("failed to start sshd: %v", err)}
}
}
return nil
}
// sshdVersion finds the major and minor versions of the sshd binary.
func sshdVersion(ctx context.Context) (int, int, error) {
image, err := reg.ReadString(sshdRegKey, "ImagePath")
if err != nil {
return 0, 0, err
}
image = strings.Trim(string(image), `"`)
opts := run.Options{
OutputType: run.OutputStdout,
ExecMode: run.ExecModeSync,
Name: "powershell.exe",
Args: []string{
"-c",
fmt.Sprintf(`(Get-Item "%s").VersionInfo.FileVersion`, image),
},
}
res, err := run.WithContext(ctx, opts)
if err != nil {
return 0, 0, fmt.Errorf("failed to run powershell command (Get-Item %q).VersionInfo.FileVersion: %v", image, err)
}
galog.V(2).Debugf("Got version info string %s querying for service image path %q.", res.Output, sshdRegKey)
fields := strings.Split(strings.TrimSpace(res.Output), ".")
if len(fields) < 2 {
return 0, 0, fmt.Errorf("service image path %q: not enough values in version %q (split to %v) to determine major and minor version", sshdRegKey, res.Output, fields)
}
major, err := strconv.Atoi(fields[0])
if err != nil {
return 0, 0, fmt.Errorf("service image path %q: major version %q is not an int", sshdRegKey, fields[0])
}
minor, err := strconv.Atoi(fields[1])
if err != nil {
return 0, 0, fmt.Errorf("service image path %q: minor version %q is not an int", sshdRegKey, fields[1])
}
return major, minor, nil
}