This guide documents how to set up a self-hosted ZeroTier controller with a private network.
- Debian/Ubuntu Linux server
- Root access
- Basic networking knowledge
- A public IP or NAT forwarding for ZeroTier port 9993/udp (optional but recommended)
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
# 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# 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 useNote: The Node ID is also used as part of the Network ID when you create a 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.
# 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 nameipRangeStart/ipRangeEnd: Your IP pool rangetarget: Your network CIDR
# Join controller to its own network
zerotier-cli join ${NWID}
# Wait for join to complete
sleep 2
# Verify
zerotier-cli listnetworks# 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.
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.v4Allowed 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
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)
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 networkbind: Listen on all interfaces (0.0.0.0), but access is restricted by allowManagementFrom
After editing, restart ZeroTier:
systemctl restart zerotier-oneBy default, DNS is disabled on ZeroTier networks. ZeroTier does not natively resolve member ho
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
# List all network members and their status
curl -s "http://localhost:9993/controller/network/${NWID}/member" \
-H "X-ZT1-AUTH: ${TOKEN}" | jq '.'# 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 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 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"]}'# View specific member information
curl -s "http://localhost:9993/controller/network/${NWID}/member/${MEMBER_ID}" \
-H "X-ZT1-AUTH: ${TOKEN}" | jq '.'# 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}'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 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/statusNote: You need to copy the API token from the controller (`/var/lib/zerotier-one/authtoken.sec
| 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 |
Download and install from https://www.zerotier.com/download/
# On client machine
zerotier-cli join <your-network-id>Or use the GUI application.
# On client machine
zerotier-cli info
# Returns: 200 info <16-char-node-id> <version> <status>Copy the client Node ID and authorize it from the controller (see Member Management section above)
# Check network status
zerotier-cli listnetworks
# Should show: OK PRIVATE and assigned IP
# Test connectivity
ping <controller-ip>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.v4Symptoms: 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-oneSymptoms: 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 IPPossible causes:
- Client not authorized - check member list and authorize
- Controller not accessible - check firewall allows 9993/udp
- Network ID incorrect - verify 16-character ID
Debug:
# On client
zerotier-cli listpeers
# Should show controller in the listIf you locked yourself out via SSH:
- Access server console (physical or via hosting provider)
- Run:
iptables -P INPUT ACCEPTto temporarily allow all - Fix firewall rules properly
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
- 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 is private (not public)
- Members must be explicitly authorized
- Consider using IP whitelisting for sensitive members
# 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# 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-oneThis 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
- Assign IPv6 addresses to server (if not already)
- 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- Configure ZeroTier for IPv6 in network settings
# 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- 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+