AWS Deployment (ECS Fargate)
This guide deploys a full Bridge cluster on AWS using ECS Fargate (a service that runs containers without you managing servers). One CloudFormation template creates everything: the database, networking, load balancer, and the containers that make up the cluster.
What CloudFormation does for you. CloudFormation reads a template file and builds all the AWS resources it describes, in the right order. You run one command; AWS does the rest. To remove everything later, you delete the one “stack” it created.
Overview
The template builds a production-shaped cluster:
- An orchestrator that runs migrations, stays in charge, and keeps workers running.
- Proxy containers that accept connections and serve the web app.
- Session containers that the orchestrator spawns and replaces on demand.
- An Amazon RDS PostgreSQL database (the Bridge’s storage).
- Amazon EFS shared file storage for recordings (so any container can read any recording).
- An internal Network Load Balancer (NLB) by default, exposing the web app over HTTPS and the native protocol ports only to the private networks you allow.
You don’t manage the proxy and session containers directly - the orchestrator creates, replaces, and load-balances them for you.
Architecture
This diagram follows the resources and parameters in
bridge-cluster-aws.cfn.yaml. The NLB can be internal or internet-facing, but
the ECS workers always run with private IPs only.
flowchart LR
Clients["Approved clients<br/>(browser + native tools)"]
Britive["Britive platform"]
Targets["Target networks<br/>(VPC, peering, TGW, VPN, Direct Connect, NAT)"]
subgraph LbSubnets["Load balancer subnets<br/>LoadBalancerSubnetIds"]
NLB["Network Load Balancer<br/>443 + one listener per enabled protocol"]
end
subgraph WorkerSubnets["Private subnets (NAT-routed)<br/>WorkerSubnetIds"]
Proxy["Proxy workers<br/>ECS Fargate, private IPs"]
Session["Session workers<br/>ECS Fargate, private IPs"]
RDS[("RDS PostgreSQL<br/>checkout + session state")]
EFS[("EFS<br/>recordings")]
end
subgraph OrchestratorSubnets["Private subnets<br/>WorkerSubnetIds"]
Orchestrator["Orchestrator<br/>+ Britive Broker when BrokerAuthToken is set<br/>ECS Fargate, private IP"]
end
Clients -->|"HTTPS / native protocols"| NLB
NLB -->|"target groups"| Proxy
Proxy -->|"session control + relay"| Session
Session -->|"protocol connection"| Targets
Proxy --> RDS
Session --> RDS
Orchestrator --> RDS
Proxy --> EFS
Session --> EFS
Orchestrator -->|"spawn / replace workers"| Proxy
Orchestrator -->|"spawn / replace workers"| Session
Orchestrator <-->|"outbound TLS"| Britive
Orchestrator -->|"Bridge API via NLB 443"| NLB
Private is the default and recommended deployment model. For nearly all real deployments, keep Bridge behind an internal/private NLB reachable only from your corporate network, VPN, zero-trust network, or private connectivity into AWS. Some native protocols can carry credentials or session data in clear text depending on the client and target configuration, so exposing native listeners on an internet-facing load balancer increases the chance of credential exposure.
The template can still create an internet-facing NLB by setting
LoadBalancerScheme=internet-facing and restricting ClientCidr to a narrow,
reviewed client range. Use that mode only when it is an explicit requirement. In
that model, restrict the deployment to browser-only access over HTTPS and do not
expose native cleartext protocol listeners publicly.
Before You Begin
You need:
- An AWS account and the AWS CLI installed and signed in
(
aws sts get-caller-identityshould show your account). - Permission to create IAM roles, ECS, RDS, EFS, and load balancers
(the deploy uses
CAPABILITY_NAMED_IAM). - An existing VPC with three private, NAT-routed subnets for workers, database, and recording storage.
- Network routes from those private worker subnets to every target Bridge should reach. That can be same-VPC routing, VPC peering, Transit Gateway, VPN, Direct Connect, or NAT egress for internet-reachable targets. Target security groups and firewalls must also allow traffic from the Bridge worker security group or worker subnet CIDRs.
- Subnets for the NLB. Use private subnets for the default internal NLB. Use
public subnets only when you intentionally set
LoadBalancerScheme=internet-facing; the workers still run with private IPs only. - An ACM certificate for the hostname you’ll use (for example
bridge.example.com), in the same region. The load balancer presents this for HTTPS. - The CloudFormation template for the Bridge cluster, downloaded here: bridge-cluster-aws.cfn.yaml. Save it locally - you’ll deploy it in Step 2.
You don’t build anything. The Bridge image is published on Docker Hub as
britive/bridge and the AWS configuration is baked into it; the template selects
it automatically. You set per-deployment values through CloudFormation parameters
(no config file to mount).
Deploy the Cluster
Choose the Image
The Bridge image lives on Docker Hub at britive/bridge. Use a specific
version tag (for example v2.2.0 - use the latest release version):
britive/bridge:latestECS Fargate can pull this public image directly, so in most cases there’s
nothing to do here - the template already defaults ImageUri to
britive/bridge:latest.
Optional - mirror to your own ECR. Some organizations prefer to pull from a
private registry (for Docker Hub rate limits or network policy). If so, copy the
image into your ECR and pass that URI as ImageUri:
REGION=us-west-2
ACCOUNT=111122223333
ECR=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/bridge
aws ecr create-repository --repository-name bridge --region us-west-2 || true
aws ecr get-login-password --region us-west-2 \
| docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com
docker pull --platform linux/arm64 britive/bridge:latest
docker tag britive/bridge:latest $ECR:latest
docker push $ECR:latestPrepare the Secrets
The cluster needs a few shared secrets. Generate them once and keep them safe - you’ll pass the same values on every future update.
echo "EncryptionKeyB64: $(openssl rand -base64 32)" # encrypts stored credentials
echo "ClusterToken: $(openssl rand -hex 32)" # secures internal cluster calls
echo "HostKeySeed: $(openssl rand -hex 16)" # stable SSH fingerprint across workers
echo "DbPassword: $(openssl rand -hex 16)" # RDS database passwordThe HostKeySeed makes every session worker present the same SSH host key, so users’ SSH clients don’t warn about a changed fingerprint each time a worker is replaced. Keep it constant.
Deploy the CloudFormation Stack
Run this from the folder where you saved bridge-cluster-aws.cfn.yaml, filling in your own values. This single command creates the entire cluster:
aws cloudformation deploy \
--template-file bridge-cluster-aws.cfn.yaml \
--stack-name bridge \
--region us-west-2 \
--s3-bucket <a bucket you own in this region> \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
VpcId="vpc-xxxxxxxx" \
WorkerSubnetIds="subnet-private-a,subnet-private-b,subnet-private-c" \
LoadBalancerSubnetIds="subnet-lb-a,subnet-lb-b,subnet-lb-c" \
NatSubnetId="subnet-public-a" \
CertificateArn="arn:aws:acm:us-west-2:111122223333:certificate/xxxx" \
ClientCidr="10.20.0.0/16" \
BridgeUrl="https://bridge.example.com" \
BritiveTenant="https://acme.britive-app.com"That is the whole command. The database password, the cluster token, the encryption key and the SSH host-key seed are generated into Secrets Manager, so there is nothing to invent and nothing to store.
--s3-bucket is required, not optional: the template is larger than
CloudFormation’s 51,200-byte limit for an inline body, so the CLI has to stage it
somewhere. Any bucket you own in the same region will do. The console needs no
bucket, because it stages the upload itself.
Telnet and NETCONF listen on 2323 and 2830 here, not 23 and 830. The container
runs as an unprivileged user, and Fargate does not grant it the capability to bind
a port below 1024 — nor will it: capabilities.add on Fargate accepts only
SYS_PTRACE. So the cluster configs move both, exactly as SSH moves from 22 to
2222. The load balancer listens on the same ports, so this is the port your clients
connect to. A single-container deployment keeps 23 and 830, because Docker does
grant that capability.
CAPABILITY_IAM is sufficient — the template names none of its roles, so
CAPABILITY_NAMED_IAM is not required.
The template defaults ImageUri to britive/bridge:latest (Docker Hub), so you
don’t pass it unless you mirrored the image to your own ECR - then add
ImageUri="<your-ecr-uri>:latest" to the overrides above.
LoadBalancerScheme defaults to internal, so you may omit it from the command
if you are using the recommended private deployment. To create an internet-facing
load balancer, set LoadBalancerScheme="internet-facing", set
LoadBalancerSubnetIds to public subnets, and keep SubnetIds as private,
NAT-routed worker/data subnets. Keep ClientCidr limited to the smallest
reviewed client range.
Parameter Reference
| Parameter | Required | Description |
|---|---|---|
VpcId | Yes | The VPC to deploy into. |
WorkerSubnetIds | Yes | Three private subnets. Everything runs here: the orchestrator, the broker, the spawned workers, the database, and the file storage. They get no public IP and reach the internet through the stack’s NAT gateway. They must also route to the targets Bridge connects to, and those targets must allow the worker security group. |
LoadBalancerSubnetIds | Yes | Subnets for the NLB. Private for the default internal scheme; public only with LoadBalancerScheme=internet-facing. |
NatSubnetId | Yes | A public (internet-gateway routed) subnet to host the NAT gateway. It must not be one of WorkerSubnetIds: those route through this NAT, so a NAT inside one of them would have no path out. |
CertificateArn | Yes | ACM certificate for the HTTPS (443) listener. |
ClientCidr | Yes | The only network range allowed to reach the load balancer. For an internal NLB, your routed private or VPN range. For internet-facing, a narrow office or VPN egress CIDR — never a broad internet range with native protocol listeners enabled. |
LoadBalancerScheme | No | Defaults to internal. Use internet-facing only for a reviewed public endpoint, paired with a narrow ClientCidr. |
ImageUri | No | Defaults to britive/bridge:latest. Override to use your own mirrored registry. |
BridgeUrl | No | The URL users reach Bridge at. Used for the OAuth redirect, so it must match your certificate and what Britive has registered. |
BritiveTenant | For Britive login | Your Britive tenant. Empty means Bridge starts and serves sessions but Login with Britive is not offered — see the warning below, which also covers what else changes when you set it. |
BrokerAuthToken | No | Britive Broker pool token. Empty disables the broker. Setting it also lets Bridge retrieve its own license. |
BrokerTenantSubdomain | No | Tenant subdomain the broker registers with. Not a credential. |
NatEipAllocationId | No | Leave empty and the stack allocates an Elastic IP for the NAT. Supply an eipalloc-… only if the account is at its EIP quota. |
Credentials
None of these is required. Leave one empty and the stack generates it into AWS Secrets Manager; fill one in only to reuse a value a previous deployment already established.
| Parameter | Fill in when | Description |
|---|---|---|
DbPassword | Never, normally | Master password for the RDS database. |
HostKeySeed | Rebuilding a deployment | Seed for the session workers’ SSH host key. Reuse the old value to keep the fingerprint clients already trust; a new one makes every SSH client warn. |
ClusterToken | Rebuilding a deployment | Shared secret for calls between cluster containers. |
EncryptionKeyB64 | Rebuilding a deployment | Key that encrypts stored checkout credentials. Supply the value the old deployment held. Filling this in later, on a stack that generated its own, replaces the key and makes every stored credential unreadable. |
BridgeLicense | No broker token | A signed offline license. Leave empty when BrokerAuthToken is set: Bridge fetches one itself. A license is bound to one tenant and must match BritiveTenant. |
LdapBindPassword | Using LDAP login | Password for the LDAP bind DN. |
AuditExportToken | Streaming to a SIEM | Bearer token for the export destination. |
SshProvisionKeyPem | The broker provisions Linux targets | Privileged SSH key the broker uses. |
No credential is passed to a container as an environment variable. Each one
gets a Secrets Manager secret, and the task definition carries only its ARN — the
ECS agent fetches the value when the container starts. That matters because a task
definition is readable by anyone with ecs:DescribeTaskDefinition, so a credential
placed there is a credential published to every such reader.
Network egress. The stack provisions its own NAT gateway (with an Elastic IP), a
private route table, and an S3 gateway endpoint, so the private worker/orchestrator
subnets reach ECR, S3, CloudWatch Logs, the AWS APIs, and the Britive platform without
you wiring egress yourself. You only supply a public subnet for the NAT
(NatSubnetId) and private subnets for the workers (WorkerSubnetIds). If a worker
subnet can’t reach the internet, tasks fail to start with an ECR GetAuthorizationToken … i/o timeout — check the NAT is healthy and the private route table points 0.0.0.0/0
at it. The NAT’s Elastic IP is exported as the NatEgressIp stack output; allowlist it
on any target that filters by source IP.
The deploy takes several minutes. CloudFormation prints progress and finishes
with Successfully created/updated stack.
Point DNS at the Load Balancer
Get the load balancer’s DNS name from the stack outputs:
aws cloudformation describe-stacks --stack-name bridge --region us-west-2 \
--query 'Stacks[0].Outputs' --output tableCreate a DNS record (a CNAME, or an Alias record in Route 53) for your hostname
(bridge.example.com) pointing at that load balancer DNS name. For the default
internal NLB, publish this record in private DNS or another DNS zone resolvable
only from your approved networks. Your certificate’s hostname must match.
Verify
Check the cluster is running
All containers should be on your new image:
aws ecs list-tasks --cluster bridge --region us-west-2 # the cluster is named after the stackYou should see an orchestrator, one or more proxies, and one or more session
workers, all RUNNING.
Confirm the orchestrator started cleanly and the license loaded
Tail its logs:
aws logs tail /bridge/<stack-name> --follow --region us-west-2Look for log lines showing the database connected, migrations applied, and the
license status (operational=true) if you’ve installed one.
Open the web app
Visit your hostname (https://bridge.example.com/) and log
in.
Updating the Cluster Later
Upgrading to v2.2.0 from an earlier release: set BritiveTenant and use
BridgeUrl in the same update.
Before v2.2.0 the Britive tenant, the OAuth redirect and the license were part of the
image, which meant every deployment built from one image authenticated against the same
Britive instance. They are now supplied by the stack. An update that takes the v2.2.0
image without setting BritiveTenant succeeds and the cluster comes up healthy, but no
tenant is configured anywhere, so Login with Britive disappears from the web app.
The v2.2.0 template also renames the CloudFormation parameter BridgeApiUrl to
BridgeUrl. Replace the old name in your deployment command. BridgeUrl is the
public browser address used for the OAuth callback. It is separate from
BRIDGE_API_URL, which the co-located broker uses to call Bridge over loopback.
Setting BritiveTenant also narrows which protocols are enabled. The two are
coupled by how the stack supplies configuration. With the parameter empty, no
Secrets Manager config is attached and the container uses the config baked into the
image, which enables every protocol. Setting the parameter attaches a Secrets
Manager config, and that config takes over completely: it turns on SSH, RDP, MySQL
and browser VNC, and every protocol it does not name falls back to Bridge’s own
default of off.
So a cluster that was serving WinRM, PostgreSQL or the HTTP proxy on an empty
BritiveTenant stops serving them the moment you set one. Add the protocols you
need to the BridgeConfig section of the template before you update. See
Configuration for the settings.
A configuration change reaches warm workers too. The stack keeps Bridge’s
configuration in a Secrets Manager secret, which a task reads when it starts.
Changing a parameter used to build the configuration, such as BritiveTenant or
BridgeUrl, produces new task definition revisions, and the orchestrator service
rolls onto them straight away.
Session and proxy workers follow. The orchestrator compares the task definition each worker was created from against the one it should be using now, so a new revision cycles warm workers whether or not the image changed. You do not need to stop them by hand.
To move to a newer Bridge release, re-run the same deploy command with the new
version in ImageUri (for example ImageUri="britive/bridge:v2.2.0", or your
mirrored ECR equivalent). Pass every other parameter unchanged.
The orchestrator handles the rollout for you: it cycles the proxy containers onto the new image and replaces idle session workers, with no manual container restarts. See Operations for more on upgrades.
If you use the lower-level update-stack command instead of deploy, pass
UsePreviousValue=true for every secret parameter so you don’t have to re-enter
them.
Troubleshoot
| Symptom | Likely Cause | Fix |
|---|---|---|
| Stack fails to create | Missing IAM capability, or a parameter (subnet, cert) is wrong | Read the failure reason in the CloudFormation events; confirm the VPC, subnets, and certificate exist in the same region. |
| Containers crash on start | Can’t reach RDS, or a secret is malformed | Check /bridge/<stack-name> logs; confirm the database came up and EncryptionKeyB64 is valid base64. |
| Sessions fail to connect to targets | Worker subnets cannot route to the target, or target firewalls block the worker source | Confirm the private worker subnets have a route to the target network and the target allows inbound traffic from TaskSg or the worker subnet CIDRs. |
| Can’t reach the web app | ClientCidr doesn’t include your address, private routing is missing, or DNS not set | Confirm your client or VPN network is within ClientCidr; verify private DNS and routing to the internal load balancer. |
| Footer shows “Unlicensed” / native protocols unavailable | No valid license installed (running in limited mode) | Install a license in Admin → License, or bake one into the config - see Licensing. |
| No “Login with Britive” button, or login fails to authenticate | BritiveTenant is empty or does not match the tenant your users belong to | Set BritiveTenant on the stack and update. Confirm the tenant hostname is exact; the redirect Bridge sends is derived from BridgeUrl, so that must be the URL users actually reach it at. |
| Old version still serving | ImageUri unchanged, or a moving tag was cached | Use a specific version tag (for example britive/bridge:v2.2.0) in ImageUri and redeploy. |
Next Steps
- Configure authentication
- Install and manage your license.
- Review protocols, recording, operations for the Admin console, upgrades, and backups.