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
cloudflared access tcpturns 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.- 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
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¶
- Runs in the foreground – keep the terminal open for as long as you need the connection.
13306is 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 PermitOpen – localhost: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:
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_usersis unset on every prod host, so a tunnel needs ahost_varschange and abaseline.ymlrun 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 aslocalhostand is therefore rejected even though the tunnel works. Seemariadb_usersininventories/prod/host_vars/<host>/main.ymlfor 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
plectouser isSELECT-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¶
- SSH management – granting the tunnel on a host
- Cloudflare Zero Trust – tunnels, Access policies, lockdown
- Getting started – SSH configuration
- Vault – where passwords belong