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
All of the steps and configurations you will see in this guide are available to checkout from my GitHub Gist:
$ 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
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
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
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:
$ openssl verify -CAfile ca.pem server.pem
server.pem: OK
3. Configuring a NGinx Server
Our NGinx server needs to perform three actions:
- Serve
ca.pem
over80
so our clients can install the certificate. - Redirect
pihole.stj.me
from80
to443/ssl
. - Serve
pihole.stj.me
over443/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
Make sure to augment your /etc/hosts
with the new subdomain (until we put it in dnsmasq):
$ 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:
$ curl http://localhost/ca.pem
-----BEGIN CERTIFICATE-----
...
----------END CERTIFICATE-----
Redirecting to https:
$ 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:
$ 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
- Download the
ca.pem
from your server via the URL above - Open
Keychain Access.app
- Go to the
System
keychain - Click
File > Import Items
or useshift+cmd+I
- Double-click on your certificate
- Expand the
Trust
section and selectAlways Trust
forWhen using this certificate
- Close
Keychain Access.app
- Visit
https://pihole.stj.me/admin/
successfully!
IOS
- Visit the
ca.pem
URL above in Safari - Click
Allow
when prompted about downloading aconfiguration profile
- Go to the
Settings
app - Click on
Profile Downloaded
- Click
Install
, enter your passcode, clickInstall
, and clickInstall
again - Go back to the
Settings
app - Go to
General > About > Certificate Trust Settings
- Enable
Full Trust
for your Root Certificate - Visit
https://pihole.stj.me/admin/
successfully!
Windows 10
- Download the
ca.pem
from your server via the URL above - Open
Microsoft Management Console
viaStart > Run > mmc.exe
- Click
File > Add/Remove Snap-in
- Select
Certificates
- Select
Computer Account
andLocal computer
- Click
OK
to close the dialog - Navigate to
Trusted Root Certification Authorities
- Right-click and select
All Tasks > Import
- Follow the wizard and select the
ca.pem
file you downloaded - Visit
https://pihole.stj.me/admin/
successfully!
Top comments (2)
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.
LetsEncrypt have revoked around 3 million certs last night due to a bug that they found. Are you impacted by this, Check out ?
DevTo
[+] dev.to/dineshrathee12/letsencrypt-...
GitHub
[+] github.com/dineshrathee12/Let-s-En...
LetsEncryptCommunity
[+] community.letsencrypt.org/t/letsen...