Mkcert all the things

by George Cadwallader
After Ade Attwood's recent dive into TLS and creating server certificates with a root ca. This is a much simpler way with mkcert...
Nov 9, 2020

After my recent dive into TLS, I did a quick talk on about generating a root certificate, and then signing a server certificate with the root to get a valid TLS internally. Thus you can get away without Letsencypt, or similar, for your local network or development environments. Although I think it is important to understand how this works, this is far too much work for setting up TSL on a development environment. We can use mkcert to handle all this legwork for us.

Install

The application is a command line app written in go, therefore we can install it as a static binary. We can download it, make it executable and run it. Once you have it installed you will need to install the root ca onto your devices. There is a nice install command through the CLI to install it on the development machine you are on.

1
mkcert -install

Now if you want to test your development site from your phone it is not uncommon to visit that site from your phone. This will display the dreaded invalid certificate warning. To fix this annoyance, we need to get the root ca onto the phone. The easiest way to do this is to get the cert onto a web server. This can be actioned with the below script. When the server is booted go to the URL from your phone and install the certificate.

1
2
3
4
5
6
7
<br />
# Lets get to a tmp directory<br />
mkdir /tmp/root-ca-server &amp;&amp; cd /tmp/root-ca-server<br />
# Copy the root cert into this directory<br />
cp "$(mkcert -CAROOT)/rootCA.pem" ./root.pem<br />
# Start a web server<br />
npx http-server

Generate a cert

Now it’s time to create your cert. Before we do this, we need to get the “Certificate Subject Alternative Name”. This is basically the domain name you will be using to access your web service.

Using the hostname

1
hostname --fqdn

Using puppet facts

For me the hostname did not get the full domain name given to me by the router. It only returned the hostname of the machine. In the end, I had to use puppet facts. If you have this installed, then this is the way to go.

1
facter fqdn

Generate the certificate

The below command will generate a server certificate that we can now install on the servers.

1
2
<br /><br />
mkcert -cert-file server.crt -key-file server.key "$(facter fqdn)"<br /><br />

You can also generate wildcard certificates – you only need to generate one certificate. Note, this will involve some DNS or hosts file work to the domain names routed to your development machine.

1
2
<br /><br />
mkcert -cert-file server.crt -key-file server.key "*.$(facter fqdn)"<br /><br />

Use the certificate

Nginx

1
2
3
4
5
6
7
8
9
server {
  listen [::]:443 ssl http2;
  listen 443 ssl http2;

  ssl_certificate /path/to/server.crt;
  ssl_certificate_key /path/to/server.key;

  ... Rest of the server config
}

Apache2

1
2
3
4
5
6
<VirtualHost *:443>
  SSLCertificateFile    /path/to/server.crt
  SSLCertificateKeyFile /path/to/server.key  

  ... Rest of the server config
</VirtualHost>

Envoy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static_resources:
  listeners
:
  - name
: listener1
    address
:
      socket_address
: { address: 0.0.0.0, port_value: 8000 }
    filter_chains
:
    - filters
:
      - name
: envoy.filters.network.http_connection_manager
        typed_config
:
          "@type"
: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix
: service
          route_config
:
            name
: local_route
            virtual_hosts
:
            - name
: local_service
              domains
: ["*"]
              routes
:
              - match
: { prefix: "/" }
                route
: { cluster: service }
          http_filters
:
            - name
: envoy.filters.http.router
      tls_context
:
        common_tls_context
:
          alpn_protocols
: "h2"
          tls_certificates
:
            - certificate_chain
:
                filename
: "/path/to/server.crt"
              private_key
:
                filename
: "/path/to/server.key"