Concept 12. By Nicolas Dao · Last reviewed

Coming in, or going out?

Should your people come into the private network, or should one service go out to the public, and is anything still listening on a public address?

What you'll be able to decide

Which way each connection should go: what gets a public endpoint, who comes in over a mesh network, and which service goes out through a tunnel; why a VPN or a bastion is rarely the answer now; and how to check that nothing else answers on a public address.

Private access is a question of direction. What the public must reach gets a public endpoint, on purpose, and only one per job. Your own machines (laptops, CI runners, agent boxes) come in over a mesh network that opens no port; a VPN or a bastion does the same job at more cost and with more to run. A service the public or a client's staff must see, on a box with no open port, goes out through a tunnel: the box calls out, nobody calls in. Decide the direction per connection, then list what still answers on a public address. Most boxes need a way in and a way out, and the two don't compete.

In short

The options

  1. A. A public endpoint

    Something listens on the internet, on purpose: the product's own API or site. Right for what the public must reach, and only one door per job.

  2. B. A VPN or a bastion

    AWS's own way to bring people into a VPC: an endpoint billed by the hour before anyone connects, with certificates and a directory to set up, or one more server to patch.

  3. C. Coming in, over a mesh network

    Your own machines join a private network that opens no port, and one policy file decides who reaches what. Any protocol: SSH, databases, internal services.

  4. D. Going out, through a tunnel

    The box opens a connection out to a public network, and visitors reach the service down it, behind a login if it isn't for the public. HTTP only, with no inbound port.

The rule of thumb

Tailscale is for my machines coming in; Tunnel is for a service going out to the public.

From Tailscale

How the answer changes with scale

  1. One box: agents, previews, webhooks, an internal tool

    Both, on the same box: services go out through a Tunnel, your machines come in over Tailscale, every app binds to 127.0.0.1, and no inbound port stays open except a locked-down SSH, or none.

  2. A VPC with private databases, and a team that needs them

    Two subnet routers in two zones, tagged so their keys don't expire, with split DNS to the VPC resolver. No bastion, no Client VPN, no public address on the database.

  3. A client's staff who will never install an agent

    An Access login in the browser, through a Tunnel: free up to 50 users, then $7 per user a month.

  4. A public app with real traffic

    A public endpoint, on purpose. Tunnel replicas give failover, not load balancing: a load balancer carries the load, or CloudFront in front of an internal one.

The problem, in plain words

Two kinds of people need to reach a system. The public, who use the product. And your own side: a developer running a query on the production database, a CI runner applying a migration, an agent reading a private service, a client reviewing a preview of their new site.

The old answer gives both the same kind of door: a public address, with a password, a security group full of home IP addresses, or a VPN gateway in front of it. Each of those doors answers on the internet, and a public IP on an always-on box is a target from day one.

So ask a different question first: which way does the connection go? Your own machines can come in, into a private network that never opens a port. A service that must be seen can go out: it opens a connection to a public network, and its visitors come back down that connection, so nothing on your side listens at all. Only what the public must reach needs a public endpoint, and then it should be the only one.

The picture

Four ways to reach a service. The first three are designs; the fourth is the trap inside the third.

  1. A public endpoint

    1. Anyone on the internet
    2. A public address that answers
    3. The service

    Right for what the public must reach. A public IP on an always-on box is a target from day one.

  2. Coming in: a mesh

      • Laptop
      • CI runner
      • Agent box
    1. Tailscale: the policy file decides
    2. Subnet router in the VPC
    3. Private database

    No port is opened, and nothing in the VPC gets a public address.

  3. Going out: a tunnel

      • A visitor
      • A client's staff
    1. Your domain on Cloudflare, Access login
    2. The connection the box opened
    3. Service on 127.0.0.1

    The box calls out, nobody calls in.

  4. A tunnel that closes nothing

    1. Anyone on the internet
    2. The box's public IP
    3. Service on 0.0.0.0, around Cloudflare and the login

    The service still listens on every address: bind it to 127.0.0.1 and drop inbound traffic.

Four ways a connection reaches a service: a public address that anyone can reach; coming in over a mesh network, from your own machines to a private database; going out through a tunnel the box opened, to a public name behind a login; and a tunnel that closes nothing, because the service still listens on every address.

The options

A public endpoint, for what the public must reach. The product's own API has to answer on the internet, and that's fine: it's the job. A function URL is my favourite way to put Lambda on the internet, API Gateway comes in when you need what only it does at the door, and a load balancer in front of a Fargate service once the API is busy all day. Two rules come with it. One door per job: a public URL with auth NONE bypasses everything API Gateway enforces, so a function URL behind API Gateway gets IAM auth, or no URL at all. And nothing private gets one: function URLs are public endpoints only, so an API that must stay off the internet is API Gateway's REST private API. The public door doesn't even have to be yours. CloudFront's VPC origins reach an internal load balancer or instance, so nothing of yours faces the internet.

A VPN or a bastion: AWS's own way in. The classic way to bring people into a VPC. AWS Client VPN in Sydney is $0.15 per subnet association an hour (about $110 a month) plus $0.05 per connection-hour, and it comes with certificates and a directory or SAML to set up. A bastion is one more server to patch. Both are too expensive and too complicated for the job. Don't build either so a team can reach a database: a mesh network does it with no bastion to patch and no VPN endpoint billed by the hour. The same goes for two networks that must talk privately (a VPC and a VPS, two clouds): no site-to-site VPN needed.

Coming in: a mesh network. Tailscale builds a private network between your devices with WireGuard, and handles the hard parts: keys, NAT traversal, and who may reach what. It reaches a box behind any NAT without opening a port. Access is one policy file, in code, instead of security groups full of home IP addresses. Into AWS, one small subnet router in the VPC advertises its range, and a laptop, a CI runner or an agent box reaches a private Aurora endpoint as if it were local. Any protocol works: SSH, databases, internal services.

Coming in moves the risk from the internet to your own devices. Tailworm, in October 2026, showed what that means: from a device already authorised on the tailnet, Tailscale SSH and tailscale serve could lead to root on the servers that device was allowed to reach. Most of the findings needed an approved account or a compromised device in the tailnet, and none was a break-in from the internet. Tailscale shipped the fix, 1.104.4, about four days after the report, and subnet routers weren't in any finding. The lesson outlives the bug: give each device only what it needs, and keep the features layered on top of the network small.

Going out: an outbound tunnel. Cloudflare Tunnel turns it around: the box calls out, nobody calls in. cloudflared runs next to the service and opens outbound connections to Cloudflare, and a hostname on your domain routes down that connection to a port on localhost. No inbound port, no public IP to defend, no load balancer, and Cloudflare's TLS and DDoS protection in front. Put an Access login on anything that isn't for the public, and nobody reaches it without signing in. It's an HTTP proxy with Cloudflare's limits: 125 seconds to answer, 100 MB per request on Free and Pro, and several replicas give failover, not load balancing. For a single box, previews, internal tools and webhook receivers, that's exactly enough.

Inside AWS, neither. One service calling another in the same AWS network isn't anyone coming in or going out. Security groups and VPC endpoints do that job, not a mesh. Leave NACLs at their default and do the work in security groups, with rules that reference other security groups instead of IP ranges: NACLs are stateless and need the return traffic's ephemeral ports opened, so tightening one silently breaks responses.

The mesh and the tunnel don't compete. They point in opposite directions: one is the way in for your team, the other the way out for the public. There's nothing to choose between them inside one system. Most boxes I run use both.

A concrete scenario

A company in Sydney runs a booking product on AWS: an API on Lambda function URLs, Aurora in private subnets, a nightly import as a Fargate scheduled task, and one VPS for its agents and for client previews. Here's everyone who needs to reach something, and the door each one gets:

Who connects To what Which way The door
Customers The API Coming in, from the public A public endpoint, on purpose: the function URL
Stripe The app, with payment events Neither Straight to EventBridge in the company's account: no public webhook endpoint to write or secure
Developers, the CI runner, the agent box Aurora Coming in, from its own machines Tailscale: two subnet routers in two zones, split DNS to the VPC resolver. No bastion, no Client VPN, no public address on the database
The client's staff, who won't install anything A preview on the VPS Going out A Tunnel on a real hostname, behind an Access login, with the preview bound to 127.0.0.1
The company's engineers A shell on the VPS Coming in Tailscale, with the firewall dropping all other inbound traffic
The nightly import Its container image and a partner's API Out, and only out A public subnet, a public IP and a security group that allows no inbound traffic. It has an address, and nothing answers on it. No NAT gateway, at about $43 a month before data, for every zone
The API's functions Aurora Inside AWS Security groups that reference each other. Not Tailscale

One door faces the internet, on purpose: the API. Every other connection is opened from the inside, or never crosses the internet at all. The two things I'd check first are the ones that quietly undo it: a preview still listening on 0.0.0.0, reachable around the tunnel and its login, and a database security group someone opened to a home IP address.

The trade-offs

A public endpoint A VPN or a bastion A mesh (coming in) A tunnel (going out)
Who connects Anyone Your people, into the VPC Devices on your tailnet, allowed by the policy file Anyone, or only people who pass an Access login
Answers on a public address Yes, on purpose Its gateway does, by design No: no port is opened No: the box calls out
Fits The product's own API and site Rarely, now that a mesh does the job better Your machines reaching private databases and services, any protocol One HTTP service on a box: previews, internal tools, webhooks
Before anyone connects A function URL is free; a load balancer has a monthly floor About $110 a month per subnet association, or a server to run Standard $8 per user a month Tunnel free; Access free up to 50 users
The trap A second door: a public URL with auth NONE bypasses API Gateway More to run than the job needs Device keys expire after 180 days, and an expired subnet router cuts everyone off It closes nothing: a service on 0.0.0.0 is reachable around it. A stopped tunnel shows error 1016

The rule of thumb

Decide the direction first, then count the doors. Your own machines come in, over a mesh. A service the public or a client must see goes out, through a tunnel, unless it's the product itself at real volume, which gets a public endpoint on purpose. Services inside AWS talk through security groups. Then list everything that answers on a public address. Anything on that list that isn't meant for the public is a door to close.

When the answer changes with scale

  • One box (agents, previews, webhooks): both, on the same box. Services go out through a Tunnel, your machines come in over Tailscale, every app binds to 127.0.0.1, and no inbound port stays open except a locked-down SSH, or none.
  • A VPC with private databases: two subnet routers in two zones, tagged so their keys don't expire, with split DNS pointing at the VPC resolver. One router is one point of failure.
  • A client's staff who'll never install an agent: an Access login in the browser, through a Tunnel: free up to 50 users, then $7 per user a month.
  • A public app with real traffic: a public endpoint, on purpose. Tunnel replicas give failover, not load balancing, and Tailscale Funnel only serves *.ts.net names, on three ports, and is in beta. A load balancer carries the load, or CloudFront in front of an internal one.
  • Client work: the client's own tailnet on a Standard plan, signed in with their identity provider. The free Personal plan is for non-commercial use.

Before you open a door

  • Bind to 127.0.0.1, then drop inbound. A tunnel doesn't close anything: if the service still listens on 0.0.0.0, it's reachable on the box's public IP, around Cloudflare and around your Access login. Keep your own SSH path before you drop the rest.
  • Tag the servers. Device keys expire after 180 days by default, and tagged devices have key expiry disabled by default. Make enrolment keys single-use or ephemeral: a captured reusable key lets an attacker enrol devices.
  • Keep the layers on top small. Run Tailscale 1.104.4 or later on every node, set TS_SSH_DISABLE_FORWARDING=1 where you don't need forwarding, and use check mode and non-root users in the SSH rules. Four bulletins in one release tell you SSH and Serve are the layers to keep small. For a shell on an AWS instance, SSM needs no open port and is governed by IAM.
  • Close the second door. A function URL behind API Gateway gets IAM auth, or no URL at all.
  • Network access isn't AWS sign-in. Tailscale decides who can reach the network; Identity Center decides who can touch the account. A client gets both.
  • Put it in code. The policy file, tags and auth keys in Pulumi next to the VPC; the tunnel, its hostname-to-service rules and its DNS records in the same program.

Questions that change the answer

  • Who connects: your own machines and tools, or the public and a client's staff? Your machines come in; for the public, a service goes out, or the product gets a public endpoint.

  • Will the people connecting install an agent? Staff who never will are better served by an Access login in a browser, through a Tunnel.

  • Is it HTTP? SSH and databases through a Tunnel need cloudflared on every client, so they belong on the mesh.

  • Must it answer on your own domain, at real volume? Tailscale Funnel serves *.ts.net names only, and tunnel replicas give failover, not load balancing.

  • Is it one service calling another inside AWS? Then it's neither: security groups and VPC endpoints do that job.

  • What answers on a public address today? Anything that isn't meant for the public is a door to close.

Doesn't cover

Check your call

Decide before you open each answer.

  1. A developer must run a query against a production Aurora database that has no public address.

    Coming in: Tailscale. A subnet router in the VPC (two, in two zones), split DNS for the private names, and a policy rule that gives that developer that endpoint. No bastion, no Client VPN, and no public address on the database.

  2. A client's marketing team must review a preview of their new site, on a box with no public port. They won't install anything.

    Going out: a Cloudflare Tunnel on a real hostname, behind an Access login in the browser. Bind the preview to 127.0.0.1, so the tunnel is the only way to it.

  3. An engineer wants to reach Postgres on that same box, through the tunnel that's already there.

    That's coming in, not going out. A database through a tunnel needs cloudflared on every client: put the engineer's laptop on Tailscale instead.

  4. Payment events from Stripe must reach an app that runs on AWS.

    No public endpoint at all: Stripe delivers events straight to EventBridge in your account. On a box with no public port, a Tunnel receives the webhook instead.

  5. An IT manager asks for AWS Client VPN so that five staff can reach an internal admin tool in the VPC.

    First ask whether they'll install an agent. If they will, Tailscale; if they won't, a Tunnel with an Access login. Client VPN costs about $110 a month per subnet association before anyone connects, plus certificates and a directory or SAML to set up.

The technology on this site

Read next

  1. 08 Where may the data go? Must the data, the prompts and the logs stay in Australia, and what does each service do with them by default? Residency is a property of every hop a request takes, not of the app: the database, the files, the email, the analytics, the logs and the model. Most global platforms send data wherever they like by default; some keep it in Australia if you choose a region or a profile, sometimes for a premium; some have only a best-effort hint, or nothing. List every hop, check each one, and the answer is the weakest hop. Where it must stay, keep it in the client's own AWS account in Sydney; where it needn't, the global tools are simpler.
  2. 07 Pay for the work, or pay to be ready? Should this part of the system cost nothing when it's idle, or run all the time so it's always ready? Every service has a cost shape: a floor (what it costs doing nothing) and a slope (what each unit of work adds). Scale-to-zero has no floor and the steepest slope, plus a wait after a quiet spell. Always-on has a floor and a flatter slope. A commitment discounts the floor for years. Read two numbers, the load at 3 am and the average against the peak: near zero at night means pay per use; high all day means pay to be ready, then commit to the floor once it's proven.
  3. 11 Who holds your keys, and for how long? Should people, pipelines and apps carry long-lived keys or borrow short-lived roles, where does each secret live, and which code you didn't write runs next to them? Every credential answers three questions, usually by accident: who holds it, how long it works, and what else runs next to it. A long-lived key works from anywhere until someone revokes it, and outlives the person who made it. A borrowed role works for one session: a person through SSO, a pipeline through OIDC, a function through its own role, so there's nothing stored to leak. The secrets that can't be a role (a database password, a Stripe key, a model provider's key) go in the secret store, one job per key, read at runtime by reference. And the code you pull runs with all of them: a tag can be moved, so pin it to a SHA, a digest or a hash.

Want this stack in your AWS account?

Cloudless Labs is a Sydney consultancy. We migrate on-premises systems to AWS, build serverless apps on this stack, and coach teams to ship with Claude Code and Codex.

Talk to us

Contact Us

For general inquiries, please contact us at:

For e-learning capabilities, please contact: