Ensure a well-working pet server!

Ensure: A local idempotent configuration management library for Bash.
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.

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-recursivelyis much better than-rin this case). - You must set the special variable
ENSURE_CHANGEDto0at the start, and to1when changes were made before returning. - Values other than
0must 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.txtinstead ofensure_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