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:

policyedit

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

create_iam_group

Under Attach permissions policy* select the blog-letsencrypt policy you just created.

add_iam_policy

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.

add_iam_user

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

add_user_to_group

Click Next again.

You’ll see a review and create page:

review_and_create

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

security_credentials

Scroll down to Access Keys, and click Create access key.

access_key

Click Other on the next page

other

Click Next. Put in letsencrypt for the description,and click Create access key again.

You’ll see something like

other

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

ssl

Click Add SSL certificate. You should see something like this:

create_wildcard_cert

  1. Put *.yourdomain.com in as the domain name.
  2. Select Use DNS Challenge
  3. Put in your email address so LetsEncrypt can send you notifications if there’s an issue with your certificate later.
  4. Select Route 53 for the DNS provider
  5. Set the AWS access key to the one you created for your r53-acme-user IAM user earlier
  6. Set the AWS secret key
  7. 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:

ssl_cert_list

Add a proxy host

Click hosts, then proxy host from the submenu.

proxy_submenu

You’ll see a dialog like this, hit Add Proxy Host

empty_proxy_list

  1. 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.
  2. The hostname should be the name of the container we’re proxying - the demo service we set up earlier uses http 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 the nginx-proxy-manager and demo services to both use the external ssl_proxy_network network.
  3. 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.

add_proxy_host

Select the *.yourdomain.com certificate you created earlier

add_cert_to_host

Finally, turn on Force SSL and HTTP/2 Support

proxy_settings

Confirm things are working

Go to yourserver.yourdomain.com and you should see the demo page

demo_page

And there should be a lock icon in your browser.

ssl_working

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

acl_01

Click on Authorization

Put in a username and password. I used demo and demo here.

acl_auth

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.

grant_network_access

Save. Go back to Hosts -> Proxy Hosts, and use the … menu on the right of your demo proxy entry to Edit.

add_acl_to_proxy

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.

add-ssl-to-admin-ui

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.