How to for self hosting behind NAT?

Hello, I’m currently using opnsense and wireguard for routing and VPN.
Am looking into trying out this new Zero Trust way.
So far I managed to install it using the ‘Local with Docker’, have the ZAC working but on attempt to import an identity JWT on Android client that is on 5G (not LAN with docker) it cannot reach the controller, it’s trying to connect on the docker network alias instead of FQDN.

From android log:

10-19 20:34:04.011 6128 6156 W ziti-sdk:ziti_ctrl.c:602 ctrl_next_ep(): ctrl[https://ziti-edge-controller:1280] no controllers are online

Which hosts should be accessible publicly and what kind of port forwarding should be set up in my scenario?
I have my own domain with wildcard Letsencrypt, using nginx as reverse proxy for several services.
Managed to put ZAC behind Authelia, now to learn how it all actually works!

Hi @ZerotrustExplorer, welcome to the community and to OpenZiti!

It sounds to me that you didn't setup the advertised host properly when starting everything up. You can correct this a few ways but by far the easiest would be to dump what you have and start again. With a zero trust overlay you must have your security setup right from the start properly or else things won't work correctly as they can't form proper mTLS links. This means a couple of key points:

  • no tls termination/inspections at any load balancer/proxies, you'll need to pass TLS through to controllers/routers
  • the certs that are served have the proper SANS (DNS/IP but DNS is preferable)
  • the "advertised" addresses are all accessible from whatever networks you plan to use (which often is "the" internet but not always)

In your controller and router config files there are 'advertised' addresses. These addresses will be pointing to that docker hostname and that's what's causing your problem here.

You can "fix" the PKI by finding each server cert, learning the ziti pki command to 'fix' the pki and repair everything if you like. I just think it'll be faster to dump it all and make sure you setup the external dns entry properly from the start.

You want an external FQDN and port for the controller and for one router. I think this video is still pretty relevant the big difference is that since that video we've implement SNI so that you don't need four ports (although you still have the option of having all four ports) https://www.youtube.com/watch?v=Fk2sE0ydVo8 The ports referenced there are the ones that the quickstarts use but of course you can change those too...

hopefully that helps

Thanks for the full reply! Will have a go on a fresh VM and will report back.

Oh you might find this video useful too since you're proxying your own stuff. Uses HA proxy but i'm sure you'll figure out the differences between HA Proxy and nginx. This shows you how to use SNI and a proxy to only expose one port...

Looking around for guides I found this post from you from 2023 with a full docker-compose yaml and env file, is it still relevant today?

That or should I use this guide? Deploying with Docker | NetFoundry Documentation

I assume there is no openziti repository for use with apt for debian/ubuntu?

I would think it's still relevant. There is a newer "deployment" docker image that doesn't install a bunch of dev tools if you'd rather go that approach. It also has a small "bootstrapper" that tries to make it easy to install/configure.

You can add the repo yes. See the controller for example Controller Deployment | NetFoundry Documentation

That'll install the ziti and ziti-controller package. You'll see this mentioned:

The openziti package provides the ziti CLI and is installed as a dependency.

So am trying with the below:

VM IP: 192.168.1.250

/etc/hosts on the VM:

mydomainname.tld <WAN_ADDRESS>

.env:
ZITI_IMAGE=openziti/quickstart
ZITI_VERSION=latest
ZITI_CONTROLLER_RAWNAME=ziti-controller

ZITI_CTRL_PORT=8440
ZITI_EDGE_CONTROLLER_PORT=8441
ZITI_EDGE_ROUTER_PORT=8442
ZITI_EDGE_ROUTER_LISTENER_BIND_PORT=10080
ZITI_ZAC_PORTTLS=8443

EXTERNAL_DNS=mydomainname.tld
ZITI_NETWORK_NAME=${EXTERNAL_DNS}
ZITI_CONTROLLER_HOSTNAME=${EXTERNAL_DNS}

ZITI_EDGE_ROUTER_RAWNAME=${EXTERNAL_DNS}
ZITI_EDGE_ROUTER_DESIRED_RAWNAME=${EXTERNAL_DNS}
ZITI_EDGE_ROUTER_HOSTNAME=${EXTERNAL_DNS}

ZITI_EDGE_ROUTER_ROLES=public

docker-compose-yaml from: Connect Desktop Tunneler to Docker Quickstart on seperate host - #3 by TheLumberjack

router port forwards:

incoming on WAN ADDRESS: 8440 - 8442 TCP to 192.168.250

incoming on WAN ADDRESS: 6262 TCP to 192.168.250

incoming on WAN ADDRESS:10080 TCP to 192.168.250

I can use nginx to reverse proxy zac so am not including 8443 here.

I would use things like SNI but I already am using mydomainname.tld for a webserver, on nginx and pihole I specify hostnames.

On running docker-compose up it remains stuck at:

ziti-console-1 | waiting for server key to exist...
ziti-edge-router-1 | [ 25.387] ERROR ziti/router/env.(*networkControllers).connectToControllerWithBackoff.func2: {error=[error connecting ctrl (dial tcp <WAN_ADDRESS>:6262: i/o timeout)] endpoint=[tls:ziti:6262
]} unable to connect controller

Not sure how to proceed.

6262 is a port in the config file that is for the controller's control plane. There must be a missing env var that wasn't set. I think it'd be ZITI_CTRL_ADVERTISED_PORT. I can see I do set that for the HA Proxy demo (see here).

I had an old compose file and .env file on a public machine so I stood it up to make sure it still works. It uses the quickstart image if that matters to you. Here's the full compose and full .env I used so you can adapt to your needs.

docker compose file

services:
  ziti-controller:
    image: "${ZITI_IMAGE}:${ZITI_VERSION}"
    env_file:
      - ./.env
    ports:
      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_ADVERTISED_PORT:-6262}:${ZITI_CTRL_ADVERTISED_PORT:-6262}
    environment:
      - ZITI_CTRL_NAME=${ZITI_CTRL_NAME:-ziti-edge-controller}
      - ZITI_CTRL_EDGE_ADVERTISED_ADDRESS=${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}
      - ZITI_CTRL_EDGE_ADVERTISED_PORT=${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
      - ZITI_CTRL_EDGE_IP_OVERRIDE=${ZITI_CTRL_EDGE_IP_OVERRIDE:-127.0.0.1}
      - ZITI_CTRL_ADVERTISED_PORT=${ZITI_CTRL_ADVERTISED_PORT:-6262}
      - ZITI_EDGE_IDENTITY_ENROLLMENT_DURATION=${ZITI_EDGE_IDENTITY_ENROLLMENT_DURATION}
      - ZITI_ROUTER_ENROLLMENT_DURATION=${ZITI_ROUTER_ENROLLMENT_DURATION}
      - ZITI_USER=${ZITI_USER:-admin}
      - ZITI_PWD=${ZITI_PWD}
    networks:
      ziti:
        aliases:
          - ziti-edge-controller
    volumes:
      - ziti-fs:/persistent
    entrypoint:
      - "/var/openziti/scripts/run-controller.sh"

  ziti-controller-init-container:
    image: "${ZITI_IMAGE}:${ZITI_VERSION}"
    depends_on:
      - ziti-controller
    environment:
      - ZITI_CTRL_EDGE_ADVERTISED_ADDRESS=${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}
      - ZITI_CTRL_EDGE_ADVERTISED_PORT=${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
    env_file:
      - ./.env
    networks:
      ziti:
    volumes:
      - ziti-fs:/persistent
    entrypoint:
      - "/var/openziti/scripts/run-with-ziti-cli.sh"
    command:
      - "/var/openziti/scripts/access-control.sh"

  ziti-edge-router:
    image: "${ZITI_IMAGE}:${ZITI_VERSION}"
    env_file:
      - ./.env
    depends_on:
      - ziti-controller
    ports:
      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_LISTENER_BIND_PORT:-10080}:${ZITI_ROUTER_LISTENER_BIND_PORT:-10080}
    environment:
      - ZITI_CTRL_ADVERTISED_ADDRESS=${ZITI_CTRL_ADVERTISED_ADDRESS:-ziti-controller}
      - ZITI_CTRL_ADVERTISED_PORT=${ZITI_CTRL_ADVERTISED_PORT:-6262}
      - ZITI_CTRL_EDGE_ADVERTISED_ADDRESS=${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}
      - ZITI_CTRL_EDGE_ADVERTISED_PORT=${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
      - ZITI_ROUTER_NAME=${ZITI_ROUTER_NAME:-ziti-edge-router}
      - ZITI_ROUTER_ADVERTISED_ADDRESS=${ZITI_ROUTER_ADVERTISED_ADDRESS:-ziti-edge-router}
      - ZITI_ROUTER_PORT=${ZITI_ROUTER_PORT:-3022}
      - ZITI_ROUTER_LISTENER_BIND_PORT=${ZITI_ROUTER_LISTENER_BIND_PORT:-10080}
      - ZITI_ROUTER_ROLES=public
    networks:
      - ziti
    volumes:
      - ziti-fs:/persistent
    entrypoint: /bin/bash
    command: "/var/openziti/scripts/run-router.sh edge"


networks:
  ziti:

volumes:
  ziti-fs:

.env file

# OpenZiti Variables
ZITI_IMAGE=openziti/quickstart
ZITI_VERSION=latest

# the user and password to use
# Leave password blank to have a unique value generated or set the password explicitly
ZITI_USER=admin
ZITI_PWD=password

ZITI_INTERFACE=0.0.0.0

# controller name, address/port information
ZITI_CTRL_EDGE_ADVERTISED_ADDRESS=ec2-3-18-113-172.us-east-2.compute.amazonaws.com
ZITI_CTRL_ADVERTISED_ADDRESS=ec2-3-18-113-172.us-east-2.compute.amazonaws.com
#ZITI_CTRL_EDGE_IP_OVERRIDE=10.10.10.10

ZITI_CTRL_ADVERTISED_PORT=8700
ZITI_CTRL_EDGE_ADVERTISED_PORT=8700

# The duration of the enrollment period (in minutes), default if not set. shown - 7days
ZITI_EDGE_IDENTITY_ENROLLMENT_DURATION=110080
ZITI_ROUTER_ENROLLMENT_DURATION=110080

ZITI_ROUTER_LISTENER_BIND_PORT=8701

If you want to add the ZAC to that compose you'll need to download a zac of your choosing and update the config accordingly. Me, I exec into the container as root:

docker compose exec -it --user root ziti-controller bash

grab a script to fetch zac i use:

curl -fsSL https://raw.githubusercontent.com/dovholuknf/openziti-scripts/refs/heads/main/fetch-zac.sh -o fetch-zac.sh
chmod +x fetch-zac.sh
#install unzip as it's necessary for fetch-zac
apt update && apt install unzip
./fetch-zac.sh

which will show you:

Latest release found: app-ziti-console-v3.12.6
Downloading artifact from: https://github.com/openziti/ziti-console/releases/download/app-ziti-console-v3.12.6/ziti-console.zip
Artifact saved to: /persistent/zac/app-ziti-console-v3.12.6.zip
Extracting /persistent/zac/app-ziti-console-v3.12.6.zip to /persistent/zac/ziti-console-v3.12.6
./fetch-zac.sh: line 35: unzip: command not found
Extraction complete.
      - binding: zac
        options:
          location: "/persistent/zac/ziti-console-v3.12.6"
          indexFile: index.html

Then add that to the bottom of the controller config file:

vi /persistent/ziti-edge-controller.yaml

restart the controller:

docker compose restart ziti-controller

And now you'll have the ZAC available if you want

Thank you for your patience with me!

So for the first test setup I used the following env file and hosts file:

  GNU nano 8.4                                                                                            .env *                                                                                                    
# OpenZiti Variables
ZITI_IMAGE=openziti/quickstart
ZITI_VERSION=latest

the user and password to use

Leave password blank to have a unique value generated or set the password explicitly

ZITI_USER=admin
ZITI_PWD=password

ZITI_INTERFACE=0.0.0.0

controller name, address/port information

ZITI_CTRL_EDGE_ADVERTISED_ADDRESS=openziti.hostname.tld
ZITI_CTRL_ADVERTISED_ADDRESS=openziti.hostname.tld
#ZITI_CTRL_EDGE_IP_OVERRIDE=10.10.10.10

ZITI_CTRL_ADVERTISED_PORT=8700
ZITI_CTRL_EDGE_ADVERTISED_PORT=8700

The duration of the enrollment period (in minutes), default if not set. shown - 7days

ZITI_EDGE_IDENTITY_ENROLLMENT_DURATION=110080
ZITI_ROUTER_ENROLLMENT_DURATION=110080

ZITI_ROUTER_LISTENER_BIND_PORT=8701

/etc/hosts on VM contains

192.168.1.250 openziti.hostname.tld

Used direct copy of docker-compose.yml example.

on attempt to run with ‘docker compose up’ it keeps giving these back:

ziti-edge-router-1 | waiting for ``https://openziti.hostname.tld:8700
ziti-controller-init-container-1 | waiting for ``https://openziti.hostname.tld:8700

sudo ss -lpn shows:

tcp LISTEN 0 4096 0.0.0.0:10080 0.0.0.0:* users:(("docker-proxy",pid=1423,fd=7))
tcp LISTEN 0 4096 0.0.0.0:8700 0.0.0.0:* users:(("docker-proxy",pid=1262,fd=7))
tcp LISTEN 0 4096 0.0.0.0:3022 0.0.0.0:* users:(("docker-proxy",pid=1415,fd=7))

So services are running, I guess it’s not accepting the certificate?

curl https://openziti.hostname.tld:8700 shows:

curl: (60) SSL certificate problem: self-signed certificate in certificate chain
More details here: 


curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.

curl https://openziti.hostname.tld:8700 –insecure shows:

{"data":{"apiVersions":{"edge":{"v1":{"apiBaseUrls":["``https://openziti.hostname.tld:8700/edge/client/v1"],"path":"/edge/client/v1"}},"edge-client":{"v1":{"apiBaseUrls":["https://openziti.hostname.tld:8700/edge/client/v1"],"path":"/edge/client/v1"}},"edge-management":{"v1":{"apiBaseUrls":["https://openziti.hostname.tld:8700/edge/management/v1"],"path":"/edge/management/v1"}},"edge-oidc":{"v1":{"apiBaseUrls":["https://openziti.hostname.tld:8700/oidc"],"path":"/oidc"}}},"buildDate":"2025-10-16T15:35:19Z","capabilities":["OIDC_AUTH"],"revision":"1bd146994aa6","runtimeVersion":"go1.24.7","version":"v1.7.0"},"meta``":{}}

Could I use the pem and key from my wildcard certs from nginx webserver?

After the zac step and restarting the controller did not come up again if I check with: sudo ss -lpn

docker compose up log showed these lines, seems to break controller start:

ziti-controller-1 | [ 0.309] FATAL ziti/controller/subcmd.NewEdgeInitializeCmd.func2: already initialized: Ziti Edge default admin already defined
ziti-controller-1 | --- There was an error while initializing the controller ---

That was embarassing: I messed up my port forwarding.

Your example works as intended!

next up:

Console Configuration | NetFoundry Documentation to put a proper cert in place for the console.

And adding 2fa to the console, how does that work without Authelia?
Could only change my password in console.