A deliberately defensive Ansible baseline for new VPS hosts with sane hardening defaults for:
- Rocky Linux 9+
- AlmaLinux 9+
- Debian 12+
- Ubuntu 22.04+
The playbook applies a small, distro-aware baseline:
- baseline admin packages
- upgrade installed distro packages after the initial firewall/SSH safety steps
- distro-native NTP/time synchronization with an explicit synchronization check
- persistent journald storage and split sysctl tuning/security drop-ins
- SSH hardening with a managed
sshd_config.ddrop-in firewalldon Rocky Linux / AlmaLinux hosts andufwon Debian-family hostsfail2ban- automatic security updates with
dnf-automaticorunattended-upgrades - zram via the distro-native generator package
Warning
This repo is intended as a secure bootstrap baseline for new VPS servers.
It is not designed to be applied blindly to already-running or long-lived servers. By default it can perform a full package upgrade, enforce SSH policy, enable automatic security updates, and activate host firewall management. Those are reasonable first-run bootstrap defaults, but they can be disruptive on an established host if you have not reviewed and adapted the variables first.
site.yml: main playbookinventory/hosts.yml: tracked example inventoryinventory/hosts.local.yml: optional local inventory override, ignored by gitgroup_vars/all.yml: baseline tunablesroles/: distro-aware roles
playbook.yml is kept as a compatibility wrapper that imports site.yml.
-
Install the required collections:
ansible-galaxy collection install -r requirements.yml
-
Add your servers to
inventory/hosts.local.ymlor editinventory/hosts.yml.site.ymltargets thevpsgroup, so put baseline-managed hosts there.For a fresh VPS where you are connecting as
root:all: children: vps: hosts: my-vps: ansible_host: 203.0.113.10 ansible_user: root ansible_become: false
For a host where you connect with a sudo-capable user:
all: children: vps: hosts: my-vps: ansible_host: 203.0.113.10 ansible_user: deploy
-
Adjust any defaults in
group_vars/all.yml.By default the firewall only opens the configured SSH port. Add any application ports you need to
firewall_allowed_tcp_portsorfirewall_allowed_udp_ports.On Rocky Linux / AlmaLinux hosts, the
firewalldbackend also supportsfirewall_allowed_servicesfor named firewalld services (e.g. HTTPS). On Debian-family hosts,firewall_allowed_servicesis not supported.On Debian-family hosts, the UFW backend rebuilds the managed ruleset when the desired policies, allowed port lists, or managed IPv6 setting change so removed ports are converged too. By default it also manages
/etc/default/ufwIPV6=explicitly based on detected default IPv6 connectivity; overridefirewall_ufw_ipv6if needed. It currently requiresfirewall_default_outgoing_policy: allow, because the baseline does not manage explicit outbound allow rules. When the managed UFW ruleset changes, the rebuild path usesufw resetand then reapplies the desired rules.The baseline keeps
zramenabled with a moderatebase_swappinessfor small web/app VPS instances. For DB-heavy hosts, consider disablingzramor loweringbase_swappiness.Time synchronization is enforced as part of the baseline. Increase
time_sync_wait_retriesortime_sync_wait_delayif your provider's NTP service typically takes longer to report synchronized. -
Run the baseline:
ansible-playbook site.yml
Or use the helper script:
./deploy.sh
The play already runs one host at a time (
serial: 1) and stops on the first host error (any_errors_fatal: true). Use-lif you want to start with a single named host anyway:ansible-playbook site.yml -l my-vps
- The playbook runs with
become: trueby default, but it can also be used during bootstrap asrootwithansible_become: false. - Installed distro packages are upgraded on every playbook run while
base_upgrade_installed_packages: true, after the initial firewall/SSH safety steps and time synchronization checks but before the remaining baseline config is applied. Disable it if you need to manage package upgrades separately. - Debian-family automatic updates are explicitly limited to security origins. On Ubuntu, this can leave some security-related updates pending if they require new dependencies from the non-security release pocket.
- On older Rocky Linux / AlmaLinux images, the initial package sync may erase obsolete legacy packages such as
network-scriptsso the host can move to the current package set cleanly. - On Rocky Linux / AlmaLinux hosts, the baseline ensures
NetworkManageris installed and enabled before that initial package sync. - On Rocky Linux / AlmaLinux hosts, the firewall role reconciles the selected firewalld zone more explicitly: it requires an explicit interface binding target, manages the zone target, removes stale ports/services from that zone, reloads firewalld to collapse runtime-only drift, and currently requires
firewall_default_outgoing_policy: allow. - Time synchronization is managed explicitly and must report synchronized before the play continues. The baseline uses
chronyon Rocky Linux / AlmaLinux hosts andsystemd-timesyncdon Debian-family hosts. - When changing
sshd_port, the play asserts the final firewall policy permits that port, temporarily keeps the current Ansible SSH port open, reconnects Ansible on the new port, and only then removes the transitional port allowance. - On Debian-family systemd hosts, the SSH role disables
ssh.socketand managesssh.servicedirectly sosshd_portchanges are authoritative even on images that default to socket activation. - On SELinux-enabled Rocky Linux / AlmaLinux hosts, non-default SSH ports are added to the SELinux
ssh_port_tpolicy, and only the last custom SSH SELinux port previously managed by this repo is removed again whensshd_portchanges. zram_enabledandautomatic_updates_enabledcurrently control whether those roles run on future plays. Setting them tofalsedoes not remove zram or automatic update configuration that a previous run already applied.- SSH password auth is disabled by default, so ensure key-based access is working before applying it.
- The SSH role refuses to disable password auth unless one of the checked users has a non-empty
authorized_keysfile. By default it checksansible_user; overridesshd_authorized_keys_check_usersor setsshd_skip_authorized_keys_check: trueif you rely on external SSH auth such asAuthorizedKeysCommandor SSH certificates. - On Rocky Linux / AlmaLinux hosts, EPEL is enabled by default because
fail2banis commonly sourced from it.
If the current controller-to-host SSH port is not already represented by
ansible_port, set sshd_current_connection_port_override explicitly for the
first port-change run.
When host key verification is enabled, expect the first connection to the new
port to be treated as a separate known_hosts entry. Review the new-port host
key rather than blindly trusting it:
ssh-keygen -F "[203.0.113.10]:2222"
ssh-keygen -R "[203.0.113.10]:2222"
ssh-keyscan -p 2222 203.0.113.10