Skip to content

SSH Tunnels

ctx manages SSH tunnels per context with automatic connection, health monitoring, and reconnection.

Configuration

ssh:
  bastion:
    host: bastion.myproject.com
    port: 22
    user: admin
    identity_file: ~/.ssh/myproject_ed25519

tunnels:
  - name: postgres
    description: "Production Database"
    local_port: 5432
    remote_host: db.internal
    remote_port: 5432
    auto_connect: true            # Start automatically on context switch

  - name: redis
    description: "Redis Cache"
    local_port: 6379
    remote_host: redis.internal
    remote_port: 6379

  - name: nomad
    description: "Nomad UI"
    local_port: 4646
    remote_host: 10.0.1.10
    remote_port: 4646

Commands

ctx tunnel list                  # List defined tunnels for current context
ctx tunnel up                    # Start all tunnels
ctx tunnel up <name>             # Start specific tunnel
ctx tunnel down                  # Stop all tunnels
ctx tunnel down <name>           # Stop specific tunnel
ctx tunnel status                # Show running tunnel status

Bastion Configuration

The ssh.bastion section defines the jump host used for all tunnels:

ssh:
  bastion:
    host: bastion.example.com    # Bastion hostname
    port: 22                     # SSH port (default: 22)
    user: deploy                 # SSH username
    identity_file: ~/.ssh/id_ed25519  # Private key path

Tunnel Options

Field Type Description
name string Required. Tunnel identifier
description string Human-readable description
local_port int Required. Local port to bind
remote_host string Required. Remote host to tunnel to
remote_port int Required. Remote port
auto_connect bool Start automatically on context switch

Auto-Connect

When auto_connect: true on a tunnel:

  • Tunnel starts automatically when you run ctx use <context>
  • Tunnel starts after VPN connection (if configured)
  • Tunnel stops when you switch contexts or run ctx deactivate

Database Access via Tunnels

A common pattern is to combine tunnels with database configuration:

ssh:
  bastion:
    host: bastion.example.com
    user: deploy
    identity_file: ~/.ssh/deploy_key

tunnels:
  - name: postgres
    local_port: 5432
    remote_host: db.internal
    remote_port: 5432
    auto_connect: true

databases:
  - name: primary
    type: postgres
    host: localhost       # Connect via tunnel
    port: 5432
    database: production
    username: app_user

Then:

ctx use myproject-prod
# Tunnel auto-connects

psql -h localhost -U app_user -d production
# Connected via tunnel to db.internal

Multiple Tunnels

You can define multiple tunnels per context:

tunnels:
  - name: postgres
    local_port: 5432
    remote_host: db.internal
    remote_port: 5432
    auto_connect: true

  - name: redis
    local_port: 6379
    remote_host: redis.internal
    remote_port: 6379
    auto_connect: true

  - name: elasticsearch
    local_port: 9200
    remote_host: es.internal
    remote_port: 9200
    # Not auto-connect - start manually when needed

Start specific tunnels:

ctx tunnel up postgres           # Start just postgres
ctx tunnel up                    # Start all tunnels
ctx tunnel down elasticsearch    # Stop just elasticsearch

Tunnel Status

Check which tunnels are running:

ctx tunnel status

Output shows:

  • Tunnel name and description
  • Local and remote endpoints
  • Connection status (running/stopped)
  • Process ID if running

Health Monitoring

ctx monitors tunnel health and automatically reconnects if:

  • The SSH connection drops
  • The bastion host becomes unreachable
  • The tunnel process crashes

Inheritance

Tunnels are replaced (not merged) when using context inheritance. If a child context defines any tunnels, it completely replaces the parent's tunnel list.

# base.yaml
tunnels:
  - name: db
    local_port: 5432
    remote_host: db.internal
    remote_port: 5432

# child.yaml
extends: base
tunnels:
  - name: cache
    local_port: 6379
    remote_host: redis.internal
    remote_port: 6379
# Only 'cache' tunnel exists - 'db' is NOT inherited

To include parent tunnels, you must redefine them in the child context.