diff --git a/cookbooks/fb_ssh/README.md b/cookbooks/fb_ssh/README.md new file mode 100644 index 00000000..12e5609f --- /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 00000000..7bd6f9dc --- /dev/null +++ b/cookbooks/fb_ssh/attributes/default.rb @@ -0,0 +1,22 @@ +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 00000000..0c4310e3 --- /dev/null +++ b/cookbooks/fb_ssh/libraries/default.rb @@ -0,0 +1,8 @@ +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 00000000..3dad37d2 --- /dev/null +++ b/cookbooks/fb_ssh/metadata.rb @@ -0,0 +1,12 @@ +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 00000000..2ac16d1c --- /dev/null +++ b/cookbooks/fb_ssh/recipes/default.rb @@ -0,0 +1,78 @@ +# +# Cookbook:: fb_ssh +# Recipe:: default +# +# Copyright:: 2019, Phil Dibowitz + +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 00000000..02da6608 --- /dev/null +++ b/cookbooks/fb_ssh/resources/authorization.rb @@ -0,0 +1,62 @@ +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 00000000..2761d3f4 --- /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 00000000..ec86ca03 --- /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 00000000..607a1676 --- /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 %>