Skip to main content

Reverse Tunnels - Public URL Access to Devices

Overview

Reverse Tunnels enable exposing device web services (dashboards, cameras, APIs) via public URLs using AWS IoT Secure Tunneling + Cloudflare. This provides secure, authenticated access to devices without requiring them to have public IPs or open inbound firewall ports. Example Use Cases:
  • Access robot web dashboard at https://device-abc123.fleet.roboticks.io
  • View camera feed at https://device-camera01.fleet.roboticks.io
  • Monitor sensor data via device API

Architecture

User Browser

Cloudflare (TLS termination, DDoS protection)

API Gateway / Proxy (source access token)

AWS IoT Secure Tunnel (encrypted connection)

Device Agent (destination access token)

Local Service (e.g., http://localhost:8080)

Components

  1. Database: reverse_tunnels table tracks tunnel configurations
  2. Backend API: CRUD endpoints for managing tunnels
  3. IoT Tunneling Service: Creates/manages AWS IoT Secure Tunnels
  4. Device Agent: Fetches config on startup, establishes tunnels
  5. Cloudflare: DNS routing and TLS termination
  6. Proxy Service: Routes traffic from Cloudflare to IoT tunnels

Database Schema

CREATE TABLE reverse_tunnels (
    id INTEGER PRIMARY KEY,
    device_id INTEGER NOT NULL,  -- FK to fleet_devices

    -- Configuration
    name VARCHAR(255) NOT NULL,
    description TEXT,
    public_url VARCHAR(500) UNIQUE NOT NULL,  -- e.g., device-abc123.fleet.roboticks.io
    is_enabled BOOLEAN DEFAULT TRUE,

    -- AWS IoT Secure Tunnel
    tunnel_id VARCHAR(255),          -- AWS tunnel ID
    tunnel_arn VARCHAR(500),         -- AWS tunnel ARN
    source_access_token TEXT,        -- For proxy to connect
    destination_access_token TEXT,   -- For device to connect

    -- Target Service
    local_port INTEGER DEFAULT 8080,
    protocol VARCHAR(10) DEFAULT 'http',  -- http/https/ws/wss

    -- Security
    require_auth BOOLEAN DEFAULT TRUE,
    allowed_user_ids JSON,

    -- Metrics
    total_requests INTEGER DEFAULT 0,
    last_accessed_at TIMESTAMP,

    -- Lifecycle
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP,
    expires_at TIMESTAMP,

    FOREIGN KEY (device_id) REFERENCES fleet_devices(id) ON DELETE CASCADE
);

API Endpoints

Management Endpoints (for users)

List device reverse tunnels:
GET /api/v1/reverse-tunnels/devices/{dsn}/reverse-tunnels
Authorization: Bearer {jwt_token}

Response:
{
  "items": [
    {
      "id": 1,
      "device_id": 42,
      "name": "Web Dashboard",
      "public_url": "device-abc123.fleet.roboticks.io",
      "local_port": 8080,
      "protocol": "http",
      "is_enabled": true,
      "tunnel_id": "aws-tunnel-id-123",
      "total_requests": 1523,
      "created_at": "2025-11-14T20:00:00Z"
    }
  ],
  "total": 1
}
Create reverse tunnel:
POST /api/v1/reverse-tunnels/devices/{dsn}/reverse-tunnels
Authorization: Bearer {jwt_token}
Content-Type: application/json

{
  "name": "Web Dashboard",
  "description": "Robot control interface",
  "local_port": 8080,
  "protocol": "http",
  "require_auth": true
}

Response: ReverseTunnelResponse (201 Created)
Update reverse tunnel:
PUT /api/v1/reverse-tunnels/devices/{dsn}/reverse-tunnels/{id}
Authorization: Bearer {jwt_token}
Content-Type: application/json

{
  "is_enabled": false
}

Response: ReverseTunnelResponse (200 OK)
Delete reverse tunnel:
DELETE /api/v1/reverse-tunnels/devices/{dsn}/reverse-tunnels/{id}
Authorization: Bearer {jwt_token}

Response: {"message": "Reverse tunnel deleted successfully"}

Device Configuration Endpoint

Get tunnel config (called by device on startup):
GET /api/v1/reverse-tunnels/devices/{dsn}/reverse-tunnels-config
X-Device-Certificate: {device_cert}

Response:
{
  "tunnels": [
    {
      "id": 1,
      "name": "Web Dashboard",
      "is_enabled": true,
      "local_port": 8080,
      "protocol": "http",
      "tunnel_id": "aws-tunnel-id-123",
      "tunnel_arn": "arn:aws:iot:...",
      "destination_access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "public_url": "device-abc123.fleet.roboticks.io",
      "expires_at": "2025-11-15T08:00:00Z"
    }
  ]
}

AWS IoT Secure Tunneling

Service Methods

The IoTSecureTunnelService provides methods for managing reverse tunnels:
from app.services.iot_secure_tunnel_service import iot_secure_tunnel_service

# Create tunnel
tunnel_info = iot_secure_tunnel_service.create_reverse_tunnel(
    reverse_tunnel=tunnel,
    device=device,
    timeout_minutes=720  # 12 hours (AWS max)
)
# Returns: {
#     "tunnel_id": "...",
#     "tunnel_arn": "...",
#     "source_access_token": "...",
#     "destination_access_token": "...",
#     "expires_at": datetime(...),
#     "service": "HTTP-8080"
# }

# Close tunnel
success = iot_secure_tunnel_service.close_reverse_tunnel(
    reverse_tunnel=tunnel,
    delete=True  # Delete immediately vs graceful drain
)

# Rotate tunnel (close old, create new)
tunnel_info = iot_secure_tunnel_service.rotate_reverse_tunnel(
    reverse_tunnel=tunnel,
    device=device,
    timeout_minutes=720
)

# Ensure tunnel is active (creates/rotates if needed)
tunnel_info = iot_secure_tunnel_service.ensure_reverse_tunnel_active(
    reverse_tunnel=tunnel,
    device=device,
    timeout_minutes=720
)

Tunnel Lifecycle

  1. Creation: When user creates reverse tunnel, AWS IoT Secure Tunnel is created immediately
  2. Device Fetch: Device calls /reverse-tunnels-config on startup
  3. Activation: Endpoint ensures tunnel is active (rotates if expired/expiring)
  4. Expiration: Tunnels expire after 12 hours (AWS max)
  5. Rotation: Celery task rotates tunnels expiring within 1 hour
  6. Cleanup: Celery task closes expired tunnels

Automatic Maintenance

Two Celery Beat tasks handle tunnel maintenance: 1. Cleanup Expired Tunnels (runs hourly):
# app/tasks/tunnel_cleanup.py
@shared_task(name="cleanup_expired_reverse_tunnels")
def cleanup_expired_reverse_tunnels():
    """Close tunnels that have expired"""
    # Finds tunnels with expires_at < now
    # Closes AWS tunnels
    # Clears tunnel_id/tokens from DB
2. Rotate Expiring Tunnels (runs every 30 minutes):
@shared_task(name="rotate_expiring_reverse_tunnels")
def rotate_expiring_reverse_tunnels():
    """Rotate tunnels expiring within 1 hour"""
    # Finds tunnels with expires_at < now + 1 hour
    # Closes old tunnel, creates new one
    # Updates DB with new tunnel_id/tokens
Configure in Celery Beat:
# celeryconfig.py or celery app
CELERYBEAT_SCHEDULE = {
    'cleanup-expired-reverse-tunnels': {
        'task': 'cleanup_expired_reverse_tunnels',
        'schedule': crontab(minute=0),  # Every hour
    },
    'rotate-expiring-reverse-tunnels': {
        'task': 'rotate_expiring_reverse_tunnels',
        'schedule': crontab(minute='*/30'),  # Every 30 minutes
    },
}

Device Agent Implementation

The device agent should fetch tunnel config on startup and establish connections.

Example Device Agent Code

import requests
import json
from awsiot.greengrasscoreipc.clientv2 import GreengrassCoreIPCClientV2
from awsiot.greengrasscoreipc.model import IoTTunnelMessage

class ReverseTunnelManager:
    def __init__(self, device_dsn: str, api_url: str, device_cert_path: str):
        self.device_dsn = device_dsn
        self.api_url = api_url
        self.device_cert = device_cert_path
        self.tunnels = []

    def fetch_tunnel_config(self):
        """Fetch reverse tunnel configuration from backend."""
        response = requests.get(
            f"{self.api_url}/api/v1/reverse-tunnels/devices/{self.device_dsn}/reverse-tunnels-config",
            cert=self.device_cert,
            verify=True
        )
        response.raise_for_status()
        config = response.json()
        self.tunnels = config['tunnels']
        return self.tunnels

    def establish_tunnels(self):
        """Establish AWS IoT Secure Tunnels for each configuration."""
        for tunnel_config in self.tunnels:
            if not tunnel_config['is_enabled']:
                continue

            if not tunnel_config['tunnel_id'] or not tunnel_config['destination_access_token']:
                print(f"Tunnel {tunnel_config['name']} has no AWS tunnel configured, skipping")
                continue

            self.start_tunnel_agent(tunnel_config)

    def start_tunnel_agent(self, config):
        """
        Start AWS IoT Secure Tunnel local proxy agent.

        The agent forwards traffic from IoT tunnel to local service.
        """
        import subprocess

        # AWS IoT Secure Tunneling local proxy
        # See: https://github.com/aws-samples/aws-iot-securetunneling-localproxy

        command = [
            'localproxy',
            '-r', AWS_REGION,
            '-s', str(config['local_port']),  # Source port (local service)
            '-t', config['destination_access_token'],  # Device access token
        ]

        print(f"Starting tunnel agent for {config['name']}: {config['protocol']}://localhost:{config['local_port']}")

        # Run in background
        process = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )

        print(f"Tunnel agent started (PID: {process.pid}) for {config['public_url']}")
        return process

# Usage on device startup
if __name__ == "__main__":
    manager = ReverseTunnelManager(
        device_dsn="ABC123DEF456",
        api_url="https://api.roboticks.io",
        device_cert_path="/etc/roboticks/device-cert.pem"
    )

    # Fetch configuration
    tunnels = manager.fetch_tunnel_config()
    print(f"Fetched {len(tunnels)} tunnel configurations")

    # Establish tunnels
    manager.establish_tunnels()
    print("All tunnels established")

Device Requirements

  1. AWS IoT Secure Tunneling Local Proxy installed:
    # Download from AWS
    wget https://github.com/aws-samples/aws-iot-securetunneling-localproxy/releases/latest/download/localproxy-linux-x86_64
    chmod +x localproxy-linux-x86_64
    mv localproxy-linux-x86_64 /usr/local/bin/localproxy
    
  2. Device registration with valid IoT certificate
  3. Systemd service to run tunnel manager on startup:
    [Unit]
    Description=Roboticks Reverse Tunnel Manager
    After=network.target
    
    [Service]
    Type=simple
    User=roboticks
    ExecStart=/usr/local/bin/roboticks-tunnel-manager
    Restart=always
    RestartSec=10
    
    [Install]
    WantedBy=multi-user.target
    

Frontend UI

The Reverse Tunnels page (/reverse-tunnels) provides:
  1. Device Selector: Choose device to create tunnel for
  2. Create Tunnel Dialog:
    • Name
    • Description
    • Local Port (default: 8080)
    • Protocol (http/https/ws/wss)
    • Require Authentication
  3. Tunnels Table:
    • Public URL with copy button
    • Target (protocol://localhost:port)
    • Enable/Disable toggle
    • Usage metrics
    • Delete action
  4. Info Banner: Notifies that config applies on next device startup

Security Considerations

  1. Authentication:
    • require_auth=true: Public URL requires user JWT token
    • allowed_user_ids: Restrict access to specific users
    • Future: Per-tunnel API keys
  2. TLS Encryption:
    • Cloudflare provides TLS termination
    • AWS IoT Secure Tunnel encrypts data in transit
    • End-to-end encryption browser → device
  3. Access Control:
    • Users can only create tunnels for devices in their projects
    • Devices authenticate with IoT certificates
    • Proxy validates source access tokens
  4. Rate Limiting:
    • Cloudflare provides DDoS protection
    • Future: Per-tunnel rate limits
  5. Audit Trail:
    • total_requests tracks usage
    • last_accessed_at for monitoring
    • Future: Detailed access logs

Production Deployment

Required Infrastructure

  1. Cloudflare:
    • DNS: Configure *.fleet.roboticks.io → API Gateway
    • SSL/TLS: Full (strict) mode
    • Firewall: WAF rules for protection
  2. API Gateway / Proxy Service:
    • WebSocket support for IoT tunnels
    • Lambda or EC2 instance running proxy
    • Validate source access tokens
    • Forward traffic to AWS IoT endpoints
  3. AWS IoT Core:
    • Device certificates provisioned
    • IoT Secure Tunneling enabled
    • Policies configured for tunneling
  4. Celery Beat:
    • Schedule tunnel cleanup tasks
    • Monitor task execution logs

Environment Variables

# Backend
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

# Celery
CELERY_BROKER_URL=redis://...
CELERY_RESULT_BACKEND=redis://...

Troubleshooting

Tunnel Not Working

  1. Check tunnel status:
    from app.services.iot_secure_tunnel_service import iot_secure_tunnel_service
    info = iot_secure_tunnel_service.describe_tunnel("tunnel-id-123")
    print(info['status'])  # Should be "OPEN"
    print(info['destination_connection_state'])  # Should be "CONNECTED"
    
  2. Check device logs: Verify device fetched config and started local proxy
  3. Check tunnel expiration: Tunnels expire after 12 hours
    SELECT id, name, expires_at, tunnel_id
    FROM reverse_tunnels
    WHERE expires_at < NOW();
    
  4. Manually rotate tunnel:
    tunnel_info = iot_secure_tunnel_service.rotate_reverse_tunnel(
        reverse_tunnel=tunnel,
        device=device,
        timeout_minutes=720
    )
    

Device Not Fetching Config

  1. Check device registration: Verify device has valid IoT certificate
  2. Check API authentication: Device should use certificate auth, not JWT
  3. Check network connectivity: Device must reach backend API
  4. Check device agent logs: Look for HTTP errors or exceptions

Proxy Errors

  1. Invalid source access token: Tunnel may have been rotated, fetch new config
  2. Tunnel closed: Check if tunnel expired or was manually closed
  3. Connection refused: Verify local service is running on configured port

Future Enhancements

  1. Per-Tunnel API Keys: Alternative to JWT authentication
  2. Custom Domains: Allow users to map their own domains
  3. WebSocket Support: Full duplex communication
  4. Traffic Analytics: Detailed request logs and metrics
  5. Bandwidth Limiting: Per-tunnel data caps
  6. Auto-Scaling: Multiple proxy instances behind load balancer
  7. Geographic Routing: Route to nearest proxy for lower latency
  8. Tunnel Sharing: Share public URLs with external users (time-limited)

References