Skip to content

Database access from a GUI client

How to connect Sequel Ace (or TablePlus, DBeaver, mysql) from your laptop to a database on staging – and, with permission, on production.

The database servers are never exposed to the internet: port 3306 is only open to allowlisted IP ranges, hosts with zt_lockdown_enabled: true are not reachable at all outside the Cloudflare tunnel, and SSH port forwarding is switched off by default. So the connection is built in two hops, and the host has to be configured to allow the tunnel first.

How it works

Sequel Ace ──MySQL──▶ its own SSH tunnel ──▶ 127.0.0.1:13306
                                             cloudflared
                                        Cloudflare Access (login)
                                       dev-e01: sshd on :22
                                     dev-e01: MariaDB on localhost:3320
  1. cloudflared access tcp turns the Access-protected hostname into a plain TCP port on your own machine. What answers on that port is the server's SSH port – the tunnel only publishes SSH, not MySQL.
  2. The client's own SSH tunnel logs in through that local port and forwards MySQL from the server's loopback interface.

Two hops, because the GUI client cannot run a ProxyCommand itself. From a terminal you can skip hop 1 entirely – see CLI alternative.

Prerequisites

Requirement How
cloudflared brew install cloudflared
Cloudflare Access Your email must be in the ALLOW-GROUP-IT-ORG policy. The first tunnel opens a browser login; the token is then cached for 24 hours
SSH access to the host Your key must be installed on the server, and ~/.ssh/config set up – see Getting started
Port forwarding allowed for your user on that host AllowTcpForwarding no applies everywhere by default. See Allow the tunnel on the host
A database client Sequel Ace, TablePlus, DBeaver …

Allow the tunnel on the host

Without this, everything else works and the client still fails with administratively prohibited: open failed.

The exception is per host, per user, per destination – set both variables in the host's host_vars and reapply the host:

# inventories/stage/host_vars/web-01/main.yml
ssh_tcp_forwarding_users:
  - pt
ssh_tcp_forwarding_permitopen:
  - localhost:3320
ansible-playbook -i inventories/stage/hosts.yml baseline.yml -l web-01

Staging web-01 is already set up like this for pt. See SSH tunnels for the details, and note that PermitOpen is matched literally: sshd does no name lookup, so a client asking for 127.0.0.1:3320 is refused when the allowlist says localhost:3320.

Step 1 – start the access tunnel

cloudflared access tcp --hostname dev-e01.subscribed.build --url 127.0.0.1:13306
  • Runs in the foreground – keep the terminal open for as long as you need the connection.
  • 13306 is just a free local port. Pick a different one per host if you want several tunnels open at the same time.
  • The first run (or after 24 hours) opens a browser for the Access login.

Hostnames

Host Environment Tunnel hostname
web-01 (all staging sites) stage dev-e01.subscribed.build
bk-db-01 prod bk-a01.subscribed.build
elome-db-01 prod el-a01.subscribed.build
kd-db-01 prod kd-a01.subscribed.build

The hostnames come from ansible_host in inventories/<env>/hosts.yml.

Step 2 – configure Sequel Ace

Create a new connection and choose the SSH tab (not Standard – Standard has no tunnel).

Field Value Note
MySQL Host localhost The MySQL host as seen from the server – must match ssh_tcp_forwarding_permitopen exactly
Username database user From the site's .env
Password database password From the site's .env
Database database name Optional, but handy
Port 3320 MySQL port on the server, from the same allowlist
SSH Host 127.0.0.1 Your local end of the cloudflared tunnel
SSH User your operator user, e.g. pt Must be in ssh_tcp_forwarding_users on the host
SSH Key ~/.ssh/id_ed25519 The key that is authorized on the host
SSH Port 13306 Must match --url from step 1

Save it as a favourite – then it is only step 1 you have to repeat next time.

Everything under "MySQL" is seen from the server's point of view; everything under "SSH" is seen from your machine. Sequel Ace passes the MySQL host and port straight into the forwarding request, which is why they have to be written the same way as in PermitOpenlocalhost:3320, not 127.0.0.1:3320.

Basic setup – elome on staging

All staging sites live on the same host, dev-e01. Example for elome-com:

Field Value
MySQL Host localhost
Port 3320
Username / Password / Database see below
SSH Host 127.0.0.1
SSH Port 13306
SSH User pt

The database credentials are not in this repo – they live in the site's .env on the server, which is the authoritative source:

ssh pt@dev-e01.subscribed.build
sudo grep '^DB_' /home/barberklingen/elome-com-bedrock/.env

The project paths for the other staging sites are listed in inventories/stage/host_vars/web-01/main.yml under sites.

If .env points somewhere other than localhost:3320 – another port, or a container on its own address – that destination has to be added to ssh_tcp_forwarding_permitopen before the tunnel will reach it.

Never copy database credentials into this repo, a ticket, or a chat message. Read them on the host when you need them.

Other clients

TablePlus / DBeaver: same values – enable "Over SSH" / the SSH tab and fill in the SSH host 127.0.0.1 with port 13306, and the database host localhost with port 3320.

CLI alternative

If ~/.ssh/config already has the cloudflared ProxyCommand (see Cloudflare Zero Trust), you do not need cloudflared access tcp at all – ssh sets up the tunnel on its own:

# Terminal 1 – forward the server's MySQL port to 3307 locally.
# The destination must be spelled localhost:3320 to pass PermitOpen.
ssh -L 3307:localhost:3320 pt@dev-e01.subscribed.build

# Terminal 2 – here 127.0.0.1 is required: with localhost the mysql
# client ignores the port and looks for a unix socket instead.
mysql -h 127.0.0.1 -P 3307 -u <user> -p <database>

Production

Ask before you connect to a production database. Permission is per task, and read-only is not an exception – see the rules in the repo root.

Three things are different on production:

  • No host allows forwarding yet. ssh_tcp_forwarding_users is unset on every prod host, so a tunnel needs a host_vars change and a baseline.yml run first – i.e. a reviewed change, not something you enable on the spot.
  • Grants are locked to source IPs. The MySQL users on the prod database hosts are granted for specific addresses (the web servers' internal IPs, the office range, Plecto), not for localhost. A login over an SSH tunnel arrives as localhost and is therefore rejected even though the tunnel works. See mariadb_users in inventories/prod/host_vars/<host>/main.yml for who is allowed from where.
  • From an allowlisted network – e.g. the office – you can connect straight to port 3306 without a tunnel, with the user that is granted for that IP. The plecto user is SELECT-only, which is usually all you want for a look around.

If you need a new database user for GUI access, add it to mariadb_users in host_vars with the password in vault, with the narrowest privileges and host list that solves the task. See Vault and Roles reference.

Troubleshooting

Symptom Cause and fix
administratively prohibited: open failed Forwarding is not granted for your user on that host, or the destination is not in PermitOpen. Check that you asked for localhost:3320 and not 127.0.0.1:3320
connect: connection refused on 127.0.0.1:13306 The cloudflared access tcp command is not running, or the port does not match --url
bind: address already in use A tunnel is already running on that port – reuse it, or pick another port
Browser login loops, or 403 from Access Your email is not covered by the Access policy for that hostname
Sequel Ace: "SSH connection failed" Test ssh -p 13306 pt@127.0.0.1 in a terminal first. Usually a wrong SSH user or a key that is not authorized on the host
Can't connect to local MySQL server through socket … Only the local mysql client does this – point it at 127.0.0.1, not localhost
Access denied for user 'x'@'localhost' The tunnel works – the MySQL user is not granted from localhost. See Production
Connection dies after a while The Access session lasts 24 hours (cloudflare_application_session_duration). Restart the tunnel and log in again

See also