Skip to content
Kareem Schultz.
Linux & SecurityGuide 21 min read

A practical guide to hardening an Ubuntu Linux server or VPS

A step-by-step security baseline for Ubuntu servers: SSH access, sudo, UFW, updates, AppArmor, Fail2ban, logs, backups and recovery—with safe, copyable example commands.

Kareem Schultz

Technology enthusiast & systems builder · Guyana

Conceptual Linux server, shield and layered SSH, firewall, patching and monitoring controls
Original conceptual illustration. This is a guide for authorized administrators, not an automated or previously executed server audit.
Jump to a section

Start with the most important rule: do not lock yourself out

Hardening should reduce risk without destroying the recovery path. It is tempting to copy a long “security script” into a newly rented VPS and let it change everything in one pass. That is risky: an SSH configuration mistake, a firewall reset or an unexpected network setting may disconnect the only available administrator.

This guide targets Ubuntu LTS servers and VPSs, especially 24.04 LTS and newer supported releases. The commands are examples—not a universal production policy. Use the provider’s out-of-band console, take a snapshot, record existing services and keep an authenticated terminal open until a second connection has been verified.

The interactive command builder below uses fictitious values by default. It does not connect to a VPS, store your entries, execute commands or upload private credentials.

  • Use the guide only on systems you own or administer with authorization.
  • Audit first; change one layer at a time, with a documented rollback.
  • Use a test VPS or staging environment to validate any hardening automation.
  • For routed/VPN/Docker hosts, use role-specific networking rules instead of a generic baseline.

Your safe command workshop

Choose a sample administrator username, host address and existing SSH port. The blocks update locally so you can copy commands that match your own environment. Audit commands are shown first; steps that change the host are clearly marked as requiring review.

The values are never sent to a server. Username and host inputs are restricted to simple, safe formats so a typed command cannot become a shell injection. The host is the address you SSH to, not necessarily the public hostname of every application on that machine.

Provider-specific recovery: Vultr, DigitalOcean and Linode

Before applying host rules, learn the controls outside the VM. Most providers offer a cloud firewall, backups and a recovery console, but the exact access method differs. Compare the provider cards below and open the linked documentation before making an irreversible change.

Vultr describes its View Console, firewall groups and snapshot/backup workflow; DigitalOcean separates its normal network-dependent Droplet Console from the out-of-band Recovery Console; Akamai Cloud Linode uses Lish and Rescue Mode. Verify that the relevant console works with your account and available local credentials before disabling SSH authentication methods.

Cloud firewalls filter traffic before it reaches the host, while UFW or nftables protects the guest operating system. These are complementary security layers, and a mistake in either layer can block access. A provider image or snapshot is a rollback aid, not a substitute for encrypted, application-aware, tested off-host backups.

1. Baseline inventory and a recovery plan

Before installing packages or changing settings, find out what is already running. Record the Ubuntu release, kernel, listening ports, firewall rules, active network interfaces, routes, SSH configuration and current privileged identities. On a rented VPS, check the provider’s security groups and rescue console independently of the operating system.

Decide how you will get back in if the SSH daemon does not restart, the address changes or a firewall rule is wrong. A snapshot is helpful for rollback but does not replace a tested backup you can restore to another machine. Record who is authorized to make the change, what services depend on the host, and which ports must remain reachable.

Read-only inventory — inspect before changes
bash
cat /etc/os-releaseuname -ridsudo ss -tulpnip -brief addressip routesudo ufw status numberedsudo sshd -tsudo sshd -T | grep -E "^(port|permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication) "systemctl --failed

2. Create a separate administrator and confirm sudo

Use a named non-root account for administration. Ubuntu’s adduser workflow typically creates the home directory and prompts for a password. Then add that user to sudo only if their responsibilities require administrative privileges. Configure an individual SSH public key from your own workstation using ssh-copy-id or a secure provisioning method; do not publish the key in documentation.

A common mistake is creating a sudo user and immediately locking its password. SSH public-key login can still work, but sudo may prompt for a password that the user cannot supply. Keep passwordless privilege escalation out of generic public recipes: it needs a deliberate, narrowly scoped policy and tested alternatives.

Open a second terminal using the new account, test sudo -v and verify that your recovery path still works before changing root login or password-based SSH authentication.

3. Lock down SSH carefully

Ubuntu includes /etc/ssh/sshd_config.d/*.conf from its main configuration. For most OpenSSH settings, the first value read takes precedence. A late append to sshd_config is not guaranteed to override cloud-init or earlier snippets, and appending after a Match block can change the meaning of a directive.

Use a reviewed configuration drop-in with a name that wins the intended ordering, after inspecting the effective settings and any Match rules. Disabling password and root login is appropriate only after confirming a tested key-based, privileged alternative. Prefer changing the minimum necessary settings over switching off port forwarding, agent forwarding, X11 and every authentication feature by default; tunnels, management agents or applications may rely on them.

Validate with sshd -t, inspect sshd -T, reload rather than forcefully replace sessions, and test a separate login from your workstation. Do not close the original authenticated session until the verification passes.

Example SSH drop-in content — review before saving
ini
# /etc/ssh/sshd_config.d/01-local-hardening.conf# Only after a second administrator login works:PermitRootLogin noPubkeyAuthentication yesPasswordAuthentication noKbdInteractiveAuthentication noMaxAuthTries 4X11Forwarding no# Do not disable forwarding if your workflows need SSH tunnels.# Check cloud-init snippets and Match rules first.

4. Firewalls: protect what runs, not just port 22

Start with the cloud provider firewall or security group and then the host firewall. An allowed SSH connection should be intentionally reachable from a trusted source; where possible, prefer a VPN or allow-listed management IP to unrestricted internet-wide SSH. The correct port is the one currently serving your SSH daemon, not necessarily 22.

On Ubuntu, UFW simplifies the host rules. Add the existing SSH rule and any genuinely required application ports before enabling a deny-by-default inbound policy. A web server may need 80 and 443, a database usually should not be exposed publicly, and a VPN endpoint may need its own UDP port. Do not reset an existing firewall or assume a container runtime always obeys UFW’s expected path.

Remember IPv6, provider-level ACLs, reverse proxies, Docker or other container runtimes, and outbound paths. Port changes do not replace authentication controls.

5. Keep Ubuntu patched without surprise reboots

Install supported security updates and confirm unattended-upgrades is configured for your release. On recent Ubuntu Server installations it is often present and enabled already. The package reads settings from /etc/apt/apt.conf.d/, and Ubuntu recommends using a later-numbered drop-in rather than editing the packaged 50unattended-upgrades file directly.

For production, choose a maintenance and reboot policy. An update may require a restart; simply turning on automatic reboot can interrupt workloads. Check /var/run/reboot-required, apt history and unattended-upgrades logs. If a host uses third-party repositories, do not assume the default update policy includes those packages.

Inspect updates and dry-run unattended upgrades
bash
sudo apt updateapt list --upgradablesudo apt install unattended-upgradessystemctl list-timers apt-daily.timer apt-daily-upgrade.timersudo unattended-upgrade --dry-run --debugsudo test -f /var/run/reboot-required && echo "Reboot required" || true

6. Fail2ban, AppArmor and service-specific protection

Fail2ban can slow repeated authentication attacks, but it does not replace strong keys, firewall access restrictions or patching. On newer Ubuntu releases the SSH jail may read from systemd journal rather than /var/log/auth.log. Check the packaged defaults and verify the active jail backend before applying a copied configuration.

AppArmor provides mandatory access controls for profiled applications. Ubuntu already loads AppArmor on supported installations. Inspect with aa-status, review denials and start with an application-specific profile plan instead of running aa-enforce on every file in /etc/apparmor.d; that can break services.

Only enable mail or web jails for services actually present and with working log sources. If you do not run Postfix, installing a mail server just to send alerts creates extra maintenance and network exposure; choose an existing monitored notification path instead.

ControlGood defaultAvoid
Fail2banVerify sshd jail, backend and ban actionForcing a nonexistent /var/log/auth.log path
AppArmorInspect and tune profiles for running workloadsBlind aa-enforce on every profile
Email alertsUse a verified relay/monitoring providerDeploying public Postfix by default
Host toolsInstall what serves a documented controlLarge security package lists without a purpose

7. Logs, detection and alert quality

Logs are useful when they answer a question and are retained long enough to investigate. Prefer the existing journal, SSH authentication logs, service-specific logs and an external log destination where warranted. Set sensible rotation and disk usage alerts; avoid blanket audit rules capturing every bind and connect syscall on a busy server until you have measured their volume and overhead.

AIDE provides file-integrity baselines, but its database must be initialized securely, moved or protected appropriately and periodically reviewed. Rootkit checkers are supporting signals—not proof that a system is uncompromised. Lynis or the Ubuntu Security Guide can help identify missing controls, but every suggestion still needs workload validation.

A script that emails every failed login every ten minutes will become noise. Use thresholds, cooldowns, deduplicated incidents, verified delivery and a recovery notification when the service returns.

Read-only service and audit status
bash
sudo journalctl -u ssh --since "1 day ago" --no-pagersudo fail2ban-client status sshdsudo aa-statussudo ufw status verbosesudo ss -tulpnjournalctl --disk-usagesystemctl --failed

8. Protect the data and plan for recovery

A hardened system can still suffer operator mistakes, disk loss, ransomware, a cloud provider outage or a damaged configuration. Use encrypted off-host backups, a retention policy and restore drills. Test a full restore, not merely whether the backup job reported success.

Store secrets outside public source code, limit who can read deployment credentials, rotate keys after suspected compromise, and ensure automation tokens can be revoked. Consider MFA or hardware-backed SSH authentication where compatible, a VPN or bastion for administration, and separate service accounts rather than shared root.

Avoid setting filesystem permissions or sysctl values globally without understanding the host. For example, setting net.ipv4.ip_forward=0 can disrupt gateways, overlay networking, container hosts and VPNs; reverse-path filtering may break asymmetric routes. Similarly, chmod on entire cron directories can interfere with package-managed services.

  • Confirm an encrypted, independent backup location and a tested recovery procedure.
  • Document the access path and account that can restore service after a network mistake.
  • Monitor certificate expiry, free disk space, time synchronization and patch state.
  • Protect secrets, SSH private keys and deployment credentials with least privilege.
  • Review role-specific controls for databases, reverse proxies, hypervisors and container networks.

9. Make verification a habit

After each change, inspect the effective settings, the active service, and behavior from another machine. A passing config parser is not the same thing as a reachable server. For a web service, also check HTTPS, application health and reverse-proxy routes. For a VPN, routing or container host, test forwarding and dependent workloads.

Recheck after OS upgrades, kernel changes, SSH package updates, cloud-image migrations and major network changes. Put the baseline, exceptions and rollback steps in version-controlled documentation. If you automate, start with read-only auditing and make write operations require explicit approval and test evidence.

Final read-only verification checklist
bash
sudo sshd -tsudo sshd -T | grep -E "^(port|permitrootlogin|passwordauthentication|pubkeyauthentication) "sudo ufw status verbosesudo fail2ban-client status sshdsudo aa-statussystemctl --failedsudo ss -tulpn# From your own workstation, test another SSH session separately.

What I would change in a one-shot hardening script

A previous comprehensive Bash approach tried to update packages, create an administrator, reset UFW, rewrite SSH settings, enforce all AppArmor profiles, initialize AIDE, install mail services and configure monitoring in one run. It covered many categories, but it also introduced unacceptable failure modes when applied indiscriminately.

I would break it into an inventory-only audit, a per-feature plan, an explicit approval and apply stage, and a verification/rollback stage. Configuration changes should be idempotent, preserve existing files, log what actually succeeded, test effective behavior, and stop safely when a dependency is missing. Never report “all controls enabled” when steps were silently skipped or failed.

Original patternProblemBetter design
Reset UFW and allow SSH/22Can remove app rules and lock out a custom-port VPSInventory then add only needed rules
Append to sshd_configCan lose to an earlier Include or MatchReview drop-ins and effective output
Lock new sudo user passwordSudo may become unusableConfirm working sudo and key login first
Disable IP forwarding globallyCan break VPN, router or container workloadsApply workload-specific network settings
Enforce every AppArmor profileMay break expected application activityTest selected profiles and monitor denials
Install Postfix and numerous scannersAdds dependencies and operational noiseUse only justified services and verified alerting
Audit all network connect/bind callsExcessive audit volume and resource useTargeted rules with retention tests
Claim success after ignored errorsConceals incomplete or ineffective controlsTruthful result reporting, exit codes and rollback

Small quality-of-life improvements for administrators

A useful guide should speed up routine checks without hiding what the commands do. The optional shell aliases below shorten frequently repeated read-only status checks. They are convenient personal shell helpers, not security controls, and do not replace a monitoring platform.

Copy them into your own ~/.bash_aliases if you understand the commands. A login shell may need to source the file. Avoid aliases that automatically restart services, disable firewalls or change credentials—those actions deserve explicit names and review.

Optional read-only Bash aliases
bash
# Add to ~/.bash_aliases after reviewalias ports='sudo ss -tulpn'alias fwstatus='sudo ufw status verbose'alias sshlogs='sudo journalctl -u ssh --since "1 day ago" --no-pager'alias failedunits='systemctl --failed'alias aptpending='apt list --upgradable'
# Refresh aliases in the current shell:source ~/.bash_aliases

Optional SSH host aliases from your workstation

For servers you administer regularly, a personal SSH client alias can make commands shorter and reduce accidental connections to the wrong hostname or port. Put this configuration in ~/.ssh/config on your workstation, not in the VPS server configuration.

This is a convenience feature, not a security boundary. Keep the private key in your local account, use a unique entry for each server, validate host fingerprints and restrict the SSH config file permissions. A common private-key filename is shown below; it is a path example, not an embedded key.

Example client SSH alias: ~/.ssh/config
ini
Host example-vps    HostName 203.0.113.10    User sysadmin    Port 22    IdentityFile ~/.ssh/id_ed25519    IdentitiesOnly yes
# From the workstation:# chmod 600 ~/.ssh/config# ssh example-vps

Beyond one server: a maintainable security baseline

For a fleet, use a configuration-management system such as Ansible to make approved changes repeatable and auditable. Terraform and OpenTofu can provision the surrounding cloud infrastructure, while Python can validate inventories and generate structured reports. Keep these responsibilities separate: infrastructure provisioning, OS configuration and runtime verification prove different things.

A good baseline should document what it supports, what it intentionally leaves unchanged and why. There is no single hardening score that proves a system is safe. The right outcome is a server that can still do its job, is patched and monitored, restricts access appropriately, and can be recovered when a change goes wrong.

Sources & further reading

These links provide the technical basis for the article. Check upstream advisories and documentation for revisions.

Read next

All articles