Production Server Security Hardening: A Comprehensive Checklist

Harden your production servers with SSH security, firewall rules, WAF configuration, intrusion detection, and automated security patching best practices.

E
ECOSIRE Research and Development Team
|March 16, 20267 min read1.5k Words|

Production Server Security Hardening: A Comprehensive Checklist

The average time to identify a data breach is 204 days. Production server hardening reduces your attack surface so that breaches are harder to initiate and faster to detect. This guide covers the concrete security measures every production server should implement, from SSH configuration to web application firewalls.

Key Takeaways

  • SSH key-only authentication and non-standard ports block 99% of automated brute-force attacks
  • A properly configured firewall reduces the attack surface from thousands of entry points to less than ten
  • Web Application Firewalls block SQL injection, XSS, and other OWASP Top 10 attacks at the network layer
  • Automated patching ensures vulnerabilities are fixed within hours of disclosure, not weeks

SSH Hardening

Configuration

# /etc/ssh/sshd_config

# Disable password authentication
PasswordAuthentication no
ChallengeResponseAuthentication no

# Disable root login
PermitRootLogin no

# Use SSH keys only
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

# Limit login attempts
MaxAuthTries 3
MaxSessions 5

# Set idle timeout (5 minutes)
ClientAliveInterval 300
ClientAliveCountMax 0

# Restrict SSH to specific users
AllowUsers deploy ubuntu

# Disable X11 forwarding
X11Forwarding no

# Use only strong algorithms
KexAlgorithms [email protected],diffie-hellman-group-exchange-sha256
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]

Fail2Ban

# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 3600

Firewall Configuration

UFW (Ubuntu)

# Reset and set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (consider changing port)
sudo ufw allow 22/tcp

# Allow HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Allow specific IPs for management
sudo ufw allow from 203.0.113.10 to any port 9090 comment "Grafana"
sudo ufw allow from 203.0.113.10 to any port 9093 comment "Alertmanager"

# Deny everything else (implicit with default deny)
sudo ufw enable
sudo ufw status verbose

AWS Security Groups

# Terraform security group
resource "aws_security_group" "app" {
  name_prefix = "app-"
  vpc_id      = aws_vpc.main.id

  # Allow HTTPS from anywhere
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS from internet"
  }

  # Allow SSH from office IP only
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["203.0.113.0/24"]
    description = "SSH from office"
  }

  # Allow all outbound
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Database security group - no public access
resource "aws_security_group" "db" {
  name_prefix = "db-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
    description     = "PostgreSQL from app servers only"
  }
}

Nginx Security Headers

# Security headers for all responses
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com;" always;

# Hide server version
server_tokens off;

# Limit request size
client_max_body_size 10m;

# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

server {
    location / {
        limit_req zone=general burst=20 nodelay;
    }

    location /api/ {
        limit_req zone=api burst=50 nodelay;
    }

    location /auth/ {
        limit_req zone=login burst=3 nodelay;
    }
}

SSL/TLS Configuration

# Modern TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

# Session configuration
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# Certificate (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

Automated Certificate Renewal

# /etc/cron.d/certbot
0 0,12 * * * root certbot renew --quiet --post-hook "nginx -s reload"

Container Security

If running Docker in production, apply these additional hardening measures:

Docker Daemon Security

{
  "userns-remap": "default",
  "no-new-privileges": true,
  "live-restore": true,
  "userland-proxy": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Container Runtime Restrictions

# docker-compose.yml security settings
services:
  api:
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

Key principles:

  • Drop all capabilities and add back only what is needed
  • Read-only filesystem prevents attackers from modifying container contents
  • No new privileges prevents privilege escalation inside containers
  • Non-root user in the Dockerfile (see our Docker deployment guide)

Image Security

  • Use official, minimal base images (Alpine variants)
  • Pin image versions (never use :latest in production)
  • Scan images for vulnerabilities with Trivy or Grype
  • Sign images with Docker Content Trust
  • Use a private registry with access controls

Database Security

PostgreSQL Hardening

# postgresql.conf security settings
listen_addresses = 'localhost'        # Only listen on localhost
ssl = on                              # Require SSL for connections
ssl_cert_file = '/path/to/server.crt'
ssl_key_file = '/path/to/server.key'
password_encryption = scram-sha-256   # Modern password hashing
log_connections = on                  # Log all connections
log_disconnections = on               # Log disconnections
log_statement = 'ddl'                 # Log DDL statements
# pg_hba.conf - restrict connections
# TYPE  DATABASE  USER  ADDRESS      METHOD
local   all       all                scram-sha-256
host    all       all   10.0.0.0/8   scram-sha-256
hostssl all       all   0.0.0.0/0    scram-sha-256
  • Never expose PostgreSQL to the public internet
  • Use dedicated database users with minimum required permissions
  • Enable connection encryption (SSL)
  • Set strong password policies
  • Regular backup verification (see our disaster recovery guide)

Intrusion Detection

OSSEC Configuration

<!-- /var/ossec/etc/ossec.conf -->
<ossec_config>
  <syscheck>
    <!-- Monitor critical files for changes -->
    <directories check_all="yes">/etc,/usr/bin,/usr/sbin</directories>
    <directories check_all="yes">/opt/app/dist</directories>

    <!-- Ignore frequently changing files -->
    <ignore>/etc/mtab</ignore>
    <ignore>/etc/resolv.conf</ignore>

    <!-- Run integrity check every 6 hours -->
    <frequency>21600</frequency>
  </syscheck>

  <rootcheck>
    <rootkit_files>/var/ossec/etc/shared/rootkit_files.txt</rootkit_files>
    <rootkit_trojans>/var/ossec/etc/shared/rootkit_trojans.txt</rootkit_trojans>
  </rootcheck>
</ossec_config>

AWS GuardDuty

resource "aws_guardduty_detector" "main" {
  enable = true

  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
  }
}

Automated Security Patching

# Ubuntu: Enable unattended security updates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

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

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Unattended-Upgrade::Mail "[email protected]";

Security Hardening Checklist

Server Level

  • SSH key-only authentication, root login disabled
  • Firewall configured with deny-by-default policy
  • Fail2Ban active on SSH and web server
  • Automatic security updates enabled
  • Non-essential services disabled
  • File integrity monitoring (OSSEC or equivalent)
  • Audit logging enabled (auditd)

Application Level

  • TLS 1.2+ on all endpoints, HSTS enabled
  • Security headers configured (CSP, X-Frame-Options, etc.)
  • Rate limiting on all public endpoints
  • Input validation and parameterized queries (no sql.raw())
  • HttpOnly, Secure cookies for authentication
  • CORS restricted to known origins
  • Error responses do not leak stack traces

Network Level

  • Database not publicly accessible
  • Internal services on private subnets
  • VPC endpoints for AWS services (no public internet)
  • WAF on public-facing endpoints
  • DDoS protection (Cloudflare, AWS Shield)

Monitoring

  • Security event alerting configured
  • Log retention for at least 90 days
  • Failed authentication attempts monitored
  • Unusual traffic patterns detected

Frequently Asked Questions

How often should we perform security audits?

Quarterly automated scans (vulnerability scanning, dependency audits) and annual manual penetration tests. High-risk applications (payment processing, healthcare data) should have external penetration tests every 6 months. Every production deployment should include automated security scanning in the CI/CD pipeline --- see our CI/CD best practices guide.

Is a WAF necessary if we already have rate limiting?

Yes. Rate limiting prevents abuse but does not inspect request content. A WAF blocks SQL injection, XSS, and other application-layer attacks by analyzing request payloads. Think of rate limiting as flood protection and WAF as content inspection --- you need both.

How do we secure an Odoo production server?

In addition to the general hardening above: disable the database manager in production (list_db = False), set a strong admin_passwd, use dbfilter to restrict database access, run Odoo behind Nginx (never expose Odoo directly), and keep Odoo and all modules updated. ECOSIRE provides Odoo security hardening as part of our managed hosting services.

What is the single most impactful security measure?

Enabling MFA (Multi-Factor Authentication) on all administrative accounts. This single control prevents 99% of credential-based attacks. Implement MFA on SSH (via PAM), AWS console, database admin tools, and application admin panels before any other hardening measure.


What Comes Next

Security hardening is an ongoing practice. Combine it with monitoring and alerting for detection, disaster recovery for resilience, and CI/CD security scanning for prevention.

Contact ECOSIRE for security hardening consulting, or explore our DevOps guide for the complete infrastructure roadmap.


Published by ECOSIRE -- helping businesses secure production infrastructure.

E

Written by

ECOSIRE Research and Development Team

Building enterprise-grade digital products at ECOSIRE. Sharing insights on Odoo integrations, e-commerce automation, and AI-powered business solutions.

Chat on WhatsApp