Skip to content

Connecting through SSH tunnels

Most production databases sit behind a bastion. Quay opens its own SSH tunnel — no ssh -L 5432:db:5432 bastion & background process, no separate config to manage.

In the connection edit modal, toggle Use SSH tunnel. Three new fields appear:

FieldNotes
Bastion hostThe jump-box address (bastion.example.com)
Port22 (default)
UserThe SSH user on the bastion
Auth methodKey file / password / agent

Quay opens the SSH connection, forwards the database port through it, and connects to the database via the tunnel. Once the tunnel is up, the rest of the connection (host / port / user / password) is addressed against the bastion-side host — usually localhost or the database’s internal hostname.

Point Quay at ~/.ssh/id_ed25519 (or any key path). Encrypted keys prompt for the passphrase once per session. Quay stores the passphrase only in memory — never to disk.

Username + password. Stored in connections.json like any other credential (mode 0600 plaintext — locked decision).

Section titled “Agent (recommended for shared workstations)”

If SSH_AUTH_SOCK is set in your environment, pick Agent. Quay delegates to ssh-agent / 1Password / Yubikey-OpenSSH-bridge / etc. No keys leave the agent.

The tunnel sends a server-side keepalive ping every 30 seconds. If three consecutive pings fail, the tunnel marks itself unhealthy and tries to reconnect with exponential backoff (2s, 5s, 10s, 30s, 60s, then steady at 60s).

While reconnecting, the connection chip in the rail shows a yellow dot + a “reconnecting…” tooltip. Queries issued during the gap get a “tunnel reconnecting; retry in a moment” error rather than a silent stall.

First connection to a bastion: Quay shows the bastion’s SSH host-key fingerprint + asks you to confirm. Subsequent connections verify against the cached fingerprint and warn if it changes — the standard “trust on first use” pattern.

The fingerprints live next to connections.json: ~/Library/Application Support/com.unclez.quay/known-hosts.json (or ~/.config/quay/known-hosts.json on Linux). Same JSON format as the rest, mode 0600, swappable in an emergency.

Quay v0.3 supports a single hop. For multi-hop (me → bastion-1 → bastion-2 → db), use ssh’s ProxyJump in your local ~/.ssh/config and have Quay’s tunnel target the proxied host:

Host bastion-2
HostName 10.0.1.5
ProxyJump bastion-1

Then in Quay’s tunnel host field: bastion-2. SSH-config-aware connection is automatic when the host matches an entry in your local config.

Multi-hop UI in Quay itself (without leaning on ~/.ssh/config) is on the v0.4 roadmap.

The connection chip shows the tunnel state at a glance:

  • 🟢 Green dot — tunnel up, last keepalive ≤ 30s ago
  • 🟡 Yellow dot — reconnecting (also visible in the chip’s tooltip)
  • 🔴 Red dot — tunnel down + reconnect failed; Quay paused retries after the backoff ceiling. Click to manually retry.

The pill updates live as keepalives fire — no need to refresh or restart Quay to see the latest state.

  • permission denied (publickey) — the bastion didn’t accept the offered key. Test the key with ssh -i <keyfile> user@bastion to confirm.
  • too many authentication failures — your agent has too many keys; SSH offered them all. Add IdentitiesOnly yes to your ssh-config for the bastion, or pick a specific key in Quay.
  • connection timed out — the bastion isn’t reachable. Firewall? VPN not up? Check independently.
  • host key verification failed — the bastion’s key changed. Either confirm the change is legitimate (the bastion was rebuilt) and re-trust through the chip’s “update host key” option, or someone’s MITMing you and you should investigate.