Back to Guides
Security Best Practices
Essential security considerations for production deployments and infrastructure hardening.
10 min read
CriticalSecuritySecurity Checklist
- • Enable HTTPS/TLS encryption for all communications
- • Implement proper authentication and authorization
- • Secure API endpoints and database connections
- • Use environment variables for sensitive configuration
- • Regular security updates and vulnerability scanning
- • Implement proper logging and monitoring
Encryption and HTTPS
SSL/TLS Certificate Setup
# Nginx HTTPS Configuration
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://backend;
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;
}
}Let's Encrypt with Certbot
# Install Certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d example.com -d www.example.com
# Auto-renewal cron job
echo "0 12 * * * /usr/bin/certbot renew --quiet" | sudo crontab -
# Docker Compose with Let's Encrypt
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt
restart: unless-stopped
certbot:
image: certbot/certbot
volumes:
- /etc/letsencrypt:/etc/letsencrypt
- /var/www/certbot:/var/www/certbot
command: certonly --webroot --webroot-path=/var/www/certbot --email admin@example.com --agree-tos --no-eff-email -d example.comAuthentication and Authorization
Environment Variables for Secrets
Best Practice
Never hardcode secrets in your configuration files. Always use environment variables or secret management systems.
# .env file (never commit to version control)
DB_PASSWORD=your_secure_database_password
JWT_SECRET=your_jwt_signing_secret
API_KEY=your_external_api_key
ENCRYPTION_KEY=your_data_encryption_key
# Docker Compose with environment files
version: '3.8'
services:
app:
image: myapp:latest
env_file:
- .env.production
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:${DB_PASSWORD}@db:5432/myapp
- JWT_SECRET=${JWT_SECRET}
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:JWT Authentication Example
# Express.js JWT Authentication Middleware
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validate user credentials
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token, user: { id: user.id, email: user.email } });
} catch (error) {
res.status(500).json({ error: 'Authentication failed' });
}
});
// Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
};
// Protected route
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({ message: 'Access granted', user: req.user });
});Database Security
PostgreSQL Security Configuration
# PostgreSQL security configuration
# postgresql.conf
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_ca_file = 'root.crt'
# Require SSL connections
ssl_prefer_server_ciphers = on
ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'
# Connection security
listen_addresses = 'localhost' # Only local connections
max_connections = 100
# Authentication
password_encryption = scram-sha-256
# pg_hba.conf - require SSL for all connections
hostssl all all 0.0.0.0/0 scram-sha-256
# Docker Compose with PostgreSQL security
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: myapp
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
- ./postgresql.conf:/etc/postgresql/postgresql.conf
- ./ssl-certs:/var/lib/postgresql/ssl
command: postgres -c config_file=/etc/postgresql/postgresql.conf
networks:
- backend
restart: unless-stopped
networks:
backend:
driver: bridge
volumes:
db_data:Connection String Security
Important
Always use SSL/TLS for database connections in production and validate certificates.
API Security
Rate Limiting
# Express.js Rate Limiting
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redisClient = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
// General rate limiting
const limiter = rateLimit({
store: new RedisStore({
client: redisClient
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false
});
// Strict rate limiting for authentication endpoints
const authLimiter = rateLimit({
store: new RedisStore({
client: redisClient
}),
windowMs: 15 * 60 * 1000,
max: 5, // limit each IP to 5 login attempts per 15 minutes
message: 'Too many login attempts'
});
app.use('/api/', limiter);
app.use('/api/auth/', authLimiter);
# Nginx Rate Limiting
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
location /api/auth/ {
limit_req zone=login burst=3 nodelay;
proxy_pass http://backend;
}
}
}Input Validation and Sanitization
# Input validation with Joi
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)/).required(),
name: Joi.string().min(2).max(50).pattern(/^[a-zA-Zs]+$/).required()
});
app.post('/api/users', async (req, res) => {
try {
// Validate input
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// Sanitize data
const sanitizedUser = {
email: value.email.toLowerCase().trim(),
name: value.name.trim(),
password: value.password
};
// Hash password before storing
const saltRounds = 12;
sanitizedUser.passwordHash = await bcrypt.hash(sanitizedUser.password, saltRounds);
delete sanitizedUser.password;
// Save user...
const user = await User.create(sanitizedUser);
res.status(201).json({ id: user.id, email: user.email, name: user.name });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Container and Infrastructure Security
Secure Dockerfile
# Secure Dockerfile example
FROM node:18-alpine AS builder
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodeuser -u 1001
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy application code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM node:18-alpine AS production
# Install security updates
RUN apk update && apk upgrade && apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodeuser -u 1001
# Set working directory
WORKDIR /app
# Copy built application
COPY --from=builder --chown=nodeuser:nodejs /app/dist ./dist
COPY --from=builder --chown=nodeuser:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodeuser:nodejs /app/package.json ./package.json
# Switch to non-root user
USER nodeuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD node healthcheck.js
# Start application with dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]Security Scanning
# Vulnerability scanning with Docker
# Scan base image vulnerabilities
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image node:18-alpine
# Scan application dependencies
npm audit
npm audit fix
# CI/CD Security Pipeline (GitHub Actions)
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:latest'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'Security Monitoring
Security Logging
# Security logging middleware
const securityLogger = (req, res, next) => {
const logData = {
timestamp: new Date().toISOString(),
ip: req.ip,
method: req.method,
url: req.originalUrl,
userAgent: req.get('User-Agent'),
userId: req.user?.id || 'anonymous'
};
// Log suspicious activities
if (req.url.includes('admin') || req.url.includes('sql') || req.url.includes('script')) {
console.warn('SECURITY WARNING:', logData);
}
// Log all authentication attempts
if (req.url.includes('/auth/')) {
console.log('AUTH ATTEMPT:', logData);
}
next();
};
app.use(securityLogger);Fail2Ban Configuration
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
[nginx-req-limit]
enabled = true
filter = nginx-req-limit
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
logpath = /var/log/nginx/error.log
findtime = 600
bantime = 7200
maxretry = 10