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 Research and Development Team
Building enterprise-grade digital products at ECOSIRE. Sharing insights on Odoo integrations, e-commerce automation, and AI-powered business solutions.
Related Articles
AI Fraud Detection for eCommerce: Protect Revenue Without Blocking Good Customers
Deploy AI fraud detection that catches 95%+ of fraudulent transactions while reducing false positives by 50-70%. Covers models, rules, and implementation.
API Gateway Patterns and Best Practices for Modern Applications
Implement API gateway patterns including rate limiting, authentication, request routing, circuit breakers, and API versioning for scalable web architectures.
CDN Performance Optimization: The Complete Guide to Faster Global Delivery
Optimize CDN performance with caching strategies, edge computing, image optimization, and multi-CDN architectures for faster global content delivery.