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
:latestin 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.
Written by
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Grow Your Business with ECOSIRE
Enterprise solutions across ERP, eCommerce, AI, analytics, and automation.
Related Articles
AI Fraud Detection for E-commerce: Protect Revenue Without Blocking Sales
Implement AI fraud detection that catches 95%+ of fraudulent transactions while keeping false positive rates under 2%. ML scoring, behavioral analysis, and ROI guide.
API Rate Limiting: Patterns and Best Practices
Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.
Cybersecurity Trends 2026-2027: Zero Trust, AI Threats, and Defense
The definitive guide to cybersecurity trends for 2026-2027—AI-powered attacks, zero trust implementation, supply chain security, and building resilient security programs.