Walkthrough · AWS

Boundary in AWS: reaching a network with no way out

The hardest of the three, and the most interesting. The target sits in a VPC with no internet access at all — no gateway, no NAT, nothing. One worker cannot reach both HashiCorp and the target, so we chain two together and let them dial each other.

· · Intermediate · 3–4 hours · Start with the guide →

He who loves Mum, Ei, and Arsenal — and is still chasing the moon🌙

Proudly a HelloCloud ACE candidate.

A note, humbly: I am not an expert, and none of this is a brag. It is all about learning how to learn — and how to unlearn — and cherishing the process somewhere along the way. If you got this far, I hope you learned something too.

Read it, then build it 🛠️

This page is written to be followed along with, not just read. Open the Excalidraw board beside it and work through the steps yourself — every screen I filled in is captured there, in the order I filled it in.

Reading gives you the shape of the thing. Only doing it gives you the understanding. I did not learn any of this by reading, and I do not think anyone does.

Open the AWS board ↗

Do the other two first. This build assumes you have already done the Azure walkthrough — worker install, registration, credential stores and targets are covered there in detail and only summarised here. Start with What is HashiCorp Boundary? if the vocabulary is new.

Why one worker is not enough

In the Azure build, our single worker had two jobs: reach HashiCorp's control plane outbound, and reach the target VM over the peering. It could do both because it had a NAT gateway for the first and a peering for the second.

Now imagine the target's network has no internet access at all.

A worker placed in that network could reach the target, but could never register with HCP. It has no path to the internet. A worker placed in the other network could register fine, but might not be allowed to reach across into a network that is deliberately sealed.

So we use two, and chain them. Worker 1 lives in the boundary VPC and can reach HCP. Worker 2 lives in the profile VPC next to the target. Worker 2 never talks to HashiCorp at all. It talks to worker 1, and worker 1 talks to HashiCorp.

The session path becomes: laptop → HCP managed worker → worker 1 → worker 2 → target.

The idea that makes it work

Before building anything, there is one concept to get straight, and it explains every firewall rule that follows.

Data flowing downward while TCP connections are established upward
The traffic goes one way; the connections are made the other way. Open the AWS board ↗

Your data flows down the chain toward the target. But every TCP connection in that chain is established in the opposite direction. Each worker dials its upstream and holds the connection open.

Which means, and this is the payoff:

Before you start

Money. This is the most expensive of the three: four instances plus a NAT gateway. NAT gateways bill hourly regardless of traffic. Set a budget alert, and do the clean-up step the same day.

Step 1 · Two VPCs and a peering

The boundary VPC

Address range 192.168.80.0/24, with two subnets:

The profile VPC

Address range 192.168.90.0/24, one private subnet, holding worker 2 and the target. No internet gateway. No NAT gateway. This network has no path to the internet in either direction, which is the entire point of the exercise.

Peer them — and write the routes yourself

Create a VPC peering connection from the boundary VPC to the profile VPC, and accept it on the other side.

AWS does not add the routes for you. This is the big difference from Azure and Google Cloud. After the peering is active you must edit the route table in each VPC and add an entry pointing at the other VPC's range via the peering connection. Miss either one and traffic fails in a way that looks like a firewall problem but is not.

Step 2 · Security groups as a chain

Write these in the order traffic dials, and they become obvious. Remember: each worker dials up, so inbound rules appear on the upstream machine.

bastion SG          (temporary — delete this machine later)
  inbound   SSH 22    from your own public IP
  outbound  SSH 22    to worker 1 and worker 2

worker 1 SG         (boundary VPC, private subnet)
  inbound   SSH 22    from the bastion          (setup only)
  inbound   TCP 9202  from worker 2             (worker 2 dials in)
  outbound  TCP 9202  to HCP managed workers    (worker 1 dials out)

worker 2 SG         (profile VPC, private subnet)
  inbound   SSH 22    from the bastion          (setup only — removable)
  outbound  TCP 9202  to worker 1
  outbound  SSH 22    to the target

target SG           (profile VPC, private subnet)
  inbound   SSH 22    from worker 2 only
  outbound  nothing

Look at worker 2's rules once the bastion is gone: no inbound rules at all. Nothing connects to it. It reaches worker 1 by dialling out, and it reaches the target by dialling out. That is the deepest machine in the chain, and it is also the one with the least exposure.

Step 3 · Four machines

MachineVPC / subnetAddressPurpose
bastionboundary / publicpublic IPCopy binaries. Delete afterwards.
worker 1boundary / private192.168.80.252Talks to HCP. Accepts worker 2.
worker 2profile / private192.168.90.225Talks to worker 1. Dials the target.
targetprofile / private192.168.90.199The server we want.

Step 4 · Two workers

Install Boundary on both worker machines exactly as in the Azure walkthrough — same download, same systemd unit. The configuration files differ.

Worker 1 — the one that reaches HCP:

hcp_boundary_cluster_id = "<your-cluster-id>"

listener "tcp" {
  address = "0.0.0.0:9202"
  purpose = "proxy"
}

worker {
  public_addr       = "192.168.80.252"
  auth_storage_path = "/home/ubuntu/boundary/worker1"
  tags {
    automated = ["ansible"]
    owner     = ["kst"]
    country   = ["india"]
    region    = ["ap-south-1"]
  }
}

events {
  audit_enabled        = true
  observations_enabled = true
  sysevents_enabled    = true
  sink "stderr" {
    name        = "all-events"
    event_types = ["*"]
    format      = "cloudevents-json"
  }
}

Worker 2 uses the same shape, but its upstream is worker 1 rather than HCP, it is configured to dial 192.168.80.252:9202 instead of the cluster. Register worker 1 first, then worker 2; the chain has to be built from the top down, because worker 2 has nothing to dial until worker 1 exists.

Turn the event logs on. The events block above is worth keeping. When something in a multi-hop chain does not work, the worker's own log is the fastest way to find out which link is missing — far faster than guessing at security groups.

Controller's view: worker 1 reachable directly, worker 2 reachable via worker 1
How the controller sees the chain once both are registered. Worker 2 is not unreachable — it is reachable via worker 1, and the controller knows that because worker 1 reports it.

Step 5 · Target and egress filter

Create the credential store and SSH target as before, pointing at 192.168.90.199:22. Then set the egress worker filter to select worker 2 — the one sitting next to the target:

"ap-south-1" in "/tags/region"

The controller now knows worker 2 makes the final hop, and it already knows worker 2 is reachable via worker 1. It works the rest out itself.

Step 6 · Proving the whole chain

Connect with boundary connect ssh. Then, while the session is open, go and look at all four machines. This is the most instructive twenty minutes in the entire series.

Your laptop

lsof on the laptop showing connections to the controller on 443 and a worker on 9202
lsof -nP -iTCP -a -c boundary. Several connections on 443 to the controller, and one on 9202 to 3.224.38.35 — an HCP managed worker. Your session enters the chain through HashiCorp's machine, because your own workers have private addresses.

Worker 1

ss on worker 1 showing outbound connections to HCP and inbound from worker 2
Two queries. dport = :9202 shows worker 1 dialling out to HCP. sport = :9202 shows connections arriving in from 192.168.90.225 — worker 2. Worker 1 is both a client and a server, and it opened the upstream half itself.

Worker 2

ss on worker 2 showing outbound connections to worker 1 and nothing inbound
The important half of this picture is the empty half. Worker 2 has outbound connections to 192.168.80.252:9202, which is worker 1. The sport = :9202 query returns nothing. Nobody connects to worker 2, ever.

The target

ss on the target showing no 9202 connections and one SSH connection from worker 2
Four queries. No connections on 9202 in either direction — the target does not speak Boundary's protocol and never did. One connection on port 22, from 192.168.90.225, owned by sshd. Ordinary SSH from a neighbour.

Read those four in order and the architecture stops being a diagram. Your laptop talks to HashiCorp. Worker 1 talks to HashiCorp and hears from worker 2. Worker 2 talks to worker 1 and hears from nobody. The target hears plain SSH from a machine on its own subnet. At no point does anything require an inbound path from the internet.

How the controller works out the route

A fair question at this point: worker 2 has no configuration mentioning the target. It does not know the address. So how does it know where to connect?

It is told, in this order:

  1. You ask. You say "connect me to tssh_TnK5FbClBJ". The controller opens that target's record and reads the address off it — 192.168.90.199, port 22.
  2. The controller picks the worker. Your egress filter says worker 2, so worker 2 will do the dialling.
  3. The instruction goes down the chain. When worker 2 asks "is this session real?", the answer is not just yes. It is: yes — and when the bytes arrive, open a TCP connection to 192.168.90.199:22.
  4. Worker 2 dials the target. A plain, ordinary TCP connect, the same thing ssh would do from that machine.
  5. It becomes a pipe. Worker 2 holds two connections, one facing up the chain and one facing the target, and copies bytes between them.

Worker 2 never reads a config file about your target and never looks anything up. It is a courier that gets told the address at the moment of delivery.

AWS gotchas

Peering routes are manual

The one that will get you. Azure and Google Cloud add peering routes automatically; AWS does not. Two route tables, one entry each, and the symptom of forgetting is silence rather than an error.

Build the chain from the top down

Register worker 1 before worker 2. Worker 2's whole existence depends on having an upstream to dial, and registering it first produces a confusing failure.

The bastion is scaffolding

It exists to copy a binary onto two machines. Delete it the moment both workers are running. A public SSH host with no remaining purpose is exactly the kind of thing that quietly becomes permanent. Mine outlived its usefulness by several days because nothing forced the issue.

Do not add hops for their own sake

Multi-hop is not a more secure version of the single-worker design; it answers a different question. Reach for it when the network genuinely has no way out. The security benefit of a third hop over a second is close to zero if the second was already private, and every hop is another machine to patch.

Clean up

Terminate all four instances, delete the NAT gateway — it is the expensive part — then the peering, then both VPCs. Remove both workers, the target and the credential store from the Boundary admin console.

Where to go next. You have now built all three designs. The comparison — why they differ, how to choose, and the one config line that decides which you get — is in Who actually carries the packets? To replace the password login with a real company account, see Boundary and Entra ID.