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.
| HTTP | HTTPS | |
|---|---|---|
| Encryption | None | TLS encryption |
| Port | 80 | 443 |
| URL | http:// | https:// |
| Data visible to attackers | Yes | No |
| Required for modern web | No | Yes |
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:
| Symmetric | Asymmetric | |
|---|---|---|
| Keys | One shared key | Public key + private key |
| Speed | Fast | Slow |
| Used for | Encrypting data | Key exchange, certificates |
| Example | AES-256-GCM | RSA, 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
| Type | Validation | Cost | Use Case |
|---|---|---|---|
| DV (Domain Validated) | Domain ownership only | Free (Let’s Encrypt) | Most websites |
| OV (Organization Validated) | Domain + organization | $50-200/year | Business sites |
| EV (Extended Validation) | Domain + organization + legal | $100-500/year | Banks, enterprise |
| Self-signed | None (you sign it yourself) | Free | Development 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
| Directive | Meaning |
|---|---|
max-age=63072000 | Remember for 2 years (in seconds) |
includeSubDomains | Apply to all subdomains too |
preload | Submit 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
| Version | Status | Notes |
|---|---|---|
| SSL 2.0, 3.0 | Insecure | Disabled everywhere, do not use |
| TLS 1.0 | Deprecated | Disabled in all major browsers since 2020 |
| TLS 1.1 | Deprecated | Disabled in all major browsers since 2020 |
| TLS 1.2 | Acceptable | Minimum acceptable version |
| TLS 1.3 | Recommended | Faster, 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
| Topic | Best Practice |
|---|---|
| HTTPS | Required for every site and API — no exceptions |
| TLS version | TLS 1.2 minimum, TLS 1.3 recommended |
| Certificates | Let’s Encrypt for free, automated DV certificates |
| Auto-renewal | Certbot auto-renews — verify with --dry-run |
| HSTS | Enable with long max-age after testing |
| HTTP redirect | Always redirect HTTP (port 80) to HTTPS (port 443) |
| Mixed content | All resources must load over HTTPS |
| Mobile apps | Consider certificate pinning (pin to public key) |
| Old TLS | Disable SSL 2/3, TLS 1.0, and TLS 1.1 |
| Testing | Use SSL Labs — aim for A+ rating |
Related Articles
- Security Tutorial #1: Web Security Basics — OWASP Top 10
- Security Tutorial #2: Authentication — Passwords, Hashing, JWT
- Security Tutorial #3: Authorization — RBAC, OAuth 2.0, OpenID Connect
- Security Tutorial #4: SQL Injection and XSS — How to Prevent Them
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.