← Back to dashboard

BeyondTrust Gateway Hosts

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:

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.


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_keygcp_jumpoint_docker_deploy_keygcp_jumpoint_deploy_key
Azure azure_aci_deploy_keyazure_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 provisioningrunning, 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
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

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.

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.