Debian, Ubuntu

How to create locally trusted SSL certificates in local development environment on Linux with mkcert

A Certificate Authority (CA) is an entity that issues digital certificates. Our goal is to become a certificate authority (CA) like Let’s Encrypt, Verisign etc.. Issue yourself certificates for local development domains!

So, how do you remove the “your connection is not private” error in the browser when you visit a localhost domain?

mkcert is a simple zero-config tool written by Filippo Valsorda in Go for making locally trusted development certificates with any names you’d like without any configuration. This will help you since it is impossible to get certificates from trusted Certificate Authorities for local names that don’t have a valid DNS record. So let’s dive in to install and use mkcert.

We will show you how to become a Certificate Authority in few steps.

How to Install mkcert on Ubuntu / Debian

To install mkcert on any Ubuntu or Debian system, first, install certutil dependencies:

sudo apt-get update
sudo apt install wget libnss3-tools

Once this has been installed, download mkcert binary package from Github. Check mkcert releases page for the latest version. The latest release is.v1.3.0

export VER="v1.3.0"
wget -O mkcert https://github.com/FiloSottile/mkcert/releases/download/${VER}/mkcert-${VER}-linux-amd64

Once the file has been downloaded, make the file executable and place the binary under /usr/local/bin

chmod +x mkcert
sudo mv mkcert /usr/local/bin

How to Install mkcert on CentOS

Installation of mkcert on CentOS is similar to Ubuntu / Debian installation. You only need to install nss-tools tools first.

sudo yum install nss-tools

Once installed, download the binary package like for Ubuntu installation.

export VER="v1.3.0"
wget -O mkcert https://github.com/FiloSottile/mkcert/releases/download/${VER}/mkcert-${VER}-linux-amd64

Once the file has been downloaded, make the file executable and place the binary under /usr/local/bin

chmod +x  mkcert
sudo mv mkcert /usr/local/bin

How to use mkcert to generate locally trusted SSL certificates

mkcert has support for the following root stores:

  • macOS system store
  • Windows system store
  • Linux variants that provide either
    • update-ca-trust (Fedora, RHEL, CentOS)
    • update-ca-certificates (Ubuntu, Debian)
  • Firefox (macOS and Linux only)
    Chrome and Chromium
    Java (when JAVA_HOME is set)
    To get the help page for mkcert, pass the option–help.
Usage of mkcert:

	$ mkcert -install
	Install the local CA in the system trust store.

	$ mkcert example.org
	Generate "example.org.pem" and "example.org-key.pem".

	$ mkcert example.com myapp.dev localhost 127.0.0.1 ::1
	Generate "example.com+4.pem" and "example.com+4-key.pem".

	$ mkcert "*.example.it"
	Generate "_wildcard.example.it.pem" and "_wildcard.example.it-key.pem".

	$ mkcert -uninstall
	Uninstall the local CA (but do not delete it).

Advanced options:

	-cert-file FILE, -key-file FILE, -p12-file FILE
	    Customize the output paths.

	-client
	    Generate a certificate for client authentication.

	-ecdsa
	    Generate a certificate with an ECDSA key.

	-pkcs12
	    Generate a ".p12" PKCS #12 file, also know as a ".pfx" file,
	    containing certificate and key for legacy applications.

	-csr CSR
	    Generate a certificate based on the supplied CSR. Conflicts with
	    all other flags and arguments except -install and -cert-file.

	-CAROOT
	    Print the CA certificate and key storage location.

	$CAROOT (environment variable)
	    Set the CA certificate and key storage location. (This allows
	    maintaining multiple local CAs in parallel.)

	$TRUST_STORES (environment variable)
	    A comma-separated list of trust stores to install the local
	    root CA into. Options are: "system", "java" and "nss" (includes
	    Firefox). Autodetected by default.

You can get your CA root directory using:

mkcert -CAROOT
/home/vasilij/.local/share/mkcert

You need to start by installing the local CA in your system trust store.

mkcert -install
Using the local CA at "/home/vasilij/.local/share/mkcert"

Once done, you can start generating SSL certificates for your domains. As an example, I’ll generate a new certificate valid for the following names:

- "symfony.local"
- "*.symfony.local"
- "localhost"
- "127.0.0.1"
- "::1"

The output will be like below:

mkcert symfony.local '*.symfony.local' localhost 127.0.0.1 ::1
Using the local CA at "/home/vasilij/.local/share/mkcert" ✨

Created a new certificate valid for the following names 📜
 - "symfony.local"
 - "*.symfony.local"
 - "localhost"
 - "127.0.0.1"
 - "::1"

Reminder: X.509 wildcards only go one level deep, so this won't match a.b.symfony.local ℹ️

The certificate is at "./symfony.local+5.pem" and the key at "./symfony.local+5-key.pem" ✅

You should be able to view the contents of the certificate:

cat ./symfony.local+5.pem 
-----BEGIN CERTIFICATE-----
MIIEVDCCArygAwIBAgIRAJhl6Nd/HzuUhW+KbbzDAo4wDQYJKoZIhvcNAQELBQAw
-----END CERTIFICATE-----

The private key is:

cat ./symfony.local+5-key.pem 
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWSWAZ7nmmfISs
-----END PRIVATE KEY-----

Testing mkcert certificate

Let’s now test mkcert certificate using a simple nginx configuration file. Forgive me this example is done on Ubuntu 19.04 system.

sudo apt-get install nginx

Create a simple web page

cat /etc/nginx/sites-enabled/project.conf 
server {
    listen 80 default_server;

    server_name __PROJECT_DOMAIN__;
    root /var/www/project/__DOCUMENT_ROOT__;

    location ^~ /.well-known {
        allow all;
        root /data/letsencrypt/;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2 default_server;

    ssl_certificate /etc/nginx/ssl/symfony.local.crt;
    ssl_certificate_key /etc/nginx/ssl/symfony.local.key;

    server_name __PROJECT_DOMAIN__;
    root /var/www/project/__DOCUMENT_ROOT__;

    access_log /var/log/nginx/project_access.log;
    error_log /var/log/nginx/project_error.log;

     # strip app.php/ prefix if it is present
    rewrite ^/app\.php/?(.*)$ /$1 permanent;

    location / {
        index app.php;
        try_files $uri @rewriteapp;
    }

    location @rewriteapp {
        rewrite ^(.*)$ /app.php/$1 last;
    }

    # expire
    location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff|woff2|eot|ttf)$ {
        try_files $uri /website.php/$1?$query_string;
        access_log off;
        expires 30d;
        add_header Pragma public;
        add_header Cache-Control "public";
    }

    # pass the PHP script to FastCGI server from upstream phpfcgi
    location ~ ^/(app|app_dev|config|_intellij_phpdebug_validator)\.php(/|$) {
        fastcgi_pass php-upstream;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SYMFONY_ENV dev;
        fastcgi_param HTTPS off;
    }
}

Make sure your /etc/hosts file has a record for used domains.

127.0.0.1 symfony.local