install-ssh.psm1 11 KB

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