Skip to main content
Popsink connectors are always the client. A connector worker pod opens an outbound TCP connection to your database and keeps it open; nothing in Popsink ever listens for a connection coming from your source. So the only networking question that ever matters is:
Can a pod in the Popsink namespace open a TCP socket to the source host and port?
If the answer is yes, there is nothing to configure. If the answer is no — because the database lives in a data centre, behind a corporate firewall, or in a VPC that is not the cluster’s — you need a tunnel. This page covers the three shapes we see in practice. This is a data plane concern only. The control plane never reaches into your network at all; see Control plane and data plane for why.

Choosing a shape

A rule of thumb: IPsec if the source is a data centre and your network team is willing, SSH if the source is a single database with a bastion already in front of it, an overlay if no inbound rule is ever going to be approved. They also compose — an IPsec tunnel to the data centre plus an SSH tunnel to one stubborn host inside it is a perfectly normal end state. Whichever you pick, the tunnel only has to carry the database port. Popsink needs no inbound path, no reverse tunnel and no callback.

IPsec site-to-site VPN

The classic enterprise answer: a permanent encrypted tunnel between the network the Popsink data plane runs in and the network the source lives in, terminated by a gateway on each side. Once it is up, the source is simply routable and connectors are configured with its private address as if it were local.

What to agree on before anything is built

IPsec fails at the first mismatched parameter, and the error messages are famously unhelpful. Fill this table in with your counterpart before touching a gateway. Keep the traffic selectors as narrow as both sides can live with. A /32 on the remote side and a single egress address on ours is a much easier security review than two /16s.

The routing gotcha

This is the step that goes wrong on almost every first attempt. Connector workers are pods, so their source address is a pod IP from the cluster’s CNI range — not a node IP and not something the remote side has ever heard of. Two consequences:
  1. If you declare the pod CIDR as the local traffic selector, the remote firewall team has to accept a range that means nothing to them, and overlapping RFC 1918 space becomes likely.
  2. If you declare the node or subnet CIDR instead, traffic must be SNAT’d to that address on the way out, or it will leave with a pod IP that the tunnel’s policy drops.
The clean answer on managed Kubernetes is to give the cluster a stable egress address and present that to the tunnel:
Put the node group in private subnets whose route table sends 0.0.0.0/0 at a NAT gateway with a fixed Elastic IP, and attach the Site-to-Site VPN to the same VPC. The remote selector is then the NAT gateway’s EIP or the private subnet CIDR, and traffic is SNAT’d for you.
Check for CIDR overlap first. If your data centre and the cluster VPC both use 10.0.0.0/16, no amount of IPsec configuration will fix it — one side has to renumber or you have to NAT the remote subnet into a spare range. Confirm this on day one, not after the change window.

MTU

ESP encapsulation adds overhead, and a database connection that completes its handshake then hangs on the first large result set is almost always a fragmentation problem. Clamp TCP MSS on the gateway:
Cloud-managed gateways do this themselves; self-managed strongSwan does not.

Verifying

From inside the cluster, before you create the connector:
A succeeded here means every connector in that namespace will be able to connect. A timeout means routing or firewall; a connection refused means the tunnel is fine and the database is not listening where you think it is.

SSH tunnel

The only pattern Popsink implements itself. When the source sits behind a bastion (jump host) that you already expose, the connector opens an SSH session to the bastion and forwards the database port through it — no change to your network, no gateway, no ticket to the network team. It is configured per connector, in the connector form, and is supported by:

PostgreSQL

MySQL

Oracle

SQL Server

IBM i (DB2)

Configuration

The Host and Port of the connector itself stay as the database sees them from the bastion — usually a private address such as 10.90.4.12 or db.internal, not something resolvable from the cluster.

Preparing the key

Generate a key pair dedicated to Popsink — never reuse an operator’s personal key — and leave it without a passphrase, since no one is present to type one when a connector restarts.
Paste the single-line output of step 3 into SSH Private Key. It is stored encrypted at rest with your deployment’s connectorConfigEncryptionKey, like every other connector credential.

Hardening the bastion account

The Popsink account needs to forward one port and nothing else. Express that in authorized_keys rather than trusting it to behave — this account cannot then open a shell, forward anything but the database, or be used to pivot:
  • restrict turns everything off — PTY, agent forwarding, X11, ~/.ssh/rc, and all port forwarding — and keeps doing so for any option OpenSSH adds later;
  • port-forwarding puts back only forwarding, which restrict had just removed. Omitting it is the usual mistake: permitopen narrows forwarding, it does not re-enable it, so the tunnel fails with administratively prohibited;
  • permitopen then narrows that forwarding to exactly one destination;
  • from pins the cluster’s egress address (see the NAT gateway discussion above — you need a stable egress IP for this to be usable).
Give the account no shell either (/usr/sbin/nologin) so the key is good for the forward and nothing else.

Limitations

Each connector establishes its own SSH session. Ten connectors behind the same bastion means ten concurrent sessions from the same account — make sure MaxSessions and MaxStartups in sshd_config are not set low enough to start refusing them.
The connector forwards through one bastion. A chain of two jump hosts is not expressible in the form; put a ProxyJump on the bastion side, or use an IPsec tunnel instead.
Connector workers restart unattended — on upgrade, on node drain, on failure. A key that needs a human is a connector that stops replicating at 3 a.m.
Rotating the key pair means updating the connector configuration. Plan for a short pause on that connector; replication resumes from its last offset, so nothing is lost.

Overlay and gateway services — Tailscale and ngrok

These sidestep the network team entirely: both ends dial out to a coordination service and the source becomes privately routable on a virtual network. No inbound firewall rule, no gateway appliance, no static IP, and in both cases the source is never published on the public internet. That makes them the practical answer when the source sits in a network you cannot re-architect — a customer’s data centre, an acquisition still on its own infrastructure, anywhere an inbound rule would take a quarter to approve.

Tailscale (WireGuard mesh)

The usable pattern is a subnet router: one host inside the source network advertises the database subnet to the tailnet, and the Popsink cluster joins the same tailnet and accepts those routes.
1

Advertise the source subnet

On a small Linux host inside the source network:
Approve the advertised route in the Tailscale admin console, or set autoApprovers in the ACL so it does not need a human.
2

Join the cluster to the tailnet

Install the Tailscale Kubernetes operator in the cluster and expose the source as a Service that connectors resolve normally. An ephemeral, tagged auth key is the right credential here — not a personal one, which disappears when its owner leaves.
3

Lock it down with ACLs

The default tailnet policy is allow-all. Replace it so the Popsink tag can reach the database port and nothing else:
4

Point the connector at the private address

10.90.4.12:1521 — the same address the subnet router sees. Verify with the netshoot pod from the IPsec section before creating the connector.
Tailscale keys expire. A node whose key expires drops off the tailnet and every connector through it stalls. Use tagged nodes (whose keys do not expire by default) or put the renewal in your calendar.

ngrok (Gateway, internal endpoints)

Use ngrok’s site-to-site connectivity, not the public ngrok tcp endpoint you may have seen in tutorials. The database is published as an internal endpoint — a .internal address that is not addressable from the public internet at all and can only be reached through a forward-internal traffic policy — and the cluster consumes it through a Kubernetes-bound endpoint that the ngrok Operator projects as an ordinary Service. The result is that a connector talks to a normal Kubernetes DNS name, and nothing is ever exposed publicly. Both ends only make outbound connections on TCP 443.
1

Publish the database as an internal endpoint

Run the agent next to the database, in the source network. The .internal suffix is what makes the endpoint private — internal domains are namespaced to your account, so the name is yours alone:
2

Install the ngrok Kubernetes Operator

In the cluster running the Popsink data plane. It dials out to ngrok on TCP 443 and needs no inbound rule.
3

Bind the endpoint into the cluster

A CloudEndpoint bound to kubernetes forwards to the internal endpoint. Name it for the namespace the connectors run in — the URL’s host and namespace parts become the Service:
The operator projects an ExternalName Service called source-db in the popsink namespace pointing at its own forwarder.
4

Point the connector at the Service

Host source-db.popsink, port 1521 — the connector resolves it through cluster DNS like any in-cluster service, with no SSH tunnel and no VPN fields to fill in. Verify with the netshoot pod from the IPsec section first.
Because the binding is kubernetes, the endpoint is reachable only from inside clusters where the operator is installed — it is not public, and it is not reachable from elsewhere in your ngrok account either. Traffic policy also sees conn.k8s.pod.* metadata, so access can be scoped to specific workloads.
The trade-off to weigh at review time is not exposure, it is dependency: the data path runs through ngrok’s cloud, so ngrok availability becomes replication availability, and you are trusting their transport. Connections are end-to-end encrypted between the two agents, and enabling TLS on the database itself keeps the credentials and rows opaque to the transport regardless.

Firewall rules, by pattern

Note that the last two patterns need no inbound rule at all on either side — that is the whole reason to reach for them. In every row, the data plane still needs its own outbound HTTPS to the control plane — that requirement is unchanged and independent of the tunnel.

Troubleshooting

Deployments and environments

One deployment per isolated network — why a tunnel is often the alternative to a second deployment.