Configure Node.js SSL certificates and HTTPS security hardening for production servers

Intermediate 45 min May 27, 2026 98 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up production-grade SSL/TLS encryption for Node.js applications with certificate management, secure cipher suites, and comprehensive security headers including HSTS implementation.

Prerequisites

  • Node.js 18+ installed
  • Root or sudo access
  • Domain name configured
  • Basic Express.js knowledge

What this solves

This tutorial configures production-grade SSL/TLS security for Node.js applications running on Express.js. You'll implement secure certificate management, harden TLS protocols, configure strong cipher suites, and deploy essential security headers including HTTP Strict Transport Security (HSTS). This approach protects against man-in-the-middle attacks, ensures encrypted data transmission, and meets modern security compliance requirements.

Step-by-step configuration

Install required packages

Install Node.js security middleware and certificate management tools for production HTTPS implementation.

sudo apt update
sudo npm install -g helmet express-rate-limit express-slow-down
npm install helmet express-rate-limit express-slow-down morgan compression
sudo dnf update -y
sudo npm install -g helmet express-rate-limit express-slow-down
npm install helmet express-rate-limit express-slow-down morgan compression

Create SSL certificate directory structure

Establish secure directory structure for SSL certificates with appropriate permissions and ownership.

sudo mkdir -p /etc/ssl/nodejs/{certs,private}
sudo chown -R root:ssl-cert /etc/ssl/nodejs
sudo chmod 755 /etc/ssl/nodejs/certs
sudo chmod 750 /etc/ssl/nodejs/private
Never use chmod 777. SSL private keys require restricted access. Use chmod 750 for private directories and ensure proper group ownership for security.

Generate self-signed certificate for development

Create self-signed certificates for development and testing environments. Replace with production certificates from a trusted CA.

sudo openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
  -keyout /etc/ssl/nodejs/private/server.key \
  -out /etc/ssl/nodejs/certs/server.crt \
  -subj "/C=US/ST=State/L=City/O=Organization/CN=example.com"

Configure certificate permissions

Set secure permissions for SSL certificates ensuring only authorized users can access private keys.

sudo chmod 644 /etc/ssl/nodejs/certs/server.crt
sudo chmod 600 /etc/ssl/nodejs/private/server.key
sudo chown root:ssl-cert /etc/ssl/nodejs/private/server.key

Create hardened Express.js HTTPS server

Configure Express.js with production-grade SSL/TLS settings, security middleware, and comprehensive security headers.

const express = require('express');
const https = require('https');
const fs = require('fs');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const compression = require('compression');
const morgan = require('morgan');

const app = express();

// Security middleware configuration
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:"],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  noSniff: true,
  xssFilter: true,
  referrerPolicy: { policy: 'same-origin' }
}));

// Rate limiting configuration
const limiter = 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, please try again later.'
  },
  standardHeaders: true,
  legacyHeaders: false
});

// Speed limiting configuration
const speedLimiter = slowDown({
  windowMs: 15  60  1000, // 15 minutes
  delayAfter: 50, // allow 50 requests per 15 minutes at full speed
  delayMs: 500 // add 500ms delay after 50 requests
});

app.use(limiter);
app.use(speedLimiter);
app.use(compression());
app.use(morgan('combined'));

// SSL/TLS configuration with strong cipher suites
const sslOptions = {
  key: fs.readFileSync('/etc/ssl/nodejs/private/server.key'),
  cert: fs.readFileSync('/etc/ssl/nodejs/certs/server.crt'),
  // TLS configuration
  secureProtocol: 'TLSv1_2_method',
  ciphers: [
    'ECDHE-ECDSA-AES256-GCM-SHA384',
    'ECDHE-RSA-AES256-GCM-SHA384',
    'ECDHE-ECDSA-CHACHA20-POLY1305',
    'ECDHE-RSA-CHACHA20-POLY1305',
    'ECDHE-ECDSA-AES128-GCM-SHA256',
    'ECDHE-RSA-AES128-GCM-SHA256'
  ].join(':'),
  honorCipherOrder: true,
  secureOptions: require('constants').SSL_OP_NO_SSLv2 | 
                 require('constants').SSL_OP_NO_SSLv3 |
                 require('constants').SSL_OP_NO_TLSv1 |
                 require('constants').SSL_OP_NO_TLSv1_1
};

// Application routes
app.get('/', (req, res) => {
  res.json({
    message: 'Secure Node.js server running with HTTPS',
    timestamp: new Date().toISOString(),
    secure: req.secure,
    protocol: req.protocol
  });
});

app.get('/health', (req, res) => {
  res.status(200).json({
    status: 'healthy',
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    timestamp: new Date().toISOString()
  });
});

// HTTPS server startup
const server = https.createServer(sslOptions, app);

server.listen(443, () => {
  console.log('Secure HTTPS server running on port 443');
  console.log('TLS configuration: TLSv1.2+ with strong cipher suites');
  console.log('Security headers: HSTS, CSP, X-Frame-Options enabled');
});

// HTTP to HTTPS redirect server
const httpApp = express();
httpApp.use((req, res) => {
  res.redirect(301, https://${req.headers.host}${req.url});
});

httpApp.listen(80, () => {
  console.log('HTTP redirect server running on port 80');
});

// Graceful shutdown handling
process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully');
  server.close(() => {
    console.log('Process terminated');
  });
});

Create systemd service for Node.js HTTPS server

Configure systemd service to manage the Node.js HTTPS server with proper user permissions and automatic restart capabilities.

sudo useradd --system --shell /bin/false --home-dir /var/lib/nodejs --create-home nodejs
sudo usermod -a -G ssl-cert nodejs
[Unit]
Description=Node.js HTTPS Server
After=network.target

[Service]
Type=simple
User=nodejs
Group=nodejs
WorkingDirectory=/home/nodejs
ExecStart=/usr/bin/node secure-server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=443
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodejs-https

Security settings

NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/var/lib/nodejs AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target

Configure Node.js application ownership

Set proper ownership and permissions for the Node.js application files and directories.

sudo cp /home/nodejs/secure-server.js /home/nodejs/secure-server.js.bak
sudo chown -R nodejs:nodejs /home/nodejs
sudo chmod 755 /home/nodejs
sudo chmod 644 /home/nodejs/secure-server.js

Install and configure production certificates

Replace self-signed certificates with production certificates from Let's Encrypt or a trusted Certificate Authority.

sudo apt install -y certbot
sudo certbot certonly --standalone -d example.com
sudo ln -sf /etc/letsencrypt/live/example.com/fullchain.pem /etc/ssl/nodejs/certs/server.crt
sudo ln -sf /etc/letsencrypt/live/example.com/privkey.pem /etc/ssl/nodejs/private/server.key
sudo dnf install -y certbot
sudo certbot certonly --standalone -d example.com
sudo ln -sf /etc/letsencrypt/live/example.com/fullchain.pem /etc/ssl/nodejs/certs/server.crt
sudo ln -sf /etc/letsencrypt/live/example.com/privkey.pem /etc/ssl/nodejs/private/server.key

Configure firewall rules for HTTPS

Open firewall ports for HTTP and HTTPS traffic while maintaining security.

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status numbered
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-services

Enable and start the HTTPS service

Start the systemd service and enable automatic startup on boot.

sudo systemctl daemon-reload
sudo systemctl enable nodejs-https
sudo systemctl start nodejs-https
sudo systemctl status nodejs-https

Configure automatic certificate renewal

Set up automated certificate renewal with systemd timers to maintain continuous SSL coverage.

[Unit]
Description=Certbot Renewal Service

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet
ExecStartPost=/bin/systemctl reload nodejs-https
[Unit]
Description=Run certbot renewal twice daily

[Timer]
OnCalendar=--* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable certbot-renewal.timer
sudo systemctl start certbot-renewal.timer
sudo systemctl list-timers certbot-renewal

Configure advanced TLS security settings

Implement OCSP stapling

Configure Online Certificate Status Protocol stapling for improved SSL performance and security validation.

const ocspCache = new Map();
const crypto = require('crypto');

// OCSP stapling configuration
const ocspOptions = {
  cert: fs.readFileSync('/etc/ssl/nodejs/certs/server.crt'),
  key: fs.readFileSync('/etc/ssl/nodejs/private/server.key'),
  ca: fs.readFileSync('/etc/ssl/certs/ca-certificates.crt'),
  requestOCSP: true,
  rejectUnauthorized: true
};

// Add to existing sslOptions in secure-server.js
Object.assign(sslOptions, ocspOptions);

Configure security monitoring

Set up logging and monitoring for SSL/TLS connections and security events. This integrates with existing Node.js monitoring solutions for comprehensive observability.

const winston = require('winston');

const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ 
      filename: '/var/log/nodejs/security.log',
      level: 'warn'
    }),
    new winston.transports.File({ 
      filename: '/var/log/nodejs/ssl-access.log' 
    })
  ]
});

// Add security event logging middleware
app.use((req, res, next) => {
  securityLogger.info({
    type: 'ssl_connection',
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    protocol: req.protocol,
    secure: req.secure,
    timestamp: new Date().toISOString()
  });
  next();
});

Create log directories with proper permissions

Establish secure logging directories for SSL and security event monitoring.

sudo mkdir -p /var/log/nodejs
sudo chown nodejs:nodejs /var/log/nodejs
sudo chmod 755 /var/log/nodejs

Verify your setup

Test SSL certificate installation, TLS configuration, and security headers implementation.

# Check certificate validity
sudo openssl x509 -in /etc/ssl/nodejs/certs/server.crt -text -noout

Test HTTPS connection

curl -I https://example.com

Verify TLS configuration

echo | openssl s_client -connect example.com:443 -servername example.com

Check security headers

curl -I -H "Host: example.com" https://example.com

Verify service status

sudo systemctl status nodejs-https

Check SSL certificate expiration

echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -dates -noout

Test security configuration

Validate SSL/TLS security implementation using online tools and command-line testing.

# Test SSL Labs rating (manual check at ssllabs.com/ssltest/)

Test HSTS preload eligibility

curl -H "Host: example.com" -I https://example.com | grep -i strict

Verify cipher suite strength

nmap --script ssl-enum-ciphers -p 443 example.com

Check certificate chain

echo | openssl s_client -connect example.com:443 -showcerts

Test HTTP to HTTPS redirect

curl -I http://example.com

Common issues

SymptomCauseFix
Permission denied accessing certificatesIncorrect file permissions or ownershipsudo chmod 600 /etc/ssl/nodejs/private/server.key && sudo chown root:ssl-cert /etc/ssl/nodejs/private/server.key
Node.js cannot bind to port 443Missing CAP_NET_BIND_SERVICE capabilityAdd AmbientCapabilities=CAP_NET_BIND_SERVICE to systemd service
SSL handshake failuresWeak cipher suites or outdated TLS versionUpdate cipher suite configuration and ensure TLSv1.2+ only
Certificate chain incompleteMissing intermediate certificatesUse fullchain.pem from Let's Encrypt or concatenate intermediate certs
HSTS not workingHeaders not properly configuredVerify helmet configuration includes HSTS settings
High memory usageSSL session cache not optimizedConfigure SSL session timeout and cache limits

Next steps

Running this in production?

Ready for production scale? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle infrastructure security hardening for businesses that depend on uptime. From initial setup to ongoing operations.