Back to articles

September 2026

Why databases shouldn’t be exposed directly to the internet

Opening a database port to the public internet is a common shortcut-and a bad default. In QubicDB and Skripsido I keep engines private and put TLS, allowlists, and a controlled edge in front of them.

Internet traffic stops at a TLS edge; the database stays private on localhost

Many tutorials still treat a public database port as normal: install Postgres or MySQL on a server, open 5432 or 3306 to 0.0.0.0, and connect from a laptop or an app. It works-until scanners, stolen credentials, or a protocol-level bug turn that open port into a direct path to every row you store.

This article is about a safer default I use in production: keep the database engine off the public internet, and only expose a controlled edge that can enforce TLS, identity, and network policy before any query reaches the data store. I apply that rule in two products I ship-QubicDB and Skripsido.

The usual shortcut

Unsafe pattern: clients and scanners reach Postgres on a public 5432 port
Unsafe pattern: clients and scanners reach Postgres on a public 5432 port

Exposing the database directly feels efficient. One host, one port, one connection string. No proxy to maintain. No VPN to set up. For a weekend project with disposable data, the risk can look abstract.

On anything that holds real user data, the same shortcut widens the attack surface. The database speaks a rich protocol designed for trusted networks. When that protocol faces the open internet, every authentication weakness, every misconfigured bind address, and every unpatched CVE becomes reachable from anywhere.

What goes wrong

Public database ports attract continuous scanning. Automated tools look for open Postgres, MySQL, MongoDB, and Redis endpoints around the clock. Weak or reused passwords fail fast under credential stuffing. Even strong passwords are not a full control when the service itself is reachable without a prior network gate.

A direct path also skips application-layer context. Your API might rate-limit users, check sessions, and audit writes. A raw database connection does none of that. Whoever authenticates as the DB user can run whatever the role allows-often more than a single product feature needs.

Finally, databases were not designed as internet front doors. TLS can be enabled, but many deployments still ship with cleartext or opportunistic settings. Protocol downgrades, unexpected SSL negotiation, and administrative interfaces become problems you inherit the moment the port is public.

A better mental model

Treat the database like an internal service, not a product surface. The only things that should face the public internet are application edges you intentionally harden: HTTPS APIs, dashboards behind sessions, or a narrow TLS proxy that understands who may connect.

In practice that means three separations:

  • Network: the engine listens on localhost or a private interface, never on a public bind.
  • Path: remote clients reach data through an edge that terminates TLS and applies policy before forwarding to the private listener.
  • Identity: credentials and roles stay least-privilege; network allowlists or private mesh reduce who can even attempt a login.

Patterns that keep the engine private

Safer pattern: clients reach an API or TLS proxy, then a private path to a localhost-only database
Safer pattern: clients reach an API or TLS proxy, then a private path to a localhost-only database

Several patterns all share the same idea-hide the database and control the door:

Application-only access. Clients never talk to the database. The backend on a private network holds the credentials and issues queries. This is the simplest safe model for most web and mobile products.

TLS proxy or gateway in front of the engine. When tools like psql, GUI clients, or external services must speak the database protocol, terminate TLS and enforce IP allowlists (deny by default) at a proxy that forwards only to 127.0.0.1 on the worker. The container or DB process is never published on a public interface.

VPN or private mesh. Operators and workers join a private network. The database never needs a public address; management and data-plane traffic stay on the mesh.

Firewall + bind address discipline. Even without a custom proxy, binding to localhost and blocking inbound DB ports at the host firewall removes the accidental “open to the world” failure mode.

These patterns combine well. A private listener plus a TLS edge plus an allowlist is stronger than any single control alone.

How I apply this in QubicDB and Skripsido

QubicDB is a Database-as-a-Service product. Tenants provision managed Postgres instances and connect apps from outside-so the temptation to “just publish 5432” is exactly the failure mode the product must avoid. Tenant engines stay on private listeners. Remote access goes through a controlled edge: TLS termination, IP allowlists (deny by default), and forwarding only to localhost on the worker. The dashboard and control-plane APIs are the public surfaces; the database protocol is not left open to the internet as a default.

That design matters more in a multi-tenant host than in a single-app VPS. One accidental public DB port is not one app at risk-it is every tenant behind that listener. Keeping engines private is part of isolation, not only perimeter hygiene.

Skripsido is different: a thesis-writing platform with Nuxt on the front and Go / NestJS backends over PostgreSQL. End users never receive a database connection string. Browsers and clients only hit HTTPS APIs. Auth, payments (Midtrans), and the document engine talk to Postgres from the private side of the stack. The database binds privately; credentials live with the backends; product features enforce sessions and authorization before any write happens.

Same rule, two shapes: QubicDB needs a TLS protocol edge because customers must speak Postgres securely. Skripsido needs application-only access because customers should never speak Postgres at all. Both refuse a public engine port as the default path.

Trade-offs you should expect

Convenience drops a little. Connection strings point at the edge, not at the raw engine. Local debugging may need a tunnel or a temporary allowlist entry. That friction is intentional: it forces every remote path through policy you can reason about.

Operational complexity rises when you add a proxy or VPN. You now own certificates, allowlist CRUD, and health checks for the edge. In return you gain a single place to revoke access, require modern TLS, rate-limit connection floods, and keep engine ports invisible to the public internet.

On QubicDB that cost is product infrastructure-worth it because secure remote access is the feature. On Skripsido the cost is mostly discipline: no leaked compose ports, no “temporary” public bind that becomes permanent.

A short checklist

  • Does any database port answer on a public IP?
  • Can an unknown internet host attempt a login before hitting an allowlist or private network?
  • Is TLS required for remote clients, or optional?
  • Do containers or VMs publish DB ports beyond localhost?
  • If credentials leak, is the engine still unreachable without network access?

If the answer to the first two is yes, fix the network path before polishing application features.

Takeaways

A public database port optimizes for the first connection and against every day after that. Safer systems keep the engine on a private listener, put TLS and policy at a deliberate edge, and assume scanners will find anything you leave open.

You do not need a perfect zero-trust design on day one. Start with a non-negotiable rule: the database never faces the internet. In QubicDB that rule becomes a TLS edge and allowlists in front of tenant engines. In Skripsido it becomes application-only access through hardened APIs. Everything else-proxy, VPN, least-privilege roles-is how you keep useful remote access without breaking that rule.