In the next few posts, I’m going to document how to set up Home Assistant (HA) from scratch. We’re going to want to protect the admin UI interfaces for HA and its support services with SSL, and add authentication to services that don’t provide it themselves.
We’re going to do this with Nginx Proxy Manager because it has built in support for using LetsEncrypt to obtain free SSL certificates, supports adding authentication to services that don’t do it themselves, and is overall easy to use.
Before I start writing more Home Assitant articles, let’s set up a SSL proxy server to keep everything secure.
We’re going to run the proxy server and the services behind it in containers with docker-compose
to make things easier.
Pre-requisites
- A linux machine with
docker
installed that doesn’t already have something running on port 80 - A domain that is using Route 53 for DNS
- A server on your network with a static IP, for example
10.1.2.3
- A DNS entry pointing at your server, like
demo.yourdomain.com
Setup
All screen shots and other instructions are valid as of 2023-07-22 when I wrote this post.
Rather than set up a web server and expose it to the internet so that LetsEncrypt can validate that you own the domain, we’re going to configure nginx-proxy-manager
to use a DNS01 challenge. This lets LetsEncrypt validate ownership of your domain by special records that will be added to your domain’s DNS entries during certificate creation / renewal by our nginx-proxy-manager
container.
nginx-proxy-manager
supports many DNS providers. I use Route 53 so I’ll use that for this article.
Set up a domain in AWS Route 53
If you already have a domain, you can transfer it to Route 53. I think I pay about $1.50 a month for the domains I host there. And that’s in total, not per domain. My homelab doesn’t get a ton of DNS queries each month.
If you don’t own a domain or don’t want to transfer one you own to R53, you can use Amazon for your registrar. They support many tlds, and some of the ones they support cost less than $13 a year - in my opinion it’s worth a dollar a month to have a separate domain for a homelab.
Set up Route 53
You don’t want to use your root IAM credentials with nginx-proxy-manager
. It is never a good idea to use root IAM credentials for anything. Instead, we’re going to create an IAM user that only has privileges to affect your Route53-hosted domains.
This is pretty tedious, so it’s a good thing you only need to do it once. Log in to the AWS console and select Identity and Access Management (IAM).
Create an IAM policy
We’re going to start by creating a policy that grants control over Route53, so select Policies from the sidebar, then hit the Create Policy button.
Creating the policy first makes it easier to attach to the IAM group when we create it in the next step.
I’ve already created a working policy, so instead of manually adding permissions, click JSON to the right of where it says Policy Editor and paste in this JSON snippet
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:ListHostedZonesByName",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets",
"route53:GetHostedZone"
],
"Resource": [
"arn:aws:route53:::hostedzone/ZG2ZIACD7OSSR"
]
}
]
}
If you want to restrict it to only controlling a specific domain, update the "Resource": "*"
line - that’s out of scope for this post, though.
As of 2023-07-22, it should look like this:
Click Next, and give your new policy a name and a description, for example blog-letsencrypt
.
Create an IAM group
Now you need to create a group, so click User Groups in the side bar, then hit the Create Group button.
Give it a name like blog-le-users
Under Attach permissions policy* select the blog-letsencrypt
policy you just created.
Hit the Create Group button on the bottom right of the page.
Create an IAM user
Now that the group and policy are created, you can create your r53 IAM user. Click Users in the side bar, then click the Add Users button on the right side of the screen.
Give it a name like r53-acme-user
and click Next.
Don’t bother to check the Provide user access to the AWS Management Console
checkbox, this user will only be used by your proxy manager. Select the blog-le-users
group
Click Next again.
You’ll see a review and create page:
Click Create user on the bottom right of the page.
One last thing - you’ll need to create access credentials for the user. Click on Users in the side bar again, then your brand new r53-acme-user
user.
You’ll see an info page about the user, click the Security Credentials tab
Scroll down to Access Keys, and click Create access key.
Click Other on the next page
Click Next. Put in letsencrypt
for the description,and click Create access key again.
You’ll see something like
You only get one chance to see the secret key, so store it in your password manager. It isn’t a huge deal if you lose it, you can always create another access key for your user. Copy the access key & secret keys into your password manager for later use.
Install nginx-proxy-manager
Now that you have the DNS domain on Route 53 and have created an IAM user with rights to update it, you can set up nginx-proxy-manager and have it generate LetsEncrypt SSL certificates.
For the SSL proxy, I like to set it to use an external docker network. This lets you run other docker containers behind it easily, without having to cram them all into the same docker-compose.yaml
file.
Creating the network is easy - docker network create ssl_proxy_network
and we’re good to go.
Here’s the docker-compose.yaml
file I use to start nginx-proxy-manager
. As of 2023-07-21, there’s a bug getting Route 53 to work with the latest tag, but it does work with the docker image with github-pr-2971
tag. I’ll update this post when that fix gets merged upstream.
Configure docker-compose
version: '3'
services:
nginx-proxy-manager:
# image: 'jc21/nginx-proxy-manager:latest'
# Use github-pr-2971 until the fix is merged
image: jc21/nginx-proxy-manager:github-pr-2971
restart: unless-stopped
container_name: nginx-proxy-manager
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./nginx/data:/data
- ./nginx/letsencrypt:/etc/letsencrypt
- /etc/hostname:/etc/hostname:ro
- /etc/localtime:/etc/localtime:ro
- /etc/machine-id:/etc/machine-id:ro
- /etc/timezone:/etc/timezone:ro
# environment:
# DISABLE_IPV6: 'true'
networks:
default:
external:
name: ssl_proxy_network
You can download this here and put it on your server.
Now you can run docker-compose up -d
and docker-compose
will download the image, create the nginx/data
and nginx/letsencrypt
directories, and start the proxy manager. The image is roughly 700 megs, and on my test Raspberry Pi, it took a couple of minutes to download and start. Run docker-compose logs -f
to see what’s happening inside the container as it does initial setup. On slower hardware, it can take over a minute to start up and get the service running, especially on the first run, so be patient.
Usage
Set up a demo container to proxy
I’m going to use a simple nginx
demo server to demo SSL proxying. We don’t need anything fancy, just something that will serve a web page so we can confirm the proxy is working. Make a demo
directory, and put the following snippet into its docker-compose.yaml
file.
I deliberately did not set up port forwarding in this configuration file because I don’t want it to be accessible from outside our ssl_proxy_network
docker network - that’s what the proxy is for.
version: '3'
services:
demo:
image: nginxdemos/hello
restart: unless-stopped
container_name: demo
volumes:
- /etc/hostname:/etc/hostname:ro
- /etc/localtime:/etc/localtime:ro
- /etc/machine-id:/etc/machine-id:ro
- /etc/timezone:/etc/timezone:ro
networks:
default:
external:
name: ssl_proxy_network
Start it up with docker-compose up -d
. Because you’re using the same ssl_proxy_network
that you created earlier, and your nginx-proxy-manager
is also using that network, the two containers will be able to communicate with each other. Open yourserver.example.com
in your browser, it should give you an error message about being unable to connect. You want to see that because you don’t want the service accessible except through the proxy.
Configure the Proxy Server
Now that you have the demo
backend and the nginx-proxy-manager
proxy running, let’s set up proxying.
First, log into the proxy manager at https://yourserver.example.com:81
. The default username is admin@example.com
, and the password is changeme
. It’ll make you change those when you first log in.
Add an SSL certificate.
To make things easier later, you’re going to create a wildcard SSL certificate for your domain. This will let you run as many services as you like as servicename.example.com
on the server, and not have to specify port numbers, just add new DNS entries pointing at your server.
Select SSL Certificates. You’ll see
Click Add SSL certificate. You should see something like this:
- Put
*.yourdomain.com
in as the domain name. - Select Use DNS Challenge
- Put in your email address so LetsEncrypt can send you notifications if there’s an issue with your certificate later.
- Select Route 53 for the DNS provider
- Set the AWS access key to the one you created for your r53-acme-user IAM user earlier
- Set the AWS secret key
- Agree to the terms of service, and hit save.
This can take a few minutes, especially if your machine is low powered or the LetsEncrypt backend is under load.
If everything went according to plan, you should see something like this:
Add a proxy host
Click hosts, then proxy host from the submenu.
You’ll see a dialog like this, hit Add Proxy Host
- You could have several different DNS names point at the same backend, but for this example, you only want
demo.yourdomain.com
, so stick that in the Domain Names field. - The hostname should be the name of the container we’re proxying - the
demo
service we set up earlier useshttp
on port 80, so enter those here. Even though there isn’t a port entry in the docker-compose file, the ports on the backend will be accessible from other containers on the same docker network, and this is why you configured thenginx-proxy-manager
anddemo
services to both use the externalssl_proxy_network
network. - I haven’t encountered problems with any backends using them, so I turn on Cache Assets, Websockets Support and Block Common Exploits since
nginx-proxy-manager
can add them.
Don’t hit save yet, you still need to attach an SSL certificate. Click on the SSL tab at the top of the dialog.
Select the *.yourdomain.com
certificate you created earlier
Finally, turn on Force SSL and HTTP/2 Support
Confirm things are working
Go to yourserver.yourdomain.com
and you should see the demo page
And there should be a lock icon in your browser.
Add authentication
Some services you’re going to want to run (like our demo
) don’t have any user authentication. Fortunately, nginx-proxy-manager
lets you insert a basic auth login step before a connection is created to the service it’s proxying.
Click Access Lists in the main menu, and Add Access List.
Put in a name - I used demo-acl
Click on Authorization
Put in a username and password. I used demo
and demo
here.
I don’t care where the service is accessed from as long as they have a valid username & password, so click on the Access tab, so I’m allowing from 0.0.0.0/8
- you could put in your lan’s network and netmask if you wanted to be more strict.
Save. Go back to Hosts -> Proxy Hosts, and use the … menu on the right of your demo
proxy entry to Edit.
Select your new ACL and Save
Now when you go to the service, it should prompt you with a basic auth dialog.
Lock down the proxy manager management UI
Now that you have SSL proxying working, there’s one last thing to do - putting the management interface behind itself, so that it is protected by SSL too.
In this example, I’m running nginx-proxy-manager on cthulhu, one of the HC2s in my homelab. The container is unimaginatively named nginxproxymanager
, so I set up the proxy manager to route cthulhu.miniclusters.rocks to port 81 of the nginxproxymanager
as seen below.
After you confirm it’s working, do a docker-compose down
in your proxy manager directory, delete or comment out the - '81:81'
line in docker-compose.yaml
, and restart it with docker-compose up -d
. If you’ve done it correctly, if you try to access port 81
on your host you’ll get a connection refused, but if you connect to it with https://yourmachine.example.com
, you’ll see the admin interface come up and it’ll have a lock icon to show it’s an SSL connection.
Update 2023-07-30: Add the instructions on securing the management interface with SSL that I forgot to commit.