Security Best Practices
Security is not one tool you install — it is a set of habits you apply at every layer: the host (the server machine itself), the access paths (how people and apps log in), the data (what you store), the secrets (passwords and keys), and the pipeline (how code reaches production). This page is the capstone of the DevOps docs: a practical checklist you can work through top to bottom on a fresh Ubuntu server. Every item explains why it matters, because a checklist you don’t understand is one you’ll skip. We target Ubuntu 22.04/24.04 LTS throughout.
The layered model
A good way to think about security is “defence in depth” — many small walls instead of one big one. If an attacker gets past one layer (say, they steal a password), the next layer (key-only SSH, a firewall, least privilege) should still stop them. No single control below is enough on its own.
| Layer | What you protect | Main controls |
|---|---|---|
| Host | The server OS | Patching, firewall, fail2ban |
| Access | Logins and permissions | Key-only SSH, least privilege, sudo |
| Data | Stored information | Encryption, backups |
| Secrets | Passwords, API keys | Secret managers, never in Git |
| Pipeline | Your CI/CD (build/deploy automation) | Dependency and image scanning |
Host hardening
Hardening means removing or locking down anything an attacker could use. Start by keeping the system patched — most real-world breaches exploit known bugs that already have fixes.
sudo apt update && sudo apt upgrade -y
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Output:
Configuring unattended-upgrades
Automatically download and install stable updates? Yes
unattended-upgrades automatically installs security patches, so you stay protected even when you forget to log in. Next, put up a firewall with ufw (Uncomplicated Firewall) — deny everything by default, then allow only what you need.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow "22"/tcp
sudo ufw allow "80","443"/tcp
sudo ufw enable
Output:
Firewall is active and enabled on system startup
A closed port can’t be attacked. If a service doesn’t need to be reachable from the internet (databases especially), do NOT open its port. Bind databases to
127.0.0.1and reach them over SSH tunnels or a private network instead.
Block brute-force attempts with fail2ban
fail2ban watches your logs and temporarily bans IP addresses that repeatedly fail to log in. This stops automated password-guessing attacks cold.
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local to enable the SSH jail:
[sshd]
enabled = true
maxretry = 4
bantime = 1h
findtime = 10m
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Output:
Status for the jail: sshd
|- Filter
| `- Currently failed: 1
`- Actions
`- Currently banned: 3
Access: least privilege and key-only SSH
Least privilege means every user and process gets the minimum access it needs — nothing more. If an account is compromised, the damage is limited to what that account could do. In practice: don’t run apps as root, give each service its own system user, and grant sudo only to humans who need it.
The single biggest SSH win is turning off password logins entirely and using SSH keys (a cryptographic key pair where the private half never leaves your laptop). Keys can’t be guessed the way passwords can.
ssh-copy-id deploy@your-server
sudo nano /etc/ssh/sshd_config.d/hardening.conf
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
sudo systemctl restart ssh
Open a second terminal and confirm you can still log in before you close your current session. Locking yourself out of a cloud server with no console access is a painful, avoidable mistake.
Data: encryption and backups
Encryption protects data if the disk or a backup file is stolen. Always serve web traffic over HTTPS (encryption in transit) — get free certificates with Certbot.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d example.com
Backups protect you from ransomware, mistakes, and hardware failure. A backup you’ve never restored is just a hope — test it. Follow the 3-2-1 rule: 3 copies, on 2 types of media, with 1 copy off-site.
sudo -u postgres pg_dump appdb | gzip > /var/backups/appdb-$(date +%F).sql.gz
| Approach | When to use | When NOT to |
|---|---|---|
| Snapshots | Whole-server recovery, fast rollback | As your only backup (same provider = single point of failure) |
Logical dumps (pg_dump) | Portable, restore to any version | Very large databases (slow) |
Secrets: never in Git
A secret is anything that grants access — passwords, API keys, tokens, private keys. The classic disaster is committing one to Git, because Git history keeps it forever even after you “delete” it. Store secrets in environment files outside the repo, or in a secret manager.
echo ".env" >> .gitignore
git rm --cached .env
Scan your repo for leaked secrets before they reach a remote:
docker run --rm -v "$(pwd):/repo" zricethezav/gitleaks:latest detect --source=/repo
Output:
leaks found: 0
Pipeline: scan before you ship
Your CI/CD pipeline is the perfect place to catch problems automatically. Two scans matter most: dependency scanning (finds known-vulnerable libraries) and image scanning (finds vulnerabilities in your Docker images).
scan:
image: aquasec/trivy:latest
script:
- trivy fs --exit-code 1 --severity HIGH,CRITICAL .
- trivy image --exit-code 1 --severity CRITICAL myapp:latest
The --exit-code 1 flag fails the build when serious issues are found, so vulnerable code never gets deployed.
Best Practices
- Patch automatically with
unattended-upgrades— known, unpatched bugs cause most breaches. - Default-deny your firewall and only open ports a service truly needs.
- Disable password and root SSH login; use keys and a non-root deploy user.
- Run apps under their own unprivileged system user, never as
root. - Keep secrets out of Git, scan for leaks, and rotate any key that may have leaked.
- Encrypt traffic with HTTPS and test that your backups actually restore.
- Fail the CI/CD build on HIGH/CRITICAL findings so vulnerabilities can’t ship.