Homeassistant with Traefik and SSH

So the idea is to have a home router listen on port 443 for HTTPS and SSH connection simultanously, route HTTPS traffic to a local homeassistant instance and SSH traffic to a local SSH server:

Port 443 is choosen as SSH port, because it shows the fewest problems in some network scenarios anything else beside standard web ports are blocked. The connection to home assistant is TLS secured. As TLS indicates the server name in the unencrypted protocol part (server name indication, SNI) its possible to add more https endpoints to the picture.

As SSH does not use the TLS protocol there is no way for Traefik to differentiate several SSH hosts or differentiate SSH traffic from any other non TLS traffic. The whole thing is possible by use of the HostSNI(*) matching. If you need more advanced traffic handling you might be interested in HAProxy which is able to guess traffic by inspecting the first few protocol bytes.

Setup Traefik

The Traefik setup consists of three files:

  • docker-compose.yml: We use a dockerized Traefik
  • traefik.yml: Static configuration
  • config/service.yml: Dynamic configuration via files

docker-compose.yml

First create a docker compose file:

version: '3'

services:
  reverse-proxy:
    image: traefik:v2.2
    command: --configFile=/etc/traefik.yml
    restart: always
    ports:
      - "80:80"
      - "443:443"
      # The Web UI (enabled by api.insecure=true)
      - "8080:8080"
    volumes:
      # Access to docker is disabled, we use file provider, no docker provider
      #- /var/run/docker.sock:/var/run/docker.sock
      - ./config/:/config/:ro
      - ./traefik.yml:/etc/traefik.yml:ro
      - ./traefik_letsencrypt/:/etc/traefik_letsencrypt/

The configuration is readonly as it is not going to be changed from within docker.

As we don’t use the docker config provider the socket access is disabled. traefik_letsencrypt is a folder which needs to be created on the local host before starting the container. In this folder traefik stores its letsencrypt certificates. Its mounted to the host to be able to backup the letsencrypt files when updating the conatiner. Alternatively you may want to use a volume.

Please note that I used versioned image names and not the latest tag as adviced in the article Dockerfile best practices.

traefik.yml

traefik.yml is the static configuration:

entryPoints:
  web:
   address: ":80"
  web-secure:
   address: ":443"
accessLog: {}
log:
  level: INFO
providers:
  file:
    directory: /config
    watch: true
api:
  insecure: true

certificatesResolvers:
  letsencrypt:
    acme:
      email: "add_your_email_here"
      storage: "/etc/traefik_letsencrypt/traefik_letsencrypt.json"
      #caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
      httpChallenge:
        entryPoint: web

To test the setup first use the staging acme server. The non staging server has a rate limit.

We need to listen to port 80 to be able to serve the http challenge.

config/service.yml

tcp:
  routers:
    router-ssh:
      entryPoints:
        - web-secure
      rule: HostSNI(`*`)
      service: service-ssh
  services:
    service-ssh:
      loadBalancer:
        servers:
          - address: add_ssh_hostname_here:22

http:
  routers:
    router-homeassistant:
      rule: Host(`internal-homeassistant-hostname`, `homeassistant.example.com`)
      tls:
        certResolver: letsencrypt
        domains:
          - main: ".example.com"
      service: service-homeassistant
      entryPoints:
        - web

    router-homeassistant-https:
      rule: Host(`internal-homeassistant-hostname`, `homeassistant.example.com`)
      tls:
        certResolver: letsencrypt
        domains:
          # Explicitely set the domain name so that letsencrypt does
          # try to request certificate for internal hostname accidently
          - main: "homeassistant.example.com"
      service: service-homeassistant
      entryPoints:
        - web-secure
      tls: {}

  services:
    service-homeassistant:
      loadBalancer:
        servers:
          - url: http://internal-homeassistant-hostname:8123

Things to adapt:

  • add_ssh_hostname_here: The internal hostname for the SSH server
  • internal-homeassistant-hostname: The internal hostname for the homeassistant installation, so you can test the setup in the local network
  • homeassistant.example.com: The external hostname for the homeassistant installation

At this point you should be able to start the Traefic container:

mkdir traefik_letsencrypt
docker-compose up -d

Setup Home Assistant

As final step home assistant needs to be configured to accept the X_Forwarded_For HTTP header. I add the whole docker default IP range here:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 127.0.0.1
    # docker IP range:
    - 172.0.0.0/8

Please not that the setting base_url is deprecated since homeassistant release 0.110. You don’t need to use the new settings external_url and internal_url as homeassistant will automatically guess, my setup is working so far without.