Skip to content
AWS Deployment (ECS Fargate)

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-identity should 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):

image
britive/bridge:latest

ECS 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:

terminal
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:latest

Prepare 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.

terminal
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 password

The 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:

terminal
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

ParameterRequiredDescription
VpcIdYesThe VPC to deploy into.
WorkerSubnetIdsYesThree 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.
LoadBalancerSubnetIdsYesSubnets for the NLB. Private for the default internal scheme; public only with LoadBalancerScheme=internet-facing.
NatSubnetIdYesA 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.
CertificateArnYesACM certificate for the HTTPS (443) listener.
ClientCidrYesThe 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.
LoadBalancerSchemeNoDefaults to internal. Use internet-facing only for a reviewed public endpoint, paired with a narrow ClientCidr.
ImageUriNoDefaults to britive/bridge:latest. Override to use your own mirrored registry.
BridgeUrlNoThe URL users reach Bridge at. Used for the OAuth redirect, so it must match your certificate and what Britive has registered.
BritiveTenantFor Britive loginYour 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.
BrokerAuthTokenNoBritive Broker pool token. Empty disables the broker. Setting it also lets Bridge retrieve its own license.
BrokerTenantSubdomainNoTenant subdomain the broker registers with. Not a credential.
NatEipAllocationIdNoLeave 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.

ParameterFill in whenDescription
DbPasswordNever, normallyMaster password for the RDS database.
HostKeySeedRebuilding a deploymentSeed 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.
ClusterTokenRebuilding a deploymentShared secret for calls between cluster containers.
EncryptionKeyB64Rebuilding a deploymentKey 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.
BridgeLicenseNo broker tokenA signed offline license. Leave empty when BrokerAuthToken is set: Bridge fetches one itself. A license is bound to one tenant and must match BritiveTenant.
LdapBindPasswordUsing LDAP loginPassword for the LDAP bind DN.
AuditExportTokenStreaming to a SIEMBearer token for the export destination.
SshProvisionKeyPemThe broker provisions Linux targetsPrivileged 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:

terminal
aws cloudformation describe-stacks --stack-name bridge --region us-west-2 \
  --query 'Stacks[0].Outputs' --output table

Create 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:

terminal
aws ecs list-tasks --cluster bridge --region us-west-2   # the cluster is named after the stack

You 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:

terminal
aws logs tail /bridge/<stack-name> --follow --region us-west-2

Look 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

SymptomLikely CauseFix
Stack fails to createMissing IAM capability, or a parameter (subnet, cert) is wrongRead the failure reason in the CloudFormation events; confirm the VPC, subnets, and certificate exist in the same region.
Containers crash on startCan’t reach RDS, or a secret is malformedCheck /bridge/<stack-name> logs; confirm the database came up and EncryptionKeyB64 is valid base64.
Sessions fail to connect to targetsWorker subnets cannot route to the target, or target firewalls block the worker sourceConfirm 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 appClientCidr doesn’t include your address, private routing is missing, or DNS not setConfirm your client or VPN network is within ClientCidr; verify private DNS and routing to the internal load balancer.
Footer shows “Unlicensed” / native protocols unavailableNo 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 authenticateBritiveTenant is empty or does not match the tenant your users belong toSet 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 servingImageUri unchanged, or a moving tag was cachedUse a specific version tag (for example britive/bridge:v2.2.0) in ImageUri and redeploy.

Next Steps

Last updated on