BeyondTrust/POV

← Back to dashboard

BeyondTrust Gateway Hosts

Audience: operator · Profile: both · Read this when: a resource sits in a private network and something has to broker a session into it.

What is it?

A BeyondTrust Gateway is the host that brokers PRA sessions into a private cloud network. Every tunnel the dashboard builds — the cloud-database protocol tunnel, the Kubernetes API tunnel, a VM Shell Jump, a Rancher or Portainer Web Jump — reaches its target through one, because the target has no public address to reach directly.

The dashboard has always run one of these per cloud and managed it invisibly: ensure a host before something needs a tunnel, reap it when nothing does. That answers "is there a gateway?" but not "what gateways do we have?" — which is the question you actually have once sessions start queueing behind a single host.

Containers → Gateways answers it. The tab is one inventory of every gateway host the dashboard put in a cloud, plus a form to add more. It appears when BeyondTrust is enabled (pra_enabled), alongside the Cloud, Portainer and Kubernetes (Rancher) tabs.

The Gateways tile in the dashboard's Containers section links straight there (/containers#gateways) and counts every gateway host across all three clouds, with the running total underneath — so a host that failed to come up is visible from the landing page. The tile is gated on the same beyondtrust flag as the tab.

This page is about the gateway hosts. Which PRA Gateway a jump item routes through — by name, from the bt_jumpoint_name setting or a deploy form's picker — is covered in Privileged Remote Access. The two are related but not the same thing, and the next section is the reason why.


Two kinds of gateway, one table

The tab lists both, distinguished by a managed badge on the name. Both are hosts the dashboard put in a cloud; there is a third kind that is deliberately not in this table.

managed requested
Created by the auto-ensure, when something needs a tunnel you, on this tab
How many exactly one per cloud as many as you like — no cap
Reference counted yes — see below no
Torn down automatically, once nothing is using it only when you remove it
Deletable from the UI no yes
created_by system your username

They are in one table on purpose: the page's job is to show everything the dashboard put in the cloud, and splitting the list by who owns the lifecycle would hide the shared host that most sessions actually run through.

Why the managed one is reference-counted

Its lifecycle is a ref-counted ensure/idle pair, so a shared host can be reclaimed when the last thing using it goes away without a teardown yanking it out from under something else. These hold a reference on it — while any of them is live, the idle teardown leaves the host alone:

Why the managed one can't be deleted

Remove doesn't render on a managed row, and the API refuses it (HTTP 400) if you call it anyway. Deleting the row would not delete the gateway for long: the next thing that needs a tunnel re-ensures the host, and you would be left with a live gateway the registry said was gone — and, because node firewalls are computed from these rows, a broker whose /32 nothing allows.

If you want the managed host gone, remove what is holding it: the idle teardown reclaims it on the next teardown pass once the count reaches zero.

Why there is no cap on the requested ones

"Three in us-central1 and two in us-east-2" is the stated use case, and the right number is a function of session load — which the dashboard cannot see. Cloud quotas are the real ceiling, so the dashboard doesn't invent a smaller one.


A third kind: a Gateway on an agent host

A remote agent can run a Gateway on its own host, inside a network the dashboard cannot route to at all. The dashboard never touches a cloud API for this one — it queues an agent_gateway job and the agent starts a privileged container beside itself. Today the driver is the POV feature, which generates the whole policy for a control node VM it created.

It is not in this tab, and that is not an oversight. The gateways inventory is per-cloud — a row's cloud is one of aws, azure, gcp — and nothing writes a row for a Gateway on somebody's own hardware. So an agent-installed Gateway is absent from:

You see it on its POV row instead, which shows whether a Gateway is named and whether a deploy key is stored, plus a button for the live PRA answer.

It is its own PRA Gateway, not another node

This is the important difference from one Gateway, many nodes. Cloud hosts all launch with the same per-cloud deploy key, which is why they cluster into a single PRA Gateway. An agent-installed Gateway takes a deploy key you paste onto that POV, so it is a separate, separately-named Gateway — nothing joins the cloud one, and adding one gives the cloud Gateway no capacity.

It may also live in an entirely different PRA appliance. The tenant comes from the POV's explicit entry in the BeyondTrust tenant registry, and a POV whose tenant was deleted or disabled is an error rather than a quiet fall back to the default — because the fallback would install a customer's Gateway into somebody else's appliance.

Capability requirements are the same as the GCP host in the table below: a tunnel needs NET_ADMIN, NET_RAW, IPC_LOCK and /dev/net/tun. Docker cannot grant that set granularly, which is why the agent's policy.yaml has to say privileged: true and why the agent refuses to start one without it. Everything else about the agent side — the four grants, the 2.4.0 image floor — is in Remote Agents.

Its status is read live, never stored

The POV row's Gateway state is a question asked of PRA at the moment you press the button, not a column. A stored status is how a row still reads connected three weeks after the environment was suspended.

connected is deliberately tri-state: some appliance versions spell the field differently and simply do not answer, and reporting that as disconnected would send you to debug a Gateway that is fine. So "unknown — check the appliance directly" is a distinct result from "disconnected".


How it works

One PRA Gateway, many nodes

This is the part worth internalising, because it is what makes adding a gateway safe:

Every gateway host in a cloud is launched with that cloud's configured Gateway deploy key. A PRA Gateway's identity comes from its deploy key, so hosts sharing one don't become separate Gateways — they join the same PRA Gateway as additional cluster nodes.

Two consequences:

Cloud Resolved from
AWS aws_ecs_docker_deploy_key, else the Password Safe secret titled bt_ps_deploy_key_title
GCP gcp_cloud_run_docker_deploy_key → gcp_jumpoint_docker_deploy_key → gcp_jumpoint_deploy_key
Azure azure_aci_deploy_key → azure_aci_docker_deploy_key

What a gateway host actually is

The shape is per-cloud, and the same for both kinds:

Cloud Host Managed host's name
AWS the gateway container as an ECS task on an EC2 container instance in the configured cluster bt_ecs_host_name, default dashboard-sandbox-jumpoint-host
GCP a privileged container on a Container-Optimized-OS GCE VM (the tunnel needs NET_ADMIN/NET_RAW/IPC_LOCK + /dev/net/tun, which serverless can't grant) gcp_jumpoint_name, default clouddb-shared-jumpoint
Azure a VM running the gateway container clouddb-jumpoint

On GCP the tab's hosts also appear in the GCE Container Instances table on the Cloud tab, badged Gateway — see Cloud Containers.


Prerequisites

Requirement Notes
PRA enabled pra_enabled, under Settings → Integrations → Privileged Remote Access. Gates the tab entirely.
A Gateway deploy key for that cloud The key above. Without it the ensure has nothing to launch and the job fails with the specific missing value in its log.
A gateway subnet for that cloud bt_ecs_jumpoint_subnet_id (AWS), the region's jumpoint_subnet_id / azure_aci_subnet_id (Azure), the zone's derived subnetwork (GCP).
AWS only: an ECS-capable instance profile The host's profile (bt_ecs_host_instance_profile, default ecsInstanceRole) must carry the AWS-managed AmazonEC2ContainerServiceforEC2Role policy. Without ecs:RegisterContainerInstance the host's ECS agent is denied, treats it as terminal and exits — see below.
A per-region config set, to deploy outside the default region Settings → Multi-region. This is not optional politeness — see Placement.
admin:write To deploy. Removing needs admin:delete; viewing the list needs only a login.

Deploying a gateway

Containers → Gateways → the deploy row at the top of the panel:

Field Notes
Cloud AWS / Azure / GCP. Changing it reloads the region list and re-suggests a name, because both are per-cloud.
Region A picker, not free text. (configured default) leaves it blank — resolved at deploy time — which is what a single-region install wants.
Zone GCP only, optional. Blank resolves from the region; the placeholder shows what that will be.
Name Prefilled with a free, cloud-legal default (gw-<region>-01). Editable.

Deploy gateway enqueues a gateway_deploy job and returns — the row appears immediately as provisioning with a Job link. The work runs on the durable worker, so a gunicorn recycle mid-deploy can't strand a half-built host:

Step What happens
Launching Ensures the host under the name you gave. Passing a name is precisely what marks this gateway as requested — the ensure skips its managed-adoption write, because the row already exists and is owned by the job.
Recording placement Writes back where the host actually landed (zone, egress IP), not what was asked for.
Updating node firewalls Re-applies the Rancher + Portainer node allow-lists. Best-effort — a firewall hiccup logs and never fails the gateway job.
Done Row flips to running with its host id.

Why the row records where it landed rather than what you picked. A blank GCP zone is resolved at launch, and a capacity-exhausted zone falls through to a sibling — so the requested zone is not reliably the real one. Teardown deletes by (zone, name), so a row holding the requested zone would later fail to find its own host.

Placement and the region picker

The picker offers only the configured default region plus every region that has a per-region config set of its own (the same list the Rancher and Portainer node pickers use — region_config.deployable_regions). That restriction is the feature, not a limitation.

The tab used to take the region as free text, and typing an unconfigured region was worse than a validation error. Every regional id a gateway needs — subnet, security group, ECS cluster — resolves through region_config.resolve_region, which falls each field back to the flat config keys. So the host came up on the default region's network while the row, the job, and you all said otherwise. Nothing failed; the gateway was simply somewhere else, with no line of sight to the private targets in the region you meant.

Restricting the picker makes "the region I picked" and "the region it lands in" the same thing. To deploy somewhere new, add that region under Settings → Multi-region first; it then shows up in the picker.

GCP has a second route to the same silent relocation: a gateway's subnet is derived from its zone, so a zone in another region would quietly override the region you chose. The deploy endpoint refuses that pair (HTTP 400) rather than queueing a job that lands elsewhere, and picking a new region clears any leftover zone.

Names, and why they matter more than they look

The name is not cosmetic — it is what keeps the managed and requested hosts apart in the cloud itself, because the managed idle teardown finds its host by name tag. A requested gateway wearing the managed name is one the idle teardown would terminate out from under you. So:


Removing a gateway

Remove on a requested row confirms (sessions routed through it will drop — they will; PRA may be brokering through that node right now), then enqueues a gateway_teardown job: it deletes the host, marks the row deleted, clears the recorded egress IP, and re-applies the node firewalls so the departed gateway's /32 leaves the allow-lists.

The managed row has no Remove button, and the service layer refuses it independently of the API — see above.


Reading the list

Column Notes
Status provisioning → running, or error; deleting while a teardown runs. Deleted rows are hidden.
Name Plus the managed badge where it applies.
Cloud / Region – region means "the configured default", resolved at deploy time.
Host EC2 instance id, GCE instance, or Azure VM name.
Created by system for the managed gateway; a username for requested ones.
Actions Job (the deploy job) and Remove (requested only).

An empty list is normal on a fresh install. The managed gateway is adopted into a row on its first ensure, not on install — so it appears the first time anything needs a tunnel. A deployment that has been running the shared gateway for months gets it registered rather than duplicated the next time anything ensures it.

A failed gateway keeps its row, in error, with the reason attached, so the failure stays visible instead of vanishing; open its job for the detail.


Node firewalls: the consequence that bites

The Rancher and Portainer management nodes have public, source-restricted IPs and are reached over a PRA Web Jump — so the source hitting them is a gateway's egress IP. Since every gateway in a cloud is a node of one PRA Gateway cluster and PRA may broker through any of them, allowing one remembered IP is a coin flip.

So the dashboard allows a /32 for every live gateway it deployed in that cloud — managed and requested — and re-applies the set on every gateway deploy and teardown, and on every node deploy. AWS and GCP egress IPs are ephemeral (a reclaim/recreate changes them), which is why the IP is re-recorded on each ensure rather than trusted once.

A gateway that is not in the allow list can reach nothing, which looks exactly like a broken Web Jump. Check Settings → Containers → Effective firewall sources; the gateway should be listed under Web-Jump Gateways.

A pre-existing Gateway you run yourself can't be auto-detected — the dashboard only knows about hosts it deployed. Add its egress IP to rancher_allowed_source_cidrs / portainer_allowed_source_cidrs by hand.


Permissions

Action Requires
See the tab and the list any authenticated user
Deploy a gateway admin:write
Remove a gateway admin:delete

The name-suggestion endpoint also requires admin:write, so a non-admin sees the inventory but gets no prefilled name — the deploy they can't perform anyway.

API

Endpoint Notes
GET /api/gateways Every gateway, managed and requested. ?cloud=aws\|azure\|gcp filters.
GET /api/gateways/suggest-name?cloud=&region= A free, cloud-legal default.
POST /api/gateways/deploy {cloud, region, zone, name} → 202 with a job_id.
DELETE /api/gateways/{id} → 202 with a job_id. 400 on the managed gateway.

Both mutating endpoints are enqueue-only and write an audit record (gateway_deploy / gateway_teardown).

Configuration reference

Gateway hosts are configured under Settings → Integrations → Privileged Remote Access (and Settings → Multi-region for per-region overrides). Nothing here is specific to the requested gateways — they reuse the managed host's configuration, which is why a new one needs no setup of its own.

Key Purpose
bt_ecs_launch_type / bt_ecs_cluster / bt_ecs_jumpoint_subnet_id / bt_ecs_image AWS gateway host: how and where the ECS task runs
bt_ecs_host_name Name of the managed AWS host (default dashboard-sandbox-jumpoint-host) — reserved against requested names
aws_ecs_docker_deploy_key / bt_ps_deploy_key_title The AWS Gateway deploy key, direct or by Password Safe secret title
gcp_jumpoint_name Name of the managed GCP gateway VM (default clouddb-shared-jumpoint); sanitized to RFC1035
gcp_jumpoint_zone Zone override for a GCP gateway — applied only when it is inside the requested region
gcp_cloud_run_docker_deploy_key The GCP Gateway deploy key (with two legacy fallbacks)
azure_aci_subnet_id / azure_aci_deploy_key Azure gateway subnet and deploy key
azure_vm_jumpoint_mode shared (default) or aci — which shape a VM deploy borrows; the managed VM is clouddb-jumpoint
azure_jumpoint_tunnel_pool / bt_ecs_jumpoint_tunnel_pool / gcp_jumpoint_tunnel_pool Network-tunnel lease pool, per cloud. Blank derives it from the gateway subnet; per-region jumpoint_tunnel_pool wins; off disables it. See Network tunnels need an address pool
bt_jumpoint_name Unrelated to host placement: the PRA Gateway a jump item routes through, by name. Adding a host to that Gateway's cluster does not change this value

Network tunnels need an address pool

A Protocol Tunnel working tells you nothing about whether a Network Tunnel will. A protocol tunnel opens an outbound TCP socket; a network tunnel leases the operator a real address on the target network for the life of the session. That lease needs three things from the cloud fabric, and the dashboard does all three.

Azure AWS GCP
1 the container sees the real network --network host on the container networkMode: "host" on the ECS task — always had it konlet runs the container on the host network
2 the fabric forwards for it enableIPForwarding on the NIC SourceDestCheck=false on the host canIpForward on the instance
3 the pool is registered secondary ipconfigs on the NIC secondary private IPs on the host's ENI an alias IP range on the NIC

Requirement 1 is why a protocol tunnel is no evidence: an outbound TCP socket is NAT'd out of a bridged container quite happily, so it works while the gateway cannot see the target subnet at all. Azure was the only cloud that had this wrong.

Requirement 3 is the one with a manual counterpart. The gateway asks DHCP for its lease first; no cloud answers, because cloud DHCP only serves an address already bound to a NIC. It falls back to the pool, then ARPs to validate the address it picked. An address the fabric does not know about gets no reply:

Timeout: No valid ARP reply received from 10.99.5.202 in 2000ms
Allocated IP address [10.99.5.202] not an existing Azure resource?  Is it configured as a
  secondary IP address on the virtual NIC of the Gateway VM?
setup: Exception - Address: 0.0.0.0 not found

A route table is not the answer on any of them — the product is cloud-aware and asks for registered addresses by name.

Where the pool comes from

Blank (the default) derives it from the gateway subnet: 8 addresses from the top, because every cloud allocates dynamically from the bottom (.4 upward), so the top stays clear of real hosts longest. Each cloud's reserved addresses are excluded, and they differ — Azure and AWS reserve .1/.2/.3, GCE reserves .1 and the second-to-last. For a 10.99.5.0/24:

Cloud Derived pool
Azure, AWS 10.99.5.247–10.99.5.254
GCP 10.99.5.240/29

To pin one instead, set jumpoint_tunnel_pool for the region (or the flat per-cloud key) to a range 10.99.5.200-10.99.5.207, a CIDR, or a single address. off disables the pool entirely. An unparseable value falls back to deriving rather than leaving the gateway with no pool, and an oversized one is capped — a pasted /16 would otherwise try to create 65k addresses.

GCP takes a CIDR, not a range. An alias IP range is a prefix, so 10.99.5.240/29 is accepted and 10.99.5.240-10.99.5.247 is refused rather than silently widened to the enclosing block — widening would hand you addresses you never listed, which is how you collide with a live host.

Prefer the per-region key. A pool only makes sense inside its own subnet's prefix, so a flat key is the wrong answer in every region but the default one — the same trap described in Placement and the region picker.

One pool per subnet

The pool is derived from the subnet, so every gateway in a subnet derives the same addresses and only the first can own them. A second gateway's registration is refused (AWS: the address is already assigned; GCE: alias ranges may not overlap). This degrades rather than breaks — pool registration is never fatal, so the second gateway comes up and simply carries no network tunnel, with the reason in the log.

The exception is GCP's paired mode (gcp_vm_jumpoint_mode = paired), which builds a gateway per VM. There the collision is guaranteed and would fail the instance insert outright, so that path opts out of the pool explicitly. Network tunnels are a property of the shared gateway.

GCP gateways cannot be upgraded in place

Azure and AWS apply requirements 2 and 3 on reuse as well as create — both are live API calls — so an existing gateway picks the pool up on the next ensure without a rebuild.

GCE is different: canIpForward and alias ranges are settable only when the instance is created (or while it is TERMINATED). The ensure path is called routinely by VM, cloud-database and Kubernetes deploys and reuses the shared gateway, so repairing in place would mean stopping a gateway and dropping every live session on it. Instead it logs:

tunnel-pool: gateway 'clouddb-shared-jumpoint' predates network-tunnel support
(canIpForward=false, no alias IP range) — protocol tunnels are unaffected, but a
NETWORK tunnel needs the VM recreated

Delete the gateway VM and let the next deploy recreate it.

It has to match the appliance, and nothing checks that for you

The pool must equal Managed IP Addresses for Protocol Tunnel on the Gateway in the Pathfinder console (app.beyondtrust.io → Privileged Remote Access → Jump → the Gateway → Gateway Platform). That section only appears once Enable Protocol Tunnel Method is ticked, which is why it reads as "missing" if you go looking for the IP list first. It is not in the appliance's /login.

The dashboard can neither read nor write that setting, so a mismatch is invisible until a session fails its ARP check. For that reason the resolved pool is logged at INFO when the gateway is ensured:

gateway-host(azure): network-tunnel pool on clouddb-jumpoint = 10.99.5.247, … — this must
match Managed IP Addresses for Protocol Tunnel on the Gateway in the Pathfinder console

AWS and GCP log the same line against their own gateway; on GCP it names the alias range.

Success looks like the gateway binding a lease as a /32 on its interface (10.99.5.204/32). Note the pool addresses do not answer ping — they are registered with the fabric but not bound in the guest OS — and that is fine: the agent checks ARP, not ICMP.


Troubleshooting

The Gateways tab isn't there — it is gated on pra_enabled. Toggle BeyondTrust on under Settings → Integrations; it applies immediately, no restart.

"The <cloud> gateway could not be started" — the ensure path returned nothing because a prerequisite is missing: the deploy key, the project, or the gateway subnet. The job log names the specific one. Check Settings → Integrations → Privileged Remote Access.

The region I want isn't in the picker — it has no per-region config set. Add one under Settings → Multi-region. The picker is deliberately not the full region catalog; a region without its own subnet would put the gateway on the default region's network.

"Zone <z> is not in region <r>" — a GCP gateway's subnet follows its zone, so that pair would come up outside the region you chose. Clear the zone (blank resolves correctly from the region) or pick one inside it.

"A gateway named <n> already exists in <cloud>" — names are unique per cloud, case-insensitively, and the managed host's name is reserved even when it has no row yet. That reservation is load-bearing: the managed idle teardown acts on that name, so a requested host wearing it would be terminated automatically.

Azure rejects a name that looks short enough — the limit is 15 characters, not Azure's 64, because the in-guest hostname is name[:15].

A new gateway brokers a Web Jump that can't reach the node — the node firewall allows a /32 per gateway, refreshed on each gateway deploy/teardown. Confirm it appears under Web-Jump Gateways in Settings → Containers; if not, its egress IP was never recorded — redeploy the gateway, or add the IP to the node's *_allowed_source_cidrs manually.

A Web Jump times out after its node was recreated — on GCP and AWS the Rancher and Portainer nodes take an ephemeral address, so a recreate moves them, and a Web Jump carries the URL it was made with. (An Azure-hosted node keeps its Standard, static IP, so it does not hit this.) The registration now compares the two and re-points the jump item (destroy + recreate) when they disagree; before that it reused the stored id unconditionally, so the item kept dialling the dead address and no redeploy could converge it. If you are on an older build, tear the node down (Stop, which removes the jump item) rather than redeploying it.

A Gateway shows in PRA but no node is connected — and the host was rebuilt — PRA keeps the dead node. A recreated host registers under the same Gateway name but from a fresh address, so the appliance ends up listing the old node alongside the new one, and the Gateway can read as disconnected while something is in fact connected. Compare node counts before and after: that, not the name, is what tells the two apart. This is the usual explanation for an agent-installed Gateway whose job went green while PRA looked empty — see A third kind.

I can't remove the managed gateway — by design; it is created and reclaimed by the reference-counted lifecycle. Remove whatever still holds a reference and the idle teardown reclaims it.

A Web Jump connects, then drops — "the endpoint has disconnected"

The gateway ran out of memory. A Web Jump renders the target UI in a headless Chromium on the gateway itself (sra-web.bin) — unlike a tunnel, which only forwards bytes — so a host sized for tunnels is not sized for this. At 1 GB the kernel OOM-kills the renderer and every session on that node dies at once.

It is a nasty one to read from symptoms, because which error you get depends on timing:

When it dies What PRA shows
Mid-session "Waiting for Reconnect — the endpoint has disconnected"
Before the session is up "internal timeout starting session" — identical to a blocked firewall

The second is the trap: it sends you to the node firewall, which may be perfectly correct. Distinguish them at the source — on a GCP gateway:

gcloud compute instances get-serial-port-output <gateway> --zone <zone>

A confirmed kill looks like Out of memory: Killed process … (sra-web.bin), usually with Chromium's own ThreadPoolServi invoked oom-killer just above it and global_oom (the whole VM, not a container limit). The Containers page cannot tell you this — its RUNNING badge is the instance status, so a VM whose gateway container is dead still reads healthy.

Defaults are now sized for a Web Jump (gcp_jumpoint_machine_type = e2-medium, Azure Standard_B2s, AWS bt_ecs_host_instance_type = t3.small). A gateway built before that keeps its old size — the launcher is idempotent on name and reuses the VM without touching its shape, so raising the setting is not enough. Resize it in place, or delete it and let the next ensure rebuild it. Either way its ephemeral IP changes, so the node firewalls need the new /32: a gateway deploy/teardown re-applies them, or add it manually.

Concurrency is the usual trigger — one session may fit where two do not. Budget for the number of Web Jumps you expect to have open at once, not for one.

The AWS host never joins the cluster

"Gateway host i-… did not register with the ECS cluster … within 180s" — the EC2 host booted, but its ECS agent never registered, so no Gateway task can be placed on it. The usual cause is the host's instance profile (bt_ecs_host_instance_profile, default ecsInstanceRole) missing ecs:RegisterContainerInstance. Attach the AWS-managed AmazonEC2ContainerServiceforEC2Role policy to that role; /var/log/ecs/ecs-agent.log on the host confirms it.

This one is worth knowing even if you never deploy a gateway by hand, for two reasons:

ecsInstanceRole is AWS's own default role name, so it is easy to already have from a console wizard or an older setup-aws.sh run. Current setup-aws.sh attaches the policy on both the create and the reuse path; a role left over from before only converges once that script runs again.

A row is stuck in provisioning — the row is written before the job runs, so it reflects the job's state. Open its Job link. A failed job leaves the row in error with the reason, and records the host id it got as far as creating — so a host that came up but never became usable is still removable from this tab rather than being an orphan you have to find in a cloud console.

A gateway I deployed by hand isn't listed — the registry only holds hosts the dashboard deployed. A gateway you run yourself is invisible to the tab, and to the node-firewall automation that reads it.