Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Connect to a Database Remotely Without Leaving It Open to the Internet

Sean

Platform Writer

Aug 20, 2026
8 min read

Most database servers bind to 127.0.0.1 on purpose, and the instructions for remote access tell you to change that. The better answer is usually an SSH tunnel, which gives you a local connection to a remote database without opening a port to anyone.

Connect to a Database Remotely Without Leaving It Open to the Internet

There is a well-worn path here: change the bind address, grant a user access from any host, open the firewall, connect. It works, and it is also how a large number of databases end up findable by internet-wide scanners. The scanners are not hypothetical — an exposed database with a weak password is found in hours, not months.

So this covers both: the tunnel you should probably use, and how to do direct exposure properly if you genuinely need it.

Table of contents

The SSH tunnel, which needs no firewall change at all

If you can already SSH to the server, you can already reach the database. Port forwarding maps a local port on your machine to a port on the remote host, through the encrypted SSH connection you already trust.

# Forward local 3307 to the remote server's 3306
ssh -L 3307:127.0.0.1:3306 user@your_server_ip -N

# In another terminal, connect as if the database were local
mysql -u appuser -h 127.0.0.1 -P 3307 -p

# Postgres, same idea
ssh -L 5433:127.0.0.1:5432 user@your_server_ip -N
psql -h 127.0.0.1 -p 5433 -U appuser mydb

Reading the forward argument left to right: local port, then the address and port to connect to from the perspective of the remote server. That inner 127.0.0.1 is the server’s own loopback, which is exactly where a properly configured database is listening.

The -N means do not run a command, just hold the tunnel open. Add -f to background it.

Use a different local port than the default — 3307 rather than 3306 — so you never confuse the tunnel with a local database and run a migration against the wrong one. That mistake is more common than it should be and there is no undo.

Every GUI client supports this natively. In TablePlus, DBeaver, DataGrip or Sequel Ace there is an SSH tab beside the connection settings; fill in the SSH host and key and the client manages the tunnel itself.

The advantages are worth stating plainly: no firewall change, no database port exposed anywhere, traffic encrypted regardless of whether the database supports TLS, and access controlled by SSH keys you already manage. Revoking someone’s access is revoking their SSH key.

If you must expose the port, do these four things

Sometimes a tunnel is not available — a managed analytics tool, a third-party service, a CI runner without SSH access. Then the exposure needs to be deliberate rather than broad.

1. Bind to the interface, not to everything.

# /etc/mysql/mysql.conf.d/mysqld.cnf
bind-address = 10.0.0.5      # the private network address, not 0.0.0.0

# Postgres: postgresql.conf
listen_addresses = '10.0.0.5'

If the client is on the same private network, bind to the private address and the public internet never sees the port at all.

2. Restrict at the firewall, by source address.

# Allow one address, deny the rest
sudo ufw allow from 203.0.113.42 to any port 3306 proto tcp
sudo ufw deny 3306

3. Scope the database user to a host, not to a wildcard. A grant to 'appuser'@'%' means from anywhere. Name the address.

CREATE USER 'analytics'@'203.0.113.42' IDENTIFIED BY 'a-long-random-password';
GRANT SELECT ON mydb.* TO 'analytics'@'203.0.113.42';
FLUSH PRIVILEGES;

Note the SELECT only. A reporting tool does not need DROP, and the day something goes wrong you will be glad the credential could not have caused it.

4. Require TLS. An unencrypted connection across the public internet sends credentials and query results in the clear.

ALTER USER 'analytics'@'203.0.113.42' REQUIRE SSL;

Diagnosing a connection that will not open

Work outward from the server, because each step rules out a whole layer:

# 1. Is the database listening, and on what address?
sudo ss -ltnp | grep -E '3306|5432'

# 2. From the client, can you reach the port at all?
nc -vz your_server_ip 3306

# 3. Does the database answer?
mysqladmin -h your_server_ip -u appuser -p ping

The output of step one is the most informative line in this whole article. 127.0.0.1:3306 means the database is loopback-only and no firewall change will help — fix the bind address or use a tunnel. 0.0.0.0:3306 means it is listening on every interface, and if that machine has a public address, so does your database.

Then read the error you actually got, because the three common ones point in different directions:

  • Connection timed out — a firewall is dropping packets. Nothing answered.
  • Connection refused — you reached the machine and nothing is listening on that port. Wrong port, or the database is bound elsewhere.
  • Access denied for user — the network path works completely. This is a credentials or grant problem, and it is good news.

People routinely spend an hour on firewall rules for an access-denied error, which is purely an authentication failure. The distinction saves real time.

What actually happens when you leave it open

Worth being specific rather than vague, because the abstract warning does not land.

Internet-wide scanning services continuously index every reachable port on every routable address. A database on a public IP appears in those indexes within hours. From there it is automated: a list of default and common usernames, a dictionary of passwords, and a script.

The outcomes are well documented. Ransom notes replacing your tables, where the data was dropped rather than encrypted and there is nothing to buy back. Silent data exfiltration, noticed months later or never. Your server enrolled into a botnet. A regulatory disclosure obligation because personal data was in those tables.

The mitigations are not exotic. Do not expose the port. If you must, restrict it by source address, require TLS, scope grants narrowly, and use a password long enough that guessing is pointless.

And keep tested backups off the machine. Every incident above is survivable with a restore, and is not survivable without one.

How this fits the rest of the stack

The recurring theme is that a database port should be reachable by your application and by nothing else, and that everything else — your laptop, a reporting tool, a migration — should get there through something that authenticates you first.

RunxBuild takes that as the default rather than as a configuration you have to remember. Managed MySQL and Postgres run with private networking, so the application reaches the database over an internal path instead of the public internet, alongside user management, connection limits and backups you did not have to script. Web services in Node, Next.js, Python, Go, Ruby, Java, .NET or Docker deploy from your GitHub repository on the same network, with the connection string as an environment variable rather than a credential pasted into a config file. To see what a service, a managed database and storage add up to, the RunxBuild hosting calculator lists them as separate line items.

Useful related references:

FAQ

How do I connect to a remote MySQL database from my laptop?

The safest route is an SSH tunnel: ssh -L 3307:127.0.0.1:3306 user@server -N, then connect to 127.0.0.1:3307 as though the database were local. No firewall change is needed and no database port is exposed. Every major GUI client can manage the tunnel for you from its SSH tab.

Should I change bind-address to 0.0.0.0?

Only if you have a specific reason and have restricted access another way. Binding to all interfaces on a machine with a public address publishes your database to the internet, where scanners find it within hours. Bind to a private network address if the client is internal, and use a tunnel otherwise.

What is the difference between connection refused and connection timed out?

Refused means you reached the machine and nothing is listening on that port — wrong port, or the service is bound to a different address. Timed out means a firewall silently dropped the packets. Access denied means the network path is fine and only the credentials or grants are wrong, which is a different problem entirely.

How do I limit which host a database user can connect from?

Create the user with an explicit host rather than a wildcard: 'analytics'@'203.0.113.42' instead of 'analytics'@'%'. Grant only the privileges that use actually needs — a reporting tool wants SELECT and nothing more — and add REQUIRE SSL so the connection is encrypted in transit.

Is an SSH tunnel slower than connecting directly?

Marginally, because of the encryption overhead, and the difference is irrelevant for interactive queries and administrative work. For bulk data transfer it is measurable but usually still acceptable. The security trade is heavily in the tunnel’s favour for anything a human is doing by hand.

#connect to database remotely#mysql#postgres#ssh tunnel#database security