diff --git a/README.rst b/README.rst index ee2de6b..f45634b 100644 --- a/README.rst +++ b/README.rst @@ -41,3 +41,47 @@ Installs the ssh daemon configuration file included in this formula by values from pillar. ``pillar.example`` results in the generation of the default ``sshd_config`` file on Debian Wheezy. +``openssh.known_hosts`` +----------------------- + +Manages the site-wide ssh_known_hosts file and fills it with the +public SSH host keys of all minions. You can restrict the set of minions +whose keys are listed by using the pillar data ``openssh:known_hosts:target`` +and ``openssh:known_hosts:expr_form`` (those fields map directly to the +corresponding attributes of the ``mine.get`` function). + +The Salt mine is used to share the public SSH host keys, you must thus +configure it accordingly on all hosts that must export their keys. Two +mine functions are required, one that exports the keys (one key per line, +as they are stored in ``/etc/ssh/ssh_host_*_key.pub``) and one that defines +the public hostname that the keys are associated to. Here's the way to +setup those functions through pillar:: + + # Required for openssh.known_hosts + mine_functions: + public_ssh_host_keys: + mine_function: cmd.run + cmd: cat /etc/ssh/ssh_host_*_key.pub + public_ssh_hostname: + mine_function: grains.get + key: id + +The above example assumes that the minion identifier is a valid DNS name +that can be used to connect to the host. If that's not the case, you might +want to use the ``fqdn`` grain instead of the ``id`` one. The above example +also uses the default mine function names used by this formula. If you have to +use other names, then you should indicate the names to use in pillar keys +``openssh:known_hosts:mine_keys_function`` and +``openssh:known_hosts:mine_hostname_function``. + +You can also integrate alternate DNS names of the various hosts in the +ssh_known_hosts files. You just have to list all the alternate DNS names as a +list in the ``openssh:known_hosts:aliases`` pillar key. Whenever the IPv4 or +IPv6 behind one of those DNS entries matches an IPv4 or IPv6 behind the +official hostname of a minion, the alternate DNS name will be associated to the +minion's public SSH host key. + +``openssh.moduli`` +----------------------- + +Manages the system wide ``/etc/ssh/moduli`` file. diff --git a/openssh/config.sls b/openssh/config.sls index 13c3e96..dc87341 100644 --- a/openssh/config.sls +++ b/openssh/config.sls @@ -17,10 +17,24 @@ sshd_config: {% if salt['pillar.get']('openssh:generate_' ~ keyType ~ '_keys', False) %} ssh_generate_host_{{ keyType }}_key: cmd.run: + {%- if salt['pillar.get']('openssh:generate_' ~ keyType ~ '_size', False) %} + {%- set keySize = salt['pillar.get']('openssh:generate_' ~ keyType ~ '_size', 4096) %} + - name: ssh-keygen -t {{ keyType }} -b {{ keySize }} -N '' -f /etc/ssh/ssh_host_{{ keyType }}_key + {%- else %} - name: ssh-keygen -t {{ keyType }} -N '' -f /etc/ssh/ssh_host_{{ keyType }}_key + {%- endif %} - creates: /etc/ssh/ssh_host_{{ keyType }}_key - user: root +{% elif salt['pillar.get']('openssh:absent_' ~ keyType ~ '_keys', False) %} +ssh_host_{{ keyType }}_key: + file.absent: + - name: /etc/ssh/ssh_host_{{ keyType }}_key + +ssh_host_{{ keyType }}_key.pub: + file.absent: + - name: /etc/ssh/ssh_host_{{ keyType }}_key.pub + {% elif salt['pillar.get']('openssh:provide_' ~ keyType ~ '_keys', False) %} ssh_host_{{ keyType }}_key: file.managed: diff --git a/openssh/defaults.yaml b/openssh/defaults.yaml new file mode 100644 index 0000000..3c1b379 --- /dev/null +++ b/openssh/defaults.yaml @@ -0,0 +1,8 @@ +openssh: + sshd_config: /etc/ssh/sshd_config + sshd_config_src: salt://openssh/files/sshd_config + banner: /etc/ssh/banner + banner_src: salt://openssh/files/banner + ssh_known_hosts: /etc/ssh/ssh_known_hosts + dig_pkg: dnsutils + ssh_moduli: /etc/ssh/moduli diff --git a/openssh/files/ssh_known_hosts b/openssh/files/ssh_known_hosts new file mode 100644 index 0000000..a5040c5 --- /dev/null +++ b/openssh/files/ssh_known_hosts @@ -0,0 +1,38 @@ +{# +# vi:syntax=jinja +#} + +{%- set target = salt['pillar.get']('openssh:known_hosts:target', '*') -%} +{%- set expr_form = salt['pillar.get']('openssh:known_hosts:expr_form', 'glob') -%} +{%- set keys_function = salt['pillar.get']('openssh:known_hosts:mine_keys_function', 'public_ssh_host_keys') -%} +{%- set hostname_function = salt['pillar.get']('openssh:known_hosts:mine_hostname_function', 'public_ssh_hostname') -%} +{#- Lookup IP of all aliases so that when we have a matching IP, we inject the alias name + in the SSH known_hosts entry -#} +{%- set aliases = salt['pillar.get']('openssh:known_hosts:aliases', []) -%} +{%- set aliases_ips = {} -%} +{%- for alias in aliases -%} + {%- for ip in salt['dig.A'](alias) + salt['dig.AAAA'](alias) -%} + {%- do aliases_ips.setdefault(ip, []).append(alias) -%} + {%- endfor -%} +{%- endfor -%} +{#- Loop over targetted minions -#} +{%- set host_keys = salt['mine.get'](target, keys_function, expr_form=expr_form) -%} +{%- set host_names = salt['mine.get'](target, hostname_function, expr_form=expr_form) -%} +{%- for host, keys in host_keys|dictsort -%} + {%- set ip4 = salt['dig.A'](host) -%} + {%- set ip6 = salt['dig.AAAA'](host) -%} + {%- set names = [host_names.get(host, host)] -%} + {%- for ip in ip4 + ip6 -%} + {%- do names.append(ip) -%} + {%- for alias in aliases_ips.get(ip, []) -%} + {%- if alias not in names -%} + {%- do names.append(alias) -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {%- for line in keys.split('\n') -%} + {%- if line -%} +{{ ','.join(names) }} {{ line }} +{% endif -%} + {%- endfor -%} +{%- endfor -%} diff --git a/openssh/files/sshd_config b/openssh/files/sshd_config index f0be13a..1e4b76e 100644 --- a/openssh/files/sshd_config +++ b/openssh/files/sshd_config @@ -139,7 +139,7 @@ # Restricting Users and Hosts # example: -# AllowUsers vader@10.0.0.1 maul@sproing.evil.com luke +# AllowUsers vader@10.0.0.1 maul@sproing.evil.com luke # AllowGroups wheel staff # # Keep in mind that using AllowUsers or AllowGroups means that anyone @@ -156,6 +156,15 @@ # AllowGroups {{ option('AllowGroups', '') }} +# Specifies the available KEX (Key Exchange) algorithms. +{{ option('KexAlgorithms', 'ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1') }} + +# Specifies the ciphers allowed for protocol version 2. +{{ option('Ciphers', 'aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se') }} + +# Specifies the available MAC (message authentication code) algorithms. +{{ option('MACs', 'hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-sha2-256,hmac-sha2-256-96,hmac-sha2-512,hmac-sha2-512-96,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96') }} + {# Handling unknown in salt template options #} {%- for keyword in sshd_config.keys() %} {#- Matches have to be at the bottem and should be handled differently -#} diff --git a/openssh/known_hosts.sls b/openssh/known_hosts.sls new file mode 100644 index 0000000..8f8d2a8 --- /dev/null +++ b/openssh/known_hosts.sls @@ -0,0 +1,16 @@ +{% from "openssh/map.jinja" import openssh with context %} + +ensure dig is available: + pkg.installed: + - name: {{ openssh.dig_pkg }} + +manage ssh_known_hosts file: + file.managed: + - name: {{ openssh.ssh_known_hosts }} + - source: salt://openssh/files/ssh_known_hosts + - template: jinja + - user: root + - group: root + - mode: 644 + - require: + - pkg: ensure dig is available diff --git a/openssh/map.jinja b/openssh/map.jinja index 566b265..639b81d 100644 --- a/openssh/map.jinja +++ b/openssh/map.jinja @@ -1,45 +1,56 @@ -{% set openssh = salt['grains.filter_by']({ +{## Start with defaults from defaults.yaml ##} +{% import_yaml "openssh/defaults.yaml" as default_settings %} + +{## +Setup variable using grains['os_family'] based logic, only add key:values here +that differ from whats in defaults.yaml +##} +{% set os_family_map = salt['grains.filter_by']({ + 'Arch': { + 'server': 'openssh', + 'client': 'openssh', + 'service': 'sshd', + }, 'Debian': { - 'server': 'openssh-server', - 'client': 'openssh-client', - 'service': 'ssh', - 'sshd_config': '/etc/ssh/sshd_config', - 'sshd_config_src': 'salt://openssh/files/sshd_config', - 'banner': '/etc/ssh/banner', - 'banner_src': 'salt://openssh/files/banner', + 'server': 'openssh-server', + 'client': 'openssh-client', + 'service': 'ssh', + }, + 'FreeBSD': { + 'service': 'sshd', + 'dig_pkg': 'bind-tools', + }, + 'Gentoo': { + 'server': 'net-misc/openssh', + 'client': 'net-misc/openssh', + 'service': 'sshd', + 'dig_pkg': 'net-dns/bind-tools', }, 'RedHat': { - 'server': 'openssh-server', - 'client': 'openssh', - 'service': 'sshd', - 'sshd_config': '/etc/ssh/sshd_config', - 'sshd_config_src': 'salt://openssh/files/sshd_config', - 'banner': '/etc/ssh/banner', - 'banner_src': 'salt://openssh/files/banner', + 'server': 'openssh-server', + 'client': 'openssh', + 'service': 'sshd', + 'dig_pkg': 'bind-utils', }, 'Suse': { - 'server': 'openssh', - 'client': 'openssh', - 'service': 'sshd', - 'sshd_config': '/etc/ssh/sshd_config', - 'sshd_config_src': 'salt://openssh/files/sshd_config', - 'banner': '/etc/ssh/banner', - 'banner_src': 'salt://openssh/files/banner', - }, - 'FreeBSD': { - 'service': 'sshd', - 'sshd_config': '/etc/ssh/sshd_config', - 'sshd_config_src': 'salt://openssh/files/sshd_config', - 'banner': '/etc/ssh/banner', - 'banner_src': 'salt://openssh/files/banner', + 'server': 'openssh', + 'client': 'openssh', + 'service': 'sshd', + 'dig_pkg': 'bind-utils', }, - 'Arch': { - 'server': 'openssh', - 'client': 'openssh', - 'service': 'sshd.socket', - 'sshd_config': '/etc/ssh/sshd_config', - 'sshd_config_src': 'salt://openssh/files/sshd_config', - 'banner': '/etc/ssh/banner', - 'banner_src': 'salt://openssh/files/banner', - }, -}, merge=salt['pillar.get']('openssh:lookup')) %} + } + , grain="os_family" + , merge=salt['pillar.get']('openssh:lookup')) +%} + +{## Merge the flavor_map to the default settings ##} +{% do default_settings.openssh.update(os_family_map) %} + +{## Merge in openssh:lookup pillar ##} +{% set openssh = salt['pillar.get']( + 'openssh', + default=default_settings.openssh, + merge=True + ) +%} + diff --git a/openssh/moduli.sls b/openssh/moduli.sls new file mode 100644 index 0000000..d871006 --- /dev/null +++ b/openssh/moduli.sls @@ -0,0 +1,8 @@ +{% from "openssh/map.jinja" import openssh with context %} + +{% if salt['pillar.get']('openssh:moduli', False) %} +ssh_moduli: + file.managed: + - name: {{ openssh.ssh_moduli }} + - contents_pillar: openssh:moduli +{% endif %} diff --git a/pillar.example b/pillar.example index e3f7a27..80af4a6 100644 --- a/pillar.example +++ b/pillar.example @@ -22,7 +22,7 @@ sshd_config: HostbasedAuthentication: 'no' PermitEmptyPasswords: 'no' ChallengeResponseAuthentication: 'no' - AuthenticationMethods 'publickey,keyboard-interactive' + AuthenticationMethods: 'publickey,keyboard-interactive' X11Forwarding: 'yes' X11DisplayOffset: 10 PrintMotd: 'no' @@ -36,7 +36,6 @@ sshd_config: DenyUsers: 'yoda chewbaca@112.10.21.1' AllowGroups: 'wheel staff imperial' DenyGroups: 'rebel' - Deny matches: sftp_chroot: type: @@ -46,20 +45,31 @@ sshd_config: X11Forwarding: no AllowTcpForwarding: no ForceCommand: internal-sftp + # Check `man sshd_config` for supported KexAlgorithms, Ciphers and MACs first. + KexAlgorithms: 'diffie-hellman-group14-sha1,diffie-hellman-group1-sha1' + Ciphers: 'aes128-ctr,aes256-ctr' + MACs: 'hmac-sha1' openssh: auth: - joe: - - name: JOE_VALID_SSH_PUBLIC_KEY + joe-valid-ssh-key-desktop: + - user: joe present: True enc: ssh-rsa - comment: main key - - name: JOE_NON_VALID_SSH_PUBLIC_KEY + comment: main key - desktop + joe-valid-ssh-key-notebook: + - user: joe + present: True + enc: ssh-rsa + comment: main key - notebook + joe-non-valid-ssh-key: + - user: joe present: False enc: ssh-rsa comment: obsolete key - removed generate_dsa_keys: False + absent_dsa_keys: False provide_dsa_keys: False dsa: private_key: | @@ -70,6 +80,7 @@ openssh: ssh-dss NOT_DEFINED generate_ecdsa_keys: False + absent_ecdsa_keys: False provide_ecdsa_keys: False ecdsa: private_key: | @@ -80,6 +91,8 @@ openssh: ecdsa-sha2-nistp256 NOT_DEFINED generate_rsa_keys: False + generate_rsa_size: 4096 + absent_rsa_keys: False provide_rsa_keys: False rsa: private_key: | @@ -90,6 +103,7 @@ openssh: ssh-rsa NOT_DEFINED generate_ed25519_keys: False + absent_ed25519_keys: False provide_ed25519_keys: False ed25519: private_key: | @@ -98,3 +112,36 @@ openssh: -----END OPENSSH PRIVATE KEY----- public_key: | ssh-ed25519 NOT_DEFINED + + known_hosts: + # The next 2 settings restrict the set of minions that will be added in + # the generated ssh_known_hosts files (the default is to match all minions) + target: '*' + expr_form: 'glob' + # Name of mining functions used to gather public keys and hostnames + # (the default values are shown here) + mine_keys_function: public_ssh_host_keys + mine_hostname_function: public_ssh_hostname + # List of DNS entries also pointing to our managed machines and that we want + # to inject in our generated ssh_known_hosts file + aliases: + - cname-to-minion.example.org + - alias.example.org + +# specify DH parameters (see /etc/ssh/moduli) + moduli: | + # Time Type Tests Tries Size Generator Modulus + 20120821045639 2 6 100 2047 2 DD2047CBDBB6F8E919BC63DE885B34D0FD6E3DB2887D8B46FE249886ACED6B46DFCD5553168185FD376122171CD8927E60120FA8D01F01D03E58281FEA9A1ABE97631C828E41815F34FDCDF787419FE13A3137649AA93D2584230DF5F24B5C00C88B7D7DE4367693428C730376F218A53E853B0851BAB7C53C15DA7839CBE1285DB63F6FA45C1BB59FE1C5BB918F0F8459D7EF60ACFF5C0FA0F3FCAD1C5F4CE4416D4F4B36B05CDCEBE4FB879E95847EFBC6449CD190248843BC7EDB145FBFC4EDBB1A3C959298F08F3BA2CFBE231BBE204BE6F906209D28BD4820AB3E7BE96C26AE8A809ADD8D1A5A0B008E9570FA4C4697E116B8119892C604293680B09D63 + 20120821045830 2 6 100 2047 2 DD2047CBDBB6F8E919BC63DE885B34D0FD6E3DB2887D8B46FE249886ACED6B46DFCD5553168185FD376122171CD8927E60120FA8D01F01D03E58281FEA9A1ABE97631C828E41815F34FDCDF787419FE13A3137649AA93D2584230DF5F24B5C00C88B7D7DE4367693428C730376F218A53E853B0851BAB7C53C15DA7839CBE1285DB63F6FA45C1BB59FE1C5BB918F0F8459D7EF60ACFF5C0FA0F3FCAD1C5F4CE4416D4F4B36B05CDCEBE4FB879E95847EFBC6449CD190248843BC7EDB145FBFC4EDBB1A3C959298F08F3BA2CFBE231BBE204BE6F906209D28BD4820AB3E7BE96C26AE8A809ADD8D1A5A0B008E9570FA4C4697E116B8119892C6042936814C2FFB + 20120821050046 2 6 100 2047 2 DD2047CBDBB6F8E919BC63DE885B34D0FD6E3DB2887D8B46FE249886ACED6B46DFCD5553168185FD376122171CD8927E60120FA8D01F01D03E58281FEA9A1ABE97631C828E41815F34FDCDF787419FE13A3137649AA93D2584230DF5F24B5C00C88B7D7DE4367693428C730376F218A53E853B0851BAB7C53C15DA7839CBE1285DB63F6FA45C1BB59FE1C5BB918F0F8459D7EF60ACFF5C0FA0F3FCAD1C5F4CE4416D4F4B36B05CDCEBE4FB879E95847EFBC6449CD190248843BC7EDB145FBFC4EDBB1A3C959298F08F3BA2CFBE231BBE204BE6F906209D28BD4820AB3E7BE96C26AE8A809ADD8D1A5A0B008E9570FA4C4697E116B8119892C60429368214FC53 + 20120821050054 2 6 100 2047 5 DD2047CBDBB6F8E919BC63DE885B34D0FD6E3DB2887D8B46FE249886ACED6B46DFCD5553168185FD376122171CD8927E60120FA8D01F01D03E58281FEA9A1ABE97631C828E41815F34FDCDF787419FE13A3137649AA93D2584230DF5F24B5C00C88B7D7DE4367693428C730376F218A53E853B0851BAB7C53C15DA7839CBE1285DB63F6FA45C1BB59FE1C5BB918F0F8459D7EF60ACFF5C0FA0F3FCAD1C5F4CE4416D4F4B36B05CDCEBE4FB879E95847EFBC6449CD190248843BC7EDB145FBFC4EDBB1A3C959298F08F3BA2CFBE231BBE204BE6F906209D28BD4820AB3E7BE96C26AE8A809ADD8D1A5A0B008E9570FA4C4697E116B8119892C60429368218E83F + +# Required for openssh.known_hosts +mine_functions: + public_ssh_host_keys: + mine_function: cmd.run + cmd: cat /etc/ssh/ssh_host_*_key.pub + python_shell: True + public_ssh_hostname: + mine_function: grains.get + key: id