I’ve previously written about managing the OpenSSH server with Ansible. That example focused on my BSD servers. I also manage Ubuntu and CentOS machines as well as my FreeBSD and OpenBSD. While the BSD machines are very similar, Ubuntu and CentOS might are two different operating systems. Can I manage all of them by hand? Sure. But some of these call their SSH service ssh, while others call it sshd. They store their SFTP servers in different directories.
I want to manage all of these simultaneously. With one script. When I need to change my sshd configuration, I want the change to propagate across the network, to all of my machines, simultaneously.
Ansible makes it pretty easy to write a single sshd configuration that works on all systems, through templates. A template is a Jinja2 file that fills in variables. Jinja2 is closely related to Python, but it isn’t Python.
The first thing you must do is decide what how you want your SSH server configured. Most operating systems ship a very inclusive example sshd_config, with most options commented out out. But to see how your SSH server is actually configured, run sshd -T
.
# sshd -T
port 22
protocol 2
addressfamily any
listenaddress [::]:22
listenaddress 0.0.0.0:22
usepam 1
serverkeybits 1024
logingracetime 120
keyregenerationinterval 3600
x11displayoffset 10
…
This prints out the running sshd configuration, with all the options as the running instance uses them.
Generate this configuration across your operating systems. Compare them. Which settings do you really want? What differences don’t matter in your environment? Which differences are set in your server by default? (If you aren’t familiar with the innards of OpenSSH configuration, permit me to suggest a book on the subject.)
Some operating systems set their default sshd_config options differently. For example, FreeBSD’s default is UsePam yes. CentOS and Ubuntu default to UsePam no. I’m safest if I specify UsePAM in my configuration file, to avoid ambiguity.
But OpenBSD doesn’t use PAM. If I use that configuration item in an sshd_config for that machine, then sshd won’t run.
Similarly, there’s the location of the sftp-server program. Different operating systems store these helper programs in different locations.
The way to handle all of these is to use a group variables, and assign the things that change a variable. Here’s my new cross-platform template for sshd_config.
#{{ ansible_managed }}
Protocol 2
ListenAddress {{ ansible_ssh_host }}
X11Forwarding yes
PubkeyAuthentication yes
AuthorizedKeysFile /etc/ssh/authorized_keys/%u
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
{% if ansible_system == 'OpenBSD' %}
#no PAM on OpenBSD
{% else %}
UsePAM yes
{% endif %}
Subsystem sftp {{ program_sftp_server }}
Each per-operating-system group file defines the variable program_sftp_server. Ansible automatically populates the variable ansible_system with the operating system name. And ansible_ssh_host comes from a per-host variable.
I would rather use something like “unless ansible_system=’OpenBSD'” to print UsePAM in the configuration file for the operating systems that need it, but that doesn’t seem to be an option in Jinja2.
The playbook changes a little as well:
---
- hosts: managed-sshd
user: ansible
sudo: yes
tasks:
- name: create key directory
action: file path=/etc/ssh/authorized_keys state=directory
owner=0 group=0 mode=0755
- name: upload user key
action: copy src=/home/ansible/crossplatform/etc/ssh/authorized_keys/{{ item }}
dest=/etc/ssh/authorized_keys/
owner=0 group=0 mode=644
with_items: sshusers
- name: sshd configuration file update
template: src=/home/ansible/crossplatform/etc/ssh/common-sshd_config.j2
dest=/etc/ssh/sshd_config
backup=yes
owner=0 group=0 mode=0644
validate='/usr/sbin/sshd -T -f %s'
notify:
- restart sshd
handlers:
- name: restart sshd
service: name={{ service_sshd }} state=restarted
The key difference here is that I’ve defined a per-OS variable, service_sshd. The operating systems can call their sshd servers sshd, ssh, or derangedweasel for all I care. I figure it out once, set it in the variable file, and get on with my life.
A new book is emerging..
Not quite yet. Ansible is moving too quickly to write a book on.
Marvelous, Thanks for sharing this info. Looks great.
Out of curiosity, why are you using
AuthorizedKeysFile /etc/ssh/authorized_keys/%u
? Is it because it’s easier than copying the keys to various home directories or there’s another reason?Chistian, it’s a question of scale and authorization.
We don’t want users managing their own authorized_keys files. We have hundreds of hosts. Users are expected to change their keypairs every so often. This means that we need to have an easy way to update those key files everywhere. We also don’t want an intruder who breaks into a desktop uploading their own authorized_keys file to a host.