3.8 KiB
Host hardening
The playbooks target Debian or Ubuntu hosts using systemd. They create a separate admin account with password-required sudo and SSH public key access, configure Fail2ban and UFW, enable unattended security updates, disable SSH root and password login, and move SSH to a configurable port. The default replacement port is 2222; changing it reduces scan noise but is not a security boundary.
Configure inventory
Copy the tracked inventory examples as shown in README.md. Add each server under the debian_servers group in inventory/hosts.yml. Set ansible_user to an existing account with sudo access and ansible_port to its current SSH port. Put the admin public key in inventory/host_vars/<alias>.yml. Set admin_user, ssh_port, and ssh_legacy_port in the shared settings or override them per host. Every active inventory .yml file is ignored by Git.
Create a SHA-512 crypt password hash with openssl passwd -6, which prompts for the password. Copy inventory/group_vars/all/vault.yml.example to inventory/group_vars/all/vault.yml, insert the hash, and encrypt the file:
ansible-vault encrypt inventory/group_vars/all/vault.yml
Keep the Vault password outside the repository. The password hash is encrypted in Vault and its Ansible task output is suppressed.
Configure ingress
Add only required service ports to ufw_allowed_rules in a host's inventory file:
ufw_allowed_rules:
- port: "80"
proto: tcp
- port: "443"
proto: tcp
- port: "51820"
proto: udp
SSH ports are handled by ssh_port, ssh_legacy_port, and ssh_keep_legacy_port. A rule may set from_ip to restrict its source. To remove an existing rule, move its port, protocol, and source into ufw_remove_rules, run the playbook once, then remove that cleanup entry. UFW defaults to denying inbound and routed traffic and allowing outbound traffic.
Docker-published ports can bypass UFW's normal host INPUT filtering. Publish only intended host ports and keep application and database ports on Docker networks or bind them locally.
Roll out one host
Replace server1 with the host alias from inventory/hosts.yml in these commands. Keep a working SSH session open during the port change.
-
Inspect the host and its current listeners:
ansible-playbook playbooks/discover.yml -l server1 -
Bootstrap the admin account through the existing SSH account:
ansible-playbook playbooks/bootstrap-admin.yml -l server1 --ask-become-pass --ask-vault-passOmit
--ask-become-passif the existing account already has passwordless sudo. Verify a fresh SSH login asadmin_useron the old port and check thatsudoasks for that account's password. -
Change
ansible_userin the host inventory toadmin_user. Keepansible_porton the old port, and leavessh_keep_legacy_port: true. Apply hardening:ansible-playbook playbooks/harden.yml -l server1 -K --ask-vault-passUFW allows both SSH ports before it is enabled. SSH then listens on both ports. On hosts using
ssh.socket, the playbook updates the systemd socket listeners as well. -
Verify a fresh SSH login as the admin on
ssh_port, plus UFW and Fail2ban status. Changeansible_porttossh_port, setssh_keep_legacy_port: false, and rerun the hardening command. The old SSH listener and its UFW rule are then removed. -
Add or retire future service ports through that host's
ufw_allowed_rulesandufw_remove_rules, then rerun the hardening playbook with-l <alias>.
Ongoing work
- Review native listeners before closing a service port.
- Review container port publishing separately from UFW rules.
- Keep secrets in Vault and runtime secret files, and verify a backup restore.
- Review container updates after DIUN alerts; they are not applied automatically.