# 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](../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/.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: ```sh 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: ```yaml 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. 1. Inspect the host and its current listeners: ```sh ansible-playbook playbooks/discover.yml -l server1 ``` 2. Bootstrap the admin account through the existing SSH account: ```sh ansible-playbook playbooks/bootstrap-admin.yml -l server1 --ask-become-pass --ask-vault-pass ``` Omit `--ask-become-pass` if the existing account already has passwordless sudo. Verify a fresh SSH login as `admin_user` on the old port and check that `sudo` asks for that account's password. 3. Change `ansible_user` in the host inventory to `admin_user`. Keep `ansible_port` on the old port, and leave `ssh_keep_legacy_port: true`. Apply hardening: ```sh ansible-playbook playbooks/harden.yml -l server1 -K --ask-vault-pass ``` UFW 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. 4. Verify a fresh SSH login as the admin on `ssh_port`, plus UFW and Fail2ban status. Change `ansible_port` to `ssh_port`, set `ssh_keep_legacy_port: false`, and rerun the hardening command. The old SSH listener and its UFW rule are then removed. 5. Add or retire future service ports through that host's `ufw_allowed_rules` and `ufw_remove_rules`, then rerun the hardening playbook with `-l `. ## 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.