DEV Community

Cover image for Bring Your Own Certificate Authority
St. John Johnson
St. John Johnson

Posted on

Bring Your Own Certificate Authority

Over the past few years, I've been running a number of internal services for my household. These include automation tools with Home Assistant, anti-tracking with Pi-Hole, and media management with Plex.

Even in my home network, I still want these services to all use TLS. Until last week, I was doing this via a single complicated Nginx server using path proxying to the internal services. That service had a single (obfuscated) domain name that I used Let's Encrypt (ever 90 days) with DNS validation.
This was quite complicated to manage, share with the family, and not all the services I wanted to use supported alternative paths.

This all changed last week when my friend told me his household has their own Root Certificate! It immediately clicked, I could have all the subdomains I want with valid TLS certificates and not have to expose any information to the internet! Brilliant!

In this article, I'm going to walk through creating your own root certificate for your domain, generating server certificates, configuring your servers, and installing the CA on your client machines.

0. Know your Environment

My target systems were going to be a variety of OSX, iOS, and Windows devices. After doing some research (e.g. trial and error), I found that OSX/iOS has some new limitations on certificates that affect the work we need to do. The quick summary is:

  • RSA keys must use key sizes greater than or equal to 2048 bits
  • DNS name of the server must be in the Subject Alternative Name (SAN)
  • Server certificates must contain "serverAuth" ExtendedKeyUsage (EKU)
  • Server certificates must be valid for 825 days or fewer

And for me to make the certificates, I wanted the process to be quite repeatable (as I'll be adding new services over time) and I needed it to support sane future defaults. For those reasons, I ended up using cfssl by CloudFlare.

$ brew install cfssl
🍺  /usr/local/Cellar/cfssl/1.4.1: 5 files, 44.2MB

$ cfssl version
Version: 1.4.1
Runtime: go1.13.4
Enter fullscreen mode Exit fullscreen mode

All of the steps and configurations you will see in this guide are available to checkout from my GitHub Gist:

$ git clone
$ cd 77c5515720954a97f2b9866bc6ab85e0
$ make
Enter fullscreen mode Exit fullscreen mode

1. Generating your Root Certificate

We need to start by generate our CA (Certificate Authority). To do this, we need to provide a config to cfssl about our new root domain. Here is my ca-csr.json. You can also see the defaults via cfssl print-defaults csr

Pay close attention to the RSA/2048 as that is required for OSX/iOS.

Now we use the gencert command to create our new CA. Piping the command to the cfssljson command converts the cfssl JSON output into files.

$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca
2020/02/23 00:21:49 [INFO] generating a new CA key and certificate from CSR
2020/02/23 00:21:49 [INFO] generate received request
2020/02/23 00:21:49 [INFO] received CSR
2020/02/23 00:21:49 [INFO] generating key: rsa-2048
2020/02/23 00:21:49 [INFO] encoded CSR
2020/02/23 00:21:49 [INFO] signed certificate with serial number 505123895045664503620591058435184552164152724476
Enter fullscreen mode Exit fullscreen mode

That will give you your new CA (ca.pem) and private key (ca-key.pem).

2. Generating a Server Certificate

With that new CA, we can now mint a certificate for our first service (pi-hole). Generating a certificate requires two configs, one for the CA config and another CSR for the server itself.

Here is my ca-config.json:

Pay close attention to the 1 year expiration and "server auth" being listed in the usages section (i.e. EKU).

Here is my server-csr.json:

Key is RSA/2048 again and we also put the DNS entry in hosts (i.e. SAN)

Now that we have our configs, we can generate our certificate:

$ cfssl gencert \
    -ca=ca.pem \
    -ca-key=ca-key.pem \
    -config=ca-config.json \
    -profile=web-servers \
    server-csr.json | cfssljson -bare server
2020/02/23 09:16:46 [INFO] generate received request
2020/02/23 09:16:46 [INFO] received CSR
2020/02/23 09:16:46 [INFO] generating key: rsa-2048
2020/02/23 09:16:46 [INFO] encoded CSR
2020/02/23 09:16:46 [INFO] signed certificate with serial number 380799927774623048949672171568330922769086552071
Enter fullscreen mode Exit fullscreen mode

Now you have a server certificate for with (server.pem) and private key (server-key.pem).

We can quickly verify it with openssl:

$ openssl verify -CAfile ca.pem server.pem
server.pem: OK
Enter fullscreen mode Exit fullscreen mode

3. Configuring a NGinx Server

Our NGinx server needs to perform three actions:

  1. Serve ca.pem over 80 so our clients can install the certificate.
  2. Redirect from 80 to 443/ssl.
  3. Serve over 443/ssl with the server keys we generated.

To serve Pi-Hole, I'm using a docker container that is on the same virtual network via docker-compose (see below).

Here is my nginx.conf file that supports those actions.

As I said above, I've put everything in Docker-Machine with the two containers to make it easier to demonstrate here (docker-compose.yml):

With all this, we can get the service running using a simple command:

$ docker-compose up nginx
Starting 77c5515720954a97f2b9866bc6ab85e0_pihole_1 ... done
Starting 77c5515720954a97f2b9866bc6ab85e0_nginx_1 ... done
Attaching to 77c5515720954a97f2b9866bc6ab85e0_pihole_1, 77c5515720954a97f2b9866bc6ab85e0_nginx_1
Enter fullscreen mode Exit fullscreen mode

Make sure to augment your /etc/hosts with the new subdomain (until we put it in dnsmasq):

$ echo "" | sudo tee -a /etc/hosts
$ grep /etc/hosts
Enter fullscreen mode Exit fullscreen mode

And we can now verify that the actions work as expected:

Serving the certificate over port 80:

$ curl http://localhost/ca.pem
----------END CERTIFICATE-----
Enter fullscreen mode Exit fullscreen mode

Redirecting to https:

$ curl -I
HTTP/1.1 301 Moved Permanently
Server: nginx/1.17.8
Date: Mon, 24 Feb 2020 03:50:40 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Enter fullscreen mode Exit fullscreen mode

Serving pi-hole with our server certificates:

$ curl -I
curl: (60) SSL certificate problem: unable to get local issuer certificate

$ curl -I --cacert ca.pem
HTTP/1.1 200 OK
Server: nginx/1.17.8
Date: Mon, 24 Feb 2020 03:52:28 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Pi-hole: The Pi-hole Web interface is working!
X-Frame-Options: DENY
Set-Cookie: PHPSESSID=fvj75itvvq3e8pel87gkl7gdk2; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Enter fullscreen mode Exit fullscreen mode

4. Installing the Clients

Great, we have a working service, time to make sure your clients can talk to your services without having to pass the CA around (or --insecure).

For the rest of this guide, we're going to assume this is running on a server in the network with the IP That means from whatever device in your trusted network, you should be able to access Additionally, you have already configured to point to via DNSMasq or something else on your network.


  1. Download the ca.pem from your server via the URL above
  2. Open Keychain
  3. Go to the System keychain
  4. Click File > Import Items or use shift+cmd+I
  5. Double-click on your certificate
  6. Expand the Trust section and select Always Trust for When using this certificate
  7. Close Keychain
  8. Visit successfully!


  1. Visit the ca.pem URL above in Safari
  2. Click Allow when prompted about downloading a configuration profile
  3. Go to the Settings app
  4. Click on Profile Downloaded
  5. Click Install, enter your passcode, click Install, and click Install again
  6. Go back to the Settings app
  7. Go to General > About > Certificate Trust Settings
  8. Enable Full Trust for your Root Certificate
  9. Visit successfully!

Windows 10

  1. Download the ca.pem from your server via the URL above
  2. Open Microsoft Management Console via Start > Run > mmc.exe
  3. Click File > Add/Remove Snap-in
  4. Select Certificates
  5. Select Computer Account and Local computer
  6. Click OK to close the dialog
  7. Navigate to Trusted Root Certification Authorities
  8. Right-click and select All Tasks > Import
  9. Follow the wizard and select the ca.pem file you downloaded
  10. Visit successfully!

Top comments (2)

lawchristopher profile image

This is exactly the scenario I needed. Thank you for this clear and detailed explanation. I think this seriously needs to be used more. Now I can shut my home network from the internet and only VPN to use services like self hosted BitWarden.

dineshrathee12 profile image
Dinesh Rathee

LetsEncrypt have revoked around 3 million certs last night due to a bug that they found. Are you impacted by this, Check out ?