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
| Policy | Result |
|---|---|
default deny incoming | No uninvited traffic reaches your services. |
default allow outgoing | Your server can reach updates, APIs, etc. |
allow 22, 80, 443 | Only 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?
| File | Purpose |
|---|---|
jail.local | Defines which services are protected (SSH, PostgreSQL, etc.). |
fail2ban.local | Defines 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
| Task | Status | Command/Config |
|---|---|---|
| UFW firewall | ✅ | sudo ufw enable |
| SSH key-only auth | ✅ | PasswordAuthentication no |
| Root login disabled | ✅ | PermitRootLogin no |
| Fail2ban installed | ✅ | sudo 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 updates | ✅ | unattended-upgrades |
| Log monitoring | ✅ | tail -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.