Files
pragmatic-deployer/docs/hardening.md
T

71 lines
3.8 KiB
Markdown

# 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/<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:
```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 <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.