Bring Your Own Certificate Authority
Tags: ssl, nginx, certificates, pihole
Published: 2020-02-24 05:17:22

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](https://support.apple.com/en-us/HT210176) 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](https://github.com/cloudflare/cfssl).

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

$ cfssl version
Version: 1.4.1
Runtime: go1.13.4
```

All of the steps and configurations you will see in this guide are available to checkout from [my GitHub Gist](https://gist.github.com/stjohnjohnson/77c5515720954a97f2b9866bc6ab85e0):

```bash
$ git clone https://gist.github.com/77c5515720954a97f2b9866bc6ab85e0.git
...
$ cd 77c5515720954a97f2b9866bc6ab85e0
$ make
...
```

## 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`

{% gist https://gist.github.com/stjohnjohnson/77c5515720954a97f2b9866bc6ab85e0 file=ca-csr.json %}

_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.

```bash
$ 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
```

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`:

{% gist https://gist.github.com/stjohnjohnson/77c5515720954a97f2b9866bc6ab85e0 file=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`:

{% gist https://gist.github.com/stjohnjohnson/77c5515720954a97f2b9866bc6ab85e0 file=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:

```bash
$ 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
```

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

We can quickly verify it with openssl:

```bash
$ openssl verify -CAfile ca.pem server.pem
server.pem: OK
```

## 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 `pihole.stj.me` from `80` to `443/ssl`.
3. Serve `pihole.stj.me` 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.

{% gist https://gist.github.com/stjohnjohnson/77c5515720954a97f2b9866bc6ab85e0 file=nginx.conf %}

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`):

{% gist https://gist.github.com/stjohnjohnson/77c5515720954a97f2b9866bc6ab85e0 file=docker-compose.yml %}

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

```bash
$ docker-compose up nginx
Starting 77c5515720954a97f2b9866bc6ab85e0_pihole_1 ... done
Starting 77c5515720954a97f2b9866bc6ab85e0_nginx_1 ... done
Attaching to 77c5515720954a97f2b9866bc6ab85e0_pihole_1, 77c5515720954a97f2b9866bc6ab85e0_nginx_1
```

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

```bash
$ echo "127.0.0.1 pihole.stj.me" | sudo tee -a /etc/hosts
$ grep stj.me /etc/hosts
127.0.0.1 pihole.stj.me
```

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

Serving the certificate over port 80:

```bash
$ curl http://localhost/ca.pem
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
```

Redirecting to https:

```bash
$ curl -I http://pihole.stj.me/admin/
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
Location: https://pihole.stj.me/admin/
```

Serving pi-hole with our server certificates:

```bash
$ curl -I https://pihole.stj.me/admin/
curl: (60) SSL certificate problem: unable to get local issuer certificate

$ curl -I --cacert ca.pem https://pihole.stj.me/admin/
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
```

## 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 `10.0.0.100`.  That means from whatever device in your trusted network, you should be able to access `http://10.0.0.100/ca.pem`.  Additionally, you have already configured `pihole.stj.me` to point to `10.0.0.100` via DNSMasq or something else on your network.

### OSX

1. Download the `ca.pem` from your server via the URL above
2. Open `Keychain Access.app`
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 Access.app`
8. Visit `https://pihole.stj.me/admin/` successfully!

### IOS

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 `https://pihole.stj.me/admin/` 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 `https://pihole.stj.me/admin/` successfully!