Secure your Node.js Express applications against common vulnerabilities with Helmet.js middleware and implement rate limiting to prevent abuse and DDoS attacks.
Prerequisites
- Root or sudo access
- Basic knowledge of Node.js and Express
- Understanding of HTTP headers
What this solves
Modern web applications face constant security threats from malicious actors attempting to exploit common vulnerabilities. Node.js applications, while powerful and flexible, require proper security hardening to protect against attacks like XSS, clickjacking, and brute force attempts. This tutorial shows you how to implement enterprise-grade security using Helmet.js middleware for HTTP header protection and express-rate-limit for traffic throttling.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest security patches and package definitions.
sudo apt update && sudo apt upgrade -y
Install Node.js and npm
Install the latest LTS version of Node.js along with npm package manager for dependency management.
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g npm@latest
Create project directory
Create a dedicated directory for your secure Node.js application with proper ownership and permissions.
mkdir -p ~/secure-nodejs-app
cd ~/secure-nodejs-app
npm init -y
Install Express framework
Install Express.js as the foundation for your web application along with essential development dependencies.
npm install express
npm install --save-dev nodemon
Install security packages
Install Helmet.js for HTTP header security and express-rate-limit for request throttling protection.
npm install helmet express-rate-limit express-slow-down
Create basic Express application
Create a foundational Express server that will serve as the base for implementing security middleware.
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const app = express();
const PORT = process.env.PORT || 3000;
// Basic middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Basic routes (will add security later)
app.get('/', (req, res) => {
res.json({ message: 'Secure Node.js application', status: 'running' });
});
app.get('/api/data', (req, res) => {
res.json({ data: 'Sample API endpoint', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(Server running on port ${PORT});
});
Configure Helmet.js security middleware
Implement comprehensive Helmet configuration
Configure Helmet with production-ready security headers to protect against common web vulnerabilities. Add this configuration after your basic middleware setup.
// Helmet security configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: "cross-origin" },
dnsPrefetchControl: true,
frameguard: { action: 'deny' },
hidePoweredBy: true,
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
ieNoOpen: true,
noSniff: true,
originAgentCluster: true,
permittedCrossDomainPolicies: false,
referrerPolicy: { policy: "no-referrer" },
xssFilter: true
}));
Configure custom security headers
Add additional custom security headers for enhanced protection against emerging threats and compliance requirements.
// Custom security headers
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
Implement rate limiting protection
Configure global rate limiting
Set up global rate limiting to protect your entire application from abuse and potential DDoS attacks.
// Global rate limiting
const globalLimiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP',
retryAfter: Math.round(15 60 1000 / 1000)
},
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.round(req.rateLimit.resetTime - Date.now() / 1000)
});
}
});
app.use(globalLimiter);
Configure API-specific rate limiting
Create stricter rate limits for sensitive API endpoints that require additional protection from automated attacks.
// API-specific rate limiting
const apiLimiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 50, // Stricter limit for API endpoints
message: {
error: 'API rate limit exceeded',
retryAfter: Math.round(15 60 1000 / 1000)
},
standardHeaders: true,
legacyHeaders: false
});
// Slow down middleware for progressive delays
const speedLimiter = slowDown({
windowMs: 15 60 1000, // 15 minutes
delayAfter: 10, // Allow 10 requests per windowMs without delay
delayMs: 500, // Add 500ms delay per request after delayAfter
maxDelayMs: 20000 // Maximum delay of 20 seconds
});
// Apply to API routes
app.use('/api', apiLimiter);
app.use('/api', speedLimiter);
Create authentication rate limiting
Implement aggressive rate limiting for authentication endpoints to prevent brute force attacks on user credentials.
// Authentication rate limiting
const authLimiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 5, // Limit each IP to 5 login attempts per windowMs
skipSuccessfulRequests: true, // Don't count successful requests
message: {
error: 'Too many authentication attempts',
retryAfter: Math.round(15 60 1000 / 1000)
}
});
// Sample authentication endpoint with rate limiting
app.post('/auth/login', authLimiter, (req, res) => {
// Simulate authentication logic
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
// In production, implement proper authentication
res.json({ message: 'Authentication endpoint (implement your auth logic here)' });
});
Advanced security configurations
Configure request validation
Add request validation and sanitization to prevent malicious input from reaching your application logic.
// Request size limiting and validation
app.use(express.json({
limit: '10mb',
verify: (req, res, buf, encoding) => {
// Prevent malformed JSON attacks
try {
JSON.parse(buf);
} catch (e) {
res.status(400).json({ error: 'Invalid JSON format' });
throw new Error('Invalid JSON');
}
}
}));
// Input sanitization middleware
app.use((req, res, next) => {
if (req.body) {
for (let key in req.body) {
if (typeof req.body[key] === 'string') {
req.body[key] = req.body[key].trim();
}
}
}
next();
});
Add security monitoring
Implement basic security monitoring to log and track potential security incidents and rate limit violations.
// Security monitoring and logging
const securityLog = (req, message, level = 'info') => {
const logEntry = {
timestamp: new Date().toISOString(),
ip: req.ip || req.connection.remoteAddress,
userAgent: req.get('User-Agent'),
url: req.originalUrl,
method: req.method,
message: message,
level: level
};
console.log([SECURITY-${level.toUpperCase()}], JSON.stringify(logEntry));
};
// Security event middleware
app.use((req, res, next) => {
// Log suspicious patterns
const suspiciousPatterns = ['/admin', '/.env', '/wp-admin', '/phpmyadmin'];
if (suspiciousPatterns.some(pattern => req.path.includes(pattern))) {
securityLog(req, Suspicious path access: ${req.path}, 'warning');
}
next();
});
Update package.json scripts
Add convenient npm scripts for development and production deployment with proper environment handling.
{
"name": "secure-nodejs-app",
"version": "1.0.0",
"description": "Secure Node.js application with Helmet and rate limiting",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "^4.18.2",
"helmet": "^7.1.0",
"express-rate-limit": "^7.1.5",
"express-slow-down": "^2.0.1"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}
Production deployment configuration
Create systemd service
Configure a systemd service for production deployment with proper user isolation and security settings.
sudo useradd --system --create-home --shell /bin/false nodeapp
sudo chown -R nodeapp:nodeapp ~/secure-nodejs-app
[Unit]
Description=Secure Node.js Application
After=network.target
[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/home/nodeapp/secure-nodejs-app
ExecStart=/usr/bin/node app.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000
Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/home/nodeapp/secure-nodejs-app
[Install]
WantedBy=multi-user.target
Configure reverse proxy with Nginx
Set up Nginx as a reverse proxy to handle SSL termination and additional security features. This setup works well with our Nginx reverse proxy with SSL tutorial.
server {
listen 80;
server_name example.com;
# Security headers (additional to Helmet)
add_header X-Real-IP $remote_addr;
add_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header X-Forwarded-Proto $scheme;
# Rate limiting at Nginx level
limit_req_zone $binary_remote_addr zone=app:10m rate=10r/s;
limit_req zone=app burst=20 nodelay;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
}
}
Enable and start services
Enable the systemd service and configure Nginx to start your secure application automatically on system boot.
sudo systemctl enable secure-nodejs-app
sudo systemctl start secure-nodejs-app
sudo ln -s /etc/nginx/sites-available/secure-nodejs-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Verify your setup
Test your security configuration to ensure all middleware is working correctly and providing proper protection.
# Check service status
sudo systemctl status secure-nodejs-app
Test basic functionality
curl -I http://localhost:3000
Test rate limiting (run multiple times quickly)
for i in {1..10}; do curl -s http://localhost:3000/api/data; done
Check security headers
curl -I http://localhost:3000 | grep -E "X-|Content-Security-Policy|Strict-Transport-Security"
Monitor logs
sudo journalctl -u secure-nodejs-app -f
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Application won't start | Port already in use | Check with sudo netstat -tlnp | grep :3000 and kill conflicting process |
| Rate limiting not working | Middleware order incorrect | Ensure rate limiting middleware is applied before route handlers |
| CSP blocking resources | Too restrictive Content Security Policy | Adjust CSP directives to allow necessary resources |
| 502 Bad Gateway with Nginx | Node.js service not running | sudo systemctl start secure-nodejs-app |
| Headers not appearing | Nginx overriding headers | Check Nginx configuration for conflicting header directives |
| High memory usage | Rate limiter storing too many IPs | Reduce windowMs or implement Redis store for rate limiting |
Next steps
- Configure Nginx Redis caching with SSL authentication for enhanced performance and security
- Set up JWT authentication with Redis session storage
- Monitor Node.js applications with Prometheus metrics
- Configure SSL certificates and HTTPS security hardening
- Set up centralized logging with Winston and Elasticsearch
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
APP_DIR="/opt/secure-nodejs-app"
APP_USER="nodeapp"
NODE_PORT="${1:-3000}"
# Usage message
usage() {
echo "Usage: $0 [port]"
echo " port: Port number for Node.js application (default: 3000)"
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function
cleanup() {
log_error "Installation failed. Cleaning up..."
systemctl stop secure-nodejs-app 2>/dev/null || true
systemctl disable secure-nodejs-app 2>/dev/null || true
rm -f /etc/systemd/system/secure-nodejs-app.service
userdel -r "$APP_USER" 2>/dev/null || true
rm -rf "$APP_DIR"
systemctl daemon-reload
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Validate port argument
if [[ $NODE_PORT -lt 1 || $NODE_PORT -gt 65535 ]]; then
log_error "Invalid port number: $NODE_PORT"
usage
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
log_info "Detected distribution: $ID"
echo "[1/8] Updating system packages..."
$PKG_UPDATE
echo "[2/8] Installing Node.js and npm..."
if command -v node >/dev/null 2>&1; then
log_info "Node.js already installed: $(node --version)"
else
# Install Node.js LTS
case "$PKG_MGR" in
apt)
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
$PKG_INSTALL nodejs
;;
dnf|yum)
curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash -
$PKG_INSTALL nodejs
;;
esac
fi
# Update npm to latest
npm install -g npm@latest
echo "[3/8] Creating application user and directory..."
# Create dedicated user for the application
if ! id "$APP_USER" &>/dev/null; then
useradd --system --home "$APP_DIR" --shell /bin/false "$APP_USER"
log_info "Created user: $APP_USER"
fi
# Create application directory
mkdir -p "$APP_DIR"
chown "$APP_USER:$APP_USER" "$APP_DIR"
chmod 755 "$APP_DIR"
echo "[4/8] Setting up Node.js project..."
cd "$APP_DIR"
# Initialize npm project as app user
sudo -u "$APP_USER" npm init -y
echo "[5/8] Installing dependencies..."
# Install Express and security packages
sudo -u "$APP_USER" npm install express helmet express-rate-limit express-slow-down
sudo -u "$APP_USER" npm install --save-dev nodemon
echo "[6/8] Creating application files..."
# Create main application file
cat > "$APP_DIR/app.js" << 'EOF'
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const app = express();
const PORT = process.env.PORT || 3000;
// Rate limiting configuration
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.'
});
// Slow down configuration
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 50, // allow 50 requests per 15 minutes, then...
delayMs: 500 // begin adding 500ms of delay per request above 50
});
// Apply rate limiting to all requests
app.use(limiter);
app.use(speedLimiter);
// Helmet security configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: "cross-origin" },
dnsPrefetchControl: true,
frameguard: { action: 'deny' },
hidePoweredBy: true,
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
ieNoOpen: true,
noSniff: true,
originAgentCluster: true,
permittedCrossDomainPolicies: false,
referrerPolicy: { policy: "no-referrer" },
xssFilter: true
}));
// Basic middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Secure Node.js application', status: 'running' });
});
app.get('/api/data', (req, res) => {
res.json({ data: 'Sample API endpoint', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
EOF
# Set proper ownership and permissions
chown "$APP_USER:$APP_USER" "$APP_DIR/app.js"
chmod 644 "$APP_DIR/app.js"
echo "[7/8] Creating systemd service..."
# Create systemd service file
cat > /etc/systemd/system/secure-nodejs-app.service << EOF
[Unit]
Description=Secure Node.js Application
After=network.target
[Service]
Type=simple
User=$APP_USER
WorkingDirectory=$APP_DIR
ExecStart=/usr/bin/node app.js
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=$NODE_PORT
# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$APP_DIR
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and start service
systemctl daemon-reload
systemctl enable secure-nodejs-app
systemctl start secure-nodejs-app
echo "[8/8] Configuring firewall..."
# Configure firewall based on distribution
case "$FIREWALL_CMD" in
ufw)
if command -v ufw >/dev/null 2>&1; then
ufw allow "$NODE_PORT"/tcp
log_info "UFW rule added for port $NODE_PORT"
fi
;;
firewall-cmd)
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active firewalld >/dev/null 2>&1; then
firewall-cmd --permanent --add-port="$NODE_PORT"/tcp
firewall-cmd --reload
log_info "Firewalld rule added for port $NODE_PORT"
fi
;;
esac
# Verification checks
echo "Verifying installation..."
sleep 3
if systemctl is-active --quiet secure-nodejs-app; then
log_info "✓ Service is running"
else
log_error "✗ Service is not running"
exit 1
fi
if curl -s "http://localhost:$NODE_PORT" >/dev/null 2>&1; then
log_info "✓ Application is responding"
else
log_error "✗ Application is not responding"
exit 1
fi
log_info "Installation completed successfully!"
echo ""
echo "Application details:"
echo " - Service: secure-nodejs-app"
echo " - Port: $NODE_PORT"
echo " - Directory: $APP_DIR"
echo " - User: $APP_USER"
echo ""
echo "Commands:"
echo " - Start: systemctl start secure-nodejs-app"
echo " - Stop: systemctl stop secure-nodejs-app"
echo " - Status: systemctl status secure-nodejs-app"
echo " - Logs: journalctl -u secure-nodejs-app -f"
echo ""
echo "Test the application:"
echo " curl http://localhost:$NODE_PORT"
Review the script before running. Execute with: bash install.sh