Reverse Tunnel

Securely access private services behind NAT/firewall without exposing them to the public internet. Perfect for accessing databases, internal APIs, and IoT devices on private networks from anywhere.


Overview

A reverse tunnel allows you to access services on a private network (behind NAT or firewall) without opening inbound firewall ports or exposing services directly to the internet. Unlike traditional tunnels that expose local services publicly, reverse tunnels create a secure bridge that only authorized clients can use.

Perfect for:

  • Private Databases: Access databases on home networks or private clouds without opening firewall ports
  • Internal APIs: Reach internal services without VPN complexity
  • IoT Devices: Manage devices behind NAT (cameras, sensors, home automation)
  • Remote Administration: Access development machines, home servers, or internal infrastructure

Key Features:

  • Zero inbound firewall rules required on private network
  • End-to-end encryption via QUIC (TLS 1.3)
  • JWT-based authentication for both agent and client
  • Works through NAT, firewalls, and restrictive networks
  • No public exposure of private services

Security Architecture

Reverse tunnels use a three-party architecture with defense-in-depth security:

┌──────────────────────────────────────────────────────────────────┐
│                     Security Architecture                         │
└──────────────────────────────────────────────────────────────────┘

  Client                Relay                Agent           Private Service
  (Laptop)           (Public Server)      (Private Network)   (Database)
    │                     │                     │                  │
    │  ①──QUIC/TLS 1.3──→│←──QUIC/TLS 1.3───② │                  │
    │   (JWT Auth)        │   (JWT Auth)        │                  │
    │                     │                     │                  │
    │  ③ Request Data ───→│─── Forward ───────→│─── ④ Connect ──→│
    │                     │                     │   (localhost)    │
    │                     │                     │                  │
    │←─── Response ───────│←─── Response ──────│←─── Data ────────│
    │                     │                     │                  │

┌─────────────────────────────────────────────────────────────────┐
│  Security Layers:                                                │
│  ① Client → Relay: QUIC encrypted, JWT authenticated            │
│  ② Agent → Relay: QUIC encrypted, JWT authenticated             │
│  ③ Relay never directly accesses private service                │
│  ④ Agent only accepts localhost connections                     │
└─────────────────────────────────────────────────────────────────┘

Security Flow:

  1. Agent Registration: Agent (on private network) connects to public relay with JWT token, creates persistent QUIC connection
  2. Client Authentication: Client connects to relay with separate JWT token, requests access to specific agent
  3. Authorization Check: Relay verifies both tokens, ensures client is authorized to access that agent
  4. Secure Tunneling: Relay bridges the two QUIC connections without decrypting payload
  5. Local Forwarding: Agent forwards traffic to local service (localhost only)

Why This is Secure:

  • No Public Exposure: Private service never listens on public internet
  • Mutual Authentication: Both agent and client must present valid JWT tokens
  • End-to-End Encryption: All traffic encrypted with QUIC (TLS 1.3)
  • Localhost-Only: Agent only connects to localhost, preventing lateral movement
  • Token Isolation: Separate tokens for agent and client (different scopes)
  • No Inbound Ports: Agent initiates outbound connection to relay (NAT/firewall friendly)

How It Works

Traditional Tunnel vs Reverse Tunnel

Traditional Tunnel (Public Exposure):

Local Service → Client → Relay → Internet
   :3000         QUIC      :443     Anyone can access
                                    https://myapp.relay.com

☝️ Problem: Your service is exposed to the entire internet.

Reverse Tunnel (Private Access):

Private Service ← Agent ← Relay ← Client
   :5432         localhost  :4443   Authenticated
   (private)     (QUIC)             only you

☝️ Solution: Only authorized clients can access through relay.

Connection Flow

  1. Agent Startup (Private Network):

    # Agent runs on private network, connects to relay
    localup agent \
      --relay relay.example.com:4443 \
      --agent-id "private-db" \
      --target-address "localhost:5432" \
      --token "$AGENT_TOKEN"
    
    • Agent establishes persistent QUIC connection to relay
    • Registers as agent-id: private-db
    • Waits for client connections
  2. Client Connection (Anywhere):

    # Client connects from anywhere, authenticated with token
    localup connect \
      --relay relay.example.com:4443 \
      --agent-id "private-db" \
      --local-address "localhost:19432" \
      --token "$CLIENT_TOKEN"
    
    • Client authenticates with relay using JWT
    • Requests access to agent private-db
    • Relay verifies authorization and bridges connections
  3. Data Flow:

    Client App → localhost:19432 → Client → Relay → Agent → localhost:5432 → Private Service
       psql         (local)         (QUIC)   (QUIC)   (localhost)      PostgreSQL
    

Key Points:

  • Private service never listens on public internet
  • Agent only accepts localhost connections
  • Relay cannot decrypt traffic (QUIC end-to-end encryption)
  • Client must authenticate with valid JWT token

Security Guarantees

1. No Public Exposure

Guarantee: Private services never accept connections from the internet.

  • Agent only connects to localhost (127.0.0.1)
  • No inbound firewall rules required
  • Service remains invisible to port scanners

Example:

# Private service bound to localhost only
postgres -c listen_addresses='127.0.0.1'

# Agent forwards to localhost only
localup agent --target-address "localhost:5432"

2. Mutual Authentication

Guarantee: Both agent and client must present valid JWT tokens.

  • Agent Token: Authenticates agent to relay, proves identity
  • Client Token: Authenticates client to relay, authorizes access to specific agent
  • Tokens use HMAC-SHA256 signatures (HS256 algorithm)
  • Tokens have expiration times (exp claim) enforced by relay

Token Structure:

{
  "sub": "agent-id or client-id",
  "exp": 1735689600,
  "iat": 1704067200
}

Token Verification:

# Generate agent token (long-lived for persistent connection)
localup generate-token --secret "my-secret" --sub "agent-id" --token-only

# Generate client token (short-lived for access control)
localup generate-token --secret "my-secret" --sub "client-id" --token-only

3. End-to-End Encryption

Guarantee: All traffic encrypted with QUIC (TLS 1.3), relay cannot decrypt.

  • QUIC Protocol: Modern transport protocol with built-in TLS 1.3 encryption
  • TLS 1.3: Latest TLS version with forward secrecy (ECDHE key exchange)
  • Perfect Forward Secrecy: Compromise of long-term keys doesn't decrypt past sessions
  • Zero-Knowledge Relay: Relay only routes encrypted packets, cannot inspect payload

Encryption Layers:

Application Data (e.g., SQL query)
  ↓ Encrypted at: Client → QUIC/TLS 1.3 → Relay (encrypted) → Agent → Decrypted
  ↓ Relay sees: [ENCRYPTED QUIC PACKETS]
  ↓ Agent decrypts and forwards to localhost
Private Service (e.g., PostgreSQL)

4. Network Isolation

Guarantee: Agent only accepts connections from localhost, preventing lateral network access.

  • Agent binds target address as localhost:PORT
  • Cannot reach other machines on private network
  • Prevents pivot attacks or lateral movement
  • Limits blast radius if agent is compromised

Example:

# ✅ Secure: Agent forwards to localhost only
localup agent --target-address "localhost:5432"

# ❌ Insecure: Agent could access entire network
# localup agent --target-address "192.168.1.50:5432"  # Don't do this!

Best Practice: Always use localhost or 127.0.0.1 as target address.

5. Token-Based Authorization

Guarantee: Client must provide valid token to access agent, relay enforces authorization.

  • Relay verifies client token signature and expiration
  • Client token's sub claim must match authorized agent IDs
  • Token revocation possible by rotating JWT secret
  • Short-lived tokens reduce risk of token theft

Authorization Model:

# Agent registers with token (sub: agent-id)
AGENT_TOKEN=$(localup generate-token --secret "secret" --sub "private-db" --token-only)
localup agent --agent-id "private-db" --token "$AGENT_TOKEN" ...

# Client must have valid token to connect
CLIENT_TOKEN=$(localup generate-token --secret "secret" --sub "client-1" --token-only)
localup connect --agent-id "private-db" --token "$CLIENT_TOKEN" ...

Token Expiration:

  • Agent tokens: Long-lived (days/weeks) for persistent connections
  • Client tokens: Short-lived (hours) for access control
  • Rotate tokens regularly to limit exposure

Setup Guide

Prerequisites

  1. Relay Server (Public): A publicly accessible server running the LocalUp relay
  2. Agent (Private Network): Machine on private network running the agent
  3. Client (Anywhere): Your laptop or machine connecting to the private service
  4. JWT Secret: Shared secret for token generation

Step 1: Start the Relay Server (Public)

# On public server (relay.example.com)
localup relay tcp \
  --localup-addr "0.0.0.0:4443" \
  --tcp-port-range "10000-20000" \
  --jwt-secret "your-production-secret"

Security Note: Keep --jwt-secret confidential. Anyone with this secret can generate valid tokens.

Step 2: Run the Agent (Private Network)

# On private network (e.g., home network, private cloud)
# Generate agent token
export AGENT_TOKEN=$(localup generate-token --secret "your-production-secret" --sub "private-db" --token-only)

# Start agent to expose private service
localup agent \
  --relay relay.example.com:4443 \
  --agent-id "private-db" \
  --target-address "localhost:5432" \
  --token "$AGENT_TOKEN"

What happens:

  • Agent connects to relay at relay.example.com:4443 via QUIC
  • Authenticates with AGENT_TOKEN
  • Registers as agent-id: private-db
  • Waits for client connections
  • Forwards traffic to localhost:5432 (PostgreSQL)

Security Best Practices:

  • Use localhost or 127.0.0.1 for --target-address to prevent lateral movement
  • Use a unique --agent-id for each service
  • Rotate AGENT_TOKEN regularly (weekly/monthly)
  • Run agent with minimal privileges (non-root user)

Step 3: Connect as Client (Anywhere)

# On client machine (anywhere on internet)
# Generate client token
export CLIENT_TOKEN=$(localup generate-token --secret "your-production-secret" --sub "client-1" --token-only)

# Connect to private service through relay
localup connect \
  --relay relay.example.com:4443 \
  --agent-id "private-db" \
  --local-address "localhost:19432" \
  --remote-address "localhost:5432" \
  --token "$CLIENT_TOKEN"

What happens:

  • Client connects to relay at relay.example.com:4443 via QUIC
  • Authenticates with CLIENT_TOKEN
  • Requests connection to agent-id: private-db
  • Relay bridges client and agent connections
  • Client binds local port 19432 → forwards to agent → localhost:5432

Step 4: Access the Private Service

# Connect to private PostgreSQL via local port
psql -h localhost -p 19432 -U postgres

# Or with connection string
psql "postgresql://postgres@localhost:19432/mydb"

What you're doing:

  • Connecting to localhost:19432 on your client machine
  • LocalUp client forwards to relay
  • Relay forwards to agent on private network
  • Agent forwards to localhost:5432 (PostgreSQL)

Zero inbound firewall rules needed on private network!


Complete Example

Here's a full end-to-end example with a private PostgreSQL database:

Scenario

  • Private Network: Home network with PostgreSQL at 192.168.1.100:5432
  • Relay: Public server at relay.example.com
  • Client: Your laptop anywhere on the internet
  • Goal: Access PostgreSQL from laptop without opening firewall ports

Terminal 1: Start Relay (Public Server)

# On relay.example.com (public server)
localup relay tcp \
  --localup-addr "0.0.0.0:4443" \
  --tcp-port-range "10000-20000" \
  --jwt-secret "my-production-secret-change-me"

Terminal 2: Run Agent (Private Network)

# On machine in private network (192.168.1.100)
# 1. Generate agent token (long-lived)
export AGENT_TOKEN=$(localup generate-token \
  --secret "my-production-secret-change-me" \
  --sub "home-postgres" \
  --token-only)

# 2. Start agent to expose PostgreSQL
localup agent \
  --relay relay.example.com:4443 \
  --agent-id "home-postgres" \
  --target-address "localhost:5432" \
  --token "$AGENT_TOKEN"

# Agent output:
# ✅ Connected to relay: relay.example.com:4443
# ✅ Agent ID: home-postgres
# ✅ Waiting for client connections...

Security Notes:

  • Agent uses localhost:5432 not 192.168.1.100:5432
  • This ensures agent can only access local services
  • Agent runs with minimal privileges (non-root user)

Terminal 3: Connect as Client (Laptop)

# On your laptop (anywhere on internet)
# 1. Generate client token (short-lived)
export CLIENT_TOKEN=$(localup generate-token \
  --secret "my-production-secret-change-me" \
  --sub "laptop-client" \
  --token-only)

# 2. Connect to private PostgreSQL
localup connect \
  --relay relay.example.com:4443 \
  --agent-id "home-postgres" \
  --local-address "localhost:19432" \
  --remote-address "localhost:5432" \
  --token "$CLIENT_TOKEN"

# Client output:
# ✅ Connected to relay: relay.example.com:4443
# ✅ Connected to agent: home-postgres
# ✅ Tunnel ready: localhost:19432 → private service

Terminal 4: Access Database

# Connect to private PostgreSQL via local port
psql -h localhost -p 19432 -U postgres

# Or use GUI tool (e.g., pgAdmin, DBeaver)
# Host: localhost
# Port: 19432
# User: postgres

What's happening:

  1. Your laptop connects to localhost:19432 (local port)
  2. LocalUp client encrypts and sends to relay via QUIC
  3. Relay forwards to agent on private network (still encrypted)
  4. Agent decrypts and forwards to localhost:5432 (PostgreSQL)
  5. PostgreSQL responds back through same path

Security:

  • ✅ No inbound firewall rules on private network
  • ✅ End-to-end encryption (QUIC/TLS 1.3)
  • ✅ Both agent and client authenticated with JWT
  • ✅ Agent only accesses localhost (no lateral movement)
  • ✅ Private service never exposed to internet

Security Best Practices

1. Use Localhost-Only Target Addresses

Why: Prevents lateral movement attacks if agent is compromised.

# ✅ Good: Agent can only access localhost
localup agent --target-address "localhost:5432"

# ❌ Bad: Agent can access entire private network
localup agent --target-address "192.168.1.100:5432"

Principle: Minimize blast radius by limiting what agent can access.

2. Rotate JWT Tokens Regularly

Why: Limits exposure if tokens are stolen or leaked.

Recommendation:

  • Agent Tokens: Rotate monthly (or when agent is compromised)
  • Client Tokens: Rotate weekly or use short-lived tokens (hours)
  • JWT Secret: Rotate quarterly, requires restarting relay and regenerating all tokens

Token Rotation Workflow:

# 1. Generate new token with updated secret
NEW_TOKEN=$(localup generate-token --secret "new-secret" --sub "agent-id" --token-only)

# 2. Update relay with new secret (restart required)
localup relay tcp --jwt-secret "new-secret" ...

# 3. Update agent with new token
localup agent --token "$NEW_TOKEN" ...

3. Use Strong JWT Secrets

Why: Weak secrets allow attackers to forge tokens.

Requirement:

  • Minimum 32 characters
  • Random, high-entropy (use password generator)
  • Never commit to version control
  • Store in secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault)

Generate Strong Secret:

# Good: 32+ random characters
openssl rand -base64 32
# Output: k3jF9xR2pQ4vN8mL5dH7wZ1cT6eS9yA0...

# ❌ Bad: Weak/guessable secrets
--jwt-secret "password123"
--jwt-secret "my-secret"

4. Limit Token Scope

Why: Principle of least privilege - tokens should only access what they need.

Recommendation:

  • Agent Tokens: Use --sub to identify agent (e.g., --sub "prod-db-agent")
  • Client Tokens: Use --sub to identify client (e.g., --sub "alice-laptop")
  • Future enhancement: Add scope claim to limit which agents a client can access

Example:

# Agent token for production database
AGENT_TOKEN=$(localup generate-token --secret "secret" --sub "prod-db-agent" --token-only)

# Client token for Alice (future: add scope for specific agents)
CLIENT_TOKEN=$(localup generate-token --secret "secret" --sub "alice-laptop" --token-only)

5. Run Agent with Minimal Privileges

Why: Limits damage if agent is compromised.

Recommendations:

  • Run agent as non-root user
  • Use systemd to manage agent lifecycle
  • Restrict file system access with AppArmor/SELinux
  • Use containers or VMs for isolation

Example (systemd service):

[Unit]
Description=LocalUp Agent for Private Database
After=network.target

[Service]
Type=simple
User=localup  # Non-root user
Group=localup
ExecStart=/usr/local/bin/localup agent \
  --relay relay.example.com:4443 \
  --agent-id "private-db" \
  --target-address "localhost:5432" \
  --token "${AGENT_TOKEN}"
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target

6. Monitor and Audit Access

Why: Detect unauthorized access attempts and anomalies.

Recommendations:

  • Enable relay logging (--log-level debug)
  • Monitor failed authentication attempts
  • Track which clients connect to which agents
  • Set up alerts for suspicious activity

Example (relay logs):

INFO  Agent connected: agent-id=private-db, ip=192.168.1.100
INFO  Client authenticated: client-id=alice-laptop, token-sub=alice
WARN  Authentication failed: invalid token signature, ip=203.0.113.45

7. Use Firewall Rules on Relay

Why: Defense in depth - limit who can connect to relay.

Recommendations:

  • Restrict relay port (4443) to known IP ranges if possible
  • Use cloud provider security groups (AWS Security Groups, GCP Firewall Rules)
  • Enable fail2ban for brute-force protection

Example (AWS Security Group):

Inbound Rule:
- Protocol: UDP
- Port: 4443
- Source: 0.0.0.0/0 (QUIC uses UDP)
- Description: LocalUp relay control plane

8. Validate Target Addresses

Why: Prevent SSRF (Server-Side Request Forgery) attacks.

Recommendations:

  • Hardcode target addresses in agent configuration
  • Validate target addresses are localhost/127.0.0.1
  • Reject private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)

Future Enhancement:

# Agent validates target address at startup
localup agent --target-address "localhost:5432" --validate-localhost
# Error if target is not localhost/127.0.0.1

Troubleshooting

"Connection refused" when agent connects to relay

Cause: Relay is not running or firewall is blocking QUIC.

Solution:

# Verify relay is running
lsof -i :4443

# Check firewall allows UDP (QUIC uses UDP)
sudo ufw allow 4443/udp

# Test connectivity from agent network
nc -u relay.example.com 4443

"Authentication failed" on agent startup

Cause: Agent token doesn't match relay's --jwt-secret.

Solution:

# Regenerate token with correct secret
AGENT_TOKEN=$(localup generate-token \
  --secret "correct-secret" \
  --sub "agent-id" \
  --token-only)

# Verify secret matches relay's --jwt-secret flag

"Agent not found" when client connects

Cause: Agent is not running or --agent-id mismatch.

Solution:

# Verify agent is running and registered
# Check relay logs for: "Agent connected: agent-id=..."

# Ensure client's --agent-id matches agent's --agent-id
localup connect --agent-id "private-db" ...  # Must match agent

Client tunnel hangs or disconnects

Cause: Network interruption, relay restart, or token expiration.

Solution:

# Check client logs for error messages
# Verify relay is still running
lsof -i :4443

# Regenerate tokens if expired
CLIENT_TOKEN=$(localup generate-token --secret "secret" --sub "client" --token-only)

# Restart client connection
localup connect --token "$CLIENT_TOKEN" ...

"Permission denied" when accessing local port

Cause: Local port binding failed or port already in use.

Solution:

# Check if port is in use
lsof -i :19432

# Use a different local port
localup connect --local-address "localhost:19433" ...

# Or kill the process using the port
kill -9 <PID>

Agent cannot connect to private service

Cause: Private service not running or target address incorrect.

Solution:

# Verify private service is running
lsof -i :5432  # On agent machine

# Test local connectivity
telnet localhost 5432

# Verify target address is correct
localup agent --target-address "localhost:5432" ...  # Not 192.168.x.x

Next Steps:

Was this page helpful?