...
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
31Import-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.
39function 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.
51function 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
108function 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():
128Add-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"
138New-Item -ItemType file -Force $administrator_keys_file | Out-Null
139icacls $administrator_keys_file /inheritance:r | Out-Null
140icacls $administrator_keys_file /grant SYSTEM:`(F`) | Out-Null
141icacls $administrator_keys_file /grant BUILTIN\Administrators:`(F`) | `
142 Out-Null
143
144while($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.
278function 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:
294Export-ModuleMember -Function *-*
View as plain text