Skip to content

Instantly share code, notes, and snippets.

@cameroncking
Created February 16, 2026 08:09
Show Gist options
  • Select an option

  • Save cameroncking/2fcc434fe63319fc4d27e4dc0251a9c6 to your computer and use it in GitHub Desktop.

Select an option

Save cameroncking/2fcc434fe63319fc4d27e4dc0251a9c6 to your computer and use it in GitHub Desktop.

ZeroTier Self-Hosted Controller Setup Guide

This guide documents how to set up a self-hosted ZeroTier controller with a private network.

Prerequisites

  • Debian/Ubuntu Linux server
  • Root access
  • Basic networking knowledge
  • A public IP or NAT forwarding for ZeroTier port 9993/udp (optional but recommended)

Network Planning

Before starting, decide on:

  • Network name: Something descriptive (e.g., "My ZeroTier Network")
  • IP range: Private subnet (e.g., 10.0.0.0/24, 192.168.100.0/24, 10.234.0.0/24)
  • Controller static IP: First IP in your range (e.g., 10.234.0.1)

Example configuration used in this guide:

  • Network range: 10.234.0.0/24
  • Controller IP: 10.234.0.1
  • IP pool: 10.234.0.1 - 10.234.0.254

Installation

1. Install ZeroTier

# Update system and install dependencies
apt-get update
apt-get install -y curl jq iptables iptables-persistent

# Install ZeroTier
curl -s https://install.zerotier.com | bash

# Start and enable service
systemctl start zerotier-one
systemctl enable zerotier-one

2. Get Auth Token and Node ID

# Get the API auth token
export TOKEN=$(cat /var/lib/zerotier-one/authtoken.secret)

# Get the Node ID
export NODEID=$(zerotier-cli info | cut -d " " -f 3)
echo "Node ID: $NODEID"
# Note: Save this 10-character ID for later use

Note: The Node ID is also used as part of the Network ID when you create a network.

3. Create Network

# Create network using controller's Node ID
# The network ID will be: ${NODEID}______ (10 chars + 6 underscores)
curl -X POST "http://localhost:9993/controller/network/${NODEID}______" \
  -H "X-ZT1-AUTH: ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{}'

# Returns a 16-character network ID
# Example: a87438d919de12e9
export NWID="<16-char-network-id-from-response>"

Record your Network ID - clients will need this to join.

4. Configure Network

# Configure network with your chosen IP range
curl -X POST "http://localhost:9993/controller/network/${NWID}" \
  -H "X-ZT1-AUTH: ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My ZeroTier Network",
    "ipAssignmentPools": [{"ipRangeStart": "10.234.0.1", "ipRangeEnd": "10.234.0.254"}],
    "routes": [{"target": "10.234.0.0/24", "via": null}],
    "v4AssignMode": "zt",
    "private": true
  }'

Customize the values:

  • name: Your network name
  • ipRangeStart/ipRangeEnd: Your IP pool range
  • target: Your network CIDR

5. Join Controller to Network

# Join controller to its own network
zerotier-cli join ${NWID}

# Wait for join to complete
sleep 2

# Verify
zerotier-cli listnetworks

6. Authorize Controller Node

# Authorize controller with a static IP (first IP in your range)
curl -X POST "http://localhost:9993/controller/network/${NWID}/member/${NODEID}" \
  -H "X-ZT1-AUTH: ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "authorized": true,
    "ipAssignments": ["10.234.0.1"]
  }'

Note: The controller will show lastSeen: null for its own entry - this is normal behavior.

Firewall Configuration

iptables Rules

WARNING: Run these commands in the exact order shown. If you're connected via SSH, setting def

# Flush existing rules
iptables -F
iptables -X

# 1. Allow loopback
iptables -A INPUT -i lo -j ACCEPT

# 2. Allow established/related connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# 3. Allow ZeroTier interface traffic (for client-to-client communication)
iptables -I INPUT 1 -i zt+ -j ACCEPT

# 4. Allow ICMP from your ZeroTier network
iptables -I INPUT 2 -p icmp -s 10.234.0.0/24 -j ACCEPT

# 5. Allow SSH (port 22/tcp) - CRITICAL if connected via SSH!
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# 6. Allow ZeroTier UDP port for peer connections
iptables -A INPUT -p udp --dport 9993 -j ACCEPT

# 7. Default deny policies (MUST BE LAST!)
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Save rules to persist across reboots
mkdir -p /etc/iptables
iptables-save > /etc/iptables/rules.v4

What This Does

Allowed from internet (0.0.0.0/0):

  • Port 22/tcp (SSH)
  • Port 9993/udp (ZeroTier peer connections)

Allowed only from ZeroTier network:

  • Port 9993/tcp (API) - see local.conf configuration below
  • ICMP/ping
  • All traffic on ZeroTier interfaces (zt+)

Blocked:

  • Everything else

Resulting Firewall State

Chain INPUT (policy DROP)
1. ACCEPT     all  --  anywhere             anywhere            (zt+ interface)
2. ACCEPT     icmp --  10.234.0.0/24        anywhere
3. ACCEPT     all  --  anywhere             anywhere            (loopback/established)
4. ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:22
5. ACCEPT     udp  --  anywhere             anywhere            udp dpt:9993

Chain FORWARD (policy DROP)
Chain OUTPUT (policy ACCEPT)

API Configuration

Create /var/lib/zerotier-one/local.conf:

{
  "settings": {
    "allowManagementFrom": ["127.0.0.1/32", "10.234.0.0/24"],
    "bind": ["0.0.0.0"]
  }
}

Settings explained:

  • allowManagementFrom: Only allow API access from localhost and ZeroTier network
  • bind: Listen on all interfaces (0.0.0.0), but access is restricted by allowManagementFrom

After editing, restart ZeroTier:

systemctl restart zerotier-one

DNS Configuration

Current Status

By default, DNS is disabled on ZeroTier networks. ZeroTier does not natively resolve member ho

Options for DNS

Option 1: Public DNS (no internal resolution)

# Use Quad9 or other public DNS servers
curl -X POST "http://localhost:9993/controller/network/${NWID}" \
  -H "X-ZT1-AUTH: ${TOKEN}" \
  -d '{"dns": {"servers": ["9.9.9.9", "149.112.112.112"]}}'

Option 2: Internal DNS with ZeroNSD (requires Docker or Rust build)

  • ZeroTier's official DNS server
  • Auto-resolves member names
  • Requires building from source or Docker

Option 3: Manual dnsmasq

  • Install and configure dnsmasq on the controller
  • Manually add host entries for each member
  • No automatic updates when members join

Note: ZeroTier's built-in DNS only pushes upstream DNS server IPs to clients. It does not act

Member Management

List All Members

# List all network members and their status
curl -s "http://localhost:9993/controller/network/${NWID}/member" \
  -H "X-ZT1-AUTH: ${TOKEN}" | jq '.'

Check Pending Members

# Show members waiting for authorization
curl -s "http://localhost:9993/controller/network/${NWID}/member" \
  -H "X-ZT1-AUTH: ${TOKEN}" | jq '.[] | select(.authorized != true) | .id'

Authorize a New Member

# Authorize a member (auto-assigns IP from pool)
export MEMBER_ID="<16-char-node-id>"
curl -X POST "http://localhost:9993/controller/network/${NWID}/member/${MEMBER_ID}" \
  -H "X-ZT1-AUTH: ${TOKEN}" \
  -d '{"authorized": true}'

Authorize with Static IP

# Authorize with specific IP address
curl -X POST "http://localhost:9993/controller/network/${NWID}/member/${MEMBER_ID}" \
  -H "X-ZT1-AUTH: ${TOKEN}" \
  -d '{"authorized": true, "ipAssignments": ["10.234.0.50"]}'

Get Member Details

# View specific member information
curl -s "http://localhost:9993/controller/network/${NWID}/member/${MEMBER_ID}" \
  -H "X-ZT1-AUTH: ${TOKEN}" | jq '.'

Deauthorize a Member

# Revoke network access (doesn't delete member)
curl -X POST "http://localhost:9993/controller/network/${NWID}/member/${MEMBER_ID}" \
  -H "X-ZT1-AUTH: ${TOKEN}" \
  -d '{"authorized": false}'

API Access

From Controller (localhost)

export TOKEN=$(cat /var/lib/zerotier-one/authtoken.secret)

# Get node status
curl -H "X-ZT1-AUTH: ${TOKEN}" http://127.0.0.1:9993/status

# List networks
curl -H "X-ZT1-AUTH: ${TOKEN}" http://127.0.0.1:9993/controller/network

# Get network details
curl -H "X-ZT1-AUTH: ${TOKEN}" http://127.0.0.1:9993/controller/network/${NWID}

From ZeroTier Clients

From any authorized client on the network:

export TOKEN="<your-api-token>"

# Access API via controller's ZeroTier IP
curl -H "X-ZT1-AUTH: ${TOKEN}" http://10.234.0.1:9993/status

Note: You need to copy the API token from the controller (`/var/lib/zerotier-one/authtoken.sec

API Endpoints

Endpoint Method Description
/status GET Node status
/controller/network GET List all networks
/controller/network/<id> GET/POST Network details/update
/controller/network/<id>/member GET List all members
/controller/network/<id>/member/<member> GET/POST Member details/update
/controller/network/<id>/member/<member> DELETE Delete member

Client Connection

Install ZeroTier Client

Download and install from https://www.zerotier.com/download/

Join Network

# On client machine
zerotier-cli join <your-network-id>

Or use the GUI application.

Get Client Node ID

# On client machine
zerotier-cli info
# Returns: 200 info <16-char-node-id> <version> <status>

Authorize Client

Copy the client Node ID and authorize it from the controller (see Member Management section above)

Verify Connection

# Check network status
zerotier-cli listnetworks

# Should show: OK PRIVATE and assigned IP

# Test connectivity
ping <controller-ip>

Troubleshooting

Can't ping between clients

Symptoms: Clients can join but can't ping each other

Solution: Ensure firewall allows ZeroTier interface traffic:

iptables -I INPUT 1 -i zt+ -j ACCEPT
iptables-save > /etc/iptables/rules.v4

API not accessible from ZeroTier network

Symptoms: Can ping controller but API returns connection refused

Solution: Check local.conf has your ZeroTier network in allowManagementFrom:

cat /var/lib/zerotier-one/local.conf
# Should include your network range, e.g., "10.234.0.0/24"

# Restart if changed
systemctl restart zerotier-one

Controller shows as "offline" in member list

Symptoms: Controller's own entry shows lastSeen: null, lastOnline: null

This is NORMAL! The controller doesn't track its own presence. Verify with:

zerotier-cli status
# Should show: 200 info <nodeid> <version> ONLINE

zerotier-cli listnetworks
# Should show: OK PRIVATE with your IP

Client stuck at "REQUESTING_CONFIGURATION"

Possible causes:

  1. Client not authorized - check member list and authorize
  2. Controller not accessible - check firewall allows 9993/udp
  3. Network ID incorrect - verify 16-character ID

Debug:

# On client
zerotier-cli listpeers
# Should show controller in the list

Lost access after firewall changes

If you locked yourself out via SSH:

  • Access server console (physical or via hosting provider)
  • Run: iptables -P INPUT ACCEPT to temporarily allow all
  • Fix firewall rules properly

Security Notes

Important Files

Protect these files:

  • /var/lib/zerotier-one/authtoken.secret - API authentication token
  • /var/lib/zerotier-one/identity.secret - Node private key
  • /var/lib/zerotier-one/controller.d/ - Network configurations

API Security

  • Token is root-readable only by default
  • API restricted to localhost and ZeroTier network via local.conf
  • Port 9993/tcp not exposed to internet (only 9993/udp for peer connections)

Network Security

  • Network is private (not public)
  • Members must be explicitly authorized
  • Consider using IP whitelisting for sensitive members

Backup

What to Backup

# Create backup directory
mkdir -p ~/zerotier-backup

# Copy critical files
cp /var/lib/zerotier-one/authtoken.secret ~/zerotier-backup/
cp /var/lib/zerotier-one/identity.secret ~/zerotier-backup/
cp /var/lib/zerotier-one/local.conf ~/zerotier-backup/
cp -r /var/lib/zerotier-one/controller.d/ ~/zerotier-backup/ 2>/dev/null || true
cp /etc/iptables/rules.v4 ~/zerotier-backup/

# Also save these values:
echo "Node ID: $(zerotier-cli info | cut -d ' ' -f 3)" > ~/zerotier-backup/node-info.txt
echo "Network ID: <your-network-id>" >> ~/zerotier-backup/node-info.txt

Restore

# Stop ZeroTier
systemctl stop zerotier-one

# Restore files
cp ~/zerotier-backup/authtoken.secret /var/lib/zerotier-one/
cp ~/zerotier-backup/identity.secret /var/lib/zerotier-one/
cp ~/zerotier-backup/local.conf /var/lib/zerotier-one/
cp -r ~/zerotier-backup/controller.d/ /var/lib/zerotier-one/ 2>/dev/null || true
cp ~/zerotier-backup/rules.v4 /etc/iptables/

# Restore permissions
chown -R zerotier-one:zerotier-one /var/lib/zerotier-one/
chmod 600 /var/lib/zerotier-one/*.secret

# Restore firewall
iptables-restore < /etc/iptables/rules.v4

# Start ZeroTier
systemctl start zerotier-one

IPv6 Considerations

Current Setup

This guide configures IPv4 only. The server may have link-local IPv6 addresses (fe80::), but:

  • No public/global IPv6 assigned
  • ZeroTier network configured for IPv4 only
  • No ip6tables rules configured

To Add IPv6 Support

  1. Assign IPv6 addresses to server (if not already)
  2. Add IPv6 firewall rules:
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
ip6tables -A INPUT -p udp --dport 9993 -j ACCEPT
ip6tables -P INPUT DROP
ip6tables-save > /etc/iptables/rules.v6
  1. Configure ZeroTier for IPv6 in network settings

Quick Reference

Essential Commands

# Check status
zerotier-cli status
zerotier-cli listnetworks
zerotier-cli listpeers

# View firewall
iptables -L -n --line-numbers

# Get token
export TOKEN=$(cat /var/lib/zerotier-one/authtoken.secret)

# Restart ZeroTier
systemctl restart zerotier-one

# View logs
journalctl -u zerotier-one -f

# Save firewall
iptables-save > /etc/iptables/rules.v4

File Locations

  • ZeroTier config: /var/lib/zerotier-one/
  • Local config: /var/lib/zerotier-one/local.conf
  • API token: /var/lib/zerotier-one/authtoken.secret
  • Identity: /var/lib/zerotier-one/identity.secret
  • Firewall rules: /etc/iptables/rules.v4
  • Systemd service: /etc/systemd/system/zerotier-one.service

Summary of Values to Customize:

  • Network range (e.g., 10.234.0.0/24)
  • Controller static IP (e.g., 10.234.0.1)
  • Network name
  • Your specific Network ID (16 characters)
  • Your specific Node ID (10 characters)
  • Any additional firewall ports you need

Version: ZeroTier 1.16+

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment