Developer Tools

Local HTTPS Development: Configuring Mkcert for Trusted Certificates

By DexNox Dev Team Published May 10, 2026

Modern browser policies restrict access to key APIs when applications run over unencrypted HTTP connections. Secure contexts are required to work with browser features like Service Workers, the Web Crypto API, Geolocation, and modern cookie properties such as Secure and SameSite=None. While browsers automatically treat the standard localhost loopback address (127.0.0.1) as secure, custom local development domains (e.g., dev.local) do not receive this exception. To test complex multi-tenant routing, subdomains, and production-like environments on your workstation, you must configure local HTTPS.

Using self-signed certificates is a common workaround, but it forces developers to bypass persistent browser warnings, causing friction during manual testing. More importantly, self-signed certificates do not establish a valid chain of trust, meaning security-sensitive APIs can still fail, and backend runtime environments will reject connection requests. This guide demonstrates how to establish a local Certificate Authority (CA) using mkcert to issue trusted SSL/TLS certificates across macOS, Windows, and Linux.

Public Key Infrastructure in Development

In production, public Certificate Authorities (like Let’s Encrypt or DigiCert) issue certificates after verifying domain ownership via automated challenges (HTTP-01 or DNS-01). Because these public CAs cannot verify ownership of local loopback IP addresses or non-public domains like dev.local, they cannot issue certificates for them.

To bridge this gap on your local workstation, mkcert operates as a local, private Root CA. The tool generates a unique root certificate and private key. When initialized, the root certificate is injected directly into your operating system’s system-wide root trust store, along with specific browser-based NSS databases (used by Firefox). When local servers serve certificates signed by this custom CA, the browser validates the certificate against the trusted local root, displaying a valid secure lock icon.

Because this local CA key is authorized to sign certificates for any arbitrary domain, security is critical. The private key (rootCA-key.pem) must be restricted to your local machine. If a malicious actor gains access to your root key, they could sign spoofed certificates that your workstation would trust implicitly.

Installing Mkcert on Your Workstation

To run mkcert, you must install the utility and its dependencies. The installation steps differ depending on your operating system and shell environment.

1. macOS Installation

On macOS, you install mkcert through Homebrew. You must also install the Network Security Services (nss) package to ensure Firefox profiles trust the local root CA.

# Update Homebrew repositories
brew update

# Install NSS tools to enable Firefox root CA support
brew install nss

# Install the mkcert utility
brew install mkcert

2. Windows Installation

On Windows, you can use the Chocolatey or Scoop package managers. The installation automatically configures the Windows Certificate Store integration.

# Option A: Install via Chocolatey
choco install mkcert

# Option B: Install via Scoop
scoop install mkcert

3. Linux and WSL2 Installation

For Debian-based distributions (like Ubuntu) running natively or under Windows Subsystem for Linux (WSL2), install the certutil utility before downloading and configuring the binary.

# Update local packages
sudo apt update

# Install libnss3-tools to provide certutil for Firefox and Chromium trust management
sudo apt install -y libnss3-tools

# Download the precompiled binary from the official GitHub release
curl -JLO "https://dl.filippo.io/mkcert/latest?platform=linux&arch=amd64"

# Grant execution permissions to the downloaded binary
chmod +x mkcert-v*-linux-amd64

# Move the executable binary into your system PATH
sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert

Creating the Local CA

Once the binary is installed, initialize your local Certificate Authority. This step is only run once per workstation.

# Register the local root CA in the system trust stores
mkcert -install

Upon executing this command, you will see output confirming the registration. On macOS, you will be prompted for your administrator password to modify the System Keychain. On Windows, a User Account Control dialog will prompt you to confirm the installation of the certificate into the Root Store. On Linux, the tool will update /etc/ssl/certs and write to the active NSS databases.

To verify where the CA files are stored, run:

# Print the path of the Certificate Authority directory
mkcert -CAROOT

This returns the directory containing the private key (rootCA-key.pem) and public certificate (rootCA.pem). Ensure the private key remains secure and is never shared or committed to public code repositories.

Generating Certificates for Custom Local Hostnames

With the CA established, you can generate certificates for your specific development hostnames, including wildcards and IP addresses. For example, if you want a single certificate that covers localhost, dev.local, and subdomains like api.dev.local or app.dev.local, pass them all as arguments.

# Create a dedicated configuration directory in your project
mkdir -p .config/ssl && cd .config/ssl

# Generate the certificates for loopback interfaces and custom domains
mkcert localhost dev.local "*.dev.local" 127.0.0.1 ::1

This command generates two files:

  1. localhost+4.pem: The public certificate chain containing the Subject Alternative Names (SANs) for all requested hostnames.
  2. localhost+4-key.pem: The matching unencrypted private key file.

Verify the file permissions. The private key should be restricted to prevent other local users from reading it:

# Restrict read permissions on the private key file
chmod 600 localhost+4-key.pem

Local HTTPS Server Integrations

Once the certificates are generated, integrate them into your servers. The following configurations show integrations with Nginx, Vite, and a standalone Node.js HTTPS server.

1. Nginx Configuration

Nginx acts as a reverse proxy to manage SSL termination and route traffic to internal application processes. The server block below listens on port 443, handles SSL handshakes using mkcert certificates, and proxies requests to a local service on port 3000. It also includes support for upgrading HTTP connections to WebSockets (essential for Vite’s HMR).

# Nginx virtual host configuration for dev.local on port 443 (HTTPS)
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name dev.local app.dev.local api.dev.local;

    # SSL Certificate and Key paths generated by mkcert
    ssl_certificate /etc/nginx/certs/localhost+4.pem;
    ssl_certificate_key /etc/nginx/certs/localhost+4-key.pem;

    # SSL Protocols and Cipher Suites (TLS 1.2 and 1.3 only)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';

    # Forward traffic to local development servers
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        
        # Enable WebSockets for HMR and real-time updates
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        
        # Forward client network headers
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        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;
    }
}

# Redirect HTTP (port 80) traffic to HTTPS (port 443)
server {
    listen 80;
    listen [::]:80;
    server_name dev.local app.dev.local api.dev.local;
    return 301 https://$host$request_uri;
}

2. Vite Configuration

For frontend development, Vite can run its own local HTTPS server. We can use Node’s filesystem module to read the generated certificate files directly. This configuration uses a fallback check to ensure that if a developer hasn’t generated the certificates yet, Vite logs a warning and falls back to HTTP rather than crashing.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import fs from "node:fs";
import path from "node:path";

// Locate target certificate files
const certPath = path.resolve(__dirname, "./.config/ssl/localhost+4.pem");
const keyPath = path.resolve(__dirname, "./.config/ssl/localhost+4-key.pem");

// Confirm file existence before attempting to read
const hasCertificates = fs.existsSync(certPath) && fs.existsSync(keyPath);

if (!hasCertificates) {
  console.warn(
    "\x1b[33m%s\x1b[0m",
    "WARNING: SSL Certificates not found at " + certPath + ". Falling back to HTTP."
  );
}

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    host: "dev.local",
    https: hasCertificates
      ? {
          key: fs.readFileSync(keyPath),
          cert: fs.readFileSync(certPath),
        }
      : undefined,
    hmr: {
      host: "dev.local",
      protocol: "wss",
    },
  },
});

3. Node.js HTTPS Server Configuration

If you are serving an API using a Node.js backend without a reverse proxy, you can initialize the HTTPS server natively. The following code sets up a dual-server configuration: an HTTPS server listening on port 443 (for secure API routing) and an HTTP server on port 80 that automatically redirects incoming requests to the secure protocol.

import https from "node:https";
import fs from "node:fs";
import path from "node:path";
import http from "node:http";

const HTTPS_PORT = 443;
const HTTP_PORT = 80;
const HOST = "dev.local";

// Resolve paths to the generated certificates
const certPath = path.resolve(process.cwd(), "./.config/ssl/localhost+4.pem");
const keyPath = path.resolve(process.cwd(), "./.config/ssl/localhost+4-key.pem");

if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
  console.error("ERROR: SSL Certificates are required to start the secure server.");
  console.error("Run command: mkcert localhost dev.local '*.dev.local' 127.0.0.1 ::1");
  process.exit(1);
}

const sslOptions = {
  key: fs.readFileSync(keyPath),
  cert: fs.readFileSync(certPath),
  minVersion: "TLSv1.2" as const,
};

// Application Request/Response handler
const requestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({
    status: "success",
    message: "Secure connection established.",
    protocol: req.httpVersion,
    secure: (req.socket as any).encrypted ? true : false,
    timestamp: new Date().toISOString()
  }));
};

// Initialize HTTPS Server
const httpsServer = https.createServer(sslOptions, requestHandler);

httpsServer.listen(HTTPS_PORT, HOST, () => {
  console.log(`[HTTPS] Server listening at https://${HOST}:${HTTPS_PORT}`);
});

// Initialize HTTP Redirect Server
const redirectServer = http.createServer((req, res) => {
  const hostHeader = req.headers.host || HOST;
  const domainName = hostHeader.split(":")[0];
  const targetURL = `https://${domainName}${req.url}`;
  
  res.writeHead(301, { Location: targetURL });
  res.end();
});

redirectServer.listen(HTTP_PORT, HOST, () => {
  console.log(`[HTTP] Redirect server listening on http://${HOST}:${HTTP_PORT}`);
});

Hostname Resolution Configuration

To access dev.local or app.dev.local in your browser, the operating system must resolve these hostnames to the loopback IP address 127.0.0.1.

Standard hosts files do not support wildcards (e.g., mapping *.dev.local to 127.0.0.1). If you need wildcard routing, you must configure a local DNS resolver (such as dnsmasq or unbound) or use a public wildcard routing service like nip.io or lvh.me. However, for static domains, modifying the hosts file is direct.

macOS and Linux hosts Update

# Append local domains to /etc/hosts for routing resolution
sudo tee -a /etc/hosts <<EOF

# Local Dev Routing
127.0.0.1 dev.local
127.0.0.1 app.dev.local
127.0.0.1 api.dev.local
EOF

Windows hosts Update

# Run this script in an Elevated Administrator PowerShell session
$HostsPath = "$env:windir\System32\drivers\etc\hosts"
$NewEntries = @"

# Local Dev Routing
127.0.0.1 dev.local
127.0.0.1 app.dev.local
127.0.0.1 api.dev.local
"@

if (Get-Content $HostsPath | Select-String -Pattern "dev.local") {
    Write-Host "Local dev domains already configured in hosts file." -ForegroundColor Green
} else {
    Add-Content -Path $HostsPath -Value $NewEntries
    Write-Host "Appended local dev domains to hosts file successfully." -ForegroundColor Green
}

Trusting Certificates on External Test Devices

To test mobile layouts and progressive web apps, you must connect external simulators, emulators, or physical devices to your local development environment. Because these devices run isolated operating systems, they will reject your workstation’s HTTPS certificate until you manually import and trust your local root CA certificate on them.

1. iOS Simulator

The iOS simulator runs on the macOS host and can easily import the root CA certificate.

  1. Locate your root CA directory by running mkcert -CAROOT in the terminal.
  2. Boot your target iOS simulator via Xcode or the command line.
  3. Drag the rootCA.pem file from your Finder window and drop it directly onto the screen of the active iOS Simulator.
  4. The simulator will detect the file and open Settings, prompting you to install a configuration profile. Click Install, enter the simulator passcode if prompted, and confirm the installation.
  5. Open the Settings app inside the simulator and navigate to General -> About -> Certificate Trust Settings.
  6. Under the “Enable full trust for root certificates” section, locate the “mkcert development CA” entry and toggle the switch to the ON position.
  7. Restart Safari or any running application in the simulator to clear the cache.

2. Android Emulator

For Android Virtual Devices (AVDs), use the Android Debug Bridge (adb) to transfer and install the certificate.

# Launch your active emulator and push the root CA certificate
adb push $(mkcert -CAROOT)/rootCA.pem /sdcard/Download/rootCA.pem
  1. Once the file is pushed, open the emulator and go to Settings.
  2. Navigate to Security -> Encryption & credentials -> Install a certificate -> CA certificate.
  3. Select “Install anyway” on the security warning dialog.
  4. Navigate to the /sdcard/Download/ folder in the file picker, select rootCA.pem, and save it under a recognizable name, such as “mkcert CA”.
  5. Restart Chrome or the system WebViews on the emulator.
# Verify the device status and clean active network cache
adb shell am force-stop com.android.chrome

3. Physical Mobile Devices

To test your application on a physical phone over local Wi-Fi:

  1. Re-generate certificates with local network IPs: Identify your workstation’s local IP address (e.g., 192.168.1.150). You must regenerate the certificates to include this IP address in the SAN list:
    mkcert dev.local "*.dev.local" 127.0.0.1 192.168.1.150
    
  2. Expose the CA certificate via HTTP: Start a temporary HTTP server inside your root CA folder to download the file on your phone.
    cd $(mkcert -CAROOT)
    python3 -m http.server 8000
    
  3. Download and Install: Connect your mobile device to the same Wi-Fi network. Open the phone’s browser, navigate to http://192.168.1.150:8000/rootCA.pem, and download the certificate file.
    • On iOS: Open Settings -> Profile Downloaded, tap Install, and confirm. Next, navigate to General -> About -> Certificate Trust Settings and enable full trust for the mkcert profile.
    • On Android: Go to Settings -> Security -> Encryption & credentials -> Install a certificate -> CA certificate, choose the downloaded file, and install.
  4. Configure DNS or Proxy: To resolve dev.local to 192.168.1.150 on the physical phone, you must either configure a local DNS server on your network (like a Pi-hole) or configure a manual HTTP proxy on the phone’s Wi-Fi settings pointing to a proxy server (like Squid or Charles) running on your workstation.

Comparison: Local HTTPS Setup Methods

Choosing the correct method for generating local development certificates impacts security and workflow complexity. The table below compares self-signed certificates, the mkcert CA method, and public Let’s Encrypt certificates.

Setup TypeSecurity ValidationBrowser WarningRoot CA InstallationScripting Setup Complexity
Self-Signed CLILow (custom keys)Yes (SSL Warning flag)NoneMedium
Mkcert ToolHigh (local trusted CA)No (Valid Secure Lock)Yes (automates system registry CA)Low
Let’s EncryptHighest (production domain)NoYes (Internet Root CAs)High (Requires DNS API access)

Additionally, you must resolve local domain requests to the loopback address. The table below evaluates three common methods for mapping custom domains.

MethodConfiguration EffortOffline SupportWildcard SupportSubdomain Routing
Hosts File (/etc/hosts)Manual per hostnameYesNoExplicit mapping required
dnsmasqModerate (service install)YesYes (e.g. *.dev.local)Automatic via regex
nip.io / lvh.meNoneNo (Requires DNS)Yes (e.g. 127.0.0.1.nip.io)Automatic mapping

What Breaks in Production

Setting up local HTTPS can introduce specific operational issues depending on your stack. Use these best practices to prevent common errors.

Node.js Backend-to-Backend HTTPS Calls

By default, Node.js does not query the operating system’s trust stores. When a local Node.js application attempts to make an HTTPS request to another local Node.js server using a certificate generated by mkcert, the request will fail with an error:

Error: unable to verify the first certificate

Do not bypass this error by disabling SSL validation with process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0', as this leaves your application vulnerable to man-in-the-middle attacks. Instead, direct Node.js to trust the mkcert root CA using the NODE_EXTRA_CA_CERTS environment variable.

You can set this in your shell initialization files (.bashrc or .zshrc) or prepend it directly to your application startup scripts:

# Run your Node application while pointing to the mkcert root CA path
NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" node dist/index.js

Firefox NSS Tools Prerequisite

Firefox does not share the system certificate database on macOS and Linux, opting to manage its own certificate storage. If you run mkcert -install before installing the NSS tools package, Firefox will continue to show certificate warning pages. If this happens, install the nss dependency, run the installation command again to rebuild the Firefox profile bindings, and restart the browser:

# Re-install root CA to populate newly available Firefox profile directories
mkcert -install

Team Workflows and Git Management

Do not commit domain certificates or private keys to your project’s Git repository. Sharing certificates across different machines is insecure and causes validation issues, as the local CA configurations will differ on each developer’s machine.

Instead, add the generated certificate files to your .gitignore file:

# Ignore local SSL certificate files in Git tracking
.config/ssl/*.pem

To coordinate the setup across a development team, commit a shell script to the repository that automates installation and generation on setup:

#!/usr/bin/env bash
# Development environment onboarding certificate script

set -euo pipefail

# Define certificate output path
SSL_DIR="./.config/ssl"
mkdir -p "$SSL_DIR"

if ! command -v mkcert &> /dev/null; then
    echo "ERROR: mkcert is not installed. Please install it via Homebrew, Chocolatey, or apt."
    exit 1
fi

# Ensure local CA is installed
mkcert -install

# Generate certificates for the team's standard development domains
mkcert -cert-file "$SSL_DIR/localhost+4.pem" -key-file "$SSL_DIR/localhost+4-key.pem" localhost dev.local "*.dev.local" 127.0.0.1 ::1

echo "Local certificates updated successfully in $SSL_DIR"

Conclusion

Setting up a local Certificate Authority with mkcert eliminates self-signed certificate warnings and ensures local environments behave like production. By installing the root CA on your OS and configuring servers like Nginx, Vite, or Node.js to use the generated certificates, you can verify security-sensitive integrations during the early stages of development.

Frequently Asked Questions

Where does mkcert store the local private root key?

Mkcert stores the root CA key files in your user profile application data folder. Keep this directory private to secure your workstation.

How do I configure my iOS or Android simulator to trust local mkcert paths?

Export the rootCA.pem file from mkcert, copy it to the mobile filesystem, and manually install it via the device profile settings.

Why does mkcert fail with Firefox on Linux?

Firefox does not use the system trust store on Linux by default. You need to install certutil (nss-tools) via your package manager so mkcert can automatically inject the root CA into Firefox's profiles.

Can I use wildcard domains like *.dev.local with mkcert?

Yes, mkcert supports wildcards. For example, running `mkcert dev.local '*.dev.local'` generates a single certificate that covers both the root domain and all its subdomains.