DIY Multi-Hop Boundary Sessions without HCP
Creating an unofficial ingress/egress worker setup for HashiCorp Boundary without HCP or enterprise licensing
I treat my homelab as if it has the same security requirements as a production environment because for me it is a production environment. I normally use Tailscale for protected access and avoid exposing services to the internet. Some remote networks block Tailscale or WireGuard connections, so I also need a break-glass option.
As with many of my other posts, you can see I use HashiCorp's suite of tools. Boundary provides identity-based access to systems in my homelab without SSH tunnels or exposing each system to the internet. It is also a single binary that runs without administrative access, which helps when I cannot install VPN software on a machine.
The problem
My homelab budget does not stretch to the enterprise version of Boundary or HCP (HashiCorp Cloud Platform). The community version does not support the ingress and egress workers required for multi-hop sessions.
I want multi-hop sessions so I can keep at least some VLAN isolation without letting the server in the DMZ connect directly to every server in my homelab.
The topology I wanted:
graph TB
accTitle: DIY Boundary multi-hop network topology
accDescr: Diagram of three network zones. A client laptop in the Internet zone connects to a Boundary controller hosted with a DMZ worker and FRP server in the DMZ. An internal worker in the Internal VLAN registers outbound with the controller, holds a tunnel to the FRP server, and reaches internal resources, so the DMZ never connects directly into the internal VLAN.
subgraph Internet
client[Client/Laptop]
end
subgraph DMZ
controller[Boundary Controller]
dmz_worker[DMZ Worker]
frps[FRP Server]
end
subgraph Internal_VLAN[Internal VLAN]
internal_worker[Internal Worker]
internal_resources[Internal Resources]
end
internal_worker -- 1. Register --> controller
client -- 2. Connect --> controller
client -- 3. Session --> dmz_worker
internal_worker <--> frps
dmz_worker -- 4. Connect --> frps
internal_worker -- 5. Access --> internal_resources
My DIY solution
My workaround depends on Boundary's connection flow:
- The client (my laptop) connects to the controller
- The controller provides an authorization token
- The client connects to the worker port using this token
- The worker establishes the session with the target
I run an FRP (Fast Reverse Proxy) server alongside the DMZ Boundary controller and worker. The internal workers connect outward to that server, so I do not need firewall rules that permit inbound connections to the internal workers. After registration, each internal worker gets a Boundary target pointing at a unique frps port on the Boundary server's loopback interface.
The resulting session path:
sequenceDiagram
accTitle: DIY Boundary session establishment sequence
accDescr: Sequence of nine steps showing how a session is established. The client authenticates with the Boundary controller and receives a token, connects to the internal worker target through the DMZ worker, which forwards over the FRP tunnel the internal worker already holds. A second token then lets the client connect through that established tunnel to the internal resource target.
participant Client
participant Controller as Boundary Controller
participant DMZ as DMZ Worker
participant FRP as FRP Server
participant Internal as Internal Worker
participant Target as Internal Resource
Client->>Controller: 1. Authenticate & request session
Controller->>Client: 2. Return session token
Client->>DMZ: 3. Connect to internal worker target
DMZ->>FRP: 4. Forward to internal worker via FRP tunnel
FRP->>Internal: 5. Connection established to internal worker
Client->>Controller: 6. Request session for internal resource
Controller->>Client: 7. Return session token
Client->>Internal: 8. Connect to internal resource via established tunnel
Internal->>Target: 9. Connect to target resource
Setting it up
Following the standard Boundary installation process, expose the DMZ controller API, usually port 9200, and the worker, usually port 9202, to the internet. The endpoint could be boundary.tklk.dev, with Caddy providing TLS for API access. Internal workers also need to reach the cluster worker coordinator on port 9201, but that port does not need public exposure. They can reach it through the same path used for the FRP server.
Installing FRP server
Install FRP on the DMZ server:
- Download the appropriate FRP release from GitHub
- Extract the archive and locate the
frpsbinary - Create a configuration file as shown below
- Set up a systemd service (optional) to ensure it runs automatically
This is the frps configuration I use:
bindPort = 7000
auth.method = "token"
auth.token = "your_secure_token"
# meaning the internal workers can only use this range for listening on
allowPorts = [ {start = 9500, end = 9999} ]
Internal worker setup
After the FRP server is running, configure the internal workers:
-
Install and configure a standard Boundary worker
# minimal worker config # you may wish to add more configuration options, such as using a KMS listener "tcp" { # this is the listener that frpc will connect to over the loop back address = "127.0.0.1:9202" purpose = "proxy" } worker { name = "internal-worker-1" # this is the address that the boundary controller will announce to the client # so this is what the target session should listen to on your local laptop/client public_addr = "127.0.0.1:9500" description = "Worker in the internal VLAN" controllers = ["boundary.tklk.dev:9201"] } -
Install FRP client (frpc)
-
Configure frpc to establish a reverse tunnel back to the primary server on a loopback interface
This is the frpc configuration I use:
serverAddr = "boundary.tklk.dev"
serverPort = 7000
auth.method = "token"
auth.token = "your_secure_token"
[[proxies]]
name = "boundary_internal_worker"
type = "tcp"
localIP = "127.0.0.1"
# 9202 is the Boundary worker's default port, but since you may have multiple internal workers, you might want to pick
# a unique port for each worker (which you'll also need to set in the worker config)
# It is strongly recommended that you don't use port 9202 as the DMZ worker will already likely be using this port, causing the frps connection to fail
localPort = 9202
remotePort = 9500
Registering the internal worker as a target
Register each internal worker as a Boundary target pointing to the FRP server's loopback interface and the port selected in the frpc configuration. You can do this with Terraform, the Boundary CLI, or the Boundary UI.
At minimum, the target needs 127.0.0.1 as its host, the port from the frpc configuration, and a default client port that matches the worker port.
An egress worker filter can force the DMZ worker to connect to the frps port. Because the target uses localhost, Boundary may otherwise assign the route to a different worker that cannot reach it.
Using the setup
Authenticate with Boundary, then use boundary connect to establish a connection to the internal worker target.
# Authenticate with Boundary (if not already authenticated)
boundary authenticate
# List available targets
boundary targets list -recursive
Accessing an internal resource takes two connections:
-
Establish a connection to the internal worker:
boundary connect -target-id=ttcp_1234567890 # Your internal worker target IDIf nothing else on your local machine is listening on the worker port as defined above, you'll open a connection to the internal worker. In your session list, this connection may be marked as "pending" since no traffic is being sent yet.
-
Once connected, you can access targets in the internal VLAN:
boundary connect -target-id=ttcp_0987654321 # Internal resource target ID
This avoids HCP or enterprise license fees, but it adds FRP management and a second connection step.
Limitations and considerations
- You need to manage FRP alongside Boundary
- The connection process requires an extra step compared with the enterprise solution
- Nested connections can add a small amount of latency
- Protect the FRP token to prevent unauthorized access
Single-hop sessions are likely enough for my needs, with multi-hop being a tad over the top. Building this still gave me a better understanding of Boundary's connection flow and another way to debug its networking.
Boundary can also be integrated with Vault to broker SSH credentials. Vault can be used as an SSH certificate authority, and so perhaps in a future post I might connect the two systems.