In the previous tutorial, you learned how to prevent SQL injection and XSS. Those attacks target your application logic. But there is another attack surface — the network. When data travels between your user’s browser and your server, anyone in between can read or modify it. Unless you use HTTPS.

What is HTTPS?

HTTPS is HTTP with encryption. It uses TLS (Transport Layer Security) to encrypt all data between the client and the server.

HTTPHTTPS
EncryptionNoneTLS encryption
Port80443
URLhttp://https://
Data visible to attackersYesNo
Required for modern webNoYes

Without HTTPS:

  • Anyone on the same WiFi network can read your data
  • ISPs can see and modify your traffic
  • Governments can monitor connections
  • Attackers can inject malicious content

HTTPS is not just for banks and shopping sites. Every website and API needs HTTPS. Google Chrome marks HTTP sites as “Not Secure.” Search engines rank HTTPS sites higher. Many browser features (geolocation, service workers, HTTP/2) only work over HTTPS.

How TLS Works (Simplified)

TLS is the protocol that provides encryption for HTTPS. The current version is TLS 1.3 (released in 2018). TLS 1.2 is still acceptable, but older versions (TLS 1.0, 1.1, SSL) are insecure and should be disabled.

The TLS Handshake

When your browser connects to an HTTPS server, they perform a handshake:

1. Client Hello
   Browser → Server: "I support TLS 1.3, here are the cipher suites I know"

2. Server Hello + Certificate
   Server → Browser: "Let's use TLS 1.3 with this cipher suite. Here is my certificate."

3. Certificate Verification
   Browser checks: Is the certificate valid? Is it issued by a trusted CA?
   Is the domain name correct? Has it expired?

4. Key Exchange
   Browser and server agree on a shared secret key using
   Diffie-Hellman key exchange. This key is unique to this session.

5. Encrypted Communication
   All data is now encrypted with the shared key.
   Neither side sends the key over the network.

TLS 1.3 completes this handshake in one round trip (1-RTT), down from two in TLS 1.2. This makes HTTPS connections faster.

Symmetric vs Asymmetric Encryption

TLS uses both types:

SymmetricAsymmetric
KeysOne shared keyPublic key + private key
SpeedFastSlow
Used forEncrypting dataKey exchange, certificates
ExampleAES-256-GCMRSA, ECDHE (X25519), Ed25519 (signatures)

The TLS handshake uses asymmetric encryption (slow but secure) to exchange a symmetric key (fast). Then all data is encrypted with the symmetric key.

This is why TLS is efficient — the expensive asymmetric operation only happens once during the handshake.

Certificates

A TLS certificate proves that the server is who it claims to be. Without certificates, an attacker could pretend to be google.com and intercept your data (a man-in-the-middle attack).

How Certificates Work

1. Server owner generates a key pair (public + private)
2. Server owner sends a Certificate Signing Request (CSR) to a Certificate Authority (CA)
3. CA verifies that the owner controls the domain
4. CA signs the certificate with the CA's private key
5. Browser has a list of trusted CAs built in
6. When connecting, browser verifies the certificate signature against the trusted CA list

Types of Certificates

TypeValidationCostUse Case
DV (Domain Validated)Domain ownership onlyFree (Let’s Encrypt)Most websites
OV (Organization Validated)Domain + organization$50-200/yearBusiness sites
EV (Extended Validation)Domain + organization + legal$100-500/yearBanks, enterprise
Self-signedNone (you sign it yourself)FreeDevelopment only

For most applications, a DV certificate from Let’s Encrypt is all you need. It is free, automated, and trusted by all browsers.

Let’s Encrypt: Free HTTPS in 5 Minutes

Let’s Encrypt is a free, automated Certificate Authority. It issues DV certificates that are trusted by all major browsers. Over 300 million websites use Let’s Encrypt.

Setting Up Let’s Encrypt with Certbot

Certbot is the official tool for getting Let’s Encrypt certificates.

Nginx on Ubuntu/Debian

# Install Certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx

# Get a certificate and configure Nginx automatically
sudo certbot --nginx -d example.com -d www.example.com

# Certbot will:
# 1. Verify you own the domain
# 2. Download the certificate
# 3. Configure Nginx to use HTTPS
# 4. Set up auto-redirect from HTTP to HTTPS

Auto-Renewal

Let’s Encrypt certificates expire after 90 days. Certbot sets up automatic renewal:

# Test auto-renewal
sudo certbot renew --dry-run

# Certbot adds a cron job or systemd timer automatically
# Certificates renew when they have less than 30 days left

Certbot with Docker

# docker-compose.yml
services:
  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    command: certonly --webroot -w /var/www/certbot -d example.com

  nginx:
    image: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot

Programmatic HTTPS in Go

Go’s standard library has built-in TLS support. For automatic Let’s Encrypt certificates, use the autocert package:

package main

import (
    "crypto/tls"
    "log"
    "net/http"
    "golang.org/x/crypto/acme/autocert"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, HTTPS!"))
    })

    // Auto-obtain and renew Let's Encrypt certificates
    manager := &autocert.Manager{
        Cache:      autocert.DirCache("certs"),
        Prompt:     autocert.AcceptTOS,
        HostPolicy: autocert.HostWhitelist("example.com"),
    }

    server := &http.Server{
        Addr:    ":443",
        Handler: mux,
        TLSConfig: &tls.Config{
            GetCertificate: manager.GetCertificate,
            MinVersion:     tls.VersionTLS12,
        },
    }

    // Redirect HTTP to HTTPS
    go http.ListenAndServe(":80", manager.HTTPHandler(nil))

    log.Println("Starting HTTPS server on :443")
    log.Fatal(server.ListenAndServeTLS("", ""))
}

HTTPS in Python (Flask)

For development, Flask can use adhoc SSL. For production, use a reverse proxy (Nginx) with Let’s Encrypt:

# Development only — self-signed certificate
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, HTTPS!"

if __name__ == "__main__":
    app.run(ssl_context="adhoc")  # Development only!

# Production: use Nginx + Let's Encrypt as a reverse proxy
# Flask should NOT handle TLS directly in production

HTTPS in Node.js

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

const app = express();

app.get('/', (req, res) => {
    res.send('Hello, HTTPS!');
});

// Load Let's Encrypt certificates
const options = {
    key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
    cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem'),
};

https.createServer(options, app).listen(443, () => {
    console.log('HTTPS server running on port 443');
});

// Redirect HTTP to HTTPS
const http = require('http');
http.createServer((req, res) => {
    res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
    res.end();
}).listen(80);

HSTS: Forcing HTTPS

HSTS (HTTP Strict Transport Security) tells the browser to always use HTTPS for your domain. Even if the user types http://, the browser automatically upgrades to https://.

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
DirectiveMeaning
max-age=63072000Remember for 2 years (in seconds)
includeSubDomainsApply to all subdomains too
preloadSubmit to browser preload lists

Adding HSTS

// Go
func HSTSMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Strict-Transport-Security",
            "max-age=63072000; includeSubDomains; preload")
        next.ServeHTTP(w, r)
    })
}
# Nginx
server {
    listen 443 ssl;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}

Warning: Once you enable HSTS, your site must have HTTPS working. If your certificate expires, users cannot access your site over HTTP either. Start with a short max-age (like 300 seconds) and increase it after testing.

TLS Versions

VersionStatusNotes
SSL 2.0, 3.0InsecureDisabled everywhere, do not use
TLS 1.0DeprecatedDisabled in all major browsers since 2020
TLS 1.1DeprecatedDisabled in all major browsers since 2020
TLS 1.2AcceptableMinimum acceptable version
TLS 1.3RecommendedFaster, more secure, simpler

Configure your server to support TLS 1.2 and 1.3 only:

# Nginx
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;

Certificate Pinning for Mobile Apps

Certificate pinning means your mobile app only trusts a specific certificate (or its public key), not any certificate from a trusted CA. This prevents man-in-the-middle attacks even if a CA is compromised.

Certificate Pinning in Android (Kotlin)

// Using OkHttp
val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

Certificate Pinning in iOS (Swift)

// Using URLSession
class PinnedSessionDelegate: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        guard let serverTrust = challenge.protectionSpace.serverTrust,
              let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // Compare certificate with pinned certificate
        let serverCertData = SecCertificateCopyData(certificate) as Data
        if serverCertData == pinnedCertData {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

Warning: Certificate pinning has risks. If you pin to a specific certificate and it expires, your app stops working. Pin to the public key instead, and always have a backup pin.

Common HTTPS Mistakes

1. Mixed Content

Loading HTTP resources on an HTTPS page. The browser blocks these or shows a warning.

<!-- Bad — HTTP image on HTTPS page -->
<img src="http://example.com/image.png">

<!-- Good — use HTTPS or protocol-relative URLs -->
<img src="https://example.com/image.png">
<img src="//example.com/image.png">

2. Expired Certificates

Let’s Encrypt certificates expire after 90 days. If auto-renewal fails, your site goes down. Monitor your certificate expiration:

# Check certificate expiration
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

3. Self-Signed Certificates in Production

Self-signed certificates are fine for local development. In production, browsers will show a scary warning and users will leave. Use Let’s Encrypt — it is free.

4. Not Redirecting HTTP to HTTPS

If you have HTTPS but HTTP still works, some users will connect over HTTP. Always redirect:

# Nginx — redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Testing Your HTTPS Configuration

SSL Labs

The best free tool for testing HTTPS: https://www.ssllabs.com/ssltest/

It checks:

  • Certificate validity
  • Protocol support
  • Cipher suites
  • Known vulnerabilities
  • HSTS configuration

Aim for an A+ rating.

testssl.sh

A command-line tool for testing TLS:

# Install
git clone --depth 1 https://github.com/drwetter/testssl.sh.git

# Run
./testssl.sh/testssl.sh https://example.com

cURL

Quick check from the command line:

# Check TLS version and certificate
curl -vI https://example.com 2>&1 | grep -E "SSL|TLS|subject|expire"

Prevention Checklist

TopicBest Practice
HTTPSRequired for every site and API — no exceptions
TLS versionTLS 1.2 minimum, TLS 1.3 recommended
CertificatesLet’s Encrypt for free, automated DV certificates
Auto-renewalCertbot auto-renews — verify with --dry-run
HSTSEnable with long max-age after testing
HTTP redirectAlways redirect HTTP (port 80) to HTTPS (port 443)
Mixed contentAll resources must load over HTTPS
Mobile appsConsider certificate pinning (pin to public key)
Old TLSDisable SSL 2/3, TLS 1.0, and TLS 1.1
TestingUse SSL Labs — aim for A+ rating

What’s Next?

This is the end of the first five articles in the Security for Developers series. You now understand the OWASP Top 10 risks, authentication and authorization patterns, injection prevention, and HTTPS/TLS encryption.

The series continues with more advanced topics: CSRF prevention, API security, secrets management, and Docker security. Check the Security Tutorial landing page for the full series.