Skip to content

Ensure a well-working pet server!

The Ensure logo, a red panda carressing a server

Ensure: A local idempotent configuration management library for Bash.
status-badge

Readme-driven Development!

This is a work in progress, and the README doesn't yet reflect fully what the framework can do for you. Many of the ensure functions are not yet implemented, you can find a full list of the progress at the bottom of this file.

Inspired by the big configuration management tools like Ansible or PyInfra, Ensure offers a collection of pure bash functions that ensure a specific state on a machine. The functions are idempotent, which in this context means that running your script multiple times will not break stuff and usually be really fast.

The intended and easiest use-case is to run a script locally on the same machine that should be configured, making this well-suitable for "pets not cattle" deployments in homelabs or for small vServers. But with some helper scripts it should also be usable for bigger infrastructure deployments.

Screenshot of the example script's output

Project Scope

  • Linux-based systems with bash & busybox/coreutils
  • Universal concepts instead of software-specific functions (e.g. containers instead of Docker/Podman/whatever, packages instead of apt/yum/whatever, ...)
  • Out of scope are advanced software-specific configurations, unless very simple or common
  • Out of scope are complex setups that abstract a lot from the bare Linux workflow. You can do that downstream, but from Ensure itself should be easy to derive from a function's name what exactly it does under the hood.

Installation

The preferred method is to use it as a submodule in a git repo:

git init
git submodule add https://codeberg.org/momar/ensure.git
cat <<EOF > setup.sh
#!/usr/bin/env bash
set -euo pipefail     # Enable strict mode
. ./ensure/ensure.sh  # Load ensure.sh functions

# TODO: start here with your setup!
EOF
chmod +x setup.sh
./setup.sh
git add -A
git commit -m "Setup ensure.sh & create the setup.sh script"

Example

Here's a quick example how to secure an Ubuntu or Debian server, based on Tecmint: How to Secure an Ubuntu Server for Production as well as enabling unattended upgrades for all packages:

#!/usr/bin/env bash
set -euo pipefail     # Enable strict mode
. ./ensure/ensure.sh  # Load Ensure

# Check that we're running as a non-root user, otherwise create one and run again as it
# TODO: not yet implemented
#if [ $(id -u) -eq 0 ]; then
#    UNPRIVILEGED_USERNAME=user
#    ensure_user_present --sudoer=true --initial-password="CHANGEME" "$UNPRIVILEGED_USERNAME"
#    if [ $ENSURE_CHANGED -eq 1 ]; then passwd "$UNPRIVILEGED_USERNAME"; fi  # Ask the user to set a password for the user!
#    exec sudo -u user "$0" "$@"  # Run script again, this time as the non-root user
#    # exec forks and thus ends the script here!
#fi

# Update packages, install SSH, Fail2Ban, UFW, AppArmor and Unattended Upgrades
ensure_package_up_to_date
ensure_package_up_to_date openssh-server fail2ban ufw unattended-upgrades

# Enable UFW, only allow essential services
# TODO: not yet implemented
#ensure_ufw_configured <<EOF
#default deny incoming
#default allow outgoing
#allow ssh
#allow http
#allow https
#limit ssh
#EOF

# Enable fail2ban
ensure_file_content_equals /etc/fail2ban/jail.local <<EOF
[sshd]
enabled = true
port = ssh
maxretry = 5
bantime = 3600  # Ban IP for 1 hour
findtime = 600  # Count attempts within 10 minutes
EOF
# ensure_service_active --enable --restart fail2ban  # TODO: NYI

# Enable AppArmor
# ensure_apparmor_mode enforce  # TODO: NYI

# Enable unattended upgrades for all packages and allow reboots
ensure_file_content_equals /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
Unattended-Upgrade::Origins-Pattern {
    "origin=*";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Mail "TODO@invalid";
Unattended-Upgrade::MailReport "only-on-error";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
EOF

# Set up SSH
ensure_file_content_includes ~/.ssh/authorized_keys <<EOF
ssh-rsa CHANGEME # TODO: paste your keys here!
EOF
if grep -F "CHANGEME" ~/.ssh/authorized_keys; then
    __ensure_fail "Please set your SSH public key in ~/.ssh/authorized_keys! Not continuing further to avoid locking you out!"
    exit 1
fi
ensure_file_content_includes --match='\s*#?\s*PasswordAuthentication\s.*' /etc/ssh/sshd_config <<< "PasswordAuthentication no"
#[ $ENSURE_CHANGED -eq 0 ] || ensure_service_active --reload ssh  # TODO: NYI

Rules for Creating Ensure Functions

  • Functions should be documented using shellman syntax.
  • Don't use single-character options, rather write them out (something explicit like --apply-recursively is much better than -r in this case).
  • You must set the special variable ENSURE_CHANGED to 0 at the start, and to 1 when changes were made before returning.
  • Values other than 0 must only be returned when preconditions are not satified or a fatal error occured.
  • When an action is already idempotent, but could take longer than a second (e.g. apt-get install) or require network access, it is recommended to write your own check to verify whether the action needs to be run.
  • All functions must follow the naming scheme ensure_<subject>_<state>, each translating to the English sentence "Ensure that [the] [is/are] ", e.g. "Ensure that the file contents include" or "Ensure that the user is present".
    • The function name describes what the state after running the function will look like (i.e. "exists" instead of "created")
    • The state word should describe the caused state in its whole (i.e. "exists" should only create but not modify, otherwise use the special word "present").
    • The "object" of the sentence should usually (except in very complex functions) be arguments of broader functions (e.g. ensure_file_present --mode=0775 file.txt instead of ensure_file_has_mode 0775 file.txt)

Project Progress

  • ensure_file_content_includes
  • ensure_file_content_equals
  • ensure_file_present
  • ensure_file_absent
  • ensure_package_cache_recent
  • ensure_package_up_to_date
    • non-apt package managers
  • ensure_package_absent
  • ensure_user_present [--sudoer=true] [--initial-password=...] <user>
  • ensure_service_active [--enable] [--restart|--reload] <service…>
  • ensure_service_inactive [--disable] <service…>
  • ensure_ufw_configured
  • ensure_apparmor_mode enforce|complain|disable
  • ensure_sysctl_set <key=value...> [net.core.wmem_max=8388608]
  • ensure_system_rebooted
  • ensure_cronjob_present
  • ensure_renovate_directory_up_to_date
  • ensure_url_download_up_to_date
  • ensure_git_repo_up_to_date