Configuring my Machines with Bashtard

#Bash #FreeBSD #GNU+Linux #Programming

Over the past couple weeks I’ve been spending some time here and there to work on my own application for configuring my machines. Before this I’ve tried Ansible, but found it to be very convoluted to use, and requires a lot of conditionals if your machines aren’t all running the same base system.

So I made something in Bash, with a few abstractions to make certain interactions less annoying to do manually every time. This used to be called tyilnet, but I’ve discussed the setup with a few people on IRC, and decided it would be a fun project to make it a bit more agnostic, so other people could also easily start using it. This resulted in the creation of Bashtard, pronounced as “bash”, followed by “tard” (as in “bastard”).

It works by simply writing Bash scripts to do the configuration, and provides abstractions for using the system’s package manager, service manager, and some utilities such as logging and dealing with configured values. Configuration values can be set on a per-host or per-OS basis. Since I run a varied base of OSs, including Gentoo, Debian, and FreeBSD, the per-OS configuration comes in very handy to me.

As for the reason to use Bash, I chose it because most of the systems I run already have this installed, so it doesn’t add a dependency most of the time. I would’ve liked to do it in POSIX sh, but I feel that when you’re reaching a certain level of complexity, Bash offers some very nice features which can make your code cleaner, or less likely to contain bugs. Features such as [[ ]], local, and arrays come to mind.

I’ve been kindly asked to guide potential new users to writing their first Bashtard script, known as a playbook, so if you want to know about how it works in practice, keep on reading. If you’re satisfied with your current configuration management system, this might not be quite as interesting to you, so be warned.

The first steps for a new user would obviously to install Bashtard, as it’s not in any OSs package repositories yet. A Makefile is supplied in the repository, which should make this easy enough.

git clone https://git.tyil.nl/bashtard
cd bashtard
sudo make install
hash -r

Once installed, it needs some initialization.

bashtard init

This will create the basic structure in /etc/bashtard, including a playbooks.d. Inside this playbooks.d directory, any directory is considered to be a playbook, which requires a description.txt and a playbook.bash.

cd /etc/bashtard/playbooks.d
mkdir ssh
cd ssh
echo "OpenSSH configuration" > description.txt
$EDITOR playbook.bash

The playbook.bash needs to contain 3 functions which are used by bashtard, a playbook_add(), playbook_sync(), and playbook_del(). These will be called by the bashtard subcommand add, sync, and del respectively.

I generally start with the playbook_sync() function first, since this is the function that’ll ensure all the configurations are kept in sync with my desires. I want to have my own sshd_config, which needs some templating for the Subsystem sftp line. There’s a file_template function provided by bashtard, which does some very simple templating. I’ll pass it the sftp variable to use.

playbook_sync() {
    file_template sshd_config \
        "sftp=$(config "ssh.sftp")" \
        > /etc/ssh/sshd_config
}

Now to create the actual template. The file_template function looks for templates in the share directory inside the playbook directory.

mkdir share
$EDITOR share/sshd_config

Since I already know what I want my sshd_config to look like from previous installed systems, I’ll just use that, but with a variable for the Subsystem sftp value.

# Connectivity
Port 22
AddressFamily any
ListenAddress 0.0.0.0
ListenAddress ::

# Fluff
PrintMotd yes

# SFTP
Subsystem sftp ${sftp}

# Authentication
AuthorizedKeysFile /etc/ssh/authorized_keys .ssh/authorized_keys
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication no

# Allow tyil
Match User tyil
    PubkeyAuthentication yes

# Allow public key authentication over VPN
Match Address 10.57.0.0/16
    PubkeyAuthentication yes
    PermitRootLogin prohibit-password

The ${sftp} placeholder will be filled with whatever value is returned by config "ssh.sftp". And for this to work properly, we will need to define the variable somewhere. These are written to the etc directory inside a playbook. You can specify defaults in a file called defaults, and this can be overwritten by OS-specific values, which in turn can be overwritten by host-specific values.

mkdir etc
$EDITOR etc/defaults

The format for these files is a very simple key=value. It splits on the first = to determine what the key and value are. This means you can use = in your values, but not your keys.

ssh.sftp=/usr/lib/openssh/sftp-server

This value is correct for Debian and derivatives, but not for my Gentoo or FreeBSD systems, so I’ve created OS-specific configuration files for these.

mkdir etc/os.d
cat etc/os.d/linux-gentoo
ssh.sftp=/usr/lib64/misc/sftp-server
cat etc/os.d/freebsd
ssh.sftp=/usr/lib64/misc/sftp-server

My sshd_config template also specifies the use of a Motd, so that needs to be created as well. This can again be done using the template function.

file_template "motd" \
    "fqdn=${BASHTARD_PLATFORM[fqdn]}" \
    "time=$(date -u "+%FT%T")" \
    > /etc/motd

The motd template gets saved at share/motd.

          ████████╗██╗   ██╗██╗██╗        ███╗   ██╗███████╗████████╗
          ╚══██╔══╝╚██╗ ██╔╝██║██║        ████╗  ██║██╔════╝╚══██╔══╝
             ██║    ╚████╔╝ ██║██║        ██╔██╗ ██║█████╗     ██║
             ██║     ╚██╔╝  ██║██║        ██║╚██╗██║██╔══╝     ██║
             ██║      ██║   ██║███████╗██╗██║ ╚████║███████╗   ██║
             ╚═╝      ╚═╝   ╚═╝╚══════╝╚═╝╚═╝  ╚═══╝╚══════╝   ╚═╝

Welcome to ${fqdn}, last updated on ${time}.

Lastly, we want to ensure the SSH daemon gets reloaded after every sync, so let’s add that to the playbook_sync() function as well.

svc reload "sshd"

The svc utility looks for a configuration value that starts with svc., followed by the service you’re trying to act upon, so in this case that would be svc.sshd. We can add this to our configuration files in etc. Across all my machines, sshd seems to work as the value, so I only need to add one line to etc/defaults.

svc.sshd=sshd

This should take care of all the things I want automatically synced. The playbook_add() function is intended for all one-time setup required for any playbook. In this case that means the SSH daemon’s service needs to be activated, since it is not active by default on all my setups.

playbook_add() {
    svc enable "sshd"
    svc start "sshd"
}

However, add does not call a sync, and I don’t want my SSH service to run with default configuration until a sync is initialized. So before enabling and starting the service, I will call sync manually, by running a playbook_sync first. This in turn, however, poses another problem, as playbook_sync() wants to reload the service, which it can’t do unless it is already running. To fix this, I’ll add an if statement to skip reloading if bashtard is running the add command.

playbook_add() {
    playbook_sync

    svc enable "sshd"
    svc start "sshd"
}

playbook_sync() {
    ...

    [[ $BASHTARD_COMMAND == "add" ]] && return

    svc reload "sshd"
}

Now, bashtard add sshd will run the playbook_add() function, which calls the playbook_sync() function before enabling and starting the sshd service. All that is left is the playbook_del() function, which only really needs to stop and disable the service. The templated files can be removed here as well if desired, of course.

playbook_del() {
    svc stop "sshd"
    svc disable "sshd"
}

Lastly, I configured my crond to run bashtard sync every 20 minutes, so whenever I update my configurations, it can take up to 20 minutes to propagate to all my machines. Having an abstraction to deal with cron (or SystemD timers where applicable) in Bashtard is something I’d like to add, but I have no concrete plans on how to do this, yet.

You can find the full playbook.bash source on git.tyil.nl.