Tunneling
    Zero Trust

    Deploy a Self-Hosted zrok Tunnel Server

    Expose local services, share files, and build zero-trust access workflows — without relying on a third-party cloud. ngrok-style sharing with full infrastructure control.

    At a Glance

    Projectzrok by NetFoundry (built on OpenZiti)
    LicenseApache 2.0
    Recommended PlanRamNode KVM2 (2 GB RAM) or higher
    OSUbuntu 22.04 LTS
    Controller Port18080 (proxied to 443 via Nginx)
    DatabasePostgreSQL
    DNS RequirementWildcard A record (*.zrok.example.com)
    Estimated Setup Time25–35 minutes

    What Is zrok?

    zrok (pronounced "zee-rock") is an open-source tunneling and sharing platform built on OpenZiti. It supports multiple share types:

    • Public shares — expose a local HTTP service at a publicly accessible URL
    • Private shares — share between two authenticated clients without public exposure
    • Reserved shares — persistent shares with a stable subdomain that survive restarts

    Architecture Overview

    Architecture
    [zrok client] ----> [zrok controller (your VPS)] ----> [OpenZiti network]
                               |
                        [PostgreSQL DB]
                        [zrok web UI]

    The controller handles account registration, token issuance, share management, and the web console. OpenZiti runs as an embedded component — no separate installation needed.

    Prerequisites

    • A RamNode VPS running Ubuntu 22.04 LTS with at least 1 GB RAM and 20 GB disk (2 GB KVM plan works well)
    • A domain pointed at your VPS IP (e.g., zrok.example.com)
    • Wildcard DNS configured: *.zrok.example.com → your VPS IP
    • A non-root sudo user on the server
    • Ports 80, 443, and 18080 open in your firewall
    1

    Point DNS

    Configure two DNS records at your registrar:

    TypeNameValue
    Azrok.example.comYour VPS public IP
    A*.zrok.example.comYour VPS public IP

    The wildcard record is required — zrok assigns a unique subdomain to every public share. Allow a few minutes for DNS propagation.

    2

    Install Dependencies

    Update system
    sudo apt update && sudo apt upgrade -y
    Install PostgreSQL, Nginx, Certbot
    sudo apt install -y postgresql postgresql-contrib certbot python3-certbot-nginx nginx
    Start PostgreSQL
    sudo systemctl enable --now postgresql
    3

    Create the PostgreSQL Database

    Open psql
    sudo -u postgres psql
    Create database and user
    CREATE USER zrok WITH PASSWORD 'strongpassword';
    CREATE DATABASE zrok OWNER zrok;
    GRANT ALL PRIVILEGES ON DATABASE zrok TO zrok;
    \q
    4

    Download and Install zrok

    Download and install
    cd /tmp
    curl -LO https://github.com/openziti/zrok/releases/latest/download/zrok_linux_amd64.tar.gz
    tar -xzf zrok_linux_amd64.tar.gz
    sudo mv zrok /usr/local/bin/
    sudo chmod +x /usr/local/bin/zrok
    Verify installation
    zrok version
    5

    Configure the zrok Controller

    Create system user and config directory
    sudo useradd --system --no-create-home --shell /bin/false zrok
    sudo mkdir -p /etc/zrok-controller
    sudo chown zrok:zrok /etc/zrok-controller
    Create config file
    sudo nano /etc/zrok-controller/config.yaml
    /etc/zrok-controller/config.yaml
    v: 4
    
    admin:
      secrets:
        - "PASTE_RANDOM_HEX_HERE"
    
    endpoint:
      host: 0.0.0.0
      port: 18080
    
    store:
      path: "postgres://zrok:strongpassword@localhost/zrok?sslmode=disable"
    
    ziti:
      api_endpoint: "https://zrok.example.com:1280"
    
    domain:
      base_url: "https://zrok.example.com"
    
    email:
      host: "localhost"
      port: 25
      from: "zrok@example.com"
      tls: false

    Generate the admin secret with: openssl rand -hex 32

    Set ownership and permissions
    sudo chown zrok:zrok /etc/zrok-controller/config.yaml
    sudo chmod 600 /etc/zrok-controller/config.yaml
    6

    Initialize the Controller

    Run database migration
    sudo -u zrok zrok admin bootstrap /etc/zrok-controller/config.yaml

    This creates the database tables and generates the embedded OpenZiti configuration. Safe to re-run if needed.

    7

    Create the systemd Service

    Create service file
    sudo nano /etc/systemd/system/zrok-controller.service
    zrok-controller.service
    [Unit]
    Description=zrok Controller
    After=network.target postgresql.service
    Wants=postgresql.service
    
    [Service]
    Type=simple
    User=zrok
    Group=zrok
    ExecStart=/usr/local/bin/zrok controller /etc/zrok-controller/config.yaml
    Restart=on-failure
    RestartSec=5
    StandardOutput=journal
    StandardError=journal
    
    [Install]
    WantedBy=multi-user.target
    Enable and start
    sudo systemctl daemon-reload
    sudo systemctl enable --now zrok-controller
    Check status
    sudo systemctl status zrok-controller

    If it fails, check the journal:

    View logs
    sudo journalctl -u zrok-controller -f
    8

    Configure Nginx and TLS

    Obtain a wildcard certificate. Use a DNS challenge — the certbot manual method or a DNS plugin for your provider:

    Request wildcard certificate
    sudo certbot certonly --manual --preferred-challenges dns \
      -d zrok.example.com \
      -d "*.zrok.example.com"

    Create the Nginx site config:

    /etc/nginx/sites-available/zrok
    # Redirect HTTP to HTTPS
    server {
        listen 80;
        server_name zrok.example.com *.zrok.example.com;
        return 301 https://$host$request_uri;
    }
    
    # Main zrok controller proxy
    server {
        listen 443 ssl;
        server_name zrok.example.com;
    
        ssl_certificate /etc/letsencrypt/live/zrok.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/zrok.example.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
    
        location / {
            proxy_pass http://127.0.0.1:18080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    }
    
    # Wildcard proxy for public shares
    server {
        listen 443 ssl;
        server_name *.zrok.example.com;
    
        ssl_certificate /etc/letsencrypt/live/zrok.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/zrok.example.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
    
        location / {
            proxy_pass http://127.0.0.1:18080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    }

    Important: The wildcard server block routes every *.zrok.example.com request to the controller, which identifies the correct share via the hostname.

    Enable site and restart
    sudo ln -s /etc/nginx/sites-available/zrok /etc/nginx/sites-enabled/
    sudo nginx -t
    sudo systemctl restart nginx
    9

    Create an Admin Account

    Create admin account
    zrok admin create account /etc/zrok-controller/config.yaml \
      admin@example.com \
      securepassword

    Replace the email and password with real values. Save the account token from the output. Log in at https://zrok.example.com to confirm.

    10

    Open Firewall Ports

    UFW rules
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow 18080/tcp
    sudo ufw reload

    Port 18080 is used directly by the zrok CLI for WebSocket connections during active shares. Nginx handles browser traffic on 443.

    11

    Install the zrok Client Locally

    On the machine where you want to run shares, download the zrok binary:

    Download zrok client (Linux)
    curl -LO https://github.com/openziti/zrok/releases/latest/download/zrok_linux_amd64.tar.gz
    tar -xzf zrok_linux_amd64.tar.gz
    sudo mv zrok /usr/local/bin/

    Point the client at your self-hosted controller:

    Configure client
    zrok config set apiEndpoint https://zrok.example.com
    Enable environment
    zrok enable YOUR_ACCOUNT_TOKEN
    12

    Create Your First Share

    Start a test HTTP server and create a public share:

    Start test server
    python3 -m http.server 8080
    Create public share
    zrok share public localhost:8080

    zrok will output a URL like https://abc123def.zrok.example.com. Open it in a browser to verify. Press Ctrl+C to stop the share.

    Additional Configuration

    User Registration

    By default, new accounts require an invite token. For open self-registration, add to the controller config:

    Open registration
    registration:
      registration_token_strategy: open
    Restart controller
    sudo systemctl restart zrok-controller

    For closed environments, generate invite tokens:

    Generate invite
    zrok admin generate account /etc/zrok-controller/config.yaml user@example.com

    Certificate Renewal

    Wildcard certificates must be renewed every 90 days. Add a post-hook to auto-reload Nginx:

    Create renewal hook
    sudo nano /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh
    nginx-reload.sh
    #!/bin/bash
    systemctl reload nginx
    Make executable
    sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh
    Verify renewal timer
    sudo systemctl status certbot.timer

    Troubleshooting

    Controller service fails to start

    Check the journal with sudo journalctl -u zrok-controller -n 50. Common causes: wrong database connection string, missing admin secret, or port conflict on 18080.

    Check port conflicts
    sudo ss -tlnp | grep 18080

    Public share URLs return 502

    Confirm wildcard DNS is resolving with dig abc123.zrok.example.com. Verify Nginx is proxying the Host header unchanged.

    zrok enable fails on the client

    Run zrok config show and confirm apiEndpoint is set correctly with HTTPS. Self-signed certificates will cause client failures.

    Shares disconnect after a few minutes

    Add proxy timeouts to the Nginx location blocks:

    Add to Nginx location block
    proxy_read_timeout 3600;
    proxy_send_timeout 3600;