From 110cdd4873f490a155b164282b470669e44e48e0 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 18 Sep 2020 22:41:58 -0700 Subject: [PATCH] add windows support --- cookbooks/fb_ssh/README.md | 36 ++++++++++++------ cookbooks/fb_ssh/attributes/default.rb | 2 +- cookbooks/fb_ssh/libraries/default.rb | 8 +++- cookbooks/fb_ssh/metadata.rb | 1 + cookbooks/fb_ssh/recipes/default.rb | 39 ++++++++++++++----- cookbooks/fb_ssh/resources/authorization.rb | 42 +++++++++++++++++---- 6 files changed, 95 insertions(+), 33 deletions(-) diff --git a/cookbooks/fb_ssh/README.md b/cookbooks/fb_ssh/README.md index d4fd2aca..db0df0cf 100644 --- a/cookbooks/fb_ssh/README.md +++ b/cookbooks/fb_ssh/README.md @@ -25,6 +25,9 @@ packages for ssh. You can skip package management if you have local packages or otherwise need to do your own management by setting `manage_packages` to false. +Given the many ways to manage packages on Windows, especially for SSH, +we default `manage_packages` to false on Windows. + ### Server configuration (sshd_config) The `sshd_config` hash holds configs that go into `/etc/ssh/sshd_config`. In general each key can have one of three types, bool, string/ints, or array. @@ -32,24 +35,26 @@ general each key can have one of three types, bool, string/ints, or array. Bools are translated into `yes`/`no` when emitted into the config file. These are straight-forward: -``` +```ruby node.default['fb_ssh']['sshd_config']['PubkeyAuthentication'] = true ``` Becomes: -``` + +```text PubkeyAuthentication yes ``` Strings and ints are always treated like normal strings: -``` +```ruby node.default['fb_ssh']['sshd_config']['ClientAliveInterval'] = 0 node.default['fb_ssh']['sshd_config']['ForceCommand'] = '/bin/false' ``` Becomes: -``` + +```text ClientAliveInterval 0 ForceCommand /bin/false ``` @@ -59,7 +64,7 @@ here to make management easy, one could clearly take a multi-value value key and make it a string and it would work, but we support arrays to make modifying the value later in the runlist easier. For example: -``` +```ruby node.default['fb_ssh']['sshd_config']['AuthorizedKeysFile'] = [ '.ssh/authorized_keys', '.ssh/authorized_keys2', @@ -68,13 +73,14 @@ node.default['fb_ssh']['sshd_config']['AuthorizedKeysFile'] = [ Means later it's easy for someone to do: -``` +```ruby node.default['fb_ssh']['sshd_config']['AuthorizedKeysFile']. delete('.ssh/authorized_keys2') ``` or: -``` + +```ruby node.default['fb_ssh']['sshd_config']['AuthorizedKeysFile'] << '/etc/ssh/authorized_keys/%u' ``` @@ -96,7 +102,7 @@ change the order of your match statements, so be careful. Match statements are the exception to the datatype rule above - their value is a hash, and that hash is treated the same as the top-level sshd_config hash: -``` +```ruby node.default['fb_ssh']['sshd_config']['Match Address 1.2.3.4'] => { 'PasswordAuthentication' => true, } @@ -114,7 +120,7 @@ happen: `node['fb_ssh']['authorized_principals_users']`. The format of the `authorized_principals` attribute is: -``` +```ruby node.default['fb_ssh']['authorized_principals'][$USER] = ['one', 'two'] ``` @@ -130,7 +136,7 @@ These work similarly to Authorized Principals. If you set `node['fb_ssh']['authorized_keys_users']`. The format of the items in databag is: -``` +```ruby { 'id': $USER, 'keyname1': $KEY1, @@ -138,7 +144,7 @@ These work similarly to Authorized Principals. If you set ... } ``` - + There should be one item for each user, as many keys as you'd like may be in that item. @@ -151,11 +157,17 @@ node.default['fb_ssh']['authorized_keys']['john']['key1'] = '...' Anything in the node overrides databags. +*NOTE FOR WINDOWS USERS*: On Windows the keys are managed in the homedirectory, +not in a central location. This is because usernames are often in the format of +`domain\user`, which means that `%u` causes sshd to expand the path to +`C:\ProgramData\ssh\authorized_keys\domain\\user`, which is an illegal filename +you can never make. + ### Client config (ssh_config) The client config works the same as the server config, except the special-case is `Host` keys instead of `Match` keys. As an example: -``` +```ruby node.default['fb_ssh']['ssh_config']['ForwardAgent'] = true node.default['fb_ssh']['ssh_config']['Host *.cool.com'] = { 'ForwardX11' => true, diff --git a/cookbooks/fb_ssh/attributes/default.rb b/cookbooks/fb_ssh/attributes/default.rb index aac15dfd..c2a8df4a 100644 --- a/cookbooks/fb_ssh/attributes/default.rb +++ b/cookbooks/fb_ssh/attributes/default.rb @@ -32,7 +32,7 @@ default['fb_ssh'] = { 'enable_central_authorized_keys' => false, - 'manage_packages' => true, + 'manage_packages' => !node.windows?, 'sshd_config' => { 'PermitRootLogin' => false, 'UsePAM' => true, diff --git a/cookbooks/fb_ssh/libraries/default.rb b/cookbooks/fb_ssh/libraries/default.rb index d63091ed..05af89a6 100644 --- a/cookbooks/fb_ssh/libraries/default.rb +++ b/cookbooks/fb_ssh/libraries/default.rb @@ -17,9 +17,13 @@ module FB class SSH + def self.confdir(node) + node.windows? ? 'C:/ProgramData/ssh' : '/etc/ssh' + end + DESTDIR = { - 'keys' => '/etc/ssh/authorized_keys', - 'principals' => '/etc/ssh/authorized_princs', + 'keys' => 'authorized_keys', + 'principals' => 'authorized_princs', }.freeze end end diff --git a/cookbooks/fb_ssh/metadata.rb b/cookbooks/fb_ssh/metadata.rb index 0b51cd05..a98aa59e 100644 --- a/cookbooks/fb_ssh/metadata.rb +++ b/cookbooks/fb_ssh/metadata.rb @@ -27,3 +27,4 @@ supports 'centos' supports 'debian' supports 'ubuntu' +supports 'windows' diff --git a/cookbooks/fb_ssh/recipes/default.rb b/cookbooks/fb_ssh/recipes/default.rb index f1c68763..56977db7 100644 --- a/cookbooks/fb_ssh/recipes/default.rb +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -21,11 +21,14 @@ client_pkg = value_for_platform_family( ['rhel', 'fedora'] => 'openssh-clients', ['debian'] => 'openssh-client', + # not used, but keeps the resource compiling + ['windows'] => 'openssh-client', ) svc = value_for_platform_family( ['rhel', 'fedora'] => 'sshd', ['debian'] => 'ssh', + ['windows'] => 'sshd', ) package client_pkg do @@ -34,11 +37,13 @@ end package 'openssh-server' do + only_if { node['fb_ssh']['manage_packages'] } action :upgrade notifies :restart, 'service[ssh]' end whyrun_safe_ruby_block 'handle late binding ssh configs' do + not_if { node.windows? } block do %w{keys principals}.each do |type| enable_name = "enable_central_authorized_#{type}" @@ -50,30 +55,38 @@ ) end node.default['fb_ssh']['sshd_config'][cfgname] = - "#{FB::SSH::DESTDIR[type]}/%u" + File.join(FB::SSH.confdir(node), FB::SSH::DESTDIR[type], '%u') end end end end -template '/etc/ssh/sshd_config' do +template ::File.join(FB::SSH.confdir(node), 'sshd_config') do source 'ssh_config.erb' - owner 'root' - group 'root' - mode '0644' + unless node.windows? + owner 'root' + group 'root' + mode '0644' + if node.windows? + verify '"C:/Program Files/OpenSSH-Win64/sshd.exe" -t -f %{path}' + else + verify '/usr/sbin/sshd -t -f %{path}' + end + end variables({ :type => 'sshd_config' }) - verify '/usr/sbin/sshd -t -f %{path}' # in firstboot we may not be able to get in until ssh is restarted # on the desired config, so restart immediately. Otherwise, delay ntype = node.firstboot_any_phase? ? :immediately : :delayed notifies :restart, 'service[ssh]', ntype end -template '/etc/ssh/ssh_config' do +template ::File.join(FB::SSH.confdir(node), 'ssh_config') do source 'ssh_config.erb' - owner 'root' - group 'root' - mode '0644' + unless node.windows? + owner 'root' + group 'root' + mode '0644' + end variables({ :type => 'ssh_config' }) end @@ -94,3 +107,9 @@ service_name svc action [:enable, :start] end + +if node.windows? + service 'ssh-agent' do + action [:enable, :start] + end +end diff --git a/cookbooks/fb_ssh/resources/authorization.rb b/cookbooks/fb_ssh/resources/authorization.rb index 239ec533..b1cdb18f 100644 --- a/cookbooks/fb_ssh/resources/authorization.rb +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -17,12 +17,14 @@ action_class do def manage(type) - keydir = FB::SSH::DESTDIR[type] + keydir = ::File.join(FB::SSH.confdir(node), FB::SSH::DESTDIR[type]) directory keydir do - owner 'root' - group 'root' - mode '0755' + unless node.windows? + owner 'root' + group 'root' + mode '0755' + end end unless node['fb_ssh']["authorized_#{type}_users"].empty? @@ -40,11 +42,35 @@ def manage(type) auth_map.each_key do |user| next if allowed_users && !allowed_users.include?(user) - template "#{keydir}/#{user}" do + # windows sucks and on ssh the "username" is "corp\\whatever" which is + # not a valid file name. Ugh. So we leave it in the user's homedir + if node.windows? + user = user.split('\\').last + homedir = "C:/Users/#{user}" + keyfile = "#{homedir}/.ssh/authorized_keys" + # users who don't have homedirectories, we skip + next unless ::File.exist?(homedir) + + directory "#{homedir}/.ssh" do + rights :read, user + rights :full_control, 'Administrators' + inherits false + end + else + keyfile = "#{keydir}/#{user}" + end + + template keyfile do source "authorized_#{type}.erb" - owner 'root' - group 'root' - mode '0644' + if node.windows? + rights :read, user + rights :full_control, 'Administrators' + inherits false + else + owner 'root' + group 'root' + mode '0644' + end if type == 'keys' && !auth_map[user] d = data_bag_item('fb_ssh_authorized_keys', user) d.delete('id')