← Back to Blog

Hardening Ubuntu for Industrial IoT – Firewall, Fail2ban, and Kernel Tuning for 24/7 Operation

A production-hardened guide to securing Ubuntu servers in industrial environments – UFW firewall, SSH hardening, Fail2ban intrusion prevention, and kernel tuning for maximum stability and uptime.

9 min read Roman Swetly

Hardening Ubuntu for Industrial IoT

Firewall, Fail2ban, and kernel tuning for 24/7 operation – a production-hardened security baseline.


The Challenge

Industrial IoT environments are not typical cloud deployments. Your servers run:

  • 24/7/365 – no maintenance windows, no “reboot and fix it later.”
  • In hostile networks – exposed to the public internet or semi-trusted field networks.
  • With limited physical access – remote security is not optional.
  • Handling sensitive data – telemetry, control signals, and customer infrastructure.

A single compromise can mean:

  • Production downtime ($$$).
  • Loss of customer trust.
  • Remote access to industrial control systems.

My approach: Treat every server as if it’s already under attack. Assume breach. Build defense in depth.


Security Architecture Overview

My standard Ubuntu hardening stack for industrial deployments:

┌─────────────────────────────────────────────────────┐
│                    Internet                         │
└─────────────────┬───────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────┐
│              UFW Firewall (Layer 3/4)               │
│         Allow: SSH (22), HTTP (80), HTTPS (443)     │
│         Deny: Everything else by default            │
└─────────────────┬───────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────┐
│              SSH Hardening (Layer 7)                │
│   - Key-based authentication only                   │
│   - Root login disabled                             │
│   - Port 22 (or custom) + fail2ban watch            │
└─────────────────┬───────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────┐
│              Fail2ban (Intrusion Prevention)        │
│   - SSH brute-force protection                      │
│   - PostgreSQL authentication failures              │
│   - Web application login protection                │
│   - Automatic IP banning                            │
└─────────────────┬───────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────┐
│           Kernel Tuning (Performance/Stability)     │
│   - TCP stack optimization                          │
│   - File descriptor limits                          │
│   - Memory management for long-running services     │
└─────────────────────────────────────────────────────┘

1. UFW Firewall – Default Deny

UFW (Uncomplicated Firewall) is the first line of defense. The principle: block everything, allow only what’s necessary.

Basic Configuration

# Check UFW status
sudo ufw status verbose

# Set default policies (deny all incoming, allow outgoing)
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (change port if you moved it)
sudo ufw allow 22/tcp

# Allow HTTP/HTTPS for web services
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable UFW
sudo ufw enable

# Check status
sudo ufw status numbered

What This Achieves

PolicyResult
default deny incomingNo uninvited traffic reaches your services.
default allow outgoingYour server can reach updates, APIs, etc.
allow 22, 80, 443Only three ports exposed to the internet.

Pro tip: Always enable UFW before exposing services to the internet. And keep an active SSH session open when enabling, so you don’t lock yourself out.


2. SSH Hardening – The Gateway to Your Server

SSH is the most attacked service on any public server. Brute-force attempts start within minutes of going online.

/etc/ssh/sshd_config – Production Settings

# Disable root login (use sudo instead)
PermitRootLogin no

# Only allow key-based authentication
PasswordAuthentication no
PubkeyAuthentication yes

# Disable empty passwords
PermitEmptyPasswords no

# Use only secure ciphers
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com

# Limit authentication attempts (fail2ban will handle the rest)
MaxAuthTries 3

# Disable X11 forwarding (unnecessary for headless servers)
X11Forwarding no

# Log level for fail2ban
LogLevel VERBOSE

# Allow only specific users (optional but recommended)
# AllowUsers yourusername

After Changes

sudo systemctl restart sshd

# Test with a new SSH session before closing the current one!
# If something breaks, you won't be locked out.

Verify Authentication Methods

# Check what authentication methods are active
sudo sshd -T | grep -E "passwordauthentication|pubkeyauthentication|permitrootlogin"

Result should be:

passwordauthentication no
pubkeyauthentication yes
permitrootlogin no

3. Fail2ban – Intrusion Prevention System

Fail2ban monitors logs and bans IPs that show malicious behavior. It’s your second line of defense after the firewall.

Installation

sudo apt update
sudo apt install fail2ban -y

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

Configuration Files (Ubuntu 24.04)

On modern Ubuntu, fail2ban uses systemd as the backend (not log files). The correct approach: create .local files – never edit .conf files directly.

/etc/fail2ban/jail.local – Custom Jails

[sshd]
enabled = true
backend = systemd

# Whitelist your internal network
ignoreip = 192.168.9.0/24

# Time window for counting failures (1 hour)
findtime = 1h

# Banned after 3 failures within findtime
maxretry = 3

# Ban duration (1 day)
bantime = 1d

/etc/fail2ban/fail2ban.local – Global Settings

[DEFAULT]
allowipv6 = auto

# Use systemd journal for Debian/Ubuntu 24.04
backend = systemd

Why Two Files?

FilePurpose
jail.localDefines which services are protected (SSH, PostgreSQL, etc.).
fail2ban.localDefines global behavior (backend, logging, defaults).

This separation keeps your configuration clean and upgrade-safe.


4. Extended Fail2ban Configuration – PostgreSQL Protection

Public-facing databases are prime targets. Extend fail2ban to protect PostgreSQL authentication.

/etc/fail2ban/jail.local (Add PostgreSQL Jail)

[postgresql]
enabled = true
port = 5432
filter = postgresql
logpath = /var/log/postgresql/postgresql-*.log
backend = systemd
maxretry = 3
bantime = 1d
findtime = 1h

/etc/fail2ban/filter.d/postgresql.conf

Create a custom filter for PostgreSQL auth failures:

[Definition]
failregex = ^.*FATAL:  password authentication failed for user ".*"$
ignoreregex =

Result: Three failed PostgreSQL logins = automatic 24-hour ban.


5. Managing Fail2ban – Essential Commands

Check Status

# Show all active jails
sudo fail2ban-client status

# Show details for a specific jail
sudo fail2ban-client status sshd

# Show current ban time for a jail
sudo fail2ban-client get sshd bantime

View Banned IPs

# Check iptables rules (if using iptables backend)
sudo iptables -S | grep f2b

# View fail2ban logs
sudo tail -f /var/log/fail2ban.log

Query the SQLite Database

Fail2ban stores ban history in SQLite. Useful for forensics:

# Show all banned IPs and their jail
sudo sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "select ip,jail from bips"

# Show unique banned IPs (all jails)
sudo sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "select distinct ip from bips"

# Show unique banned IPs in sshd jail only
sudo sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "select distinct ip from bips where jail='sshd'"

# Top 20 most banned IPs (across all jails)
sudo sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "select jail,ip,count(*) as count from bips group by ip order by count desc limit 20"

Unban an IP

# Interactive mode
sudo fail2ban-client -i

# Check status
status sshd

# Unban specific IP
set sshd unbanip 172.29.0.2

# Exit
quit

Ban Statistics Example

# Sample output from top 20 query
sshd|192.168.1.100|47
postgresql|10.0.0.50|12
sshd|203.0.113.45|8

This tells you:

  • Which IPs are attacking.
  • Which services are being targeted.
  • How persistent the attacks are.

6. Kernel Tuning – Performance and Stability

Industrial servers run months or years without reboots. Kernel tuning ensures stability under load.

/etc/sysctl.conf – Production Settings

# Increase file descriptor limits (critical for high-traffic services)
fs.file-max = 2097152

# TCP stack optimization
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# Reduce TIME_WAIT (allow faster connection reuse)
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# Keepalive for long-running connections (IoT devices)
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 60
net.ipv4.tcp_keepalive_probes = 5

# Memory management (prevent OOM killer on industrial devices)
vm.swappiness = 10
vm.vfs_cache_pressure = 50

# Hardened network security
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1

Apply Changes

sudo sysctl -p

Verify

# Check current limits
ulimit -n

# Check TCP keepalive settings
sudo sysctl net.ipv4.tcp_keepalive_time

7. Additional Security Practices

Automated Updates

# Enable automatic security updates
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

Monitor SSH Access

# Check last successful logins
last

# Check failed login attempts
sudo lastb | head -20

Regular Log Rotation

Ensure logs don’t fill your disk:

# Check log rotation config
sudo cat /etc/logrotate.d/rsyslog

Summary Checklist

TaskStatusCommand/Config
UFW firewallsudo ufw enable
SSH key-only authPasswordAuthentication no
Root login disabledPermitRootLogin no
Fail2ban installedsudo systemctl enable fail2ban
SSH jail configured/etc/fail2ban/jail.local
PostgreSQL jail (optional)/etc/fail2ban/filter.d/postgresql.conf
Kernel tuning applied/etc/sysctl.conf
Automatic updatesunattended-upgrades
Log monitoringtail -f /var/log/fail2ban.log

Real-World Impact

This security baseline has been running on industrial servers in challenging environments (remote sites with limited physical access) for over two years:

  • 0 successful SSH brute-force attacks.
  • 100% uptime on monitored systems.
  • Automatic IP banning prevented over 5,000 malicious login attempts in the last year alone.

Real-World Attack Statistics

Here’s a live snapshot from one of my production servers:

tail /var/log/fail2ban.log
2026-07-18 16:17:50,040 fail2ban.filter [3612534]: INFO [sshd] Found [REDACTED] - 2026-07-18 16:17:49
2026-07-18 16:18:49,494 fail2ban.filter [3612534]: INFO [sshd] Found [REDACTED] - 2026-07-18 16:18:49
2026-07-18 16:18:53,540 fail2ban.filter [3612534]: INFO [sshd] Found [REDACTED] - 2026-07-18 16:18:53
2026-07-18 16:18:57,949 fail2ban.filter [3612534]: INFO [sshd] Found [REDACTED] - 2026-07-18 16:18:57
2026-07-18 16:18:58,580 fail2ban.actions [3612534]: NOTICE [sshd] Ban [REDACTED]
2026-07-18 16:19:11,244 fail2ban.filter [3612534]: INFO [sshd] Found [REDACTED] - 2026-07-18 16:19:10
2026-07-18 16:19:15,040 fail2ban.filter [3612534]: INFO [sshd] Found [REDACTED] - 2026-07-18 16:19:14
2026-07-18 16:19:16,789 fail2ban.filter [3612534]: INFO [sshd] Found [REDACTED] - 2026-07-18 16:19:16
2026-07-18 16:19:16,801 fail2ban.actions [3612534]: NOTICE [sshd] Ban [REDACTED]
fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed: 9
|  |- Total failed: 8274
|  `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned: 89
   |- Total banned: 1243
   `- Banned IP list: [89 IPs redacted for privacy]

Key takeaways from this live data:

  • 8,274 failed attempts – this is a heavily targeted server.
  • 1,243 total bans – fail2ban has been working continuously.
  • 89 currently banned – attacks are ongoing and relentless.
  • Multiple IPs banned per minute – automated scanners are everywhere.

Final Thoughts

Security is not a one-time configuration. It’s a continuous process:

  • Monitor logs regularly.
  • Review banned IPs.
  • Keep fail2ban filters updated for new threats.
  • Test your backups (a hardened server is useless without restorable data).

For industrial IoT, the stakes are higher. Take the time to build a defense-in-depth architecture. It will save you from sleepless nights.


This hardening guide is part of my Industrial Infrastructure Platform (II-PaaS). For a detailed security audit or architecture review, feel free to reach out.

← Previous Post
Building a Sovereign Nextcloud & Matrix Cluster – Replacing Google Workspace with 100% Self-hosted Infrastructure
Next Post →
Setting up Traefik with Automatic SSL – Zero-touch Certificate Renewal for 30+ Services

Related Articles