Enable HTTPS Locally
Learn to enable HTTPS locally with self-signed certs in this Secure development tutorial. Practical steps, troubleshooting, and next steps.
Focus: enable https locally with self-signed certs
You're building a modern web app. From your browser, it works flawlessly. But your teammates, your QA folks, and your CI runner all hit cryptic SSL errors, or worse, you're shipping HTTP to production and don't know what's broken. Sound familiar? The root cause is almost always one thing: you never tested your app over HTTPS locally. In this lesson, you'll learn to enable HTTPS locally with self-signed certs — the fast, free, and safe way to simulate production TLS on your own machine — so you can catch certificate- and TLS-related bugs before your users do.
The problem this lesson solves
Modern web development demands HTTPS. Browsers are actively warning users about insecure HTTP sites, and features like geolocation, service workers, and secure cookies simply refuse to work without a valid TLS connection. Yet most local development defaults to plain http://localhost.
That mismatch creates real pain:
- You integrate a third-party API that requires HTTPS, and your local server silently rejects it.
- Your OAuth callback URL is registered as
https://, but your local server is HTTP, so the redirect fails. - You're debugging mixed-content errors that only show up in production.
- Your CI pipeline runs against a local dev server that can't handle TLS, hiding TLS-related failures.
The solution isn't to buy an expensive certificate for localhost (you can't — it would be globally trusted anyway). Instead, you generate a self-signed certificate for your local development domain, trust it in your development environment, and run your local server over HTTPS.
Core concept / mental model
Think of TLS certificates like photo ID for your website. A trusted certificate is issued by a recognized authority (like a passport office). A self-signed certificate is one you issue to yourself — like a badge you make at home. For production, you need the trusted one. For local development, your own badge is perfectly fine, as long as you show it to the right guards.
In our case, the "guards" are your local tools: the browser, your package manager, curl, or your Python HTTP client. If those tools don't know to trust your self-signed cert, they'll reject it.
A self-signed certificate becomes trusted locally in two ways:
- Add it to your system's trust store (so browsers and OS-level tools trust it).
- Add it to individual tools or clients (e.g.,
curl --cacert, Pythonverify=parameter, orREQUESTS_CA_BUNDLE).
A key concept here is custom TLS domain like localhost or dev.example. localhost is special — some tools handle it differently. For broader flexibilty, many developers use the special localhost domain or a wildcard domain like *.local (depending on your needs, as we'll see in the compare section).
Pro tip: Never reuse the same self-signed cert everywhere. Generate one per project or per machine. It's cheap and keeps your local trust store manageable.
How it works step by step
Let's break down the entire flow of enabling HTTPS locally, from generating the private key to running your server.
Step 1 — Generate a private key
Your server needs a private key to complete the TLS handshake. Use RSA (if you need maximum compatibility) or ECDSA (faster, modern). For local dev, either works.
# Generate a 2048-bit RSA private key
openssl genrsa -out server.key 2048
Step 2 — Create a certificate signing request (CSR)
The CSR contains the details of who you are and, crucially, the Subject Alternative Names (SANs) — the domains this certificate is valid for. For local development, you'll often include localhost and 127.0.0.1.
# Create CSR with SANs via -addext (OpenSSL 1.1.1+)
openssl req -new -key server.key -out server.csr \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
Step 3 — Self-sign the certificate
Use your own CSR and key to create a certificate that is valid for, say, 365 days.
openssl x509 -req -days 365 \
-in server.csr -signkey server.key \
-out server.crt \
-extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1")
Now you have three files: server.key, server.csr, and server.crt. The CSR is no longer needed.
Step 4 — Turn on HTTPS in your local server
Configure your Python web server (we'll use Flask as an example) to use the key and certificate.
Hands-on walkthrough
Let's put it all together. We'll generate the certs, run a simple HTTPS server, and connect to it securely.
Example 1 — Minimal HTTPS server with Python's http.server
# start_https_server.py
import http.server
import ssl
port = 8443
httpd = http.server.HTTPServer(('localhost', port), http.server.SimpleHTTPRequestHandler)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('server.crt', 'server.key')
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
print(f"Serving HTTPS on https://localhost:{port}")
httpd.serve_forever()
Run it:
python start_https_server.py
Expected output:
Serving HTTPS on https://localhost:8443
Now open https://localhost:8443 in a browser — you'll see a security warning because your browser doesn't trust your self-signed cert yet.
Example 2 — Trust the cert in your browser (macOS/Linux)
On macOS, double-click the .crt file, add it to System keychain in the Certificates category, and mark it as Always Trust. On Linux, copy it to /usr/local/share/ca-certificates/ and run sudo update-ca-certificates.
After trusting it, the browser shows a padlock.
Example 3 — Test with curl and Python requests
Now test connection from a client that isn't your browser:
curl --cacert server.crt https://localhost:8443 -v
Expected output (abbreviated):
* SSL connection using TLSv1.3
* ALPN: offers http/1.1
* Server certificate:
* subject: CN=localhost
* start date: ...
* expire date: ...
* subjectAltName: host "localhost" matched cert's "localhost"
* issuer: CN=localhost
* SSL certificate verify ok.
Python's requests library:
import requests
resp = requests.get('https://localhost:8443', verify='server.crt')
print(resp.status_code)
Expected output:
200
Compare options / when to choose what
| Option | Use case | Trust effort | Portability | Best for |
|---|---|---|---|---|
openssl self-signed |
Single machine, short-term dev | Manual trust store | Low — need to trust per machine | Quick prototyping, local only |
mkcert |
Multi-machine dev, shared certs | One command, auto-trusts | Medium — still per machine trust | Standard dev workflow, team consistency |
Local CA + signed certs |
Many services, local CA service | Medium — trust the CA once | High — all certs signed by local CA | Simulating internal PKI, full-stack TLS |
http.server + ssl |
Minimalistic, built-in Python | Same as self-signed | Low | Small scripts, clean Python-only approach |
Pro tip: For most web dev work,
mkcertis the sweet spot. It creates a local CA, generates certs that modern browsers trust automatically, and handles renewals gracefully.
When to choose what:
- Quick sanity test → use
opensslone-liner. - Full local HTTPS story → use
mkcert. - You need multiple services to trust each other → set up a local CA and sign each service's cert.
- You want zero external dependencies → stick with Python's
sslmodule.
Troubleshooting & edge cases
Problem: Browser still shows warning after trusting the cert.
- Make sure the domain matches exactly (e.g.,
localhostvs127.0.0.1). If you access vialocalhost, your SAN must include it. - Kill and restart your browser. Some keep cached trust stores.
- Verify your system CA store was updated. On Ubuntu, run
openssl verify server.crt(it'll fail if not in CA store) ; useupdate-ca-certificates.
Problem: Python's requests fails with SSL: CERTIFICATE_VERIFY_FAILED.
- Pass
verify='server.crt'or set the environment variableREQUESTS_CA_BUNDLEto the path of your cert. - If your cert is self-signed and not in the system trust store, you must explicitly point to it.
Problem: openssl req fails with "Invalid subjectAltName" on older OpenSSL (< 1.1.1).
- Upgrade OpenSSL or drop
-addextand use the-extfileapproach with a separate file.
Problem: curl says "unable to get local issuer certificate".
- You didn't pass
--cacert; or if you used a local CA, you must trust that CA specifically.
Problem: Your app only works when you access it via 127.0.0.1, but not localhost.
- That is a SAN mismatch. Add both
DNS:localhostandIP:127.0.0.1to your cert.
Edge case: Mac's Keychain Access won't trust the cert.
- Drag the
.crtinto System keychain (not login), then double-click and set SSL to Always Trust under the Trust dropdown.
What you learned & what's next
You now understand the core mechanics behind enabling HTTPS locally with self-signed certs:
- You can generate a private key, create a CSR, and self-sign a certificate in minutes.
- You know how to wire that certificate into Python's
http.server, Flask, or any other WSGI server using thesslmodule. - You can trust your certificate in the OS and in individual clients (
curl,requests). - You know when to use
opensslvsmkcertvs a local CA.
These skills transfer directly to installing a trusted certificate on a real web server (like Nginx or Caddy) and to debugging TLS issues in production — because the same concepts of trust, SAN, and private keys apply there.
In the next lesson in this track, you'll move from local to remote — configuring a web server to use a real, trusted certificate from Let's Encrypt. You'll see how all the pieces you learned here (private key, certificate file, TLS termination) plug into a production-grade setup.
Go ahead: enable HTTPS locally with self-signed certs in your next project, and watch your local development become a true mirror of production.
Practice recap
Take the start_https_server.py example and modify it to serve a small Flask app instead of a static directory. Add requests.get(..., verify='server.crt') to fetch a health endpoint from a small test script. Then try accessing without verify and note the SSLError — that's the exact failure you'll see in production if your cert isn't trusted.
Common mistakes
- Using the certificate for
localhostbut accessing your app via127.0.0.1— SAN mismatch kills trust. - Trusting the
.crtbut not the private key permissions — your server may refuse to start. - Forgetting to pass
verify=to Python'srequests— it silently falls back to the system CA store, causing a nasty surprise. - Confusing self-signed cert with a local CA — you can't use a single self-signed cert to sign other certs.
Variations
- Use
mkcertto automate CA creation and trust installation across OSes and browsers. - Use Python's
sslcontext withPROTOCOL_TLS_SERVERdirectly inside a customHTTPServerinstead of using a framework's built-in config. - Generate an ECDSA key (
ecparam -genkey -name prime256v1) for faster handshakes on modern hardware.
Real-world use cases
- Local development for an OAuth consumer callbacks localhost HTTPS endpoints that must match the production URL scheme.
- Testing a service mesh or microservices locally where mutual TLS is required between two Python services using
sslcontexts. - Pre-deployment CI pipeline that runs integration tests against a local HTTPS server to validate TLS configuration before pushing to a staging environment.
Key takeaways
- Self-signed certs solve the local HTTPS chicken-and-egg problem: you can test TLS without a public authority.
- A certificate is trusted only if its SANs match the hostname you connect to.
- You must explicitly trust your self-signed cert in every client: OS trust store,
curl --cacert, or Pythonverify=. mkcertis the most convenient way to manage local HTTPS for a team, whileopensslis great for one-off scripts.- TLS handshake errors are almost never 'magic' — they're almost always a SAN, trust, or key/permission issue.
- Enabling HTTPS locally is the first step to debugging mixed-content and secure-cookie bugs before they hit production.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.