Skip to content

Instantly share code, notes, and snippets.

@jeanpauldejong
Last active July 4, 2025 07:22
Show Gist options
  • Save jeanpauldejong/1274c87ce0ae0c8e27443437a5b575ea to your computer and use it in GitHub Desktop.
Save jeanpauldejong/1274c87ce0ae0c8e27443437a5b575ea to your computer and use it in GitHub Desktop.
Ubuntu 24.04 LTS Initial Setup & Hardening

Ubuntu 24.04 Initial Setup & Hardening Guide

A concise, step-by-step guide for securing and configuring a fresh Ubuntu 24.04 server.

Alternatively you can skip the text and use the cloud-init script files cloud-init.yaml, cloud-init-docker.yaml, cloud-init-lemp.yaml, or cloud-init-nginx-proxy.yml for an automated setup.

Base Installation

  • Deploy the official Ubuntu 24.04 LTS image from your cloud provider or ISO.
  • Log in as the default user (or via SSH key if provided).

Set Timezone

Ensure your server's timezone is set correctly to avoid issues with logs and scheduled tasks. Adjust the timezone as needed.

sudo timedatectl set-timezone Europe/Amsterdam

Update, Upgrade & Install Essential Packages

Install all required packages in one go:

sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw rsyslog qemu-guest-agent fail2ban unattended-upgrades
  • ufw: Uncomplicated Firewall for basic firewall management
  • rsyslog: Reliable system and kernel logging
  • qemu-guest-agent: Enhanced VM integration (for cloud/VM environments)
  • fail2ban: Protects against brute-force attacks
  • unattended-upgrades: Enables automatic security updates

Configure UFW Firewall

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
sudo ufw status verbose

Harden sysctl Settings

Create or edit /etc/sysctl.d/99-custom.conf:

# IP Spoofing protection (IPv4)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Ignore ICMP broadcast requests (IPv4)
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Disable source packet routing (IPv4)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0

# Enable TCP SYN cookies (IPv4)
net.ipv4.tcp_syncookies = 1

# Log Martians (IPv4)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# Disable source packet routing (IPv6)
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0

# Disable IPv6 redirects
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0

# Disable IPv6 router advertisements if not needed
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0

Apply changes:

sudo sysctl --system

Enable Automatic Security Updates

sudo dpkg-reconfigure --priority=low unattended-upgrades

Disable Unneeded Services

List running services:

systemctl list-units --type=service --state=running

Disable unnecessary ones, e.g.:

sudo systemctl disable --now avahi-daemon
sudo systemctl disable --now cups
sudo systemctl disable --now ModemManager
sudo systemctl disable --now multipathd
sudo systemctl disable --now multipathd.socket
sudo systemctl disable --now udisks2

Install & Configure Fail2Ban

sudo systemctl enable --now fail2ban

Create /etc/fail2ban/jail.local:

[sshd]
enabled = true
port    = ssh
logpath = %(sshd_log)s
maxretry = 5

Restart Fail2Ban:

sudo systemctl restart fail2ban

With Docker

The following steps will install Docker Engine and Docker Compose, configure basic security, and verify the installation.

Install Docker Engine

sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Enable and Start Docker

sudo systemctl enable --now docker

(Optional) Manage Docker as a Non-root User

Add your user to the docker group to run Docker without sudo:

sudo usermod -aG docker $USER
# Log out and back in for group changes to take effect

Install Docker Compose (Standalone)

sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Secure Docker

  • Only trusted users should be added to the docker group.
  • Consider enabling the Docker daemon's rootless mode for extra isolation (see Docker docs).
  • Use UFW to restrict access to Docker-exposed ports as needed.

Verify Docker Installation

docker --version
docker compose version
sudo docker run --rm hello-world

If you see a "Hello from Docker!" message, your installation is working.

With LEMP Stack

The following steps will install and configure a secure LEMP stack (Linux, Nginx, MariaDB, PHP-FPM 8.3) suitable for hosting PHP applications. MariaDB is used for its performance and open-source focus, but you can substitute MySQL if preferred.

Install Nginx, MariaDB, and PHP-FPM

sudo apt update
sudo apt install -y nginx mariadb-server php8.3-fpm php8.3-mysql php8.3-cli php8.3-curl php8.3-gd php8.3-mbstring php8.3-xml php8.3-zip php8.3-bcmath php8.3-intl php8.3-soap php8.3-redis php8.3-imagick

Secure MariaDB Installation

Run the secure installation script and follow the prompts. Use a strong root password and answer the security questions as appropriate for your environment.

sudo mysql_secure_installation

Note: Do not use the example passwords below in production. Replace all placeholder values with strong, unique passwords.

Create a Database and User (Example)

sudo mysql -u root -p

Inside the MariaDB shell:

CREATE DATABASE exampledb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'exampleuser'@'localhost' IDENTIFIED BY 'REPLACE_WITH_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON exampledb.* TO 'exampleuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Configure PHP-FPM

PHP-FPM is installed and enabled by default. You may want to adjust settings in /etc/php/8.3/fpm/php.ini for production (e.g., memory_limit, upload_max_filesize, post_max_size, date.timezone).

Configure Nginx for a PHP Site

Create a new site configuration file, e.g. /etc/nginx/sites-available/example.com:

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.php index.html index.htm;

    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~* /\.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        access_log off;
    }

    location ~ /\.ht {
        deny all;
    }
}

Enable the site and reload Nginx:

sudo mkdir -p /var/www/example.com/public
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Test PHP

Create a test file:

echo '<?php phpinfo();' | sudo tee /var/www/example.com/public/info.php

Visit http://example.com/info.php to verify PHP is working, then remove the file:

sudo rm /var/www/example.com/public/info.php

Security Note:

  • Always use strong, unique passwords for database users.
  • Adjust firewall rules to restrict access to only necessary ports (e.g., 80, 443).
  • Remove or secure default files and configurations before going live.
#cloud-config
#
# Ubuntu 24.04 Initial Setup & Hardening
# This cloud-init configures timezone, installs essential packages, configures UFW, sysctl, automatic updates, disables unneeded services, and sets up Fail2Ban.
# Set timezone
timezone: Europe/Amsterdam
# Update and upgrade packages
package_update: true
package_upgrade: true
# Install essential packages
packages:
- ufw
- rsyslog
- qemu-guest-agent
- fail2ban
- unattended-upgrades
# Write sysctl hardening settings
write_files:
- path: /etc/sysctl.d/99-hardening.conf
owner: root:root
permissions: '0644'
content: |
# IP Spoofing protection (IPv4)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP broadcast requests (IPv4)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Disable source packet routing (IPv4)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# Enable TCP SYN cookies (IPv4)
net.ipv4.tcp_syncookies = 1
# Log Martians (IPv4)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# Disable source packet routing (IPv6)
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Disable IPv6 redirects
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Disable IPv6 router advertisements if not needed
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
- path: /etc/fail2ban/jail.local
owner: root:root
permissions: '0644'
content: |
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 5
# Run commands after boot
runcmd:
# Apply sysctl settings
- sysctl --system
# Configure UFW
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow ssh
- ufw allow http
- ufw allow https
- ufw --force enable
# Enable and start Fail2Ban
- systemctl enable --now fail2ban
# Enable automatic security updates
- dpkg-reconfigure --priority=low unattended-upgrades
# Disable unneeded services
- systemctl disable --now avahi-daemon.service || true
- systemctl disable --now cups.service || true
- systemctl disable --now ModemManager.service || true
- systemctl disable --now multipathd.service || true
- systemctl disable --now multipathd.socket || true
- systemctl stop multipathd.socket || true
- systemctl disable --now udisks2.service || true
#cloud-config
#
# Ubuntu 24.04 Initial Setup & Hardening with Docker
# This cloud-init configures timezone, installs essential packages, configures UFW, sysctl, automatic updates, disables unneeded services, sets up Fail2Ban, and installs Docker Engine and Docker Compose.
# Set timezone
timezone: Europe/Amsterdam
# Update and upgrade packages
package_update: true
package_upgrade: true
# Install essential packages
packages:
- ufw
- rsyslog
- qemu-guest-agent
- fail2ban
- unattended-upgrades
- ca-certificates
- curl
- gnupg
- lsb-release
# Write sysctl hardening settings
write_files:
- path: /etc/sysctl.d/99-hardening.conf
owner: root:root
permissions: '0644'
content: |
# IP Spoofing protection (IPv4)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP broadcast requests (IPv4)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Disable source packet routing (IPv4)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# Enable TCP SYN cookies (IPv4)
net.ipv4.tcp_syncookies = 1
# Log Martians (IPv4)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# Disable source packet routing (IPv6)
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Disable IPv6 redirects
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Disable IPv6 router advertisements if not needed
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
- path: /etc/fail2ban/jail.local
owner: root:root
permissions: '0644'
content: |
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 5
# Run commands after boot
runcmd:
# Apply sysctl settings
- sysctl --system
# Configure UFW
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow ssh
- ufw allow http
- ufw allow https
- ufw --force enable
# Enable and start Fail2Ban
- systemctl enable --now fail2ban
# Enable automatic security updates
- dpkg-reconfigure --priority=low unattended-upgrades
# Disable unneeded services
- systemctl disable --now avahi-daemon.service || true
- systemctl disable --now cups.service || true
- systemctl disable --now ModemManager.service || true
- systemctl disable --now multipathd.service || true
- systemctl disable --now multipathd.socket || true
- systemctl stop multipathd.socket || true
- systemctl disable --now udisks2.service || true
# Install Docker Engine and Docker Compose
- install -m 0755 -d /etc/apt/keyrings
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
- chmod a+r /etc/apt/keyrings/docker.gpg
- |
sh -c 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list'
- apt-get update
- apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
- systemctl enable --now docker
# (Optional) Install Docker Compose standalone
- curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
- chmod +x /usr/local/bin/docker-compose
# (Optional) Add default user to docker group
- usermod -aG docker ubuntu || true
# Verify Docker installation
- docker --version
- docker compose version
- docker run --rm hello-world || true
#cloud-config
#
# Ubuntu 24.04 Initial Setup & Hardening with LEMP Stack
# This cloud-init configures timezone, installs essential packages, configures UFW, sysctl, automatic updates, disables unneeded services, sets up Fail2Ban, and installs/configures Nginx, MariaDB, and PHP-FPM.
#
# IMPORTANT: After deployment, you must manually secure MariaDB, create the database and user, and configure Nginx for your domain.
# Set variables (replace these before use)
# DOMAIN: example.com
timezone: Europe/Amsterdam
package_update: true
package_upgrade: true
packages:
- ufw
- rsyslog
- qemu-guest-agent
- fail2ban
- unattended-upgrades
- nginx
- mariadb-server
- php8.3-fpm
- php8.3-mysql
- php8.3-cli
- php8.3-curl
- php8.3-gd
- php8.3-mbstring
- php8.3-xml
- php8.3-zip
- php8.3-bcmath
- php8.3-intl
- php8.3-soap
- php8.3-redis
- php8.3-imagick
write_files:
- path: /etc/sysctl.d/99-hardening.conf
owner: root:root
permissions: '0644'
content: |
# IP Spoofing protection (IPv4)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP broadcast requests (IPv4)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Disable source packet routing (IPv4)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# Enable TCP SYN cookies (IPv4)
net.ipv4.tcp_syncookies = 1
# Log Martians (IPv4)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# Disable source packet routing (IPv6)
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Disable IPv6 redirects
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Disable IPv6 router advertisements if not needed
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
- path: /etc/fail2ban/jail.local
owner: root:root
permissions: '0644'
content: |
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 5
- path: /etc/nginx/sites-available/example.com
owner: root:root
permissions: '0644'
content: |
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html index.htm;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
access_log off;
}
location ~ /\.ht {
deny all;
}
}
- path: /etc/motd
owner: root:root
permissions: '0644'
content: |
********************************************************************
* Manual steps required for LEMP stack setup: *
* *
* 1. Secure MariaDB: *
* sudo mysql_secure_installation *
* *
* 2. Create your database and user: *
* sudo mysql -u root -p *
* (then run CREATE DATABASE, CREATE USER, GRANT, etc.) *
* *
* 3. Edit and enable your Nginx site config: *
* sudo mv /etc/nginx/sites-available/example.com /etc/nginx/sites-available/<yourdomain> *
* sudo ln -s /etc/nginx/sites-available/<yourdomain> /etc/nginx/sites-enabled/ *
* sudo nginx -t && sudo systemctl reload nginx *
* *
* 4. Remove the PHP info test file after verifying PHP: *
* sudo rm /var/www/<yourdomain>/public/info.php *
* *
* 5. Set strong passwords and never leave them in logs or files! *
********************************************************************
runcmd:
# Apply sysctl settings
- sysctl --system
# Configure UFW
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow ssh
- ufw allow http
- ufw allow https
- ufw --force enable
# Enable and start Fail2Ban
- systemctl enable --now fail2ban
# Enable automatic security updates
- dpkg-reconfigure --priority=low unattended-upgrades
# Disable unneeded services
- systemctl disable --now avahi-daemon.service || true
- systemctl disable --now cups.service || true
- systemctl disable --now ModemManager.service || true
- systemctl disable --now multipathd.service || true
- systemctl disable --now multipathd.socket || true
- systemctl stop multipathd.socket || true
- systemctl disable --now udisks2.service || true
# Enable and start services
- systemctl enable --now nginx
- systemctl enable --now php8.3-fpm
# Set up Nginx site directory
- mkdir -p /var/www/example.com/public
# Create PHP test file
- echo '<?php phpinfo();' > /var/www/example.com/public/info.php
# Reminder to remove test file after verification
#
# SECURITY NOTE:
# - Complete all manual steps listed in /etc/motd after deployment.
# - Never commit real secrets to version control.
# - Remove /var/www/<yourdomain>/public/info.php after testing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment