Correct Traefik Docker Labels to Secure Ziti for Zrok

I had a Zrok instance, shares, and accesses containers configured in a working state six months ago. However, it broke after not using it, and I spent the last few days remaking my configurations from scratch based on the Zrok website's docker-based tutorials like Self-hosting guide for Docker | Zrok. Like my previous configuration, the server hosting my Zrok instance uses Traefik and thus does not have port 80 open for Caddy. Therefore, my goal has been to use the Traefik labels I use with other containers to secure my Zrok instance. Unfortunately, I can't get Ziti to work with Traefik labels. I've only gotten my Zrok instance working by opening the container ports for Ziti. That is problematic because the data does not go through Traefik for TLS.

Here is my Zrok instance compose.yml file:

services:
  ziti-quickstart:
    image: ${ZITI_CLI_IMAGE:-docker.io/openziti/ziti-cli}:${ZITI_CLI_TAG:-latest}
    restart: unless-stopped
    # Labels won't work... must temporarily open insecure ports for Ziti
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=servnet"
      # Ziti CTRL
      - "traefik.http.services.zitictrl.loadbalancer.server.port=1280"
      - "traefik.http.routers.zitictrl.rule=Host(`ziti.libregalaxy.org`)"
      - "traefik.http.routers.zitictrl.entrypoints=websecure"
      - "traefik.http.routers.zitictrl.tls.certresolver=production"
      # Ziti Data
      - "traefik.http.services.zitidata.loadbalancer.server.port=3022"
      - "traefik.http.routers.zitidata.rule=Host(`ziti.libregalaxy.org`)"
      - "traefik.http.routers.zitidata.entrypoints=websecure"
      - "traefik.http.routers.zitidata.tls.certresolver=production"
    ports:
      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_ADVERTISED_PORT:-1280}:${ZITI_CTRL_ADVERTISED_PORT:-1280}
      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
    expose:
      - ${ZITI_CTRL_ADVERTISED_PORT:-1280}
      - ${ZITI_ROUTER_PORT:-3022}
    networks:
      servnet:
        aliases:
          - ziti.${ZROK_DNS_ZONE}
      #zrok-instance:
        # this allows other containers to use the same external DNS name to reach the quickstart container from within the
        # Docker network that clients outside the Docker network use to reach the quickstart container via port forwarding
        #aliases:
          #- ziti.${ZROK_DNS_ZONE}
    entrypoint:
      - bash
      - -euc
      - |
        ZITI_CMD+=" --ctrl-address ziti.${ZROK_DNS_ZONE}"\
        " --ctrl-port ${ZITI_CTRL_ADVERTISED_PORT:-1280}"\
        " --router-address ziti.${ZROK_DNS_ZONE}"\
        " --router-port ${ZITI_ROUTER_PORT:-3022}"\
        " --password ${ZITI_PWD:-admin}"
        echo "DEBUG: run command is: ziti $${@} $${ZITI_CMD}"
        exec ziti "$${@}" $${ZITI_CMD}
    command: -- edge quickstart --home /home/ziggy/quickstart
    user: ${ZIGGY_UID:-1000}
    environment:
      HOME: /home/ziggy
      PFXLOG_NO_JSON: "${PFXLOG_NO_JSON:-true}"
      ZITI_ROUTER_NAME: ${ZITI_ROUTER_NAME:-quickstart-router}
    volumes:
      # store the quickstart state in a named volume "ziti_home" or store the quickstart state on the Docker host in a
      # directory, ZITI_HOME 
      #- ${ZITI_HOME:-ziti_home}:/home/ziggy
      - ./ziti_home:/home/ziggy
    depends_on:
      ziti-quickstart-init:
        condition: service_completed_successfully
    healthcheck:
      test:
        - CMD
        - ziti
        - agent
        - stats
      interval: 3s
      timeout: 3s
      retries: 5
      start_period: 30s

  # this service is used to initialize the ziti_home volume by setting the owner to the UID of the user running the
  # quickstart container
  ziti-quickstart-init:
    image: busybox
    command: chown -Rc ${ZIGGY_UID:-1000} /home/ziggy
    user: root
    environment:
      HOME: /home/ziggy
    volumes:
      # store the quickstart state in a named volume "ziti_home" or store the quickstart state on the Docker host in a
      # directory, ZITI_HOME 
      #- ${ZITI_HOME:-ziti_home}:/home/ziggy
      - ./ziti_home:/home/ziggy

  # add a health check for the quickstart network
  ziti-quickstart-check:
    image: busybox
    command: echo "Ziti is cooking"
    depends_on:
      ziti-quickstart:
        condition: service_healthy

  zrok-permissions:
    image: busybox
    command:
      - /bin/sh
      - -euxc
      - |
        chown -Rc ${ZIGGY_UID:-2171} /var/lib/zrok-*;
        chmod -Rc ug=rwX,o-rwx /var/lib/zrok-*;
    volumes:
      - ./zrok_ctrl:/var/lib/zrok-controller
      - ./zrok_frontend:/var/lib/zrok-frontend

  zrok-controller:
    depends_on:
        zrok-permissions:
          condition: service_completed_successfully
    build:
      context: .
      dockerfile: ./zrok-controller.Dockerfile
      args:
        ZROK_CLI_IMAGE: ${ZROK_CLI_IMAGE:-openziti/zrok}
        ZROK_CLI_TAG: ${ZROK_CLI_TAG:-latest}
        ZROK_DNS_ZONE: ${ZROK_DNS_ZONE}  # e.g., "example.com" or "127.0.0.1.sslip.io"
        ZITI_CTRL_ADVERTISED_PORT: ${ZITI_CTRL_ADVERTISED_PORT:-1280}
        ZROK_ADMIN_TOKEN: ${ZROK_ADMIN_TOKEN} # zrok controller admin password
        ZROK_CTRL_PORT: ${ZROK_CTRL_PORT:-18080}
        ZITI_PWD: ${ZITI_PWD} # ziti controller admin password
    user: ${ZIGGY_UID:-2171}
    command: zrok controller /etc/zrok-controller/config.yml --verbose
    volumes:
      - ./zrok_ctrl:/var/lib/zrok-controller
    networks:
      servnet:
        aliases:
          - zrok.${ZROK_DNS_ZONE}
      #zrok-instance:
        #aliases:
          #- zrok.${ZROK_DNS_ZONE}
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.services.zrok.loadbalancer.server.port=18080"
      - "traefik.docker.network=servnet"
      - "traefik.http.routers.zrok.rule=Host(`zrok.libregalaxy.org`)"
      - "traefik.http.routers.zrok.entrypoints=websecure"
      - "traefik.http.routers.zrok.tls.certresolver=production"
    expose:
      - ${ZROK_CTRL_PORT:-18080}  # (not published)
    #ports:
      #- ${ZROK_INSECURE_INTERFACE:-127.0.0.1}:${ZROK_CTRL_PORT:-18080}:${ZROK_CTRL_PORT:-18080}
    environment:
      ZROK_USER_PWD: ${ZROK_USER_PWD} # admin account password     (initial user account)
      ZROK_USER_EMAIL: ${ZROK_USER_EMAIL}  # login email address (initial user account)
      ZROK_ADMIN_TOKEN: ${ZROK_ADMIN_TOKEN} # zrok controller admin password
      ZROK_API_ENDPOINT: http://zrok-controller:${ZROK_CTRL_PORT:-18080} # bridge address of the zrok controller
  
  # Not in use
  zrok-frontend:
    depends_on:
        zrok-permissions:
          condition: service_completed_successfully
    build:
      context: .
      dockerfile: zrok-frontend.Dockerfile
      args:
        ZROK_CLI_IMAGE: ${ZROK_CLI_IMAGE:-openziti/zrok}
        ZROK_CLI_TAG: ${ZROK_CLI_TAG:-latest}
        ZROK_DNS_ZONE: ${ZROK_DNS_ZONE}  # e.g., "example.com" or "127.0.0.1.sslip.io"
        ZROK_FRONTEND_PORT: ${ZROK_FRONTEND_PORT:-8080}
        ZROK_OAUTH_PORT: ${ZROK_OAUTH_PORT:-8081}
        ZROK_OAUTH_HASH_KEY: ${ZROK_OAUTH_HASH_KEY-noop}
        ZROK_OAUTH_GOOGLE_CLIENT_ID: ${ZROK_OAUTH_GOOGLE_CLIENT_ID:-noop}
        ZROK_OAUTH_GOOGLE_CLIENT_SECRET: ${ZROK_OAUTH_GOOGLE_CLIENT_SECRET:-noop}
        ZROK_OAUTH_GITHUB_CLIENT_ID: ${ZROK_OAUTH_GITHUB_CLIENT_ID:-noop}
        ZROK_OAUTH_GITHUB_CLIENT_SECRET: ${ZROK_OAUTH_GITHUB_CLIENT_SECRET:-noop}
    user: ${ZIGGY_UID:-2171}
    command: zrok access public /etc/zrok-frontend/config.yml --verbose
    volumes:
      - ./zrok_frontend:/var/lib/zrok-frontend
    networks:
      servnet:
      #zrok-instance:
    restart: unless-stopped
    labels:
      #- "traefik.enable=true"
      - "traefik.http.services.zrokfe.loadbalancer.server.port=8080"
      - "traefik.docker.network=servnet"
      - "traefik.http.routers.zrokfe.rule=Host(`zrokfe.libregalaxy.org`)"
      - "traefik.http.routers.zrokfe.entrypoints=websecure"
      - "traefik.http.routers.zrokfe.tls.certresolver=production"
    expose:
      - ${ZROK_FRONTEND_PORT:-8080}  # (not published)
      - ${ZROK_OAUTH_PORT:-8081}     # (not published)
    #ports:
      #- ${ZROK_INSECURE_INTERFACE:-127.0.0.1}:${ZROK_FRONTEND_PORT:-8080}:${ZROK_FRONTEND_PORT:-8080}
      #- ${ZROK_INSECURE_INTERFACE:-127.0.0.1}:${ZROK_OAUTH_PORT:-8081}:${ZROK_OAUTH_PORT:-8081}
    environment:
      HOME: /var/lib/zrok-frontend
      ZROK_DNS_ZONE: ${ZROK_DNS_ZONE}  # e.g., "example.com" or "127.0.0.1.sslip.io"
      ZROK_ADMIN_TOKEN: ${ZROK_ADMIN_TOKEN} # zrok controller admin password
      ZROK_API_ENDPOINT: http://zrok-controller:${ZROK_CTRL_PORT:-18080} # bridge address of the zrok controller
      ZROK_FRONTEND_SCHEME: http
      ZROK_FRONTEND_PORT: ${ZROK_FRONTEND_PORT:-8080}
      ZITI_CTRL_ADVERTISED_PORT: ${ZITI_CTRL_ADVERTISED_PORT:-1280}
      ZITI_PWD: ${ZITI_PWD} # ziti controller admin password

#volumes:
  #ziti_home:  # this will not be used if you switch from named volume to bind mount volume
  #zrok_ctrl:
  #zrok_frontend:

# define a custom network so that we can also define DNS aliases
networks:
  servnet:
    driver: bridge
    external: true
  #zrok-instance:
    #driver: bridge

When the container ports are open, "https://ziti.libregalaxy.org:1280/edge/client/v1/authenticate?method=cert" can be connected to, albeit only with HTTP. However, Zrok shares/accesses can't establish a connection when the ports are closed. I understand this is a problem with my Docker Traefik labels; I'd appreciate your help fixing them.

P.S.
I changed from a Docker volume to ./mnt in the compose.yml file because the permission-changing container did not work while it was a volume, but it did after changing it to mount locally. Any idea what is causing that behavior? The server is running Fedora Linux.

1 Like

Hello again, @itsmcb. Hrrm. I wonder why it stopped working. :thinking:

No problem. I expect it'll work with pretty much any HTTP proxy. I used Caddy as an example because it has built-in certificate management.

I'm not a Traefik expert, and I'll gladly help figure it out! Here's the rationale for a reverse proxy as part of your zrok instance, which is self-hosted with Docker.

zrok has two components that need server TLS: zrok controller and zrok frontend. The controller provides the console and API. The frontend provides a web server for public shares. The frontend can also provide a redirecting web server for OAuth on a separate port.

So, in the Docker instance example you followed, zrok-controller and zrok-frontend insecure ports are exposed to Caddy (optionally published to ZROK_INSECURE_INTERFACE for local testing) and Caddy's ports are published, e.g., 443/tcp. The ziti-quickstart secure ports are published (forwarded), too, on the ZITI_INTERFACE because Caddy doesn't have a TLS passthrough option, but you could proxy the Ziti ports with Traefik as TCP routes with passthrough enabled.

Suppose you have these assignments in your .env file, and you're not using OAuth.

ZROK_DNS_ZONE=example.com
ZROK_CTRL_PORT=18080
ZROK_FRONTEND_PORT=8080
ZROK_FRONTEND_SCHEME=https
ZROK_FRONTEND_PORT=443

Your proxy terminates TLS for:

  • https://zrok.example.com:443 :arrow_right: http://zrok-controller:18080 - zrok console and API
  • https://*.example.com:443 :arrow_right: http://zrok-frontend:8080 - everything else is a public share

When the zrok-frontend container starts, it uses ZROK_FRONTEND_SCHEME and ZROK_FRONTEND_PORT to update the public share URL template, so ensure you set those since you're not using the Caddy sample that would have set them for you.

Sure, you can use a bind mount instead of a named volume. Just ensure the run-as UID (ZIGGY_UID) has permission to use the files you mounted from the Fedora host.

BTW, did the init container log an error when it failed to set owner/mode in the mounted, named volume? I expected that to work reliably because the init container runs as root. Are you using Docker, Podman, or something else?

Thank you for your helpful response! You mentioned that I only need TLS for the Zrok controller and Zrok front end. TLS is working for those containers. The only container where it is not working is ziti-quickstart. I made this post believing the ziti-quickstart container needed TLS. Is that not the case?

Assuming it is, and if I understand correctly, you suggest that I configure TLS passthrough to enable TLS for the ziti-quickstart container. Here's a part of my Traefik config where I implemented that change (traefik/config/traefik.yml):

# Entry Points configuration
# ---
entryPoints:
  web:
    address: :80
    # Redirect HTTP to HTTPs
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: :443
  streaming:
    address: :3478
  metrics:
    address: :8082
  ziti-ctrl:
    address: :1280
  ziti-data:
    address: :3022

# TCP configuration for Ziti control and data planes
tcp:
  routers:
    to-ziti-ctrl:
      entryPoints:
        - "ziti-ctrl"
      rule: "HostSNI(`*`)"  # Catch-all for SNI
      service: ziti-ctrl
      tls:
        passthrough: true

    to-ziti-data:
      entryPoints:
        - "ziti-data"
      rule: "HostSNI(`*`)"  # Catch-all for SNI
      service: ziti-data
      tls:
        passthrough: true

  services:
    ziti-ctrl:
      loadBalancer:
        servers:          
        - address: "ziti-quickstart:1280"

    ziti-data:
      loadBalancer:
        servers:          
        - address: "ziti-quickstart:3022"

For reference, here's my Zrok instance compose.yml file. The major change is removing the labels that didn't work before.

services:
  ziti-quickstart:
    image: ${ZITI_CLI_IMAGE:-docker.io/openziti/ziti-cli}:${ZITI_CLI_TAG:-latest}
    restart: unless-stopped
    #ports:
      #- ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_ADVERTISED_PORT:-1280}:${ZITI_CTRL_ADVERTISED_PORT:-1280}
      #- ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
    expose:
      - ${ZITI_CTRL_ADVERTISED_PORT:-1280}
      - ${ZITI_ROUTER_PORT:-3022}
    networks:
      servnet:
        aliases:
          - ziti.${ZROK_DNS_ZONE}
      #zrok-instance:
        # this allows other containers to use the same external DNS name to reach the quickstart container from within the
        # Docker network that clients outside the Docker network use to reach the quickstart container via port forwarding
        #aliases:
          #- ziti.${ZROK_DNS_ZONE}
    entrypoint:
      - bash
      - -euc
      - |
        ZITI_CMD+=" --ctrl-address ziti.${ZROK_DNS_ZONE}"\
        " --ctrl-port ${ZITI_CTRL_ADVERTISED_PORT:-1280}"\
        " --router-address ziti.${ZROK_DNS_ZONE}"\
        " --router-port ${ZITI_ROUTER_PORT:-3022}"\
        " --password ${ZITI_PWD:-admin}"
        echo "DEBUG: run command is: ziti $${@} $${ZITI_CMD}"
        exec ziti "$${@}" $${ZITI_CMD}
    command: -- edge quickstart --home /home/ziggy/quickstart
    user: ${ZIGGY_UID:-1000}
    environment:
      HOME: /home/ziggy
      PFXLOG_NO_JSON: "${PFXLOG_NO_JSON:-true}"
      ZITI_ROUTER_NAME: ${ZITI_ROUTER_NAME:-quickstart-router}
    volumes:
      # store the quickstart state in a named volume "ziti_home" or store the quickstart state on the Docker host in a
      # directory, ZITI_HOME 
      #- ${ZITI_HOME:-ziti_home}:/home/ziggy
      - ./ziti_home:/home/ziggy
    depends_on:
      ziti-quickstart-init:
        condition: service_completed_successfully
    healthcheck:
      test:
        - CMD
        - ziti
        - agent
        - stats
      interval: 3s
      timeout: 3s
      retries: 5
      start_period: 30s

  # this service is used to initialize the ziti_home volume by setting the owner to the UID of the user running the
  # quickstart container
  ziti-quickstart-init:
    image: busybox
    command: chown -Rc ${ZIGGY_UID:-1000} /home/ziggy
    user: root
    environment:
      HOME: /home/ziggy
    volumes:
      # store the quickstart state in a named volume "ziti_home" or store the quickstart state on the Docker host in a
      # directory, ZITI_HOME 
      #- ${ZITI_HOME:-ziti_home}:/home/ziggy
      - ./ziti_home:/home/ziggy

  # add a health check for the quickstart network
  ziti-quickstart-check:
    image: busybox
    command: echo "Ziti is cooking"
    depends_on:
      ziti-quickstart:
        condition: service_healthy

  zrok-permissions:
    image: busybox
    command:
      - /bin/sh
      - -euxc
      - |
        chown -Rc ${ZIGGY_UID:-2171} /var/lib/zrok-*;
        chmod -Rc ug=rwX,o-rwx /var/lib/zrok-*;
    volumes:
      - ./zrok_ctrl:/var/lib/zrok-controller
      - ./zrok_frontend:/var/lib/zrok-frontend

  zrok-controller:
    depends_on:
        zrok-permissions:
          condition: service_completed_successfully
    build:
      context: .
      dockerfile: ./zrok-controller.Dockerfile
      args:
        ZROK_CLI_IMAGE: ${ZROK_CLI_IMAGE:-openziti/zrok}
        ZROK_CLI_TAG: ${ZROK_CLI_TAG:-latest}
        ZROK_DNS_ZONE: ${ZROK_DNS_ZONE}  # e.g., "example.com" or "127.0.0.1.sslip.io"
        ZITI_CTRL_ADVERTISED_PORT: ${ZITI_CTRL_ADVERTISED_PORT:-1280}
        ZROK_ADMIN_TOKEN: ${ZROK_ADMIN_TOKEN} # zrok controller admin password
        ZROK_CTRL_PORT: ${ZROK_CTRL_PORT:-18080}
        ZITI_PWD: ${ZITI_PWD} # ziti controller admin password
    user: ${ZIGGY_UID:-2171}
    command: zrok controller /etc/zrok-controller/config.yml --verbose
    volumes:
      - ./zrok_ctrl:/var/lib/zrok-controller
    networks:
      servnet:
        aliases:
          - zrok.${ZROK_DNS_ZONE}
      #zrok-instance:
        #aliases:
          #- zrok.${ZROK_DNS_ZONE}
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.services.zrok.loadbalancer.server.port=18080"
      - "traefik.docker.network=servnet"
      - "traefik.http.routers.zrok.rule=Host(`zrok.libregalaxy.org`)"
      - "traefik.http.routers.zrok.entrypoints=websecure"
      - "traefik.http.routers.zrok.tls.certresolver=production"
    expose:
      - ${ZROK_CTRL_PORT:-18080}  # (not published)
    #ports:
      #- ${ZROK_INSECURE_INTERFACE:-127.0.0.1}:${ZROK_CTRL_PORT:-18080}:${ZROK_CTRL_PORT:-18080}
    environment:
      ZROK_USER_PWD: ${ZROK_USER_PWD} # admin account password     (initial user account)
      ZROK_USER_EMAIL: ${ZROK_USER_EMAIL}  # login email address (initial user account)
      ZROK_ADMIN_TOKEN: ${ZROK_ADMIN_TOKEN} # zrok controller admin password
      ZROK_API_ENDPOINT: http://zrok-controller:${ZROK_CTRL_PORT:-18080} # bridge address of the zrok controller
  
  # Not in use
  zrok-frontend:
    depends_on:
        zrok-permissions:
          condition: service_completed_successfully
    build:
      context: .
      dockerfile: zrok-frontend.Dockerfile
      args:
        ZROK_CLI_IMAGE: ${ZROK_CLI_IMAGE:-openziti/zrok}
        ZROK_CLI_TAG: ${ZROK_CLI_TAG:-latest}
        ZROK_DNS_ZONE: ${ZROK_DNS_ZONE}  # e.g., "example.com" or "127.0.0.1.sslip.io"
        ZROK_FRONTEND_PORT: ${ZROK_FRONTEND_PORT:-8080}
        ZROK_OAUTH_PORT: ${ZROK_OAUTH_PORT:-8081}
        ZROK_OAUTH_HASH_KEY: ${ZROK_OAUTH_HASH_KEY-noop}
        ZROK_OAUTH_GOOGLE_CLIENT_ID: ${ZROK_OAUTH_GOOGLE_CLIENT_ID:-noop}
        ZROK_OAUTH_GOOGLE_CLIENT_SECRET: ${ZROK_OAUTH_GOOGLE_CLIENT_SECRET:-noop}
        ZROK_OAUTH_GITHUB_CLIENT_ID: ${ZROK_OAUTH_GITHUB_CLIENT_ID:-noop}
        ZROK_OAUTH_GITHUB_CLIENT_SECRET: ${ZROK_OAUTH_GITHUB_CLIENT_SECRET:-noop}
    user: ${ZIGGY_UID:-2171}
    command: zrok access public /etc/zrok-frontend/config.yml --verbose
    volumes:
      - ./zrok_frontend:/var/lib/zrok-frontend
    networks:
      servnet:
      #zrok-instance:
    restart: unless-stopped
    labels:
      #- "traefik.enable=true"
      - "traefik.http.services.zrokfe.loadbalancer.server.port=8080"
      - "traefik.docker.network=servnet"
      - "traefik.http.routers.zrokfe.rule=Host(`zrokfe.libregalaxy.org`)"
      - "traefik.http.routers.zrokfe.entrypoints=websecure"
      - "traefik.http.routers.zrokfe.tls.certresolver=production"
    expose:
      - ${ZROK_FRONTEND_PORT:-8080}  # (not published)
      - ${ZROK_OAUTH_PORT:-8081}     # (not published)
    #ports:
      #- ${ZROK_INSECURE_INTERFACE:-127.0.0.1}:${ZROK_FRONTEND_PORT:-8080}:${ZROK_FRONTEND_PORT:-8080}
      #- ${ZROK_INSECURE_INTERFACE:-127.0.0.1}:${ZROK_OAUTH_PORT:-8081}:${ZROK_OAUTH_PORT:-8081}
    environment:
      HOME: /var/lib/zrok-frontend
      ZROK_DNS_ZONE: ${ZROK_DNS_ZONE}  # e.g., "example.com" or "127.0.0.1.sslip.io"
      ZROK_ADMIN_TOKEN: ${ZROK_ADMIN_TOKEN} # zrok controller admin password
      ZROK_API_ENDPOINT: http://zrok-controller:${ZROK_CTRL_PORT:-18080} # bridge address of the zrok controller
      ZROK_FRONTEND_SCHEME: http
      ZROK_FRONTEND_PORT: ${ZROK_FRONTEND_PORT:-8080}
      ZITI_CTRL_ADVERTISED_PORT: ${ZITI_CTRL_ADVERTISED_PORT:-1280}
      ZITI_PWD: ${ZITI_PWD} # ziti controller admin password

#volumes:
  #ziti_home:  # this will not be used if you switch from named volume to bind mount volume
  #zrok_ctrl:
  #zrok_frontend:

# define a custom network so that we can also define DNS aliases
networks:
  servnet:
    driver: bridge
    external: true
  #zrok-instance:
    #driver: bridge

I moved the open port from the ziti-quickstart container to my Traefik container. With these changes, Zrok works fine, just like before. However, the same issue is occurring; no TLS for Ziti. I know this because visiting "https://ziti.libregalaxy.org:1280/edge/client/v1/authenticate?method=cert" returns "Warning: Potential Security Risk Ahead" in Firefox. Additionally, I used the command openssl s_client -connect ziti.libregalaxy.org:1280 -showcerts, where it returned Verify return code: 19 (self-signed certificate in certificate chain).




Yes, all Zrok containers using the named volume seem to have permission errors on my server. For example, the Zrok access file from https://docs.zrok.io/zrok-private-access/compose.yml, logs this (docker logs zrok-access_zrok-enable_1):

WARNING: STATE_DIRECTORY is undefined. Using HOME=/mnt
DEBUG: zrok state directory is /mnt/.zrok
INFO: reading enable parameters from environment variables
[ERROR]: unable to save config (mkdir /mnt/.zrok: permission denied)

I don't understand why because the permission changes without issue.
docker logs zrok-access_zrok-init_1 :arrow_right: changed ownership of '/mnt/.zrok' to 2171:2171

1 Like

You're welcome. Thanks for providing all the necessary information and nicely formatting everything. That's a big help. :slightly_smiling_face:

Your ziti-quickstart's TLS passthrough is working. Your zrok environments will trust Ziti's CA when "enabled." You may add a trusted TLS certificate for the Ziti console for Traefik to select by inspecting the request's servername property (SNI).

The idea is an additional Traefik router and service (e.g., https://ziti-quickstart:1280) for the console on the websecure entrypoint.

In your compose file, you must stop publishing ports on the ziti-quickstart container to avoid competition between Ziti and Traefik binding/listening on those ports.


Regarding the permission errors, did you modify the private access example? I couldn't trigger the same problem on a Linux server. In case you have a hardened system or strict security settings, like SELinux, you may need to add the :z option to the mount.

Upon further research, I misunderstood how TLS passthrough works. My original concern was implementing TLS (w/ Let's Encrypt) termination for the Ziti container, where Traefik would handle the encryption between the client and the proxy. This is because all of my other services are set up this way. I watched a YouTube video explaining TLS passthrough; is the idea for the Ziti container to handle the TLS (as in inside the service, not just the labels for Traefik)? I'd appreciate clarification on why it should be set up that way versus my original concern.

Upon further inspection of the browser error in Firefox, I see "Error code: SEC_ERROR_UNKNOWN_ISSUER" under the advanced section. When I viewed the certificate, I noticed NetFoundry. The Zrok project is a part of NetFoundry, which makes sense why I'm seeing that. However, I need clarification as to why they are showing up. Shouldn't the only certificate I see be that of my server domain? It seems that the mismatch is causing the browser error. I have a rudimentary understanding of certificates. I initially thought having someone else's certificate would compromise the TLS security. However, the purpose of the NetFoundry certificate is clearly a part of something I have yet to fully grasp.

Is my setup secure as it stands right now since the NetFoundry certificate is being used? The URL I'm referring to is https://ziti.libregalaxy.org:1280/edge/client/v1/authenticate?method=cert


You may add a trusted TLS certificate for the Ziti console for Traefik to select by inspecting the request's servername property (SNI).

I followed your instructions by setting up labels on the ziti-quickstart container; it is as follows:

  ziti-quickstart:
    image: ${ZITI_CLI_IMAGE:-docker.io/openziti/ziti-cli}:${ZITI_CLI_TAG:-latest}
    restart: unless-stopped
    #ports:
      #- ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_ADVERTISED_PORT:-1280}:${ZITI_CTRL_ADVERTISED_PORT:-1280}
      #- ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
    expose:
      - ${ZITI_CTRL_ADVERTISED_PORT:-1280}
      - ${ZITI_ROUTER_PORT:-3022}
    networks:
      servnet:
        aliases:
          - ziti.${ZROK_DNS_ZONE}
      #zrok-instance:
        # this allows other containers to use the same external DNS name to reach the quickstart container from within the
        # Docker network that clients outside the Docker network use to reach the quickstart container via port forwarding
        #aliases:
          #- ziti.${ZROK_DNS_ZONE}
    labels:
      - "traefik.enable=true"
      - "trafik.docker.network=servnet"
      # Ziti
      - "traefik.http.routers.ziti.rule=Host(`ziti-quickstart:1280`)"
      - "traefik.http.routers.ziti.entrypoints=websecure"
      - "traefik.http.routers.ziti.tls.certresolver=production"
      - "traefik.http.services.ziti.loadbalancer.server.port=1280"
    entrypoint:
      - bash
      - -euc
      - |
        ZITI_CMD+=" --ctrl-address ziti.${ZROK_DNS_ZONE}"\
        " --ctrl-port ${ZITI_CTRL_ADVERTISED_PORT:-1280}"\
        " --router-address ziti.${ZROK_DNS_ZONE}"\
        " --router-port ${ZITI_ROUTER_PORT:-3022}"\
        " --password ${ZITI_PWD:-admin}"
        echo "DEBUG: run command is: ziti $${@} $${ZITI_CMD}"
        exec ziti "$${@}" $${ZITI_CMD}
    command: -- edge quickstart --home /home/ziggy/quickstart
    user: ${ZIGGY_UID:-1000}
    environment:
      HOME: /home/ziggy
      PFXLOG_NO_JSON: "${PFXLOG_NO_JSON:-true}"
      ZITI_ROUTER_NAME: ${ZITI_ROUTER_NAME:-quickstart-router}
    volumes:
      # store the quickstart state in a named volume "ziti_home" or store the quickstart state on the Docker host in a
      # directory, ZITI_HOME 
      #- ${ZITI_HOME:-ziti_home}:/home/ziggy
      - ./ziti_home:/home/ziggy
    depends_on:
      ziti-quickstart-init:
        condition: service_completed_successfully
    healthcheck:
      test:
        - CMD
        - ziti
        - agent
        - stats
      interval: 3s
      timeout: 3s
      retries: 5
      start_period: 30s

It does not seem to change anything besides producing an error in Traefilk's debug log:

2024-12-19T05:15:03Z DBG github.com/traefik/traefik/v3/pkg/tcp/proxy.go:41 > Handling TCP connection address=ziti-quickstart:1280 remoteAddr=[my ip]
2024-12-19T05:15:03Z DBG github.com/traefik/traefik/v3/pkg/tcp/proxy.go:113 > Error while setting TCP connection deadline error="set tcp 172.18.0.12:1280: use of closed network connection"



I did not modify the private access example besides adding labels:

services:
  zrok-init:
    image: busybox
    # matches uid:gid of "ziggy" in zrok container image
    command: chown -Rc 2171:2171 /mnt/.zrok
    user: root
    volumes:
      - zrok_env:/mnt/.zrok:z

  # enable zrok environment
  zrok-enable:
    image: ${ZROK_CONTAINER_IMAGE:-docker.io/openziti/zrok}
    networks:
      servnet:
    depends_on:
      zrok-init:
        condition: service_completed_successfully
    entrypoint: zrok-enable.bash
    volumes:
      #- ./mnt:/mnt
      - zrok_env:/mnt:z
    environment:
      HOME: /mnt
      ZROK_ENABLE_TOKEN:
      ZROK_API_ENDPOINT:
      ZROK_ENVIRONMENT_NAME: 

  zrok-access-ai:
    image: ${ZROK_CONTAINER_IMAGE:-docker.io/openziti/zrok}
    restart: unless-stopped
    command: access private --headless --bind 0.0.0.0:7860 aiwebui
    networks:
      servnet:
    depends_on:
      zrok-enable:
        condition: service_completed_successfully
    #ports:
      #- 9191:9191  # expose the zrok private access proxy to the Docker host
    volumes:
      #- ./mnt:/mnt
      - zrok_env:/mnt:z
    labels:
      - "traefik.enable=true"
      - "traefik.http.services.aitest.loadbalancer.server.port=7860"
      - "traefik.docker.network=servnet"
      - "traefik.http.routers.aitest.rule=Host(`aitest.libregalaxy.org`)"
      - "traefik.http.routers.aitest.entrypoints=websecure"
      - "traefik.http.routers.aitest.tls.certresolver=production"
    environment:
      HOME: /mnt
      PFXLOG_NO_JSON: "true"
# Volumes lead to perm errors for some reason
volumes:
  zrok_env:

networks:
  servnet:
    driver: bridge
    external: true

I currently have SELinux disable (getenforce :arrow_right: Disabled). Adding that option doesn't change the behavior.

Creating network "zrok-access_default" with the default driver
Creating volume "zrok-access_zrok_env" with default driver
Creating zrok-access_zrok-init_1 ... done
Creating zrok-access_zrok-enable_1 ... done

ERROR: for zrok-access-ai  Container "fac4759acc95" exited with code 1.
ERROR: Encountered errors while bringing up the project.

docker logs zrok-access_zrok-init_1 

changed ownership of '/mnt/.zrok' to 2171:2171

docker logs zrok-access_zrok-enable_1 

WARNING: STATE_DIRECTORY is undefined. Using HOME=/mnt
DEBUG: zrok state directory is /mnt/.zrok
INFO: reading enable parameters from environment variables
[ERROR]: unable to save config (mkdir /mnt/.zrok: permission denied)

You're on the right track. I'll briefly expand on Ziti TLS. It's part of the topic of public key infrastructure within cryptography. One way to summarize is that Ziti brings its own private PKI for its APIs, and Firefox uses public PKIs from known issuers like Let's Encrypt, and Firefox doesn't need to recognize the issuers in Ziti's PKI.

To risk a metaphor, Firefox recognizes certificates from public authorities like local law enforcement recognizes state-issued driver's licenses, and Ziti recognizes its private authority's certificates like military police recognize training certificates for driving special equipment on mission. Conversely, a permit from the military base for driving giant forklifts doesn't authorize me to operate a regular pickup truck on public roads, so each license type must be appropriate for the task.

Ziti's APIs must terminate TLS because they employ mutual TLS (mTLS), meaning that all Ziti components mutually verify each other. Ziti APIs' TLS servers must be published directly (no proxy) or with passthrough TLS if using a proxy like Traefik so they can complete the TLS handshake without intermediaries.

You need to configure a publicly-trusted certificate for Ziti's console from an issuer that Firefox recognizes. Traefik will terminate TLS for Ziti's web console and manage the certificate on Ziti's behalf.

I ran out of time this sitting, but I think it will be helpful to you and others to add a Traefik example in the Docker instance guide and I've started working on that.


I think you're running this private access compose project example from the Docker private sharing guide.

Will you run these troubleshooting commands in your compose project, please, and provide the output?

docker compose run --rm --entrypoint=bash zrok-enable -euxc 'id; pwd; declare -A _dirs=([pwd]=$(pwd) [home]=${HOME} [mnt]=/mnt); for k in ${!_dirs[@]}; do echo "$k: ${_dirs[$k]}"; ls -lah ${_dirs[$k]}; done'

From the error, I can see the zrok-enable command fails to save the environment configuration when it runs zrok config set apiEndpoint, which is a fatal error.

I'm sure the permissions problem is solvable, but BTW there's also another approach if you don't need the zrok environment to be entirely managed by Docker. It involves enabling the zrok environment on the Docker host then mounting it on a zrok container and is described here.

I highly appreciate your detailed response! If I understand correctly, Ziti (ziti-quickstart) and the clients that connect to it use mTLS, where two encryption key pairs are exchanged instead of the single pair with traditional TLS. Additionally, these certificates are unknown to browsers like Firefox because they are only designed for the components (i.e., servers and clients) in my zero trust overlay network, not the more expansive, public internet.

I've configured my Zrok/Ziti console (zrok-controller) and Zrok frontend (zrok-frontend) to securely communicate with the internet via Traefik, which serves Let's Encrypt certificates. Since I have TLS passthrough enabled for Ziti (ziti-quickstart), Traefik will not issue a certificate but allow both components to handle the exchange themselves. Therefore, my Zrok instance meets security expectations.

I have a few questions:

  • With my current configuration, Zrok shares and accesses work perfectly. If mTLS fails, will I always see an error when connecting the tunnel share/access points, such as with zrok access?
  • Since mTLS certificates are private, why does the certificate list NetFoundry details when I view it on Firefox? Wouldn't it display details about my server instead? I assume this is because I need to configure the private certificate or because of the browser confusion discussed earlier. How would I go about modifying it? I like to customize, haha.

I ran the troubleshooting command from my Zrok Private Access container. The following is the output:

docker compose run --rm --entrypoint=bash zrok-enable -euxc 'id; pwd; declare -A _dirs=([pwd]=$(pwd) [home]=${HOME} [mnt]=/mnt); for k in ${!_dirs[@]}; do echo "$k: ${_dirs[$k]}"; ls -lah ${_dirs[$k]}; done'

WARN[0000] mount of type `volume` should not define `bind` option 
[+] Creating 1/1
 ✔ Container zrok-access_zrok-init_1  Recreated                                                                                                                                          0.1s 
[+] Running 1/1
 ✔ Container zrok-access-zrok-init-1  Started                                                                                                                                            0.3s 
WARN[0000] mount of type `volume` should not define `bind` option 
+ id
uid=2171(ziggy) gid=2171(ziggy) groups=2171(ziggy)
+ pwd
/home/ziggy
++ pwd
+ _dirs=(['pwd']='/home/ziggy' ['home']='/mnt' ['mnt']='/mnt')
+ declare -A _dirs
+ for k in ${!_dirs[@]}
+ echo 'pwd: /home/ziggy'
pwd: /home/ziggy
+ ls -lah /home/ziggy
total 4.0K
drwxrwxr-x 1 ziggy ziggy  14 Oct  2 13:11 .
drwxr-xr-x 1 root  root   10 Oct  2 13:11 ..
-rw-r--r-- 1 root  root  829 Oct  2 13:10 .bashrc
+ for k in ${!_dirs[@]}
+ echo 'home: /mnt'
home: /mnt
+ ls -lah /mnt
total 0
drwxr-xr-x 1 root root 0 Aug  9  2021 .
drwxr-xr-x 1 root root 0 Dec 20 08:14 ..
+ for k in ${!_dirs[@]}
+ echo 'mnt: /mnt'
mnt: /mnt
+ ls -lah /mnt
total 0
drwxr-xr-x 1 root root 0 Aug  9  2021 .
drwxr-xr-x 1 root root 0 Dec 20 08:14 ..
1 Like

That's an accurate paraphrase. Nicely done.

Yes. If, for example, someone launches a man-in-the-middle attack, Ziti's mTLS will reject the unauthorized request, and you'll get errors from zrok.

zrok (stylized with a lowercase z) runs on Ziti, and Ziti uses mutual TLS. Ziti is a platform, zrok's foundation is the Ziti platform, and zrok uses Ziti's APIs and SDKs to do security and networking stuff that would otherwise be a lot more difficult. You can do the same and accelerate building custom apps with built-in Ziti superpowers, possibly using zrok as an example. That's what Ziti is all about!

Yes, your zrok instance's Ziti PKI is unique to you. You can set the x509 distinguished name (DN) of your authority and the identities that it issues to anything you wish. It's not exposed as a convenient input, but it's not difficult. Here's a parallel forum topic where I gave some hints about this.

For example, replace all the values like dn* with whatever you want to appear in the server certificate's DN.

docker compose exec ziti-quickstart \
ziti pki create server \
--pki-root "/ziti-controller/pki" \
--server-file "server" \
--ca-name "intermediate-ca" \
--server-name "dnCN" \
--pki-country "dnC" \
--pki-province "dnS" \
--pki-locality "dnL" \
--pki-organization "dnO" \
--pki-organizational-unit "dnOU" \
--dns "localhost,ziti.libregalaxy.org" \
--ip "127.0.0.1,::1" \
--allow-overwrite

If it doesn't change immediately you need to restart the container. This command prints the current server certificate's DN:

openssl s_client -connect ziti.libregalaxy.org:1280 <>/dev/null \
|& openssl x509 -noout -subject

e.g.,

subject=C = US, L = Charlotte, O = NetFoundry, OU = ADV-DEV, CN = server
1 Like