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 *-*