Reverse Proxy for Nginx in a Docker Container

Reverse Proxy for Nginx in a Docker Container

Or how I learned to stop worrying and love X-Forwarded-For

This is a mini-HOWTO of sorts on using Nginx (running in a Docker container) as a reverse proxy for Home Assistant (also running in a Docker container) along with the trusted_networks authentication provider.

The focus here is on reverse proxy. It is assumed you already know how to run Home Assistant and Nginx in Docker containers, using Docker Compose, as well as configuring Nginx to handle the SSL certificates.

The easy way (or so I thought): Don’t specify X-Forwarded-For

Without the X-Forwared-For header, Home Assistant remains blissfully unaware of the Nginx reverse proxy. It thinks all requests are originating directly from the Nginx Docker container.

Here’s a sample config (minus the SSL details) of redirecting to a secure port and reverse proxy based on DNS name. Notice there is no proxy_set_header line for X-Forwared-For. I even left myself a note in the comments.

server {
    server_name homeassistant.containerhost.home;
    listen 80;
    return 301 https://homeassistant.containerhost.home;
}

# Do not use "proxy_set_header X-Forwarded-For $remote_addr;" or Home Assistant
# will block the request.
server {
    server_name homeassistant.containerhost.home;
    listen 443 ssl;
    location / {
        proxy_pass http://containerhost.home:8123;
    }
    location /api/websocket {
        proxy_pass http://containerhost.home:8123;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

I’ve been using this setup for years. And it works great for people authenticating with a username and password. But I’m getting lazy, and I don’t want to have to assign and manage passwords for everyone in the family who wants to switch on a smart bulb.

So it’s trusted_networks to the rescue!

homeassistant:
  auth_providers:
    - type: trusted_networks
      trusted_networks:
        - 192.168.1.0/24
    - type: homeassistant

But when I add trusted_networks to configuration.yaml, it doesn’t work. And that’s because Home Assistant sees all requests as coming from Nginx, not my web browser.

Because I didn’t specify X-Forwarded-For in the Nginx configuration.

Because I was being lazy. And now it’s coming back to haunt me.

So it turns out I really do need to let Home Assistant know what my client IP address is, so it will realize my device is on a trusted network and let me in without a password.

First attempt to use X-Forwarded-For

The Nginx configuration below is pretty much the same as what I had to start with, but now there’s a line for proxy_set_header X-Forwarded-For $remote_addr;. This is what lets Home Assistant know the request originated from my client device and not the Nginx Docker container.

server {
    server_name homeassistant.containerhost.home;
    listen 80;
    return 301 https://homeassistant.containerhost.home;
}

server {
    server_name homeassistant.containerhost.home;
    listen 443 ssl;
    location / {
        proxy_pass http://containerhost.home:8123;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
    location /api/websocket {
        proxy_pass http://containerhost.home:8123;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

I restart the Nginx container and… Now there are a bunch of errors in the Home Assistant container’s log output that look like this:

Received X-Forwarded-For header from an untrusted proxy 192.168.240.2

Where’s the 192.168.240.2 address coming from? It’s from Nginx.

docker inspect nginx | grep IPAddress
"IPAddress": "192.168.240.2"

Second attempt to use X-Forwarded-For

I can add the Nginx container IP address as a trusted proxy Home Assistant’s configuration.yaml, like so (the second line in the trusted_networks):

homeassistant:
  auth_providers:
    - type: trusted_networks
      trusted_networks:
        - 192.168.1.0/24
        - 192.168.240.2
    - type: homeassistant

The errors from Home Assistant’s log clear up… for now. But what happens when the Nginx container is restarted?

Nginx gets a new dynamically assigned IP and we’re back to square one.

docker inspect nginx | grep IPAddress
"IPAddress": "192.168.144.2"

And now Home Assistant is complaining about untrusted proxy 192.168.144.2 and I am sad.

Third attempt to use X-Forwarded-For

The container IP addresses (like 192.168.144.2 above) are assigned by Docker, not DHCP like other hosts on the network. So a DHCP reservation won’t help. Still, if I could just get the Nginx container to always use the same IP address each time it starts, everything would be rainbows and unicorns.

It turns out it’s possible, and with Docker Compose, it’s not terribly difficult.

Here’s what I added to my Nginx container’s compose.yml:

# Stuff above this line not shown for brevity.
        networks:
          reverse_proxy:
            ipv4_address: 172.16.0.2

networks:
  reverse_proxy:
    driver: bridge
    ipam:
      config:
        - subnet: 172.16.0.0/16
          gateway: 172.16.0.1

After the changes, I shutdown the Nginx container then bring it back up. And bingo! An unchanging IP address.

docker inspect nginx | grep IPAddress
"IPAddress": "172.16.0.2"

All I have to do now is set the Home Assistant trusted proxy to use 172.16.0.2, and I’m all set.

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 172.16.0.2

I didn’t just pull this 172.16.0.2 address out of thin air. It’s part of a block known as Class B Reserved. That means it will never conflict with an address on the internet. (Kind of like most people use 192.168.1.x for their home LAN devices.)

172.16.0.0 is the network (The /16 part means it has a subnet mask of 255.255.0.0) 172.16.0.1 is the gateway (or router) that Docker uses to get the packets to the right destination. And finally 172.16.0.2 is the statically assigned IP address I’ve given to the Nginx container.

And, Bob’s your uncle, there you have it!

The full compose.yml for Nginx looks like this:

services:
    nginx:
        image: nginx
        container_name: nginx
        hostname: nginx
        restart: unless-stopped
        ports:
          - 80:80
          - 443:443
        volumes:
          - /etc/ssl:/etc/ssl:ro
          - ./conf.d:/etc/nginx/conf.d
          - /srv/www:/usr/share/nginx/html:ro
        networks:
          reverse_proxy:
            ipv4_address: 172.16.0.2

networks:
  reverse_proxy:
    driver: bridge
    ipam:
      config:
        - subnet: 172.16.0.0/16
          gateway: 172.16.0.1

Now anytime I restart the Nginx container, it always has the 172.16.0.2 address. The X-Forwarded-For in the Nginx configuration tells Home Assistant the true IP address of my client device. Home Assistant is configured to trust the 172.16.0.2 proxy and the network my client devices are on.

And there you have it. Trusted networks with Nginx reverse proxy running in a Docker container.