k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cluster/gce/windows/testonly/install-ssh.psm1 (about)

     1  # Copyright 2019 The Kubernetes Authors.
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  
    15  <#
    16  .SYNOPSIS
    17    Library for installing and running Win64-OpenSSH. NOT FOR PRODUCTION USE.
    18  
    19  .NOTES
    20    This module depends on common.psm1. This module depends on third-party code
    21    which has not been security-reviewed, so it should only be used for test
    22    clusters. DO NOT USE THIS MODULE FOR PRODUCTION.
    23  #>
    24  
    25  # IMPORTANT PLEASE NOTE:
    26  # Any time the file structure in the `windows` directory changes, `windows/BUILD`
    27  # and `k8s.io/release/lib/releaselib.sh` must be manually updated with the changes.
    28  # We HIGHLY recommend not changing the file structure, because consumers of
    29  # Kubernetes releases depend on the release structure remaining stable.
    30  
    31  Import-Module -Force C:\common.psm1
    32  
    33  $OPENSSH_ROOT = 'C:\Program Files\OpenSSH'
    34  $USER_PROFILE_MODULE = 'C:\user-profile.psm1'
    35  $WRITE_SSH_KEYS_SCRIPT = 'C:\write-ssh-keys.ps1'
    36  
    37  # Starts the Win64-OpenSSH services and configures them to automatically start
    38  # on subsequent boots.
    39  function Start_OpenSshServices {
    40    ForEach ($service in ("sshd", "ssh-agent")) {
    41      net start ${service}
    42      Set-Service ${service} -StartupType Automatic
    43    }
    44  }
    45  
    46  # Installs open-ssh using the instructions in
    47  # https://github.com/PowerShell/Win32-OpenSSH/wiki/Install-Win32-OpenSSH.
    48  #
    49  # After installation run StartProcess-WriteSshKeys to fetch ssh keys from the
    50  # metadata server.
    51  function InstallAndStart-OpenSsh {
    52    if (-not (ShouldWrite-File $OPENSSH_ROOT)) {
    53      Log-Output "Starting already-installed OpenSSH services"
    54      Start_OpenSshServices
    55      return
    56    }
    57    elseif (Test-Path $OPENSSH_ROOT) {
    58      Log-Output ("OpenSSH directory already exists, attempting to run its " +
    59                  "uninstaller before reinstalling")
    60      powershell.exe `
    61          -ExecutionPolicy Bypass `
    62          -File "$OPENSSH_ROOT\OpenSSH-Win64\uninstall-sshd.ps1"
    63      rm -Force -Recurse $OPENSSH_ROOT\OpenSSH-Win64
    64    }
    65  
    66    # Download open-ssh.
    67    # Use TLS 1.2: needed for Invoke-WebRequest downloads from github.com.
    68    [Net.ServicePointManager]::SecurityProtocol = `
    69        [Net.SecurityProtocolType]::Tls12
    70    $url = ("https://github.com/PowerShell/Win32-OpenSSH/releases/download/" +
    71            "v7.9.0.0p1-Beta/OpenSSH-Win64.zip")
    72    $ProgressPreference = 'SilentlyContinue'
    73    Invoke-WebRequest $url -OutFile C:\openssh-win64.zip
    74  
    75    # Unzip and install open-ssh
    76    Expand-Archive -Force C:\openssh-win64.zip -DestinationPath $OPENSSH_ROOT
    77    powershell.exe `
    78        -ExecutionPolicy Bypass `
    79        -File "$OPENSSH_ROOT\OpenSSH-Win64\install-sshd.ps1"
    80  
    81    # Disable password-based authentication.
    82    $sshd_config_default = "$OPENSSH_ROOT\OpenSSH-Win64\sshd_config_default"
    83    $sshd_config = 'C:\ProgramData\ssh\sshd_config'
    84    New-Item -Force -ItemType Directory -Path "C:\ProgramData\ssh\" | Out-Null
    85    # SSH config files must be UTF-8 encoded:
    86    # https://github.com/PowerShell/Win32-OpenSSH/issues/862
    87    # https://github.com/PowerShell/Win32-OpenSSH/wiki/Various-Considerations
    88    (Get-Content $sshd_config_default).`
    89        replace('#PasswordAuthentication yes', 'PasswordAuthentication no') |
    90        Set-Content -Encoding UTF8 $sshd_config
    91  
    92    # Configure the firewall to allow inbound SSH connections
    93    if (Get-NetFirewallRule -ErrorAction SilentlyContinue sshd) {
    94      Get-NetFirewallRule sshd | Remove-NetFirewallRule
    95    }
    96    New-NetFirewallRule `
    97        -Name sshd `
    98        -DisplayName 'OpenSSH Server (sshd)' `
    99        -Enabled True `
   100        -Direction Inbound `
   101        -Protocol TCP `
   102        -Action Allow `
   103        -LocalPort 22
   104  
   105    Start_OpenSshServices
   106  }
   107  
   108  function Setup_WriteSshKeysScript {
   109    if (-not (ShouldWrite-File $WRITE_SSH_KEYS_SCRIPT)) {
   110      return
   111    }
   112  
   113    # Fetch helper module for manipulating Windows user profiles.
   114    if (ShouldWrite-File $USER_PROFILE_MODULE) {
   115      $module = Get-InstanceMetadataAttribute 'user-profile-psm1'
   116      New-Item -ItemType file -Force $USER_PROFILE_MODULE | Out-Null
   117      Set-Content $USER_PROFILE_MODULE $module
   118    }
   119  
   120    # TODO(pjh): check if we still need to write authorized_keys to users-specific
   121    # directories, or if just writing to the centralized keys file for
   122    # Administrators on the system is sufficient (does our log-dump user have
   123    # Administrator rights?).
   124    New-Item -Force -ItemType file ${WRITE_SSH_KEYS_SCRIPT} | Out-Null
   125    Set-Content ${WRITE_SSH_KEYS_SCRIPT} `
   126  'Import-Module -Force USER_PROFILE_MODULE
   127  # For [System.Web.Security.Membership]::GeneratePassword():
   128  Add-Type -AssemblyName System.Web
   129  
   130  $poll_interval = 10
   131  
   132  # New for v7.9.0.0: administrators_authorized_keys file. For permission
   133  # information see
   134  # https://github.com/PowerShell/Win32-OpenSSH/wiki/Security-protection-of-various-files-in-Win32-OpenSSH#administrators_authorized_keys.
   135  # this file is created only once, each valid user will be added here
   136  $administrator_keys_file = ${env:ProgramData} + `
   137      "\ssh\administrators_authorized_keys"
   138  New-Item -ItemType file -Force $administrator_keys_file | Out-Null
   139  icacls $administrator_keys_file /inheritance:r | Out-Null
   140  icacls $administrator_keys_file /grant SYSTEM:`(F`) | Out-Null
   141  icacls $administrator_keys_file /grant BUILTIN\Administrators:`(F`) | `
   142      Out-Null
   143  
   144  while($true) {
   145    $r1 = ""
   146    $r2 = ""
   147    # Try both the new "ssh-keys" and the legacy "sshSkeys" attributes for
   148    # compatibility. The Invoke-RestMethods calls will fail when these attributes
   149    # do not exist, or they may fail when the connection to the metadata server
   150    # gets disrupted while we set up container networking on the node.
   151    try {
   152      $r1 = Invoke-RestMethod -Headers @{"Metadata-Flavor"="Google"} -Uri `
   153          "http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys"
   154    } catch {}
   155    try {
   156      $r2 = Invoke-RestMethod -Headers @{"Metadata-Flavor"="Google"} -Uri `
   157          "http://metadata.google.internal/computeMetadata/v1/project/attributes/sshKeys"
   158    } catch {}
   159    $response= $r1 + $r2
   160  
   161    # Split the response into lines; handle both \r\n and \n line breaks.
   162    $tuples = $response -split "\r?\n"
   163  
   164    $users_to_keys = @{}
   165    foreach($line in $tuples) {
   166      if ([string]::IsNullOrEmpty($line)) {
   167        continue
   168      }
   169      # The final parameter to -Split is the max number of strings to return, so
   170      # this only splits on the first colon.
   171      $username, $key = $line -Split ":",2
   172  
   173      # Detect and skip keys without associated usernames, which may come back
   174      # from the legacy sshKeys metadata.
   175      if (($username -like "ssh-*") -or ($username -like "ecdsa-*")) {
   176        Write-Error "Skipping key without username: $username"
   177        continue
   178      }
   179      if (-not $users_to_keys.ContainsKey($username)) {
   180        $users_to_keys[$username] = @($key)
   181      }
   182      else {
   183        $keyList = $users_to_keys[$username]
   184        $users_to_keys[$username] = $keyList + $key
   185      }
   186    }
   187    $users_to_keys.GetEnumerator() | ForEach-Object {
   188      $username = $_.key
   189  
   190      # We want to create an authorized_keys file in the user profile directory
   191      # for each user, but if we create the directory before that user profile
   192      # has been created first by Windows, then Windows will create a different
   193      # user profile directory that looks like "<user>.KUBERNETES-MINI" and sshd
   194      # will look for the authorized_keys file in THAT directory. In other words,
   195      # we need to create the user first before we can put the authorized_keys
   196      # file in that user profile directory. The user-profile.psm1 module (NOT
   197      # FOR PRODUCTION USE!) has Create-NewProfile which achieves this.
   198      #
   199      # Run "Get-Command -Module Microsoft.PowerShell.LocalAccounts" to see the
   200      # build-in commands for users and groups. For some reason the New-LocalUser
   201      # command does not create the user profile directory, so we use the
   202      # auxiliary user-profile.psm1 instead.
   203  
   204      $pw = [System.Web.Security.Membership]::GeneratePassword(16,2)
   205      try {
   206        # Create-NewProfile will throw these errors:
   207        #
   208        # - if the username already exists:
   209        #
   210        #   Create-NewProfile : Exception calling "SetInfo" with "0" argument(s):
   211        #   "The account already exists."
   212        #
   213        # - if the username is invalid (e.g. gke-29bd5e8d9ea0446f829d)
   214        #
   215        #   Create-NewProfile : Exception calling "SetInfo" with "0" argument(s): "The specified username is invalid.
   216        #
   217        # Just catch them and ignore them.
   218        Create-NewProfile $username $pw -ErrorAction Stop
   219  
   220        # Add the user to the Administrators group, otherwise we will not have
   221        # privilege when we ssh.
   222        Add-LocalGroupMember -Group Administrators -Member $username
   223      } catch {}
   224  
   225      $user_dir = "C:\Users\" + $username
   226      if (-not (Test-Path $user_dir)) {
   227        # If for some reason Create-NewProfile failed to create the user profile
   228        # directory just continue on to the next user.
   229        return
   230      }
   231  
   232      # the authorized_keys file is created only once per user
   233      $user_keys_file = -join($user_dir, "\.ssh\authorized_keys")
   234      if (-not (Test-Path $user_keys_file)) {
   235        New-Item -ItemType file -Force $user_keys_file | Out-Null
   236      }
   237  
   238      ForEach ($ssh_key in $_.value) {
   239        # authorized_keys and other ssh config files must be UTF-8 encoded:
   240        # https://github.com/PowerShell/Win32-OpenSSH/issues/862
   241        # https://github.com/PowerShell/Win32-OpenSSH/wiki/Various-Considerations
   242        #
   243        # these files will be append only, only new keys will be added
   244        $found = Select-String -Path $user_keys_file -Pattern $ssh_key -SimpleMatch
   245        if ($found -eq $null) {
   246          Add-Content -Encoding UTF8 $user_keys_file $ssh_key
   247        }
   248        $found = Select-String -Path $administrator_keys_file -Pattern $ssh_key -SimpleMatch
   249        if ($found -eq $null) {
   250          Add-Content -Encoding UTF8 $administrator_keys_file $ssh_key
   251        }
   252      }
   253    }
   254    Start-Sleep -sec $poll_interval
   255  }'.replace('USER_PROFILE_MODULE', $USER_PROFILE_MODULE)
   256    Log-Output ("${WRITE_SSH_KEYS_SCRIPT}:`n" +
   257                "$(Get-Content -Raw ${WRITE_SSH_KEYS_SCRIPT})")
   258  }
   259  
   260  # Starts a background process that retrieves ssh keys from the metadata server
   261  # and writes them to user-specific directories. Intended for use only by test
   262  # clusters!!
   263  #
   264  # While this is running it should be possible to SSH to the Windows node using:
   265  #   gcloud compute ssh <username>@<instance> --zone=<zone>
   266  # or:
   267  #   ssh -i ~/.ssh/google_compute_engine -o 'IdentitiesOnly yes' \
   268  #     <username>@<instance_external_ip>
   269  # or copy files using:
   270  #   gcloud compute scp <username>@<instance>:C:\\path\\to\\file.txt \
   271  #     path/to/destination/ --zone=<zone>
   272  #
   273  # If the username you're using does not already have a project-level SSH key
   274  # (run "gcloud compute project-info describe --flatten
   275  # commonInstanceMetadata.items.ssh-keys" to check), run gcloud compute ssh with
   276  # that username once to add a new project-level SSH key, wait one minute for
   277  # StartProcess-WriteSshKeys to pick it up, then try to ssh/scp again.
   278  function StartProcess-WriteSshKeys {
   279    Setup_WriteSshKeysScript
   280  
   281    # TODO(pjh): check if such a process is already running before starting
   282    # another one.
   283    $write_keys_process = Start-Process `
   284        -FilePath "powershell.exe" `
   285        -ArgumentList @("-Command", ${WRITE_SSH_KEYS_SCRIPT}) `
   286        -WindowStyle Hidden -PassThru `
   287        -RedirectStandardOutput "NUL" `
   288        -RedirectStandardError C:\write-ssh-keys.err
   289    Log-Output "Started background process to write SSH keys"
   290    Log-Output "$(${write_keys_process} | Out-String)"
   291  }
   292  
   293  # Export all public functions:
   294  Export-ModuleMember -Function *-*