Skip to main content

TLS 1.3: What You Need to Know

Ryan Dahlberg
Ryan Dahlberg
November 8, 2025 11 min read
Share:
TLS 1.3: What You Need to Know

Introduction

Transport Layer Security (TLS) 1.3, finalized in August 2018, represents the most significant upgrade to the TLS protocol in over a decade. It addresses critical security vulnerabilities while dramatically improving connection performance.

This comprehensive guide explores TLS 1.3’s improvements, implementation strategies, migration considerations, and best practices for securing modern web applications with the latest cryptographic standards.

Key Improvements in TLS 1.3

Faster Handshake

TLS 1.3 reduces the handshake from two round trips to one:

// TLS 1.2 handshake (2-RTT)
const TLS12Handshake = {
  step1: 'Client Hello → Server',
  step2: 'Server Hello, Certificate, Key Exchange ← Server',
  step3: 'Key Exchange, Finished → Server',
  step4: 'Finished ← Server',
  step5: 'Application Data ↔',
  totalRoundTrips: 2
};

// TLS 1.3 handshake (1-RTT)
const TLS13Handshake = {
  step1: 'Client Hello (with key share) → Server',
  step2: 'Server Hello, Certificate, Finished ← Server',
  step3: 'Finished, Application Data → Server',
  totalRoundTrips: 1,
  latencyReduction: '50%'
};

// TLS 1.3 0-RTT (resumption)
const TLS13ZeroRTT = {
  step1: 'Client Hello, Early Data → Server',
  step2: 'Server Hello, Finished, Application Data ← Server',
  totalRoundTrips: 0,
  note: 'Replay attacks possible, use carefully'
};

Removed Weak Cryptography

const RemovedFeatures = {
  ciphers: [
    'RC4',
    'DES',
    '3DES',
    'AES-CBC (replaced with AEAD ciphers)',
    'MD5-based signatures',
    'SHA-1'
  ],

  keyExchange: [
    'RSA key exchange (no forward secrecy)',
    'Static Diffie-Hellman',
    'Custom DHE groups'
  ],

  features: [
    'Renegotiation',
    'Compression (CRIME attack)',
    'Non-AEAD ciphers',
    'Weak named curves'
  ],

  impact: 'Eliminates entire classes of vulnerabilities'
};

const RequiredFeatures = {
  ciphers: [
    'TLS_AES_128_GCM_SHA256',
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256'
  ],

  keyExchange: [
    'ECDHE (Elliptic Curve Diffie-Hellman Ephemeral)',
    'DHE (Diffie-Hellman Ephemeral)'
  ],

  features: [
    'Perfect Forward Secrecy (mandatory)',
    'AEAD ciphers only',
    'Encrypted handshake (after Server Hello)'
  ]
};

Implementing TLS 1.3

Node.js Configuration

const https = require('https');
const fs = require('fs');

// TLS 1.3 server configuration
const tlsOptions = {
  // Certificate and key
  key: fs.readFileSync('./certs/private-key.pem'),
  cert: fs.readFileSync('./certs/certificate.pem'),
  ca: fs.readFileSync('./certs/ca-bundle.pem'),

  // TLS version constraints
  minVersion: 'TLSv1.3',
  maxVersion: 'TLSv1.3',

  // Cipher suites (TLS 1.3 only)
  ciphers: [
    'TLS_AES_256_GCM_SHA384',
    'TLS_AES_128_GCM_SHA256',
    'TLS_CHACHA20_POLY1305_SHA256'
  ].join(':'),

  // Prefer server cipher order
  honorCipherOrder: true,

  // Session resumption
  sessionTimeout: 300, // 5 minutes

  // Client certificate authentication (optional)
  requestCert: false,
  rejectUnauthorized: true,

  // ALPN (Application-Layer Protocol Negotiation)
  ALPNProtocols: ['h2', 'http/1.1'],

  // Security options
  secureOptions:
    require('crypto').constants.SSL_OP_NO_TLSv1 |
    require('crypto').constants.SSL_OP_NO_TLSv1_1 |
    require('crypto').constants.SSL_OP_NO_TLSv1_2
};

// Create HTTPS server
const server = https.createServer(tlsOptions, (req, res) => {
  // Log TLS version
  const tlsVersion = req.socket.getProtocol();
  console.log(`Connection using: ${tlsVersion}`);

  // Log cipher suite
  const cipher = req.socket.getCipher();
  console.log(`Cipher: ${cipher.name} (${cipher.version})`);

  res.writeHead(200);
  res.end('Secure connection established\n');
});

server.listen(443, () => {
  console.log('HTTPS server running on port 443 with TLS 1.3');
});

// Monitor TLS connections
server.on('secureConnection', (tlsSocket) => {
  console.log('New secure connection:', {
    protocol: tlsSocket.getProtocol(),
    cipher: tlsSocket.getCipher(),
    authorized: tlsSocket.authorized,
    peerCertificate: tlsSocket.getPeerCertificate().subject
  });
});

// Handle TLS errors
server.on('tlsClientError', (err, tlsSocket) => {
  console.error('TLS client error:', {
    error: err.message,
    code: err.code,
    address: tlsSocket.remoteAddress
  });
});

Express.js with TLS 1.3

const express = require('express');
const helmet = require('helmet');

const app = express();

// Security headers
app.use(helmet({
  strictTransportSecurity: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      upgradeInsecureRequests: []
    }
  }
}));

// Force HTTPS redirect
app.use((req, res, next) => {
  if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
    return next();
  }
  res.redirect(301, `https://${req.headers.host}${req.url}`);
});

// TLS version logging middleware
app.use((req, res, next) => {
  if (req.socket.encrypted) {
    const tlsInfo = {
      protocol: req.socket.getProtocol(),
      cipher: req.socket.getCipher(),
      serverName: req.socket.servername
    };

    // Reject connections not using TLS 1.3
    if (tlsInfo.protocol !== 'TLSv1.3') {
      return res.status(426).json({
        error: 'Upgrade Required',
        message: 'TLS 1.3 is required'
      });
    }

    req.tlsInfo = tlsInfo;
  }

  next();
});

// Create HTTPS server
const httpsServer = https.createServer(tlsOptions, app);

httpsServer.listen(443, () => {
  console.log('Express app running with TLS 1.3 on port 443');
});

Nginx Configuration

# Nginx with TLS 1.3
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

    # TLS 1.3 only
    ssl_protocols TLSv1.3;

    # Certificate configuration
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    # Cipher suites (TLS 1.3 only)
    ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256';
    ssl_prefer_server_ciphers on;

    # Session cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_session_tickets on;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # TLS 1.3 early data (0-RTT)
    ssl_early_data on;

    location / {
        proxy_pass http://backend:3000;
        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;

        # Pass TLS information to backend
        proxy_set_header X-SSL-Protocol $ssl_protocol;
        proxy_set_header X-SSL-Cipher $ssl_cipher;
    }

    # Handle early data replays
    location /api {
        # Reject early data for non-idempotent operations
        if ($ssl_early_data = "1") {
            return 425; # Too Early
        }

        proxy_pass http://backend:3000;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

0-RTT Resumption

Understanding 0-RTT

class ZeroRTTHandler {
  constructor() {
    this.earlyDataCache = new Map();
  }

  // Check if request used 0-RTT
  isEarlyData(req) {
    return req.headers['early-data'] === '1' ||
           req.socket.isEarlyData === true;
  }

  // Validate 0-RTT request safety
  canUseEarlyData(req) {
    // Only safe for idempotent operations
    const safeMethods = ['GET', 'HEAD', 'OPTIONS'];

    if (!safeMethods.includes(req.method)) {
      return false;
    }

    // No state-changing operations
    if (req.url.includes('/api/') && req.method !== 'GET') {
      return false;
    }

    // No authentication endpoints
    if (req.url.includes('/auth/') || req.url.includes('/login')) {
      return false;
    }

    return true;
  }

  // Middleware to handle early data
  handleEarlyData(req, res, next) {
    if (this.isEarlyData(req)) {
      console.log('Early data detected:', {
        method: req.method,
        url: req.url
      });

      // Reject if not safe
      if (!this.canUseEarlyData(req)) {
        return res.status(425).json({
          error: 'Too Early',
          message: 'This operation cannot use 0-RTT'
        });
      }

      // Check for replay
      if (this.isReplayAttack(req)) {
        return res.status(400).json({
          error: 'Replay detected'
        });
      }

      // Mark request as using early data
      req.usedEarlyData = true;
    }

    next();
  }

  // Detect replay attacks
  isReplayAttack(req) {
    const signature = this.generateRequestSignature(req);

    if (this.earlyDataCache.has(signature)) {
      return true;
    }

    // Cache for 60 seconds
    this.earlyDataCache.set(signature, Date.now());

    setTimeout(() => {
      this.earlyDataCache.delete(signature);
    }, 60000);

    return false;
  }

  generateRequestSignature(req) {
    const crypto = require('crypto');

    return crypto
      .createHash('sha256')
      .update(req.method)
      .update(req.url)
      .update(req.headers['user-agent'] || '')
      .update(req.socket.remoteAddress)
      .digest('hex');
  }
}

// Usage
const zeroRTT = new ZeroRTTHandler();

app.use((req, res, next) => {
  zeroRTT.handleEarlyData(req, res, next);
});

// Routes that allow 0-RTT
app.get('/api/public/status', (req, res) => {
  res.json({
    status: 'ok',
    usedEarlyData: req.usedEarlyData
  });
});

// Routes that reject 0-RTT
app.post('/api/transaction', (req, res) => {
  if (req.usedEarlyData) {
    return res.status(425).json({
      error: 'Transaction cannot use 0-RTT'
    });
  }

  // Process transaction
  res.json({ success: true });
});

Certificate Management

Automated Certificate Renewal

const acme = require('acme-client');
const fs = require('fs').promises;

class CertificateManager {
  constructor() {
    this.client = new acme.Client({
      directoryUrl: acme.directory.letsencrypt.production,
      accountKey: await this.loadOrCreateAccountKey()
    });
  }

  async loadOrCreateAccountKey() {
    try {
      const key = await fs.readFile('./keys/account.pem');
      return key;
    } catch (error) {
      const key = await acme.crypto.createPrivateKey();
      await fs.writeFile('./keys/account.pem', key);
      return key;
    }
  }

  async obtainCertificate(domain) {
    // Generate CSR
    const [key, csr] = await acme.crypto.createCsr({
      commonName: domain,
      altNames: [`www.${domain}`]
    });

    // Get certificate
    const cert = await this.client.auto({
      csr,
      email: process.env.ADMIN_EMAIL,
      termsOfServiceAgreed: true,
      challengeCreateFn: async (authz, challenge, keyAuthorization) => {
        // HTTP-01 challenge
        if (challenge.type === 'http-01') {
          const filePath = `.well-known/acme-challenge/${challenge.token}`;
          await fs.writeFile(filePath, keyAuthorization);
        }
      },
      challengeRemoveFn: async (authz, challenge) => {
        if (challenge.type === 'http-01') {
          const filePath = `.well-known/acme-challenge/${challenge.token}`;
          await fs.unlink(filePath);
        }
      }
    });

    // Save certificate and key
    await fs.writeFile('./certs/private-key.pem', key);
    await fs.writeFile('./certs/certificate.pem', cert);

    console.log('Certificate obtained successfully');

    // Schedule renewal
    this.scheduleRenewal(domain);

    return { key, cert };
  }

  async renewCertificate(domain) {
    console.log(`Renewing certificate for ${domain}`);

    try {
      await this.obtainCertificate(domain);

      // Reload server with new certificate
      await this.reloadServer();

      console.log('Certificate renewed successfully');
    } catch (error) {
      console.error('Certificate renewal failed:', error);

      // Alert administrators
      await this.alertAdmins({
        subject: 'Certificate Renewal Failed',
        domain,
        error: error.message
      });
    }
  }

  scheduleRenewal(domain) {
    // Renew 30 days before expiration
    const renewalDate = new Date();
    renewalDate.setDate(renewalDate.getDate() + 60); // 60 days from now

    const timeUntilRenewal = renewalDate - Date.now();

    setTimeout(() => {
      this.renewCertificate(domain);
    }, timeUntilRenewal);

    console.log(`Certificate renewal scheduled for ${renewalDate}`);
  }

  async reloadServer() {
    // Send SIGHUP to reload configuration
    process.kill(process.pid, 'SIGHUP');
  }

  async checkCertificateExpiry(certPath) {
    const cert = await fs.readFile(certPath);
    const x509 = new crypto.X509Certificate(cert);

    const validTo = new Date(x509.validTo);
    const daysUntilExpiry = (validTo - Date.now()) / (1000 * 60 * 60 * 24);

    return {
      validTo,
      daysUntilExpiry,
      needsRenewal: daysUntilExpiry < 30
    };
  }
}

// Initialize certificate manager
const certManager = new CertificateManager();

// Check and renew certificates daily
setInterval(async () => {
  const status = await certManager.checkCertificateExpiry('./certs/certificate.pem');

  if (status.needsRenewal) {
    await certManager.renewCertificate('example.com');
  }
}, 24 * 60 * 60 * 1000);

Performance Monitoring

TLS Performance Metrics

class TLSPerformanceMonitor {
  constructor() {
    this.metrics = {
      handshakes: new Map(),
      connections: new Map()
    };
  }

  measureHandshake(socket) {
    const startTime = Date.now();

    socket.once('secure', () => {
      const duration = Date.now() - startTime;

      this.recordHandshake({
        protocol: socket.getProtocol(),
        cipher: socket.getCipher(),
        duration,
        resumed: socket.isSessionReused()
      });
    });
  }

  recordHandshake(data) {
    const key = `${data.protocol}:${data.cipher.name}`;

    if (!this.metrics.handshakes.has(key)) {
      this.metrics.handshakes.set(key, {
        count: 0,
        totalDuration: 0,
        resumed: 0,
        fresh: 0
      });
    }

    const stats = this.metrics.handshakes.get(key);
    stats.count++;
    stats.totalDuration += data.duration;

    if (data.resumed) {
      stats.resumed++;
    } else {
      stats.fresh++;
    }
  }

  getStatistics() {
    const stats = [];

    for (const [key, data] of this.metrics.handshakes) {
      stats.push({
        protocolCipher: key,
        averageHandshakeTime: data.totalDuration / data.count,
        totalConnections: data.count,
        resumedConnections: data.resumed,
        freshConnections: data.fresh,
        resumptionRate: (data.resumed / data.count * 100).toFixed(2) + '%'
      });
    }

    return stats;
  }

  logStatistics() {
    const stats = this.getStatistics();

    console.log('TLS Performance Statistics:');
    console.table(stats);

    // Alert on performance issues
    for (const stat of stats) {
      if (stat.averageHandshakeTime > 100) {
        console.warn(`Slow handshake detected: ${stat.protocolCipher}`);
      }

      if (parseFloat(stat.resumptionRate) < 50) {
        console.warn(`Low resumption rate: ${stat.protocolCipher}`);
      }
    }
  }
}

// Use monitor
const monitor = new TLSPerformanceMonitor();

server.on('secureConnection', (socket) => {
  monitor.measureHandshake(socket);
});

// Log statistics every hour
setInterval(() => {
  monitor.logStatistics();
}, 60 * 60 * 1000);

Migration Strategy

Gradual Migration Plan

const MigrationStrategy = {
  phase1: {
    name: 'Assessment',
    duration: '1-2 weeks',
    tasks: [
      'Inventory all TLS endpoints',
      'Check client compatibility',
      'Review certificate configuration',
      'Test 0-RTT replay protection',
      'Benchmark current performance'
    ]
  },

  phase2: {
    name: 'Development Environment',
    duration: '1-2 weeks',
    tasks: [
      'Enable TLS 1.3 in dev/staging',
      'Update server configurations',
      'Test application compatibility',
      'Verify monitoring and logging',
      'Performance testing'
    ]
  },

  phase3: {
    name: 'Gradual Rollout',
    duration: '2-4 weeks',
    tasks: [
      'Enable TLS 1.3 for 10% of traffic',
      'Monitor error rates and performance',
      'Increase to 25%, then 50%, then 100%',
      'Keep TLS 1.2 fallback available',
      'Monitor client feedback'
    ]
  },

  phase4: {
    name: 'Optimization',
    duration: 'Ongoing',
    tasks: [
      'Tune cipher suite preferences',
      'Optimize session resumption',
      'Implement 0-RTT for safe endpoints',
      'Remove TLS 1.2 support (eventually)',
      'Regular security audits'
    ]
  }
};

// Feature flag for gradual rollout
class TLSRollout {
  constructor() {
    this.rolloutPercentage = 0;
  }

  shouldUseTLS13(req) {
    // Hash client IP to determine rollout
    const hash = crypto
      .createHash('md5')
      .update(req.socket.remoteAddress)
      .digest('hex');

    const bucket = parseInt(hash.substr(0, 8), 16) % 100;

    return bucket < this.rolloutPercentage;
  }

  setRolloutPercentage(percentage) {
    this.rolloutPercentage = Math.min(100, Math.max(0, percentage));
    console.log(`TLS 1.3 rollout: ${this.rolloutPercentage}%`);
  }
}

const rollout = new TLSRollout();

// Gradual rollout schedule
setTimeout(() => rollout.setRolloutPercentage(10), 0);
setTimeout(() => rollout.setRolloutPercentage(25), 7 * 24 * 60 * 60 * 1000);
setTimeout(() => rollout.setRolloutPercentage(50), 14 * 24 * 60 * 60 * 1000);
setTimeout(() => rollout.setRolloutPercentage(100), 21 * 24 * 60 * 60 * 1000);

Testing and Validation

TLS Configuration Testing

const tls = require('tls');

class TLSConfigTester {
  async testConfiguration(host, port = 443) {
    return new Promise((resolve, reject) => {
      const socket = tls.connect(port, host, {
        servername: host,
        minVersion: 'TLSv1.3',
        maxVersion: 'TLSv1.3'
      }, () => {
        const result = {
          protocol: socket.getProtocol(),
          cipher: socket.getCipher(),
          certificate: socket.getPeerCertificate(),
          authorized: socket.authorized,
          authorizationError: socket.authorizationError
        };

        socket.end();
        resolve(result);
      });

      socket.on('error', reject);
    });
  }

  async runTests(host) {
    console.log(`Testing TLS configuration for ${host}`);

    try {
      const result = await this.testConfiguration(host);

      console.log('TLS 1.3 Connection Successful:');
      console.log(`Protocol: ${result.protocol}`);
      console.log(`Cipher: ${result.cipher.name}`);
      console.log(`Valid Certificate: ${result.authorized}`);

      // Verify certificate
      this.verifyCertificate(result.certificate);

      return result;
    } catch (error) {
      console.error('TLS 1.3 Connection Failed:', error.message);
      throw error;
    }
  }

  verifyCertificate(cert) {
    const validFrom = new Date(cert.valid_from);
    const validTo = new Date(cert.valid_to);
    const now = new Date();

    console.log(`Certificate: ${cert.subject.CN}`);
    console.log(`Issuer: ${cert.issuer.CN}`);
    console.log(`Valid from: ${validFrom}`);
    console.log(`Valid to: ${validTo}`);

    if (now < validFrom || now > validTo) {
      console.error('Certificate is not currently valid!');
    }

    const daysUntilExpiry = (validTo - now) / (1000 * 60 * 60 * 24);
    console.log(`Days until expiry: ${Math.floor(daysUntilExpiry)}`);

    if (daysUntilExpiry < 30) {
      console.warn('Certificate expires soon!');
    }
  }
}

// Run tests
const tester = new TLSConfigTester();
tester.runTests('example.com');

Conclusion

TLS 1.3 represents a major advancement in transport security, offering improved security, better performance, and simplified configuration. By removing outdated cryptography and reducing handshake latency, TLS 1.3 provides a stronger foundation for securing modern web applications.

Key takeaways:

  • TLS 1.3 reduces handshake latency by 50% (1-RTT vs 2-RTT)
  • 0-RTT resumption enables zero-latency reconnections for safe operations
  • Mandatory perfect forward secrecy protects past communications
  • Simplified cipher suites eliminate weak cryptography
  • Encrypted handshake improves privacy
  • Gradual migration minimizes risk
  • Automated certificate management is essential
  • Monitor performance metrics and error rates
  • Test thoroughly before production deployment

Migrating to TLS 1.3 should be a priority for all modern web applications, providing better security and performance for users while future-proofing your infrastructure against emerging threats.

#Encryption #TLS #HTTPS #Network Security