Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ubuntu Server Security Best Practices

By Kokil Thapa | Last reviewed: August 2026

Securing a production web server requires moving beyond default installations to implement layered defenses that stop automated attacks before they reach your application. Implementing comprehensive Ubuntu Server Security Best Practices is the foundation for running reliable Laravel, WordPress, or custom PHP applications in 2026, whether hosted locally in Kathmandu or on global cloud infrastructure. This guide covers the exact hardening steps I use on client servers to prevent unauthorized access, mitigate brute-force attempts, and maintain compliance without sacrificing deployment velocity.

What Are the Essential Ubuntu Server Security Best Practices for Production?

A secure Ubuntu server relies on defense-in-depth rather than a single silver bullet. In my experience maintaining production environments for legal-tech portals and eCommerce platforms, the most effective approach combines network-level filtering, access control hardening, automated maintenance, and application isolation. When evaluating how to secure your website and server in Nepal or abroad, these core principles remain constant regardless of the specific framework or CMS you deploy.

Ubuntu Server Defense LayersLayer 1: UFW Network Firewall (Ports 22, 80, 443 Only)Layer 2: SSH Hardening (Key-Only, No Root)Layer 3: Fail2Ban Intrusion PreventionLayer 4: Unattended Security UpgradesLayer 5: Application Permissions & Isolation
Five-layer defense model implementing Ubuntu Server Security Best Practices for production web applications

The priority order matters. Network filtering must come first because it reduces the attack surface before any service processes a request. Access control comes second because compromised credentials bypass most application-level protections. Automated patching ensures known vulnerabilities don't persist for weeks while you focus on feature development. Finally, proper file permissions contain damage if an attacker does achieve code execution through your application.

How Do You Harden SSH Access on Ubuntu 24.04?

SSH is the primary attack vector for automated bots scanning the internet. Default configurations on Ubuntu 24.04 LTS are functional but not hardened for hostile environments. On every production server I manage, from legal document portals to high-traffic WooCommerce stores, I apply these modifications immediately after provisioning.

Disable Root Login and Password Authentication

Root login via SSH provides attackers with a known username and maximum privileges upon successful compromise. Password authentication enables brute-force attacks regardless of password complexity. Edit /etc/ssh/sshd_config with these directives:

# /etc/ssh/sshd_config - Production hardened configuration
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy admin
Protocol 2

After editing, validate the configuration syntax before restarting to avoid locking yourself out:

sudo sshd -t
sudo systemctl restart sshd

Configure Key-Based Authentication Correctly

Generate ED25519 keys on your local machine, not the server. ED25519 offers better security and performance than RSA for modern OpenSSH versions:

ssh-keygen -t ed25519 -C "deploy@production-server-2026"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@your-server-ip

Store private keys securely with appropriate permissions (chmod 600). For teams managing multiple servers, consider using SSH certificates or HashiCorp Vault for centralized key management rather than distributing individual public keys. This becomes especially important when coordinating deployments across sister sites sharing infrastructure, as I do with several Nepal-based legal service platforms.

Change Default SSH Port (Optional Defense-in-Depth)

Moving SSH from port 22 to a non-standard port like 2222 reduces noise from automated scanners but doesn't provide real security against targeted attacks. If you implement this, update your UFW rules accordingly and document the port change in your team's runbook. Never rely on obscurity as your only protection.

How Should You Configure UFW Firewall Rules for Web Servers?

UFW (Uncomplicated Firewall) provides a straightforward interface to netfilter/iptables. The principle is simple: deny everything by default, then explicitly allow only what your application requires. For a typical Laravel or WordPress production server running Nginx or Apache with PHP-FPM, you need exactly three ports open.

UFW Firewall Configuration FlowDefault PolicyDENY INCOMINGALLOW 22/tcpSSH AccessALLOW 80/tcpHTTP TrafficALLOW 443/tcpHTTPS TrafficRATE LIMIT 22Brute Force ProtectionALL OTHER PORTS: BLOCKED BY DEFAULT
Essential UFW firewall rules implementing Ubuntu Server Security Best Practices for web applications
# Reset to clean state (caution: will disconnect if not careful)
sudo ufw --force reset

# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow essential services
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Optional: Rate limit SSH to slow brute force
sudo ufw limit 22/tcp comment 'SSH rate limit'

# Enable and verify
sudo ufw enable
sudo ufw status verbose

If your application uses Redis on localhost only, do not open port 6379 externally. Same for MySQL/MariaDB on port 3306 — database connections should originate from the application server itself or through an SSH tunnel for remote administration. Opening database ports to the internet violates fundamental Ubuntu Server Security Best Practices and exposes you to data exfiltration risks.

PortServiceActionRationale
22/tcpSSHAllow + Rate LimitAdministrative access; rate limiting slows automated attacks
80/tcpHTTPAllowRequired for Let's Encrypt validation and HTTP→HTTPS redirects
443/tcpHTTPSAllowEncrypted web traffic; mandatory for production
3306/tcpMySQLBlock ExternalBind to 127.0.0.1; use SSH tunnels for remote access
6379/tcpRedisBlock ExternalLocalhost-only unless running dedicated cache cluster
8080/tcpNode.js DevBlock in ProdDevelopment-only; reverse proxy through Nginx in production

How Do You Set Up Fail2Ban to Block Brute-Force Attacks?

Even with key-only SSH authentication, monitoring and blocking malicious IPs provides valuable visibility into attack patterns. Fail2Ban scans log files and updates firewall rules dynamically when it detects suspicious behavior. On production servers hosting client-facing applications, I configure it for both SSH and web application endpoints.

# Install fail2ban
sudo apt update
sudo apt install fail2ban -y

# Create local configuration (never edit jail.conf directly)
sudo nano /etc/fail2ban/jail.local

Add this configuration to /etc/fail2ban/jail.local:

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
banaction = ufw

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400

[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5

[laravel-auth]
enabled = true
port = http,https
filter = laravel-auth
logpath = /var/www/html/storage/logs/laravel.log
maxretry = 10
findtime = 300
bantime = 7200

For Laravel applications, create a custom filter at /etc/fail2ban/filter.d/laravel-auth.conf that matches failed login attempts in your application logs. This protects your authentication system at the network level before requests even reach PHP-FPM, reducing server load during credential stuffing attacks.

# Enable and start fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Verify active jails
sudo fail2ban-client status
sudo fail2ban-client status sshd

Why Is Automated Security Patching Critical for Ubuntu Servers?

Manual patching fails in practice. Between client deadlines, feature development, and operational tasks, security updates slip through the cracks. Unattended Upgrades automates installation of security patches without intervention, ensuring known vulnerabilities get patched within 24 hours of release. This single practice prevents the majority of successful server compromises I've investigated over fifteen years.

Unattended Upgrades WorkflowDaily CronTrigger at 6 AMCheck ReposSecurity UpdatesInstall PatchesAuto-ApplyReboot CheckIf RequiredEmail NotificationSuccess / Failure ReportZero Manual Intervention Required
Automated patching pipeline ensuring Ubuntu Server Security Best Practices compliance without manual effort
# Install unattended-upgrades
sudo apt install unattended-upgrades apt-listchanges -y

# Enable automatic updates
sudo dpkg-reconfigure --priority=low unattended-upgrades

# Configure /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
};

Unattended-Upgrade::Package-Blacklist {
    "php8.3-fpm";
    "nginx";
};

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Unattended-Upgrade::Mail "admin@yourdomain.com";

Blacklisting critical packages like php8.3-fpm and nginx prevents automatic restarts during business hours. Schedule reboots for off-peak times (4 AM Nepal Time works well for domestic traffic patterns). Test this configuration thoroughly in staging before deploying to production — automatic reboots during peak traffic can cause revenue loss for eCommerce clients.

What File Permissions Protect Laravel and WordPress Applications?

Incorrect file permissions are among the most common vulnerabilities I encounter when auditing existing deployments. Overly permissive settings allow attackers who gain limited access to modify application code, inject malware, or escalate privileges. Proper ownership and mode bits contain blast radius.

  • Web root ownership: Files owned by www-data:www-data or dedicated deploy user, never root
  • Directory permissions: 755 for directories (owner write, group/world read+execute)
  • File permissions: 644 for files (owner write, group/world read)
  • Storage directories: Laravel's storage/ and bootstrap/cache/ require 775 for group write access
  • Configuration files: .env set to 600 (owner read/write only), never world-readable
  • Upload directories: Restrict executable permissions; validate uploads at application level
# Set correct ownership for Laravel application
sudo chown -R deploy:www-data /var/www/html/myapp
sudo find /var/www/html/myapp -type d -exec chmod 755 {} \;
sudo find /var/www/html/myapp -type f -exec chmod 644 {} \;

# Laravel-specific writable directories
sudo chgrp -R www-data /var/www/html/myapp/storage
sudo chgrp -R www-data /var/www/html/myapp/bootstrap/cache
sudo chmod -R 775 /var/www/html/myapp/storage
sudo chmod -R 775 /var/www/html/myapp/bootstrap/cache

# Secure environment file
sudo chmod 600 /var/www/html/myapp/.env
sudo chown deploy:deploy /var/www/html/myapp/.env

When working with Laravel developers in Nepal or international teams, enforce these permissions through deployment scripts rather than manual correction. Deployer 7 handles this automatically with proper shared_dirs and writable_dirs configuration, preventing permission drift between releases. For detailed deployment workflows, see my guide on CI/CD pipeline setup for production servers.

Implementing Ubuntu Server Security Best Practices as Standard Operating Procedure

Security isn't a one-time configuration task — it's an ongoing operational discipline. The Ubuntu Server Security Best Practices outlined here form the baseline for every production server I provision, whether for a Kathmandu law firm's client portal or an international eCommerce platform. Document these procedures in your team's runbook, automate enforcement through configuration management tools like Ansible or Deployer, and audit compliance quarterly.

Start with SSH hardening and UFW today — they take thirty minutes and eliminate entire attack categories. Add fail2ban and Unattended Upgrades this week. Review file permissions during your next deployment. These incremental improvements compound into a security posture that withstands real-world threats without slowing development velocity. If you need hands-on assistance securing your production infrastructure or want a comprehensive audit of your current setup, reach out to discuss your server security requirements.

Frequently Asked Questions

Create a non-root sudo user, disable root SSH login, configure UFW to allow only ports 22, 80, and 443, enable automatic security updates via unattended-upgrades, and set up SSH key authentication before exposing the server to the internet.

Initial hardening typically costs NPR 15,000–30,000 (USD 110–225) for standard web servers. Ongoing maintenance runs NPR 5,000–10,000 monthly depending on complexity and monitoring requirements.

Use both. Fail2ban blocks attacks at the server level before they consume resources, while Cloudflare stops them at the edge. For Nepal-hosted servers with limited bandwidth, fail2ban is essential even behind Cloudflare.

Enable UFW with default deny incoming, allow outgoing. Open port 22 for SSH, 80 and 443 for HTTP/HTTPS. If using Redis or MySQL externally, restrict those ports to specific IPs only. Never expose database ports to 0.0.0.0. Test with ufw status verbose after every change. On production Laravel servers I maintain, I also rate-limit SSH with ufw limit ssh/tcp to slow automated attacks without blocking legitimate admin access during deployments.

Edit /etc/ssh/sshd_config to set PermitRootLogin no, PasswordAuthentication no, PubkeyAuthentication yes, and MaxAuthTries 3. Change the default port from 22 to reduce noise from automated scanners. Use AllowUsers or AllowGroups to restrict SSH access to specific accounts. Restart sshd after changes. I have seen countless compromised servers where root login with password was left enabled. Always deploy SSH keys via Deployer or manually before disabling password auth to avoid locking yourself out during initial setup.

Install unattended-upgrades package and configure /etc/apt/apt.conf.d/50unattended-upgrades to enable automatic installation of security updates. Set Automatic-Reboot-Time to schedule reboots during low-traffic hours like 4 AM NPT. Configure mail notifications for update results. This prevents known vulnerabilities from lingering weeks between manual maintenance windows. On client projects, I pair this with canonical-livepatch for kernel patches without rebooting, which matters for eCommerce sites where downtime directly loses revenue during peak Nepali business hours.

Incorrect ownership lets attackers modify application files or read sensitive configs. Set www-data as owner for storage and cache directories only. Keep application code owned by deploy user with 755 for directories and 644 for files. Never set 777 permissions. Configure PHP-FPM pools to run as dedicated users per site. In my experience maintaining legal-tech portals handling sensitive documents, proper permissions prevent one compromised site from accessing another's data on shared infrastructure. Always audit permissions after deployments using find commands to catch drift.

Bind database to 127.0.0.1 unless remote access is required. Use strong passwords and remove anonymous users and test databases. Create application-specific database users with minimal privileges. Enable SSL for connections if remote access is necessary. Regularly audit user grants. For Laravel applications, store credentials in .env outside web root with 600 permissions. On production systems I manage, I also configure AppArmor profiles to restrict database processes from accessing unrelated filesystem paths, adding defense-in-depth beyond network-level restrictions.

Configure rsyslog to forward logs to centralized storage. Install auditd for file integrity monitoring on critical paths like /etc and application configs. Set up logwatch for daily email summaries. Monitor auth.log for failed SSH attempts and sudo usage. Use netdata or similar for real-time resource alerts. On servers running client applications, I configure alerts for unusual outbound connections that indicate compromise. Logs alone are insufficient without alerting; reviewing terabytes manually is impossible. Pair logging with automated anomaly detection for practical security monitoring.

Disable dangerous functions like exec, shell_exec, and system in php.ini unless absolutely required. Set open_basedir to restrict file access per pool. Configure separate FPM pools with dedicated users for each application. Limit max_children based on available RAM to prevent DoS through resource exhaustion. Enable slowlog to identify problematic scripts. On Laravel applications I deploy, I also set expose_php Off and disable URL wrappers when not needed. These settings prevent many automated exploitation attempts that target misconfigured PHP installations across Nepali hosting environments.

Use Certbot with Let's Encrypt for certificates. Configure Apache or Nginx with TLS 1.2 and 1.3 only, disabling older protocols. Enable HSTS headers with long max-age. Use Mozilla's intermediate cipher suite configuration. Implement OCSP stapling for performance. Test with SSL Labs after configuration. On client projects, I automate certificate renewal via systemd timers rather than cron for reliability. Remember that SSL configuration protects data in transit but does nothing for application-layer vulnerabilities; it is one layer in comprehensive server security, not a complete solution.

Store secrets in .env files outside web root with 600 permissions owned by application user. Never commit secrets to Git. Use deployment tools like Deployer to inject environment-specific configs during release. Consider HashiCorp Vault or SOPS for complex multi-environment setups. Rotate credentials regularly and after personnel changes. On Laravel projects I maintain, I validate .env presence in deployment scripts to prevent accidental exposure. Avoid storing secrets in database or config caches. For team environments, use encrypted secret sharing rather than Slack or email transmission of credentials.

Enable SYN cookies to mitigate SYN flood attacks. Disable IP source routing and ICMP redirects. Enable reverse path filtering. Configure TCP connection timeouts to free resources faster during attacks. Harden IPv6 settings even if unused. Apply changes via /etc/sysctl.d/99-security.conf and reload with sysctl -p. These network stack hardening measures complement application-level security. On production servers, I test these settings in staging first as aggressive tuning can break legitimate traffic patterns. Document all changes since kernel parameter issues are difficult to diagnose months later during incident response.

Use separate PHP-FPM pools with unique users per site. Configure open_basedir restrictions in each pool. Set proper directory permissions preventing cross-site access. Use Apache virtual hosts or Nginx server blocks with isolated document roots. Consider systemd-nspawn or LXC containers for stronger isolation when budget prevents separate VPS instances. On shared infrastructure I manage for sister sites like translation and notary services, this isolation prevents one compromised WordPress site from affecting Laravel applications. True multi-tenancy security requires accepting that shared servers carry inherent risk regardless of configuration quality.

Neglecting application-layer security while perfecting OS hardening. Using outdated PHP versions because upgrading seems risky. Skipping backups or never testing restores. Applying security guides without understanding trade-offs for your workload. Ignoring dependency vulnerabilities in Composer or npm packages. Over-restricting permissions then loosening them ad-hoc during troubleshooting without reverting. In my experience, the biggest gap is treating security as a one-time setup rather than continuous practice. Schedule quarterly audits, test incident response procedures, and accept that perfect security is impossible while striving for resilient, recoverable systems.

Share this article

Quick Contact Options
Choose how you want to connect me: