From 36302bdf1c067c81745fe0a4494c745306c8e9f4 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 16 Aug 2019 12:14:03 -0700 Subject: [PATCH 01/10] New cookbook: fb_ssh This is a cookbook to manage SSH using the FB attribute-driven model. It handles daemon and client configs as well as authorized_keys and authorized_principals. There are few things worth noting here: 1. The API for public keys uses a `data_bag`. Grocery delivery and taste-tester have long supported data_bags even though FB doesn't use them in prod... but it doesn't matter since FB uses principals in prod and not keys, so this shouldn't be an issue no matter what. 2. The CLA for FB includes a grant of all copyrights, but the copyright, AIUI still should say me, so it does. However, I've left the maintainer as the standard FB template. --- cookbooks/fb_ssh/README.md | 154 ++++++++++++++++++ cookbooks/fb_ssh/attributes/default.rb | 39 +++++ cookbooks/fb_ssh/libraries/default.rb | 25 +++ cookbooks/fb_ssh/metadata.rb | 29 ++++ cookbooks/fb_ssh/recipes/default.rb | 92 +++++++++++ cookbooks/fb_ssh/resources/authorization.rb | 79 +++++++++ .../fb_ssh/templates/authorized_keys.erb | 5 + .../templates/authorized_principals.erb | 4 + cookbooks/fb_ssh/templates/ssh_config.erb | 31 ++++ 9 files changed, 458 insertions(+) create mode 100644 cookbooks/fb_ssh/README.md create mode 100644 cookbooks/fb_ssh/attributes/default.rb create mode 100644 cookbooks/fb_ssh/libraries/default.rb create mode 100644 cookbooks/fb_ssh/metadata.rb create mode 100644 cookbooks/fb_ssh/recipes/default.rb create mode 100644 cookbooks/fb_ssh/resources/authorization.rb create mode 100644 cookbooks/fb_ssh/templates/authorized_keys.erb create mode 100644 cookbooks/fb_ssh/templates/authorized_principals.erb create mode 100644 cookbooks/fb_ssh/templates/ssh_config.erb diff --git a/cookbooks/fb_ssh/README.md b/cookbooks/fb_ssh/README.md new file mode 100644 index 000000000..12e5609f6 --- /dev/null +++ b/cookbooks/fb_ssh/README.md @@ -0,0 +1,154 @@ +fb_ssh Cookbook +=============== +Installs and configures openssh + +Requirements +------------ + +Attributes +---------- +* node['fb_ssh']['manage_packages'] +* node['fb_ssh']['sshd_config'][$CONFIG] +* node['fb_ssh']['ssh_config'][$CONFIG] +* node['fb_ssh']['enable_central_authorized_keys'] +* node['fb_ssh']['authorized_keys_users'] +* node['fb_ssh']['enable_central_authorized_principals'] +* node['fb_ssh']['authorized_principals'][$USER][$KEYNAME] +* node['fb_ssh']['authorized_principals_users'] + +Usage +----- +### Packages +By default `fb_ssh` will install and keep updated both client and server +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. + +### 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. + +Bools are translated into `yes`/`no` when emitted into the config file. These +are straight-forward: + +``` +node.default['fb_ssh']['sshd_config']['PubkeyAuthentication'] = true +``` + +Becomes: +``` +PubkeyAuthentication yes +``` + +Strings and ints are always treated like normal strings: + +``` +node.default['fb_ssh']['sshd_config']['ClientAliveInterval'] = 0 +node.default['fb_ssh']['sshd_config']['ForceCommand'] = '/bin/false' +``` + +Becomes: +``` +ClientAliveInterval 0 +ForceCommand /bin/false +``` + +Arrays will be joined by spaces. It's worth noting that while this feature is +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: + +``` +node.default['fb_ssh']['sshd_config']['AuthorizedKeysFile'] = [ + '.ssh/authorized_keys', + '.ssh/authorized_keys2', +] +``` + +Means later it's easy for someone to do: + +``` +node.default['fb_ssh']['sshd_config']['AuthorizedKeysFile']. + delete('.ssh/authorized_keys2') +``` + +or: +``` +node.default['fb_ssh']['sshd_config']['AuthorizedKeysFile'] << + '/etc/ssh/authorized_keys/%u' +``` + +So be careful to be consistent about this. + +### Match Values +All match rules in sshd must come at the end, because Match blocks take effect +until the next match block, or the end of the file - indentation is irrelevant. + +You can use Match rules as normal. This cookbook will automatically move them +to the end of the file and keep them in the order that the users specified +them. + +This means that unlike the other keys which are sorted for easier diffing, +these are not sorted. As such, changing the order of your cookbooks could +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: + +``` +node.default['fb_ssh']['sshd_config']['Match Address 1.2.3.4'] => { + 'PasswordAuthentication' => true, +} +``` + +#### Authorized Principals + +If you set `enable_central_authorized_principals` to true, then two things will +happen: +1. Your AuthorizedPrincipalsFile will be set to `/etc/ssh/authorized_princs/%u`, + regardless of what you set it to +2. The contents of `node['fb_ssh']['authorized_principals']` will be used + to populate `/etc/ssh/authorized_princs/` with one file for each + user. To limit which users are populated, simply populate the list + `node['fb_ssh']['authorized_principals_users']`. The format of the + `authorized_principals` attribute is: + +``` +node.default['fb_ssh']['authorized_principals'][$USER] = ['one', 'two'] +``` + +#### Authorized Keys + +These work similarly to Authorized Principals. If you set +`enable_central_authorized_keys` to true, then two things will happen: +1. Your AuthorizedKeysFile will be set to `/etc/ssh/authorized_keys/%u`, + regardless of what you set it to +2. The contents of the databag `fb_ssh_authorized_keys` + will be used to populate `/etc/ssh/authorized_keys/` with key files for each + user. To limit which keys go on a user, simply populate the list + `node['fb_ssh']['authorized_keys_users']`. The format of the items + in databag is: + +``` +{ + 'id': $USER, + 'keyname1': $KEY1, + 'keyname2': $KEY2, + ... +} +``` + +There should be one item for each user, as many keys as you'd like may +be in that item. + +### 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: + +``` +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 new file mode 100644 index 000000000..58c7d4d25 --- /dev/null +++ b/cookbooks/fb_ssh/attributes/default.rb @@ -0,0 +1,39 @@ +# +# Copyright (c) 2019-present, Vicarious, Inc. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sftp_path = value_for_platform_family( + ['rhel', 'fedora'] => '/usr/libexec/openssh/sftp-server', + ['debian'] => '/usr/lib/openssh/sftp-server', +) +default['fb_ssh'] = { + 'enable_central_authorized_keys' => false, + 'manage_packages' => true, + 'sshd_config' => { + 'PermitRootLogin' => false, + 'UsePAM' => true, + 'Subsystem ftp' => sftp_path, + 'AuthorizedKeysFile' => [ + '.ssh/authorized_keys', + '.ssh/authorized_keys2', + ], + }, + 'authorized_keys' => {}, + 'authorized_keys_users' => [], + 'authorized_principals' => {}, + 'authorized_principals_users' => [], + 'ssh_config' => {}, +} diff --git a/cookbooks/fb_ssh/libraries/default.rb b/cookbooks/fb_ssh/libraries/default.rb new file mode 100644 index 000000000..d63091ed0 --- /dev/null +++ b/cookbooks/fb_ssh/libraries/default.rb @@ -0,0 +1,25 @@ +# +# Copyright (c) 2019-present, Vicarious, Inc. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module FB + class SSH + DESTDIR = { + 'keys' => '/etc/ssh/authorized_keys', + 'principals' => '/etc/ssh/authorized_princs', + }.freeze + end +end diff --git a/cookbooks/fb_ssh/metadata.rb b/cookbooks/fb_ssh/metadata.rb new file mode 100644 index 000000000..0b51cd051 --- /dev/null +++ b/cookbooks/fb_ssh/metadata.rb @@ -0,0 +1,29 @@ +# +# Copyright (c) 2019-present, Vicarious, Inc. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name 'fb_ssh' +maintainer 'Facebook' +maintainer_email 'noreply@facebook.com' +license 'Apache-2.0' +description 'Configures ssh and sshd including keys and principals' +source_url 'https://github.com/facebook/chef-cookbooks/' +long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) +# never EVER change this number, ever. +version '0.1.0' +supports 'centos' +supports 'debian' +supports 'ubuntu' diff --git a/cookbooks/fb_ssh/recipes/default.rb b/cookbooks/fb_ssh/recipes/default.rb new file mode 100644 index 000000000..73be7504a --- /dev/null +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -0,0 +1,92 @@ +# +# Cookbook:: fb_ssh +# Recipe:: default +# +# Copyright (c) 2019-present, Vicarious, Inc. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +client_pkg = value_for_platform_family( + ['rhel', 'fedora'] => 'openssh-clients', + ['debian'] => 'openssh-client', +) + +svc = value_for_platform_family( + ['rhel', 'fedora'] => 'sshd', + ['debian'] => 'ssh', +) + +package client_pkg do + only_if { node['fb_ssh']['manage_packages'] } + action :upgrade +end + +package 'openssh-server' do + action :upgrade + notifies :restart, 'service[ssh]' +end + +whyrun_safe_ruby_block 'handle late binding ssh configs' do + block do + %w{keys principals}.each do |type| + enable_name = "enable_central_authorized_#{type}" + if node['fb_ssh'][enable_name] + cfgname = "Authorized#{type.capitalize}File" + if node['fb_ssh']['sshd_config'][cfgname] + Chef::Log.warn( + "fb_ssh: Overriding sshd '#{cfgname}' per '#{enable_name}'", + ) + end + node.default['fb_ssh']['sshd_config'][cfgname] = + "#{FB::SSH::DESTDIR[type]}/%u" + end + end + end +end + +template '/etc/ssh/sshd_config' do + source 'ssh_config.erb' + owner 'root' + group 'root' + mode '0644' + variables({ :type => 'sshd_config' }) + notifies :restart, 'service[ssh]' +end + +template '/etc/ssh/ssh_config' do + source 'ssh_config.erb' + owner 'root' + group 'root' + mode '0644' + variables({ :type => 'ssh_config' }) +end + +fb_ssh_authorization 'manage keys' do + only_if { node['fb_ssh']['enable_central_authorized_keys'] } + action :manage_keys +end + +fb_ssh_authorization 'manage principals' do + only_if { node['fb_ssh']['enable_central_authorized_principals'] } + action :manage_principals +end + +service 'ssh' do + # rather than "service svc", give it a consistent name + # in case others want to notify it, and then just override + # the service name internally to the resource + service_name svc + action [:enable, :start] +end diff --git a/cookbooks/fb_ssh/resources/authorization.rb b/cookbooks/fb_ssh/resources/authorization.rb new file mode 100644 index 000000000..0a7f497d0 --- /dev/null +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -0,0 +1,79 @@ +# +# Copyright (c) 2019-present, Vicarious, Inc. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +actions [:manage_keys, :manage_principals] + +action_class do + def manage(type) + keydir = FB::SSH::DESTDIR[type] + + directory keydir do + owner 'root' + group 'root' + mode '0755' + end + + unless node['fb_ssh']["authorized_#{type}_users"].empty? + allowed_users = node['fb_ssh']["authorized_#{type}_users"] + end + if type == 'keys' + auth_map = Hash[ + data_bag('fb_ssh_authorized_keys').map { |x| [x, nil] } + ] + else + auth_map = node['fb_ssh']["authorized_#{type}"] + end + + auth_map.each_key do |user| + next if allowed_users && !allowed_users.include?(user) + + template "#{keydir}/#{user}" do + source "authorized_#{type}.erb" + owner 'root' + group 'root' + mode '0644' + if type == 'keys' + d = data_bag_item('fb_ssh_authorized_keys', user) + d.delete('id') + variables({ :data => d }) + else + variables({ :data => auth_map[user] }) + end + end + end + + Dir.glob("#{keydir}/*").each do |keyfile| + user = ::File.basename(keyfile) + if allowed_users + next if allowed_users.include?(user) + else + next if auth_map[user] + end + file keyfile do + action :delete + end + end + end +end + +action :manage_keys do + manage('keys') +end + +action :manage_principals do + manager('principals') +end diff --git a/cookbooks/fb_ssh/templates/authorized_keys.erb b/cookbooks/fb_ssh/templates/authorized_keys.erb new file mode 100644 index 000000000..2761d3f43 --- /dev/null +++ b/cookbooks/fb_ssh/templates/authorized_keys.erb @@ -0,0 +1,5 @@ +# Managed by Chef, do not modify! +<% @data.each do |name, key| %> +# <%= name %> +<%= key %> +<% end %> diff --git a/cookbooks/fb_ssh/templates/authorized_principals.erb b/cookbooks/fb_ssh/templates/authorized_principals.erb new file mode 100644 index 000000000..ec86ca034 --- /dev/null +++ b/cookbooks/fb_ssh/templates/authorized_principals.erb @@ -0,0 +1,4 @@ +# Managed by Chef, do not modify! +<% @data.each do |princ| %> +<%= princ %> +<% end %> diff --git a/cookbooks/fb_ssh/templates/ssh_config.erb b/cookbooks/fb_ssh/templates/ssh_config.erb new file mode 100644 index 000000000..607a1676c --- /dev/null +++ b/cookbooks/fb_ssh/templates/ssh_config.erb @@ -0,0 +1,31 @@ +# This file is generated by Chef. Do not modify! +<% type = @type %> +<% kw = @type == 'sshd_config' ? 'Match' : 'Host' %> +<% # Sort the keys so diffs are easier to read. %> +<% # But drop 'match' which (a) must be at the end and (b) must be ordered %> +<% node['fb_ssh'][@type].keys. reject { |x| x.start_with?(kw) }. + sort.each do |key| %> +<% val = node['fb_ssh'][@type][key] %> +<% if val.is_a?(TrueClass) || val.is_a?(FalseClass) %> +<%= key %> <%= val ? 'yes' : 'no' %> +<% elsif val.is_a?(String) %> +<%= key %> <%= val %> +<% elsif val.is_a?(Array) %> +<%= key %> <%= val.join(' ') %> +<% end %> +<% end %> + +<% node['fb_ssh'][@type].keys.select { |x| x.start_with?(kw) }. + each do |match| %> +<%= match %> +<% node['fb_ssh'][@type][match].keys.sort.each do |key| %> +<% val = node['fb_ssh'][@type][match][key] %> +<% if val.is_a?(TrueClass) || val.is_a?(FalseClass) %> + <%= key %> <%= val ? 'yes' : 'no' %> +<% elsif val.is_a?(String) %> + <%= key %> <%= val %> +<% elsif val.is_a?(Array) %> + <%= key %> <%= val.join(' ') %> +<% end %> +<% end %> +<% end %> From 989a89f799b756f6022a7156531e43c4d9224081 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Thu, 3 Oct 2019 11:58:22 -0700 Subject: [PATCH 02/10] fb_ssh: add verifier, fix principals bug --- cookbooks/fb_ssh/recipes/default.rb | 1 + cookbooks/fb_ssh/resources/authorization.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cookbooks/fb_ssh/recipes/default.rb b/cookbooks/fb_ssh/recipes/default.rb index 73be7504a..91e4ba0a7 100644 --- a/cookbooks/fb_ssh/recipes/default.rb +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -62,6 +62,7 @@ group 'root' mode '0644' variables({ :type => 'sshd_config' }) + verify '/usr/sbin/sshd -t -f %{path}' notifies :restart, 'service[ssh]' end diff --git a/cookbooks/fb_ssh/resources/authorization.rb b/cookbooks/fb_ssh/resources/authorization.rb index 0a7f497d0..23cc8a382 100644 --- a/cookbooks/fb_ssh/resources/authorization.rb +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -75,5 +75,5 @@ def manage(type) end action :manage_principals do - manager('principals') + manage('principals') end From 6d3c96e454bd3ac21eaf3fdf9616ab748d836fbd Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Thu, 3 Oct 2019 13:03:15 -0700 Subject: [PATCH 03/10] add to init for testing --- cookbooks/fb_init_sample/metadata.rb | 1 + cookbooks/fb_init_sample/recipes/default.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cookbooks/fb_init_sample/metadata.rb b/cookbooks/fb_init_sample/metadata.rb index 95ab4c440..a4fd201d5 100644 --- a/cookbooks/fb_init_sample/metadata.rb +++ b/cookbooks/fb_init_sample/metadata.rb @@ -58,6 +58,7 @@ 'fb_screen', 'fb_sdparm', 'fb_securetty', + 'fb_ssh', 'fb_storage', 'fb_stunnel', 'fb_sudo', diff --git a/cookbooks/fb_init_sample/recipes/default.rb b/cookbooks/fb_init_sample/recipes/default.rb index d844ebf8a..c090d1caf 100644 --- a/cookbooks/fb_init_sample/recipes/default.rb +++ b/cookbooks/fb_init_sample/recipes/default.rb @@ -49,7 +49,7 @@ include_recipe 'fb_launchd' end include_recipe 'fb_nsswitch' -# HERE: ssh +include_recipe 'fb_ssh' include_recipe 'fb_less' if node.linux? && !node.embedded? && !node.container? include_recipe 'fb_ethtool' From b5fdd661de58fbaef6ad6f01c6d3090edca48d4c Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 1 Nov 2019 13:02:22 -0700 Subject: [PATCH 04/10] lint --- cookbooks/fb_ssh/resources/authorization.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookbooks/fb_ssh/resources/authorization.rb b/cookbooks/fb_ssh/resources/authorization.rb index 23cc8a382..d5bd860df 100644 --- a/cookbooks/fb_ssh/resources/authorization.rb +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -60,8 +60,8 @@ def manage(type) user = ::File.basename(keyfile) if allowed_users next if allowed_users.include?(user) - else - next if auth_map[user] + elsif auth_map[user] + next end file keyfile do action :delete From 135f24f0e140bc7001b00958c775990f5ed6ca69 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 1 Nov 2019 13:03:01 -0700 Subject: [PATCH 05/10] lint --- cookbooks/fb_ssh/resources/authorization.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/cookbooks/fb_ssh/resources/authorization.rb b/cookbooks/fb_ssh/resources/authorization.rb index d5bd860df..629735461 100644 --- a/cookbooks/fb_ssh/resources/authorization.rb +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -15,8 +15,6 @@ # limitations under the License. # -actions [:manage_keys, :manage_principals] - action_class do def manage(type) keydir = FB::SSH::DESTDIR[type] From 28bf8a3a2a0fd346ace0875450266c13b6e72dc9 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 1 Nov 2019 14:00:36 -0700 Subject: [PATCH 06/10] debug --- cookbooks/fb_ssh/recipes/default.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cookbooks/fb_ssh/recipes/default.rb b/cookbooks/fb_ssh/recipes/default.rb index 91e4ba0a7..9db5625d9 100644 --- a/cookbooks/fb_ssh/recipes/default.rb +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -56,6 +56,24 @@ end end +template '/tmp/sshd_config' do + source 'ssh_config.erb' + owner 'root' + group 'root' + mode '0644' + variables({ :type => 'sshd_config' }) +end + +ruby_block 'debug' do + block do + puts ::File.read('/tmp/sshd_config') + s = Mixlib::ShellOut.new('/usr/sbin/sshd -t -f /tmp/sshd_config') + s.run_command + puts s.stdout + puts s.stderr + end +end + template '/etc/ssh/sshd_config' do source 'ssh_config.erb' owner 'root' From 6d2620f6a95f79cf10f410b77b6c34c0e122a838 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 1 Nov 2019 18:34:39 -0700 Subject: [PATCH 07/10] fix centos6 --- cookbooks/fb_ssh/attributes/default.rb | 16 ++++++++++++---- cookbooks/fb_ssh/recipes/default.rb | 18 ------------------ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/cookbooks/fb_ssh/attributes/default.rb b/cookbooks/fb_ssh/attributes/default.rb index 58c7d4d25..fb01f5844 100644 --- a/cookbooks/fb_ssh/attributes/default.rb +++ b/cookbooks/fb_ssh/attributes/default.rb @@ -19,6 +19,17 @@ ['rhel', 'fedora'] => '/usr/libexec/openssh/sftp-server', ['debian'] => '/usr/lib/openssh/sftp-server', ) + +# centos6 only supports 1... +if node.centos6? + auth_keys = '.ssh/authorized_keys' +else + auth_keys = [ + '.ssh/authorized_keys', + '.ssh/authorized_keys2', + ] +end + default['fb_ssh'] = { 'enable_central_authorized_keys' => false, 'manage_packages' => true, @@ -26,10 +37,7 @@ 'PermitRootLogin' => false, 'UsePAM' => true, 'Subsystem ftp' => sftp_path, - 'AuthorizedKeysFile' => [ - '.ssh/authorized_keys', - '.ssh/authorized_keys2', - ], + 'AuthorizedKeysFile' => auth_keys, }, 'authorized_keys' => {}, 'authorized_keys_users' => [], diff --git a/cookbooks/fb_ssh/recipes/default.rb b/cookbooks/fb_ssh/recipes/default.rb index 9db5625d9..91e4ba0a7 100644 --- a/cookbooks/fb_ssh/recipes/default.rb +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -56,24 +56,6 @@ end end -template '/tmp/sshd_config' do - source 'ssh_config.erb' - owner 'root' - group 'root' - mode '0644' - variables({ :type => 'sshd_config' }) -end - -ruby_block 'debug' do - block do - puts ::File.read('/tmp/sshd_config') - s = Mixlib::ShellOut.new('/usr/sbin/sshd -t -f /tmp/sshd_config') - s.run_command - puts s.stdout - puts s.stderr - end -end - template '/etc/ssh/sshd_config' do source 'ssh_config.erb' owner 'root' From b28333f89bf6337479611c8c2dae4052c6364e38 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 6 Dec 2019 16:40:02 -0800 Subject: [PATCH 08/10] fix sftp --- cookbooks/fb_ssh/attributes/default.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbooks/fb_ssh/attributes/default.rb b/cookbooks/fb_ssh/attributes/default.rb index fb01f5844..aac15dfda 100644 --- a/cookbooks/fb_ssh/attributes/default.rb +++ b/cookbooks/fb_ssh/attributes/default.rb @@ -36,7 +36,7 @@ 'sshd_config' => { 'PermitRootLogin' => false, 'UsePAM' => true, - 'Subsystem ftp' => sftp_path, + 'Subsystem sftp' => sftp_path, 'AuthorizedKeysFile' => auth_keys, }, 'authorized_keys' => {}, From 43232bca0b727d2393b37205c43e7073a4b26794 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 26 Jun 2020 17:19:13 -0700 Subject: [PATCH 09/10] fixes --- cookbooks/fb_ssh/README.md | 9 +++++++++ cookbooks/fb_ssh/recipes/default.rb | 5 ++++- cookbooks/fb_ssh/resources/authorization.rb | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cookbooks/fb_ssh/README.md b/cookbooks/fb_ssh/README.md index 12e5609f6..d4fd2aca2 100644 --- a/cookbooks/fb_ssh/README.md +++ b/cookbooks/fb_ssh/README.md @@ -142,6 +142,15 @@ 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. +Alternatively you can populate `node['fb_ssh']['authorized_keys'][$USER]`. +Doing so should be done similarly to the databags and each key given a name: + +```ruby +node.default['fb_ssh']['authorized_keys']['john']['key1'] = '...' +``` + +Anything in the node overrides databags. + ### 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: diff --git a/cookbooks/fb_ssh/recipes/default.rb b/cookbooks/fb_ssh/recipes/default.rb index 91e4ba0a7..f1c687634 100644 --- a/cookbooks/fb_ssh/recipes/default.rb +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -63,7 +63,10 @@ mode '0644' variables({ :type => 'sshd_config' }) verify '/usr/sbin/sshd -t -f %{path}' - notifies :restart, 'service[ssh]' + # 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 diff --git a/cookbooks/fb_ssh/resources/authorization.rb b/cookbooks/fb_ssh/resources/authorization.rb index 629735461..239ec5330 100644 --- a/cookbooks/fb_ssh/resources/authorization.rb +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -32,6 +32,7 @@ def manage(type) auth_map = Hash[ data_bag('fb_ssh_authorized_keys').map { |x| [x, nil] } ] + auth_map.merge!(node['fb_ssh']['authorized_keys']) else auth_map = node['fb_ssh']["authorized_#{type}"] end @@ -44,7 +45,7 @@ def manage(type) owner 'root' group 'root' mode '0644' - if type == 'keys' + if type == 'keys' && !auth_map[user] d = data_bag_item('fb_ssh_authorized_keys', user) d.delete('id') variables({ :data => d }) From 0fcfa34bb84d6db6522993480c09cb03fa0fbd9b Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Fri, 18 Sep 2020 22:41:58 -0700 Subject: [PATCH 10/10] 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 | 71 ++++++++++++++++++--- cookbooks/fb_ssh/resources/authorization.rb | 46 +++++++++---- 6 files changed, 128 insertions(+), 36 deletions(-) diff --git a/cookbooks/fb_ssh/README.md b/cookbooks/fb_ssh/README.md index d4fd2aca2..db0df0cf5 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 aac15dfda..c2a8df4ab 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 d63091ed0..05af89a64 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 0b51cd051..a98aa59ec 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 f1c687634..b53e79b42 100644 --- a/cookbooks/fb_ssh/recipes/default.rb +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -21,11 +21,16 @@ client_pkg = value_for_platform_family( ['rhel', 'fedora'] => 'openssh-clients', ['debian'] => 'openssh-client', + ['mac_os_x'] => 'openssh', + # not used, but keeps the resource compiling + ['windows'] => 'openssh-client', ) svc = value_for_platform_family( ['rhel', 'fedora'] => 'sshd', ['debian'] => 'ssh', + ['mac_os_x'] => 'sshd', + ['windows'] => 'sshd', ) package client_pkg do @@ -34,11 +39,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 +57,64 @@ ) 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 +directory FB::SSH.confdir(node) do + if node.windows? + rights :full_control, 'Administrators' + rights :read_execute, ['Administrators', 'Authenticated Users'] + else + owner 'root' + group node.root_group + mode '0755' + end +end + +# sshd won't start if the private keys are too readable. +Dir.glob(::File.join(FB::SSH.confdir(node), '*key')).each do |f| + file f do + if node.windows? + rights :full_control, 'Administrators' + rights :full_control, 'SYSTEM' + inherits false + else + owner 'root' + group node.root_group + mode '0600' + end + end +end + +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 node.root_group + 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 node.root_group + mode '0644' + end variables({ :type => 'ssh_config' }) end @@ -92,5 +133,15 @@ # in case others want to notify it, and then just override # the service name internally to the resource service_name svc + if node['platform'] == 'mac_os_x' + # On OS X, we must specify the plist to get the right launchd service label. + plist '/System/Library/LaunchDaemons/ssh.plist' + end 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 239ec5330..134c116be 100644 --- a/cookbooks/fb_ssh/resources/authorization.rb +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -17,21 +17,21 @@ 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 node.root_group + mode '0755' + end end unless node['fb_ssh']["authorized_#{type}_users"].empty? allowed_users = node['fb_ssh']["authorized_#{type}_users"] end if type == 'keys' - auth_map = Hash[ - data_bag('fb_ssh_authorized_keys').map { |x| [x, nil] } - ] + auth_map = data_bag('fb_ssh_authorized_keys').map { |x| [x, nil] }.to_h auth_map.merge!(node['fb_ssh']['authorized_keys']) else auth_map = node['fb_ssh']["authorized_#{type}"] @@ -40,11 +40,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 node.root_group + mode '0644' + end if type == 'keys' && !auth_map[user] d = data_bag_item('fb_ssh_authorized_keys', user) d.delete('id')