From 24a66860ec83cc17a4f0ece40d5f08f75d7613a6 Mon Sep 17 00:00:00 2001 From: palakchheda <42711310+palakchheda@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:28:42 -0700 Subject: [PATCH 1/5] feat(bridge): add Britive Bridge deployment templates, VM/k8s guides, and custom-image example Deployment options: - docker-compose: single-container host/VM - linux-vm-docker: standalone Linux VM via Docker, distro-detecting install.sh (apt/dnf/amazon-linux-extras), firewalld/ufw, SELinux :Z, Podman/rootless notes - windows-vm-docker: standalone Windows VM via Docker, install.ps1; documents the WSL2/Linux-container requirement, licensing, and PowerShell nuances - aws-ecs-fargate-nlb: Fargate + NLB TLS passthrough (no ACM cert) - aws-ecs-fargate-alb: Fargate + ALB ACM TLS termination (prod path) - aws-ecs-fargate-alb-ssh: ALB option + broker SSH key via Secrets Manager - kubernetes: manifests (namespace, secret, pvc, deployment, service, ingress) with HTTPS backend + WebSocket handling; optional External Secrets Operator and HA (RWM) overlays; public Helm chart roadmap (OCI on Docker Hub) Shared + extensibility: - platform-setup: interactive quick-setup.py to create broker pool/token, Bridge resource, response template, and admin profile - custom-image: Dockerfile extending britive/bridge with broker-script utilities (ssh, python3, mysql client, awscli v2, jq); base-distro agnostic (apt/apk/dnf) and arch-aware AWS CLI; multi-arch build-and-push.sh for Docker Hub/ECR - custom-image worked examples: Linux SSH key provisioning and Aurora MySQL temp-user/role-member checkout/checkin scripts - custom-image/k8s-overlays: ready-to-apply IRSA ServiceAccount + Deployment patch (custom image, SA binding, SSH-key Secret mount) All images default to britive/bridge:latest; Helm chart + custom image standardize on Docker Hub alongside the container image. --- Britive Bridge/README.md | 146 +++++ .../aws-ecs-fargate-alb-ssh/README.md | 98 +++ .../ecs-fargate-alb-ssh.yaml | 432 +++++++++++++ .../params.example.json | 15 + Britive Bridge/aws-ecs-fargate-alb/README.md | 89 +++ .../aws-ecs-fargate-alb/ecs-fargate-alb.yaml | 365 +++++++++++ .../aws-ecs-fargate-alb/params.example.json | 14 + Britive Bridge/aws-ecs-fargate-nlb/README.md | 107 +++ .../aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml | 272 ++++++++ .../aws-ecs-fargate-nlb/params.example.json | 13 + Britive Bridge/custom-image/.dockerignore | 4 + Britive Bridge/custom-image/Dockerfile | 103 +++ Britive Bridge/custom-image/README.md | 210 ++++++ Britive Bridge/custom-image/build-and-push.sh | 57 ++ .../custom-image/k8s-overlays/README.md | 45 ++ .../k8s-overlays/deployment-patch.yaml | 47 ++ .../k8s-overlays/serviceaccount-irsa.yaml | 27 + Britive Bridge/docker-compose/README.md | 98 +++ .../docker-compose/docker-compose.yaml | 60 ++ Britive Bridge/kubernetes/README.md | 244 +++++++ .../kubernetes/manifests/deployment.yaml | 66 ++ .../manifests/external-secret.example.yaml | 54 ++ .../manifests/ha-overlay.example.yaml | 58 ++ .../kubernetes/manifests/ingress.yaml | 38 ++ .../kubernetes/manifests/namespace.yaml | 6 + Britive Bridge/kubernetes/manifests/pvc.yaml | 19 + .../kubernetes/manifests/secret.example.yaml | 20 + .../kubernetes/manifests/service.yaml | 19 + Britive Bridge/linux-vm-docker/README.md | 242 +++++++ .../linux-vm-docker/bridge.env.example | 17 + Britive Bridge/linux-vm-docker/install.sh | 157 +++++ Britive Bridge/platform-setup/README.md | 67 ++ Britive Bridge/platform-setup/quick-setup.py | 607 ++++++++++++++++++ Britive Bridge/windows-vm-docker/README.md | 208 ++++++ .../windows-vm-docker/bridge.env.example | 19 + Britive Bridge/windows-vm-docker/install.ps1 | 132 ++++ 36 files changed, 4175 insertions(+) create mode 100644 Britive Bridge/README.md create mode 100644 Britive Bridge/aws-ecs-fargate-alb-ssh/README.md create mode 100644 Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml create mode 100644 Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json create mode 100644 Britive Bridge/aws-ecs-fargate-alb/README.md create mode 100644 Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml create mode 100644 Britive Bridge/aws-ecs-fargate-alb/params.example.json create mode 100644 Britive Bridge/aws-ecs-fargate-nlb/README.md create mode 100644 Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml create mode 100644 Britive Bridge/aws-ecs-fargate-nlb/params.example.json create mode 100644 Britive Bridge/custom-image/.dockerignore create mode 100644 Britive Bridge/custom-image/Dockerfile create mode 100644 Britive Bridge/custom-image/README.md create mode 100755 Britive Bridge/custom-image/build-and-push.sh create mode 100644 Britive Bridge/custom-image/k8s-overlays/README.md create mode 100644 Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml create mode 100644 Britive Bridge/custom-image/k8s-overlays/serviceaccount-irsa.yaml create mode 100644 Britive Bridge/docker-compose/README.md create mode 100644 Britive Bridge/docker-compose/docker-compose.yaml create mode 100644 Britive Bridge/kubernetes/README.md create mode 100644 Britive Bridge/kubernetes/manifests/deployment.yaml create mode 100644 Britive Bridge/kubernetes/manifests/external-secret.example.yaml create mode 100644 Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml create mode 100644 Britive Bridge/kubernetes/manifests/ingress.yaml create mode 100644 Britive Bridge/kubernetes/manifests/namespace.yaml create mode 100644 Britive Bridge/kubernetes/manifests/pvc.yaml create mode 100644 Britive Bridge/kubernetes/manifests/secret.example.yaml create mode 100644 Britive Bridge/kubernetes/manifests/service.yaml create mode 100644 Britive Bridge/linux-vm-docker/README.md create mode 100644 Britive Bridge/linux-vm-docker/bridge.env.example create mode 100755 Britive Bridge/linux-vm-docker/install.sh create mode 100644 Britive Bridge/platform-setup/README.md create mode 100644 Britive Bridge/platform-setup/quick-setup.py create mode 100644 Britive Bridge/windows-vm-docker/README.md create mode 100644 Britive Bridge/windows-vm-docker/bridge.env.example create mode 100644 Britive Bridge/windows-vm-docker/install.ps1 diff --git a/Britive Bridge/README.md b/Britive Bridge/README.md new file mode 100644 index 0000000..0163920 --- /dev/null +++ b/Britive Bridge/README.md @@ -0,0 +1,146 @@ +# Britive Bridge — Deployment Options + +Britive Bridge is a self-hosted container that connects to the Britive platform +through the Britive Broker and brokers clientless, browser-based sessions to +your internal resources. This directory collects **ready-to-use deployment +templates** for the most common environments, so you can pick the one that +matches your infrastructure and stand Bridge up quickly. + +Every option deploys the same `britive/bridge` container. They differ only in +**where** it runs and **how** traffic reaches it. + +--- + +## How Bridge works (the short version) + +- The Bridge container connects **outbound** to the Britive platform via the + Broker (MQTT). It registers using a **broker pool token**. +- It serves an HTTPS endpoint on **port 8080** (API + WebSocket + browser + session protocols). By default it presents an **auto-generated self-signed + certificate**; you can front it with a real certificate (ALB/ACM, Ingress + + cert-manager, or a mounted cert). +- It persists active session/checkout state to **`/data`**, so that path should + be backed by durable storage. +- Two environment variables are always required: + - `BRITIVE_BROKER_TENANT_SUBDOMAIN` — e.g. `acme` for `acme.britive-app.com` + - `BRITIVE_BROKER_AUTH_TOKEN` — the broker pool token + +Users reach Bridge at an external URL (the `BRIDGE_URL`). That URL must be +reachable from the user's browser, which is what the load balancer / ingress in +each option provides. + +--- + +## Before you start: one-time platform setup + +Regardless of which deployment you choose, the Britive platform needs a broker +pool, a token, a Bridge resource, and an admin profile. The +[`platform-setup/`](platform-setup/) directory contains an interactive script +that creates all of these for you and prints the env vars Bridge needs. + +Run it **first** — see [platform-setup/README.md](platform-setup/README.md). + +--- + +## Choosing an option + +| Option | Where it runs | TLS / external access | Persistence | Best for | +|--------|---------------|-----------------------|-------------|----------| +| [**Docker Compose**](docker-compose/) | Any Docker host / VM | Self-signed (8080) or mounted cert | Docker volume | Local trials, POCs, single-VM deployments | +| [**Linux VM (Docker)**](linux-vm-docker/) | A Linux server / VM | Self-signed (8080) or mounted cert | Docker volume | Standalone Linux host; on-prem or cloud VM, fully scripted | +| [**Windows VM (Docker)**](windows-vm-docker/) | A Windows server / VM (WSL 2) | Self-signed (8080) or mounted cert | Docker volume | Standalone Windows host (Linux container via WSL 2) | +| [**AWS ECS Fargate + NLB**](aws-ecs-fargate-nlb/) | AWS ECS Fargate | TLS passthrough via NLB:443 → container self-signed cert | EFS | Serverless AWS, minimal moving parts, no ACM cert needed | +| [**AWS ECS Fargate + ALB**](aws-ecs-fargate-alb/) | AWS ECS Fargate | ALB terminates TLS with an **ACM cert** on 443 | EFS | Production AWS with a real cert + custom domain | +| [**AWS ECS Fargate + ALB + SSH**](aws-ecs-fargate-alb-ssh/) | AWS ECS Fargate | Same as ALB option | EFS | ALB option **plus** broker SSH access to EC2 via a key in Secrets Manager | +| [**Kubernetes**](kubernetes/) | Any K8s cluster (EKS/AKS/GKE/on-prem) | Ingress (cert-manager / ALB) → backend HTTPS | PVC | Teams standardized on Kubernetes; Helm chart on the roadmap | + +### Quick guidance + +- **Just trying it out?** → Docker Compose. +- **A single Linux server (on-prem or cloud VM)?** → Linux VM (Docker) — scripted + install with per-distro handling. +- **A single Windows server?** → Windows VM (Docker) — runs the Linux image via + the WSL 2 backend (note the licensing/runtime caveats in that guide). +- **On AWS, want the simplest production path?** → ECS Fargate + ALB. +- **Don't have / don't want an ACM cert on AWS?** → ECS Fargate + NLB + (TLS passthrough to the container's self-signed cert). +- **Need the broker to SSH into EC2 instances?** → ECS Fargate + ALB + SSH. +- **Run everything on Kubernetes?** → Kubernetes manifests (Helm chart coming — + see the roadmap in that option's README). +- **Need the broker to run scripts that use extra tools** (`ssh`, `mysql`, + `aws`, `jq`, …)? → build a [custom image](custom-image/) and use it as the + image in any option above. + +--- + +## Extending the image (custom utilities) + +The broker runs your checkout/checkin scripts to mint and destroy ephemeral +credentials, and those scripts often need CLI tools not in the stock image. The +[`custom-image/`](custom-image/) directory shows how to layer them on top of +`britive/bridge` and deploy the result to **ECS or Kubernetes** unchanged — with +two worked examples (**Linux SSH** key provisioning and **Aurora MySQL** +temporary users/roles). This is orthogonal to the deployment options above: pick +a deployment, point its image at your custom build. + +--- + +## Directory layout + +``` +Britive Bridge/ +├── README.md # you are here +├── platform-setup/ # run FIRST — creates Britive platform objects +│ ├── quick-setup.py +│ └── README.md +├── custom-image/ # extend britive/bridge with extra utilities +│ ├── Dockerfile # base-distro & arch agnostic +│ ├── build-and-push.sh # multi-arch build → Docker Hub / ECR +│ ├── scripts/ # worked examples: Linux SSH + Aurora MySQL +│ ├── k8s-overlays/ # ready-to-apply: image + SSH key + IRSA SA +│ └── README.md +├── docker-compose/ +│ ├── docker-compose.yaml +│ └── README.md +├── linux-vm-docker/ # standalone Linux VM via Docker +│ ├── install.sh +│ ├── bridge.env.example +│ └── README.md +├── windows-vm-docker/ # standalone Windows VM via Docker (WSL 2) +│ ├── install.ps1 +│ ├── bridge.env.example +│ └── README.md +├── aws-ecs-fargate-nlb/ +│ ├── ecs-fargate-nlb.yaml +│ ├── params.example.json +│ └── README.md +├── aws-ecs-fargate-alb/ +│ ├── ecs-fargate-alb.yaml +│ ├── params.example.json +│ └── README.md +├── aws-ecs-fargate-alb-ssh/ +│ ├── ecs-fargate-alb-ssh.yaml +│ ├── params.example.json +│ └── README.md +└── kubernetes/ + ├── manifests/ + │ ├── namespace.yaml + │ ├── secret.example.yaml + │ ├── pvc.yaml + │ ├── deployment.yaml + │ ├── service.yaml + │ ├── ingress.yaml + │ ├── external-secret.example.yaml # optional: External Secrets Operator + │ └── ha-overlay.example.yaml # optional: multi-replica HA (RWM) + └── README.md # includes the public Helm chart roadmap +``` + +--- + +## A note on placeholders + +All templates use **placeholder values** (``, +`vpc-EXAMPLE...`, `arn:aws:acm:::...`, `bridge.example.com`, +etc.). Replace them with your real values before deploying. **Never commit real +tokens, private keys, account IDs, or certificate ARNs** to source control — +use the parameter files locally or, better, a secret store. diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md b/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md new file mode 100644 index 0000000..b609dd9 --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md @@ -0,0 +1,98 @@ +# Britive Bridge — AWS ECS Fargate + ALB + SSH key + +Everything in the [ALB option](../aws-ecs-fargate-alb/), **plus** an SSH private +key delivered to the broker so it can SSH into your EC2 instances. The key is +stored in **AWS Secrets Manager** and injected into the container at runtime — +it is **never baked into the image**. + +Use this when Bridge brokers SSH sessions to EC2 hosts and the broker needs a +private key to authenticate. + +## What it adds over the ALB option + +CloudFormation template `ecs-fargate-alb-ssh.yaml` adds: + +- A **Secrets Manager secret** holding the PEM-encoded private key +- Execution-role permission to **read that secret** at task start +- A container `Secrets` mapping that injects the key as `SSH_PRIVATE_KEY` +- A container entrypoint that writes the key to `/home/bridge/.ssh/id_ed25519` + (mode `600`) before starting Bridge +- Task-role permissions for **ECS Exec** (SSM) and CloudWatch Logs + +Everything else (ALB + ACM TLS, EFS, security groups) matches the ALB option. + +## Prerequisites + +- All prerequisites from the [ALB option](../aws-ecs-fargate-alb/) +- An **SSH key pair** whose **public** key is installed on the target EC2 + instances (in the relevant user's `authorized_keys`). You provide the + **private** key to this stack. + +## Configure + +```bash +cp params.example.json params.json +``` + +Same parameters as the ALB option, **plus**: + +| Parameter | Notes | +|-----------|-------| +| `BrokerSSHPrivateKey` | PEM-encoded private key (ed25519 or RSA). Newlines as `\n`. **Highly sensitive.** | + +### Filling in the private key safely + +Avoid pasting the key into a file by hand. Convert it to a single JSON-escaped +line and write `params.json` programmatically, e.g.: + +```bash +KEY=$(awk '{printf "%s\\n", $0}' ~/.ssh/bridge_ed25519) # escape newlines +# then inject "$KEY" into params.json with jq, and delete params.json after deploy +``` + +> **Never commit `params.json`** with a real key. Delete it locally once the +> stack is up — the key lives in Secrets Manager from then on. To rotate, update +> the secret value (or redeploy with a new key) and restart the service. + +## Deploy + +```bash +aws cloudformation deploy \ + --stack-name britive-bridge \ + --template-file ecs-fargate-alb-ssh.yaml \ + --parameter-overrides file://params.json \ + --capabilities CAPABILITY_NAMED_IAM +``` + +## After deploy + +Same as the ALB option (DNS record → `LoadBalancerDnsName`, set `BRIDGE_URL`, +verify `/api/health`). Additional output: + +- `BrokerSSHKeySecretArn` — the Secrets Manager ARN holding the broker key. + +Confirm the broker can reach a target host by checking out an SSH session +through Bridge, or inspect the running task with ECS Exec: + +```bash +aws ecs execute-command --cluster --task \ + --container bridge --interactive --command "/bin/sh" +# inside: ls -l /home/bridge/.ssh/id_ed25519 (should be mode 600) +``` + +## Security notes + +- The private key is marked `NoEcho` in CloudFormation and stored only in + Secrets Manager; it is not written to CloudWatch. +- Scope the target instances' `authorized_keys` to the minimum needed; prefer a + dedicated, low-privilege broker user. +- Rotate the key periodically by updating the secret and restarting the service. + +## Teardown + +```bash +aws cloudformation delete-stack --stack-name britive-bridge +``` + +> The Secrets Manager secret may be retained with a recovery window depending on +> account settings — delete it explicitly if you need immediate removal. diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml new file mode 100644 index 0000000..4b01e32 --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml @@ -0,0 +1,432 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + Britive Bridge ECS Fargate deployment. + ALB terminates TLS with an ACM certificate on port 443. + Backend target group uses HTTPS on port 8080 with certificate + verification disabled (container uses its own self-signed cert). + SSH private key injected from Secrets Manager for broker → EC2 access. + +Parameters: + StackNamePrefix: + Type: String + Default: britive-bridge + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,30}" + Description: Prefix used for named AWS resources. + VpcId: + Type: AWS::EC2::VPC::Id + Description: VPC where Bridge will run. + PublicSubnetIds: + Type: List + Description: Two public subnets for the internet-facing ALB, Fargate task, and EFS mount targets. + IngressCidr: + Type: String + Default: 0.0.0.0/0 + Description: CIDR allowed to reach Bridge HTTPS on the ALB. + ImageUri: + Type: String + Default: britive/bridge:latest + Description: Container image URI. Use a multi-arch image or one that matches CpuArchitecture. + CpuArchitecture: + Type: String + Default: ARM64 + AllowedValues: + - ARM64 + - X86_64 + Description: ECS task CPU architecture. + TaskCpu: + Type: String + Default: "1024" + AllowedValues: + - "512" + - "1024" + - "2048" + - "4096" + Description: Fargate task CPU units. + TaskMemory: + Type: String + Default: "2048" + AllowedValues: + - "1024" + - "2048" + - "3072" + - "4096" + - "5120" + - "6144" + - "7168" + - "8192" + - "16384" + - "30720" + Description: Fargate task memory in MiB. + DesiredCount: + Type: Number + Default: 1 + MinValue: 1 + MaxValue: 4 + Description: Number of Bridge tasks to run. + BrokerTenantSubdomain: + Type: String + Description: Britive broker tenant subdomain, for example acme. + BrokerAuthToken: + Type: String + NoEcho: true + Description: Britive broker pool authentication token. + BrokerSSHPrivateKey: + Type: String + NoEcho: true + Description: > + PEM-encoded private key (ed25519 or RSA) that the broker container uses + to SSH into EC2 instances. Stored in Secrets Manager and injected at + runtime — never baked into the container image. + AcmCertificateArn: + Type: String + Description: > + ARN of the ACM certificate for your domain (e.g. bridge.example.com + or the wildcard *.example.com). Must be in the same region as the ALB. + +Resources: + # ── Logging ─────────────────────────────────────────────────────────────── + BridgeLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /ecs/${StackNamePrefix} + RetentionInDays: 30 + + # ── ECS Cluster ─────────────────────────────────────────────────────────── + BridgeCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Sub ${StackNamePrefix}-cluster + + # ── Secrets Manager ─────────────────────────────────────────────────────── + BrokerSSHKeySecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub ${StackNamePrefix}/broker/ssh-private-key + Description: Private key used by the broker ECS task to SSH into EC2 instances + SecretString: !Ref BrokerSSHPrivateKey + + # ── IAM Roles ───────────────────────────────────────────────────────────── + BridgeExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${StackNamePrefix}-exec-role-${AWS::Region} + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: ReadBrokerSSHKey + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref BrokerSSHKeySecret + + BridgeTaskRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${StackNamePrefix}-task-role-${AWS::Region} + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: ECSExecSSM + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - ssmmessages:CreateControlChannel + - ssmmessages:CreateDataChannel + - ssmmessages:OpenControlChannel + - ssmmessages:OpenDataChannel + Resource: "*" + - Effect: Allow + Action: + - logs:DescribeLogGroups + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !GetAtt BridgeLogGroup.Arn + - PolicyName: SecretsManagerReadOnly + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:DescribeSecret + - secretsmanager:GetRandomPassword + - secretsmanager:ListSecrets + - secretsmanager:ListSecretVersionIds + Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:*" + + # ── Security Groups ─────────────────────────────────────────────────────── + + # ALB: accepts HTTPS from the world, allows all outbound to ECS tasks + BridgeAlbSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge ALB security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !Ref IngressCidr + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-alb-sg + + # ECS task: only accepts traffic from the ALB security group on port 8080 + BridgeTaskSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge task security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + SourceSecurityGroupId: !Ref BridgeAlbSecurityGroup + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-task-sg + + BridgeEfsSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge EFS security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 2049 + ToPort: 2049 + SourceSecurityGroupId: !Ref BridgeTaskSecurityGroup + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-efs-sg + + # ── EFS ─────────────────────────────────────────────────────────────────── + BridgeFileSystem: + Type: AWS::EFS::FileSystem + Properties: + Encrypted: true + PerformanceMode: generalPurpose + ThroughputMode: elastic + FileSystemTags: + - Key: Name + Value: !Sub ${StackNamePrefix}-data + + BridgeAccessPoint: + Type: AWS::EFS::AccessPoint + Properties: + FileSystemId: !Ref BridgeFileSystem + PosixUser: + Uid: "0" + Gid: "0" + RootDirectory: + Path: /bridge-data + CreationInfo: + OwnerUid: "0" + OwnerGid: "0" + Permissions: "0755" + + BridgeMountTargetA: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref BridgeFileSystem + SubnetId: !Select [0, !Ref PublicSubnetIds] + SecurityGroups: + - !Ref BridgeEfsSecurityGroup + + BridgeMountTargetB: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref BridgeFileSystem + SubnetId: !Select [1, !Ref PublicSubnetIds] + SecurityGroups: + - !Ref BridgeEfsSecurityGroup + + # ── ALB ─────────────────────────────────────────────────────────────────── + BridgeLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + + Name: !Sub ${StackNamePrefix}-alb + Scheme: internet-facing + Type: application # ALB, not NLB + Subnets: !Ref PublicSubnetIds + SecurityGroups: + - !Ref BridgeAlbSecurityGroup + + # Target group uses HTTPS so traffic to the container stays encrypted. + # TargetGroupAttributes disables cert verification so the container's + # self-signed cert is accepted without issue. + BridgeApiTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: !Sub ${StackNamePrefix}-api-https + VpcId: !Ref VpcId + Protocol: HTTPS # encrypted ALB → container + Port: 8080 + TargetType: ip + # Accept the container's self-signed cert — no CA verification + TargetGroupAttributes: + - Key: load_balancing.cross_zone.enabled + Value: "true" + HealthCheckProtocol: HTTPS + HealthCheckPort: "8080" + HealthCheckPath: /api/health # bridge health endpoint + HealthCheckIntervalSeconds: 30 + HealthCheckTimeoutSeconds: 10 + HealthyThresholdCount: 2 + UnhealthyThresholdCount: 3 + Matcher: + HttpCode: "200" + + # HTTPS:443 listener with ACM cert — terminates TLS at the ALB + BridgeApiListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + LoadBalancerArn: !Ref BridgeLoadBalancer + Port: 443 + Protocol: HTTPS + SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06 + Certificates: + - CertificateArn: !Ref AcmCertificateArn + DefaultActions: + - Type: forward + TargetGroupArn: !Ref BridgeApiTargetGroup + + # Optional: redirect HTTP → HTTPS + BridgeHttpRedirectListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + LoadBalancerArn: !Ref BridgeLoadBalancer + Port: 80 + Protocol: HTTP + DefaultActions: + - Type: redirect + RedirectConfig: + Protocol: HTTPS + Port: "443" + StatusCode: HTTP_301 + + # ── ECS Task Definition ─────────────────────────────────────────────────── + BridgeTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub ${StackNamePrefix}-task + Cpu: !Ref TaskCpu + Memory: !Ref TaskMemory + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + RuntimePlatform: + OperatingSystemFamily: LINUX + CpuArchitecture: !Ref CpuArchitecture + ExecutionRoleArn: !GetAtt BridgeExecutionRole.Arn + TaskRoleArn: !GetAtt BridgeTaskRole.Arn + Volumes: + - Name: bridge-data + EFSVolumeConfiguration: + FilesystemId: !Ref BridgeFileSystem + TransitEncryption: ENABLED + AuthorizationConfig: + AccessPointId: !Ref BridgeAccessPoint + IAM: DISABLED + ContainerDefinitions: + - Name: bridge + Image: !Ref ImageUri + Essential: true + PortMappings: + - ContainerPort: 8080 + Protocol: tcp + # Write SSH key to /root/.ssh before starting the bridge process. + # SSH_PRIVATE_KEY is injected from Secrets Manager by the ECS agent + # before this command runs, so it is available as an env var. + EntryPoint: + - "/bin/sh" + - "-c" + Command: + - "mkdir -p /home/bridge/.ssh && echo \"$SSH_PRIVATE_KEY\" > /home/bridge/.ssh/id_ed25519 && chmod 700 /home/bridge/.ssh && chmod 600 /home/bridge/.ssh/id_ed25519 && exec /entrypoint.sh" + Environment: + - Name: BRITIVE_BROKER_TENANT_SUBDOMAIN + Value: !Ref BrokerTenantSubdomain + - Name: BRITIVE_BROKER_AUTH_TOKEN + Value: !Ref BrokerAuthToken + Secrets: + - Name: SSH_PRIVATE_KEY + ValueFrom: !Ref BrokerSSHKeySecret + MountPoints: + - SourceVolume: bridge-data + ContainerPath: /data + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref BridgeLogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: bridge + + # ── ECS Service ─────────────────────────────────────────────────────────── + BridgeService: + Type: AWS::ECS::Service + DependsOn: + - BridgeApiListener + - BridgeMountTargetA + - BridgeMountTargetB + Properties: + ServiceName: !Sub ${StackNamePrefix}-service + Cluster: !Ref BridgeCluster + TaskDefinition: !Ref BridgeTaskDefinition + DesiredCount: !Ref DesiredCount + LaunchType: FARGATE + EnableExecuteCommand: true + HealthCheckGracePeriodSeconds: 60 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - !Ref BridgeTaskSecurityGroup + Subnets: !Ref PublicSubnetIds + LoadBalancers: + - ContainerName: bridge + ContainerPort: 8080 + TargetGroupArn: !Ref BridgeApiTargetGroup + +Outputs: + ClusterName: + Value: !Ref BridgeCluster + ServiceName: + Value: !Ref BridgeService + LoadBalancerDnsName: + Value: !GetAtt BridgeLoadBalancer.DNSName + Description: Point your Route 53 ALIAS record at this value. + BridgeUrl: + Description: External URL for Britive BRIDGE_URL script parameter. + Value: !Sub https://${BridgeLoadBalancer.DNSName} + DataFileSystemId: + Value: !Ref BridgeFileSystem + BrokerSSHKeySecretArn: + Description: Secrets Manager ARN for the broker SSH private key. + Value: !Ref BrokerSSHKeySecret diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json b/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json new file mode 100644 index 0000000..3657802 --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json @@ -0,0 +1,15 @@ +[ + { "ParameterKey": "StackNamePrefix", "ParameterValue": "britive-bridge" }, + { "ParameterKey": "VpcId", "ParameterValue": "vpc-EXAMPLE0000000000" }, + { "ParameterKey": "PublicSubnetIds", "ParameterValue": "subnet-EXAMPLEAAAA0000,subnet-EXAMPLEBBBB0000" }, + { "ParameterKey": "IngressCidr", "ParameterValue": "0.0.0.0/0" }, + { "ParameterKey": "ImageUri", "ParameterValue": "britive/bridge:latest" }, + { "ParameterKey": "CpuArchitecture", "ParameterValue": "ARM64" }, + { "ParameterKey": "TaskCpu", "ParameterValue": "1024" }, + { "ParameterKey": "TaskMemory", "ParameterValue": "2048" }, + { "ParameterKey": "DesiredCount", "ParameterValue": "1" }, + { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, + { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" }, + { "ParameterKey": "AcmCertificateArn", "ParameterValue": "arn:aws:acm:::certificate/" }, + { "ParameterKey": "BrokerSSHPrivateKey", "ParameterValue": "-----BEGIN OPENSSH PRIVATE KEY-----\n\n-----END OPENSSH PRIVATE KEY-----" } +] diff --git a/Britive Bridge/aws-ecs-fargate-alb/README.md b/Britive Bridge/aws-ecs-fargate-alb/README.md new file mode 100644 index 0000000..b5ce4df --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-alb/README.md @@ -0,0 +1,89 @@ +# Britive Bridge — AWS ECS Fargate + ALB (ACM TLS) + +Run Bridge on AWS ECS Fargate behind an internet-facing **Application Load +Balancer (ALB)** that **terminates TLS with an ACM certificate** on port 443. +This is the recommended production path on AWS when you have a custom domain and +want a browser-trusted certificate. + +The ALB re-encrypts to the container over HTTPS:8080; the container keeps its +self-signed cert and the ALB target group is configured to **not verify** it. + +## What it deploys + +CloudFormation template `ecs-fargate-alb.yaml` creates everything the +[NLB option](../aws-ecs-fargate-nlb/) does, plus: + +- An **ALB** with: + - HTTPS:443 listener using your **ACM certificate** (TLS 1.2/1.3 policy) + - HTTP:80 listener that **redirects to 443** + - An HTTPS target group on 8080 with health checks on `/api/health` +- A dedicated **ALB security group** (443 from your `IngressCidr`); the task SG + only accepts 8080 **from the ALB SG** + +Traffic path: `client → ALB:443 (ACM TLS) → task:8080 (HTTPS, cert not verified)`. + +## Prerequisites + +- AWS CLI v2 with credentials that can create ECS/EFS/ELB/IAM/Logs +- A **VPC** and **two public subnets** in different AZs +- An **ACM certificate** in the **same region** as the ALB, covering your + domain (e.g. `bridge.example.com` or `*.example.com`) +- A DNS zone where you can create an ALIAS/CNAME to the ALB +- Completed [platform setup](../platform-setup/) + +## Configure + +```bash +cp params.example.json params.json +``` + +Edit `params.json` — same parameters as the NLB option, **plus**: + +| Parameter | Notes | +|-----------|-------| +| `AcmCertificateArn` | ARN of your ACM cert, same region as the ALB: `arn:aws:acm:::certificate/` | + +> Keep `params.json` out of source control (broker token + cert ARN). + +## Deploy + +```bash +aws cloudformation deploy \ + --stack-name britive-bridge \ + --template-file ecs-fargate-alb.yaml \ + --parameter-overrides file://params.json \ + --capabilities CAPABILITY_NAMED_IAM +``` + +## After deploy + +```bash +aws cloudformation describe-stacks --stack-name britive-bridge \ + --query "Stacks[0].Outputs" --output table +``` + +1. Take `LoadBalancerDnsName` from the outputs and create a **DNS ALIAS/CNAME** + record for your domain (e.g. `bridge.example.com`) pointing at it. +2. Use `https://bridge.example.com` as your `BRIDGE_URL` and make sure the + Britive Bridge **resource** uses that URL. +3. Verify: + + ```bash + curl -sf https://bridge.example.com/api/health + ``` + + (No `-k` needed now — the ALB serves a trusted ACM cert.) + +## Notes + +- The ALB→container hop stays encrypted (HTTPS:8080) but skips CA verification, + which is why the container's self-signed cert is fine. Do not set + `TLS_CERT_FILE`/`TLS_KEY_FILE` on the container for this setup. +- Health checks hit `/api/health` and expect HTTP 200. +- Tighten `IngressCidr` to known client ranges for production. + +## Teardown + +```bash +aws cloudformation delete-stack --stack-name britive-bridge +``` diff --git a/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml new file mode 100644 index 0000000..24a65c2 --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml @@ -0,0 +1,365 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + Britive Bridge ECS Fargate deployment. + ALB terminates TLS with an ACM certificate on port 443. + Backend target group uses HTTPS on port 8080 with certificate + verification disabled (container uses its own self-signed cert). + +Parameters: + StackNamePrefix: + Type: String + Default: britive-bridge + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,30}" + Description: Prefix used for named AWS resources. + VpcId: + Type: AWS::EC2::VPC::Id + Description: VPC where Bridge will run. + PublicSubnetIds: + Type: List + Description: Two public subnets for the internet-facing ALB, Fargate task, and EFS mount targets. + IngressCidr: + Type: String + Default: 0.0.0.0/0 + Description: CIDR allowed to reach Bridge HTTPS on the ALB. + ImageUri: + Type: String + Default: britive/bridge:latest + Description: Container image URI. Use a multi-arch image or one that matches CpuArchitecture. + CpuArchitecture: + Type: String + Default: ARM64 + AllowedValues: + - ARM64 + - X86_64 + Description: ECS task CPU architecture. + TaskCpu: + Type: String + Default: "1024" + AllowedValues: + - "512" + - "1024" + - "2048" + - "4096" + Description: Fargate task CPU units. + TaskMemory: + Type: String + Default: "2048" + AllowedValues: + - "1024" + - "2048" + - "3072" + - "4096" + - "5120" + - "6144" + - "7168" + - "8192" + - "16384" + - "30720" + Description: Fargate task memory in MiB. + DesiredCount: + Type: Number + Default: 1 + MinValue: 1 + MaxValue: 4 + Description: Number of Bridge tasks to run. + BrokerTenantSubdomain: + Type: String + Description: Britive broker tenant subdomain, for example acme. + BrokerAuthToken: + Type: String + NoEcho: true + Description: Britive broker pool authentication token. + AcmCertificateArn: + Type: String + Description: > + ARN of the ACM certificate for your domain (e.g. bridge.example.com + or the wildcard *.example.com). Must be in the same region as the ALB. + +Resources: + # ── Logging ─────────────────────────────────────────────────────────────── + BridgeLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /ecs/${StackNamePrefix} + RetentionInDays: 30 + + # ── ECS Cluster ─────────────────────────────────────────────────────────── + BridgeCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Sub ${StackNamePrefix}-cluster + + # ── IAM Roles ───────────────────────────────────────────────────────────── + BridgeExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${StackNamePrefix}-exec-role-${AWS::Region} + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + + BridgeTaskRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${StackNamePrefix}-task-role-${AWS::Region} + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + + # ── Security Groups ─────────────────────────────────────────────────────── + + # ALB: accepts HTTPS from the world, allows all outbound to ECS tasks + BridgeAlbSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge ALB security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !Ref IngressCidr + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-alb-sg + + # ECS task: only accepts traffic from the ALB security group on port 8080 + BridgeTaskSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge task security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + SourceSecurityGroupId: !Ref BridgeAlbSecurityGroup + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-task-sg + + BridgeEfsSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge EFS security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 2049 + ToPort: 2049 + SourceSecurityGroupId: !Ref BridgeTaskSecurityGroup + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-efs-sg + + # ── EFS ─────────────────────────────────────────────────────────────────── + BridgeFileSystem: + Type: AWS::EFS::FileSystem + Properties: + Encrypted: true + PerformanceMode: generalPurpose + ThroughputMode: elastic + FileSystemTags: + - Key: Name + Value: !Sub ${StackNamePrefix}-data + + BridgeAccessPoint: + Type: AWS::EFS::AccessPoint + Properties: + FileSystemId: !Ref BridgeFileSystem + PosixUser: + Uid: "0" + Gid: "0" + RootDirectory: + Path: /bridge-data + CreationInfo: + OwnerUid: "0" + OwnerGid: "0" + Permissions: "0755" + + BridgeMountTargetA: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref BridgeFileSystem + SubnetId: !Select [0, !Ref PublicSubnetIds] + SecurityGroups: + - !Ref BridgeEfsSecurityGroup + + BridgeMountTargetB: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref BridgeFileSystem + SubnetId: !Select [1, !Ref PublicSubnetIds] + SecurityGroups: + - !Ref BridgeEfsSecurityGroup + + # ── ALB ─────────────────────────────────────────────────────────────────── + BridgeLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + + Name: !Sub ${StackNamePrefix}-alb + Scheme: internet-facing + Type: application # ALB, not NLB + Subnets: !Ref PublicSubnetIds + SecurityGroups: + - !Ref BridgeAlbSecurityGroup + + # Target group uses HTTPS so traffic to the container stays encrypted. + # TargetGroupAttributes disables cert verification so the container's + # self-signed cert is accepted without issue. + BridgeApiTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: !Sub ${StackNamePrefix}-api-https + VpcId: !Ref VpcId + Protocol: HTTPS # encrypted ALB → container + Port: 8080 + TargetType: ip + # Accept the container's self-signed cert — no CA verification + TargetGroupAttributes: + - Key: load_balancing.cross_zone.enabled + Value: "true" + HealthCheckProtocol: HTTPS + HealthCheckPort: "8080" + HealthCheckPath: /api/health # bridge health endpoint + HealthCheckIntervalSeconds: 30 + HealthCheckTimeoutSeconds: 10 + HealthyThresholdCount: 2 + UnhealthyThresholdCount: 3 + Matcher: + HttpCode: "200" + + # HTTPS:443 listener with ACM cert — terminates TLS at the ALB + BridgeApiListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + LoadBalancerArn: !Ref BridgeLoadBalancer + Port: 443 + Protocol: HTTPS + SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06 + Certificates: + - CertificateArn: !Ref AcmCertificateArn + DefaultActions: + - Type: forward + TargetGroupArn: !Ref BridgeApiTargetGroup + + # Optional: redirect HTTP → HTTPS + BridgeHttpRedirectListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + LoadBalancerArn: !Ref BridgeLoadBalancer + Port: 80 + Protocol: HTTP + DefaultActions: + - Type: redirect + RedirectConfig: + Protocol: HTTPS + Port: "443" + StatusCode: HTTP_301 + + # ── ECS Task Definition ─────────────────────────────────────────────────── + BridgeTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub ${StackNamePrefix}-task + Cpu: !Ref TaskCpu + Memory: !Ref TaskMemory + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + RuntimePlatform: + OperatingSystemFamily: LINUX + CpuArchitecture: !Ref CpuArchitecture + ExecutionRoleArn: !GetAtt BridgeExecutionRole.Arn + TaskRoleArn: !GetAtt BridgeTaskRole.Arn + Volumes: + - Name: bridge-data + EFSVolumeConfiguration: + FilesystemId: !Ref BridgeFileSystem + TransitEncryption: ENABLED + AuthorizationConfig: + AccessPointId: !Ref BridgeAccessPoint + IAM: DISABLED + ContainerDefinitions: + - Name: bridge + Image: !Ref ImageUri + Essential: true + PortMappings: + - ContainerPort: 8080 + Protocol: tcp + Environment: + - Name: BRITIVE_BROKER_TENANT_SUBDOMAIN + Value: !Ref BrokerTenantSubdomain + - Name: BRITIVE_BROKER_AUTH_TOKEN + Value: !Ref BrokerAuthToken + # No TLS_CERT_FILE / TLS_KEY_FILE set → container uses its + # auto-generated self-signed cert. ALB accepts it regardless. + MountPoints: + - SourceVolume: bridge-data + ContainerPath: /data + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref BridgeLogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: bridge + + # ── ECS Service ─────────────────────────────────────────────────────────── + BridgeService: + Type: AWS::ECS::Service + DependsOn: + - BridgeApiListener + - BridgeMountTargetA + - BridgeMountTargetB + Properties: + ServiceName: !Sub ${StackNamePrefix}-service + Cluster: !Ref BridgeCluster + TaskDefinition: !Ref BridgeTaskDefinition + DesiredCount: !Ref DesiredCount + LaunchType: FARGATE + EnableExecuteCommand: true + HealthCheckGracePeriodSeconds: 60 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - !Ref BridgeTaskSecurityGroup + Subnets: !Ref PublicSubnetIds + LoadBalancers: + - ContainerName: bridge + ContainerPort: 8080 + TargetGroupArn: !Ref BridgeApiTargetGroup + +Outputs: + ClusterName: + Value: !Ref BridgeCluster + ServiceName: + Value: !Ref BridgeService + LoadBalancerDnsName: + Value: !GetAtt BridgeLoadBalancer.DNSName + Description: Point your Route 53 ALIAS record at this value. + BridgeUrl: + Description: External URL for Britive BRIDGE_URL script parameter. + Value: !Sub https://${BridgeLoadBalancer.DNSName} + DataFileSystemId: + Value: !Ref BridgeFileSystem diff --git a/Britive Bridge/aws-ecs-fargate-alb/params.example.json b/Britive Bridge/aws-ecs-fargate-alb/params.example.json new file mode 100644 index 0000000..9a34161 --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-alb/params.example.json @@ -0,0 +1,14 @@ +[ + { "ParameterKey": "StackNamePrefix", "ParameterValue": "britive-bridge" }, + { "ParameterKey": "VpcId", "ParameterValue": "vpc-EXAMPLE0000000000" }, + { "ParameterKey": "PublicSubnetIds", "ParameterValue": "subnet-EXAMPLEAAAA0000,subnet-EXAMPLEBBBB0000" }, + { "ParameterKey": "IngressCidr", "ParameterValue": "0.0.0.0/0" }, + { "ParameterKey": "ImageUri", "ParameterValue": "britive/bridge:latest" }, + { "ParameterKey": "CpuArchitecture", "ParameterValue": "ARM64" }, + { "ParameterKey": "TaskCpu", "ParameterValue": "1024" }, + { "ParameterKey": "TaskMemory", "ParameterValue": "2048" }, + { "ParameterKey": "DesiredCount", "ParameterValue": "1" }, + { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, + { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" }, + { "ParameterKey": "AcmCertificateArn", "ParameterValue": "arn:aws:acm:::certificate/" } +] diff --git a/Britive Bridge/aws-ecs-fargate-nlb/README.md b/Britive Bridge/aws-ecs-fargate-nlb/README.md new file mode 100644 index 0000000..0fe2c7b --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-nlb/README.md @@ -0,0 +1,107 @@ +# Britive Bridge — AWS ECS Fargate + NLB + +Run Bridge serverlessly on AWS ECS Fargate, fronted by an internet-facing +**Network Load Balancer (NLB)** that passes TLS straight through to the +container. **No ACM certificate required** — the container's own self-signed +cert terminates TLS. + +Use this when you want the simplest AWS production path and don't have (or don't +want to manage) an ACM certificate and custom domain. If you do want a real cert +on a custom domain, use the [ALB option](../aws-ecs-fargate-alb/) instead. + +## What it deploys + +CloudFormation template `ecs-fargate-nlb.yaml` creates: + +- ECS **cluster**, **task definition**, and **service** (Fargate, ARM64 by default) +- An internet-facing **NLB** with a TCP:443 listener → target group TCP:8080 +- **EFS** file system + access point + mount targets, mounted at `/data` (encrypted, transit encryption on) +- **Security groups** (task allows 8080 from your `IngressCidr`; EFS allows 2049 from the task) +- **IAM** execution + task roles +- A **CloudWatch** log group (30-day retention) +- ECS Exec enabled for debugging + +Traffic path: `client → NLB:443 (TCP passthrough) → task:8080 (container self-signed TLS)`. + +## Prerequisites + +- AWS CLI v2 configured with credentials that can create ECS/EFS/ELB/IAM/Logs +- A **VPC** and **two public subnets** in different AZs +- Completed [platform setup](../platform-setup/) — you need + `BrokerTenantSubdomain` and `BrokerAuthToken` +- IAM permission to create the resources above (CAPABILITY_NAMED_IAM) + +## Configure + +Copy the example params and fill in real values: + +```bash +cp params.example.json params.json +``` + +Edit `params.json`: + +| Parameter | Notes | +|-----------|-------| +| `StackNamePrefix` | Prefix for named resources (default `britive-bridge`) | +| `VpcId` | Your VPC ID | +| `PublicSubnetIds` | **Two** public subnet IDs, comma-separated | +| `IngressCidr` | CIDR allowed to reach Bridge (default `0.0.0.0/0` — tighten this) | +| `ImageUri` | Container image (default `britive/bridge:latest`) | +| `CpuArchitecture` | `ARM64` (default) or `X86_64` — must match the image | +| `TaskCpu` / `TaskMemory` | Fargate sizing | +| `DesiredCount` | Number of tasks (1–4) | +| `BrokerTenantSubdomain` | From platform setup | +| `BrokerAuthToken` | From platform setup — **secret** | + +> Keep `params.json` out of source control (it contains the broker token). + +## Deploy + +```bash +aws cloudformation deploy \ + --stack-name britive-bridge \ + --template-file ecs-fargate-nlb.yaml \ + --parameter-overrides file://params.json \ + --capabilities CAPABILITY_NAMED_IAM +``` + +> `deploy` expects `ParameterKey/ParameterValue` pairs via +> `--parameter-overrides`. If you prefer `create-stack`/`update-stack`, pass +> the same file with `--parameters file://params.json`. + +## After deploy + +Get the outputs (NLB DNS name and the Bridge URL): + +```bash +aws cloudformation describe-stacks --stack-name britive-bridge \ + --query "Stacks[0].Outputs" --output table +``` + +Key outputs: + +- `BridgeUrl` — use this as your `BRIDGE_URL` (point a DNS record at + `LoadBalancerDnsName` if you want a friendly hostname) +- `LoadBalancerDnsName`, `ClusterName`, `ServiceName`, `DataFileSystemId` + +Make sure the Bridge **resource** in Britive (from platform setup) uses this +URL. Then verify: + +```bash +curl -sfk https:///api/health +``` + +## Notes + +- Because the NLB passes TCP through, browsers will see the container's + **self-signed certificate** and warn. For a trusted cert on a custom domain, + use the [ALB option](../aws-ecs-fargate-alb/). +- EFS keeps `/data` durable across task restarts and across AZs. +- Tighten `IngressCidr` to your corporate egress ranges for production. + +## Teardown + +```bash +aws cloudformation delete-stack --stack-name britive-bridge +``` diff --git a/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml new file mode 100644 index 0000000..e98d6d4 --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml @@ -0,0 +1,272 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: Britive Bridge ECS Fargate deployment +Parameters: + StackNamePrefix: + Type: String + Default: britive-bridge + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,30}" + Description: Prefix used for named AWS resources. + VpcId: + Type: AWS::EC2::VPC::Id + Description: VPC where Bridge will run. + PublicSubnetIds: + Type: List + Description: Two public subnets for the internet-facing NLB, Fargate task, and EFS mount targets. + IngressCidr: + Type: String + Default: 0.0.0.0/0 + Description: CIDR allowed to reach Bridge HTTPS. + ImageUri: + Type: String + Default: britive/bridge:latest + Description: Container image URI. Use a multi-arch image or one that matches CpuArchitecture. + CpuArchitecture: + Type: String + Default: ARM64 + AllowedValues: + - ARM64 + - X86_64 + Description: ECS task CPU architecture. + TaskCpu: + Type: String + Default: "1024" + AllowedValues: + - "512" + - "1024" + - "2048" + - "4096" + Description: Fargate task CPU units. + TaskMemory: + Type: String + Default: "2048" + AllowedValues: + - "1024" + - "2048" + - "3072" + - "4096" + - "5120" + - "6144" + - "7168" + - "8192" + - "16384" + - "30720" + Description: Fargate task memory in MiB. + DesiredCount: + Type: Number + Default: 1 + MinValue: 1 + MaxValue: 4 + Description: Number of Bridge tasks to run. + BrokerTenantSubdomain: + Type: String + Description: Britive broker tenant subdomain, for example acme. + BrokerAuthToken: + Type: String + NoEcho: true + Description: Britive broker pool authentication token. +Resources: + BridgeLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /ecs/${StackNamePrefix} + RetentionInDays: 30 + BridgeCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Sub ${StackNamePrefix}-cluster + BridgeExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${StackNamePrefix}-exec-role-${AWS::Region} + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + BridgeTaskRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${StackNamePrefix}-task-role-${AWS::Region} + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + BridgeTaskSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge task security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + CidrIp: !Ref IngressCidr + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-task-sg + BridgeEfsSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge EFS security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 2049 + ToPort: 2049 + SourceSecurityGroupId: !Ref BridgeTaskSecurityGroup + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-efs-sg + BridgeFileSystem: + Type: AWS::EFS::FileSystem + Properties: + Encrypted: true + PerformanceMode: generalPurpose + ThroughputMode: elastic + FileSystemTags: + - Key: Name + Value: !Sub ${StackNamePrefix}-data + BridgeAccessPoint: + Type: AWS::EFS::AccessPoint + Properties: + FileSystemId: !Ref BridgeFileSystem + PosixUser: + Uid: "0" + Gid: "0" + RootDirectory: + Path: /bridge-data + CreationInfo: + OwnerUid: "0" + OwnerGid: "0" + Permissions: "0755" + BridgeMountTargetA: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref BridgeFileSystem + SubnetId: !Select [0, !Ref PublicSubnetIds] + SecurityGroups: + - !Ref BridgeEfsSecurityGroup + BridgeMountTargetB: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref BridgeFileSystem + SubnetId: !Select [1, !Ref PublicSubnetIds] + SecurityGroups: + - !Ref BridgeEfsSecurityGroup + BridgeLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Name: !Sub ${StackNamePrefix}-nlb + Scheme: internet-facing + Type: network + Subnets: !Ref PublicSubnetIds + BridgeApiTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: !Sub ${StackNamePrefix}-api + VpcId: !Ref VpcId + Protocol: TCP + Port: 8080 + TargetType: ip + HealthCheckProtocol: TCP + HealthCheckPort: "8080" + BridgeApiListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + LoadBalancerArn: !Ref BridgeLoadBalancer + Port: 443 + Protocol: TCP + DefaultActions: + - Type: forward + TargetGroupArn: !Ref BridgeApiTargetGroup + BridgeTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub ${StackNamePrefix}-task + Cpu: !Ref TaskCpu + Memory: !Ref TaskMemory + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + RuntimePlatform: + OperatingSystemFamily: LINUX + CpuArchitecture: !Ref CpuArchitecture + ExecutionRoleArn: !GetAtt BridgeExecutionRole.Arn + TaskRoleArn: !GetAtt BridgeTaskRole.Arn + Volumes: + - Name: bridge-data + EFSVolumeConfiguration: + FilesystemId: !Ref BridgeFileSystem + TransitEncryption: ENABLED + AuthorizationConfig: + AccessPointId: !Ref BridgeAccessPoint + IAM: DISABLED + ContainerDefinitions: + - Name: bridge + Image: !Ref ImageUri + Essential: true + PortMappings: + - ContainerPort: 8080 + Protocol: tcp + Environment: + - Name: BRITIVE_BROKER_TENANT_SUBDOMAIN + Value: !Ref BrokerTenantSubdomain + - Name: BRITIVE_BROKER_AUTH_TOKEN + Value: !Ref BrokerAuthToken + MountPoints: + - SourceVolume: bridge-data + ContainerPath: /data + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref BridgeLogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: bridge + BridgeService: + Type: AWS::ECS::Service + DependsOn: + - BridgeApiListener + - BridgeMountTargetA + Properties: + ServiceName: !Sub ${StackNamePrefix}-service + Cluster: !Ref BridgeCluster + TaskDefinition: !Ref BridgeTaskDefinition + DesiredCount: !Ref DesiredCount + LaunchType: FARGATE + EnableExecuteCommand: true + HealthCheckGracePeriodSeconds: 60 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - !Ref BridgeTaskSecurityGroup + Subnets: !Ref PublicSubnetIds + LoadBalancers: + - ContainerName: bridge + ContainerPort: 8080 + TargetGroupArn: !Ref BridgeApiTargetGroup +Outputs: + ClusterName: + Value: !Ref BridgeCluster + ServiceName: + Value: !Ref BridgeService + LoadBalancerDnsName: + Value: !GetAtt BridgeLoadBalancer.DNSName + BridgeUrl: + Description: External URL for Britive BRIDGE_URL script parameter. + Value: !Sub https://${BridgeLoadBalancer.DNSName} + DataFileSystemId: + Value: !Ref BridgeFileSystem diff --git a/Britive Bridge/aws-ecs-fargate-nlb/params.example.json b/Britive Bridge/aws-ecs-fargate-nlb/params.example.json new file mode 100644 index 0000000..a633557 --- /dev/null +++ b/Britive Bridge/aws-ecs-fargate-nlb/params.example.json @@ -0,0 +1,13 @@ +[ + { "ParameterKey": "StackNamePrefix", "ParameterValue": "britive-bridge" }, + { "ParameterKey": "VpcId", "ParameterValue": "vpc-EXAMPLE0000000000" }, + { "ParameterKey": "PublicSubnetIds", "ParameterValue": "subnet-EXAMPLEAAAA0000,subnet-EXAMPLEBBBB0000" }, + { "ParameterKey": "IngressCidr", "ParameterValue": "0.0.0.0/0" }, + { "ParameterKey": "ImageUri", "ParameterValue": "britive/bridge:latest" }, + { "ParameterKey": "CpuArchitecture", "ParameterValue": "ARM64" }, + { "ParameterKey": "TaskCpu", "ParameterValue": "1024" }, + { "ParameterKey": "TaskMemory", "ParameterValue": "2048" }, + { "ParameterKey": "DesiredCount", "ParameterValue": "1" }, + { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, + { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" } +] diff --git a/Britive Bridge/custom-image/.dockerignore b/Britive Bridge/custom-image/.dockerignore new file mode 100644 index 0000000..657fe86 --- /dev/null +++ b/Britive Bridge/custom-image/.dockerignore @@ -0,0 +1,4 @@ +README.md +build-and-push.sh +.dockerignore +**/*.md diff --git a/Britive Bridge/custom-image/Dockerfile b/Britive Bridge/custom-image/Dockerfile new file mode 100644 index 0000000..cd41305 --- /dev/null +++ b/Britive Bridge/custom-image/Dockerfile @@ -0,0 +1,103 @@ +# syntax=docker/dockerfile:1 +# +# Custom Britive Bridge image +# =========================== +# Extends the stock britive/bridge image (which bundles the broker + bridge.sh) +# with the extra CLI utilities that the broker's checkout/checkin scripts need: +# +# - openssh-client : ssh, ssh-keygen (Linux SSH key provisioning example) +# - python3 : JSON encoding in the SSH checkout payload +# - mysql/mariadb : mysql client (Aurora MySQL examples) +# - jq : parse Secrets Manager JSON +# - awscli v2 : read DB master creds from AWS Secrets Manager +# - ca-certificates, bash, coreutils (base64/tr/head) +# +# The broker fetches the actual checkout/checkin scripts from the Britive +# platform at runtime, so they do NOT need to be baked in. We optionally COPY +# the example scripts under /opt/britive-broker/custom-scripts for reference / +# local testing — see the COPY block near the bottom (commented by default). +# +# The base image's distro is detected at build time, so this Dockerfile works +# whether britive/bridge is Debian/Ubuntu (apt), Alpine (apk), or RHEL-like +# (dnf/microdnf) based. +# +# Build (multi-arch recommended so it runs on Fargate ARM64 and x86 nodes): +# docker buildx build --platform linux/amd64,linux/arm64 \ +# -t /britive-bridge-custom:latest --push . +# +# Or single-arch for local use: +# docker build -t britive-bridge-custom:latest . + +ARG BASE_IMAGE=britive/bridge:latest +FROM ${BASE_IMAGE} + +# Root needed to install packages; the base image's own entrypoint/user is +# preserved (we do not override USER, ENTRYPOINT, or CMD). +USER root + +# TARGETARCH is provided automatically by buildx (amd64 / arm64). Falls back to +# the build host arch for a plain `docker build`. +ARG TARGETARCH + +# Single RUN layer. The leading `set -eu` aborts the build on any failure. +RUN set -eu; \ + # ── 1. Install OS packages via whichever package manager exists ────────── + if command -v apt-get >/dev/null 2>&1; then \ + export DEBIAN_FRONTEND=noninteractive; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates curl unzip bash coreutils \ + openssh-client python3 jq \ + default-mysql-client; \ + rm -rf /var/lib/apt/lists/*; \ + PKG=apt; \ + elif command -v apk >/dev/null 2>&1; then \ + apk add --no-cache \ + ca-certificates curl unzip bash coreutils \ + openssh-client python3 jq \ + mysql-client \ + aws-cli; \ + PKG=apk; \ + elif command -v microdnf >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1; then \ + DNF=$(command -v dnf || command -v microdnf); \ + $DNF install -y \ + ca-certificates curl unzip bash coreutils \ + openssh-clients python3 jq \ + mariadb; \ + $DNF clean all || true; \ + PKG=dnf; \ + else \ + echo "ERROR: no supported package manager (apt/apk/dnf) in base image" >&2; \ + exit 1; \ + fi; \ + # ── 2. AWS CLI v2 ──────────────────────────────────────────────────────── + # apk already installed aws-cli above (the official v2 installer is glibc- + # only and won't run on Alpine/musl). For apt/dnf (glibc) use the official + # v2 installer, picking the bundle that matches the target architecture. + if [ "$PKG" != "apk" ]; then \ + case "${TARGETARCH:-$(uname -m)}" in \ + amd64|x86_64) AWS_ARCH=x86_64 ;; \ + arm64|aarch64) AWS_ARCH=aarch64 ;; \ + *) echo "ERROR: unsupported arch '${TARGETARCH:-$(uname -m)}'" >&2; exit 1 ;; \ + esac; \ + curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip" -o /tmp/awscliv2.zip; \ + unzip -q /tmp/awscliv2.zip -d /tmp; \ + /tmp/aws/install; \ + rm -rf /tmp/aws /tmp/awscliv2.zip; \ + fi; \ + # ── 3. Sanity check: every utility the example scripts call must resolve ── + for c in ssh ssh-keygen base64 python3 jq aws mysql bash; do \ + command -v "$c" >/dev/null 2>&1 || { echo "MISSING: $c" >&2; exit 1; }; \ + done; \ + echo "All required utilities present." + +# ── Optional: bake the example scripts in for reference / local testing ────── +# In production the Britive platform delivers checkout/checkin scripts to the +# broker at runtime, so this is NOT required. Uncomment to ship them in-image. +# COPY scripts/ /opt/britive-broker/custom-scripts/ +# RUN chmod -R 0755 /opt/britive-broker/custom-scripts + +# Drop back to the base image's runtime user if it defined one. The stock image +# runs as the 'bridge' user; restore it so we don't run the broker as root. +# (If the base image's user is not 'bridge', adjust or remove this line.) +USER bridge diff --git a/Britive Bridge/custom-image/README.md b/Britive Bridge/custom-image/README.md new file mode 100644 index 0000000..4cd1de6 --- /dev/null +++ b/Britive Bridge/custom-image/README.md @@ -0,0 +1,210 @@ +# Britive Bridge — Custom Image (extend the broker with utilities) + +The stock `britive/bridge` image ships the **broker** (and `bridge.sh`). The +broker runs your **checkout/checkin scripts** to create and destroy ephemeral +credentials. Those scripts call command-line tools — `ssh`, `mysql`, `aws`, +`jq`, `python3`, … — that aren't all present in the stock image. + +This directory shows how to build a **custom image** that layers those tools on +top of `britive/bridge`, then deploy it anywhere the stock image goes +(**ECS** or **Kubernetes**) by simply pointing at your image instead. + +Two worked examples are included: + +1. **Linux SSH** — JIT SSH access by provisioning a one-time `ed25519` key on a + target host and registering a proxied bridge session. +2. **Aurora MySQL** — JIT database access by creating/dropping a temporary MySQL + user (or granting/revoking a role), using master creds from AWS Secrets Manager. + +--- + +## How it fits together + +``` + build & push deploy (ECS or K8s) + Dockerfile ─────────────► your-registry/ ──────────────► broker container + (FROM britive/bridge britive-bridge- runs checkout/checkin + + ssh/mysql/aws/jq...) custom:latest scripts that now have + all the tools they need +``` + +- The broker still pulls the **actual scripts from the Britive platform** at + checkout time (uploaded during profile setup). You do **not** have to bake + scripts into the image — you bake in the **utilities** they depend on. +- The example scripts live under [`scripts/`](scripts/) for reference and local + testing, and can optionally be baked in (see the commented `COPY` in the + `Dockerfile`). + +--- + +## What the examples need (and the Dockerfile installs) + +| Utility | Linux SSH | Aurora MySQL | Notes | +|---------|:---------:|:------------:|-------| +| `ssh`, `ssh-keygen` | ✓ | | provision the one-time key on the target | +| `python3` | ✓ | | JSON-encode the private key in the payload | +| `base64`, `tr`, `head` | ✓ | ✓ | coreutils — present in most bases, installed to be safe | +| `mysql` (client) | | ✓ | run `CREATE/DROP USER`, `GRANT/REVOKE` | +| `aws` (CLI v2) | | ✓ | read DB master creds from Secrets Manager | +| `jq` | | ✓ | parse the secret JSON | +| `bridge.sh` | ✓ | | already in the base image (`/opt/britive-broker/scripts/bridge.sh`) | + +The `Dockerfile` is **base-distro agnostic** (detects apt / apk / dnf) and +**arch-aware** for the AWS CLI (x86_64 + arm64), so the resulting image runs on +both Fargate ARM64 and mixed Kubernetes nodes. + +--- + +## Prerequisites + +- Docker with **buildx** (for multi-arch builds) — `docker buildx version` +- A container registry you can push to: **Docker Hub** (to keep images and the + planned Helm chart in one place — see the [k8s option](../kubernetes/)) or + **Amazon ECR** +- Completed [platform setup](../platform-setup/) and a working Bridge deployment + pattern ([ECS](../aws-ecs-fargate-alb/) or [Kubernetes](../kubernetes/)) + +--- + +## 1. Build & push + +```bash +# Docker Hub +REGISTRY=docker.io/yourorg ./build-and-push.sh + +# Amazon ECR (script auto-creates the repo and logs in) +REGISTRY=.dkr.ecr.us-west-2.amazonaws.com ./build-and-push.sh +``` + +Override the base, tag, or platforms as needed: + +```bash +REGISTRY=docker.io/yourorg TAG=v1 \ + BASE_IMAGE=britive/bridge:latest \ + PLATFORMS=linux/amd64,linux/arm64 \ + ./build-and-push.sh +``` + +Local single-arch build (no push) for testing: + +```bash +docker build -t britive-bridge-custom:latest . +docker run --rm britive-bridge-custom:latest \ + sh -c 'for c in ssh ssh-keygen mysql aws jq python3; do command -v $c; done' +``` + +--- + +## 2. Deploy the custom image + +### ECS (Fargate) + +Use any of the AWS ECS options in this repo and set the image to yours. With the +CloudFormation templates, that's the `ImageUri` parameter: + +```jsonc +// params.json +{ "ParameterKey": "ImageUri", + "ParameterValue": "docker.io/yourorg/britive-bridge-custom:latest" } +``` + +For the **SSH example**, the broker needs its provisioning private key. The +[ALB + SSH template](../aws-ecs-fargate-alb-ssh/) already injects a key from +Secrets Manager to `/home/bridge/.ssh/id_ed25519` — use that template with your +custom `ImageUri`. + +For the **MySQL example**, the broker calls AWS Secrets Manager and reaches +Aurora. Grant the ECS **task role** `secretsmanager:GetSecretValue` on the DB +secret, and make sure the task's security group can reach the Aurora endpoint +(3306). + +### Kubernetes + +Ready-to-apply overlays live in [`k8s-overlays/`](k8s-overlays/) — they patch +the base [Kubernetes deployment](../kubernetes/) with your custom image, the +SSH-key mount, and the IRSA service account. After the base manifests are up: + +```bash +# 1. Broker SSH provisioning key (Linux SSH example) +kubectl -n britive-bridge create secret generic bridge-ssh-key \ + --from-file=id_ed25519=./bridge_ed25519 + +# 2. IRSA service account (Aurora MySQL example) — edit the role ARN first +kubectl apply -f k8s-overlays/serviceaccount-irsa.yaml + +# 3. Patch the Deployment: your image + SA + SSH mount +kubectl -n britive-bridge patch deployment britive-bridge \ + --type merge --patch-file k8s-overlays/deployment-patch.yaml +``` + +Edit the placeholders first (`image:` → your build, +`eks.amazonaws.com/role-arn` → your role). Need only one example? Apply just its +pieces — see [`k8s-overlays/README.md`](k8s-overlays/README.md). The MySQL +example also needs pod egress to the Aurora endpoint on 3306. + +--- + +## The two examples in detail + +### Linux SSH — `scripts/linux-ssh/` + +`checkout_remote_bridge.sh` / `checkin_remote_bridge.sh`. On checkout: derive an +OS username from the user's email, generate a one-time `ed25519` keypair, SSH to +the target (as a privileged provisioning user) to create the user + install the +public key (optionally passwordless sudo), then register the session with +`bridge.sh checkout-create` and return a browser URL. Checkin reverses it. + +**Broker container needs:** `ssh`, `ssh-keygen`, `base64`, `python3`, `bridge.sh`. + +**Target host needs:** a privileged provisioning account (default +`britivebroker`) reachable by the broker's key, with root or passwordless sudo. + +Key script variables (set as Britive profile/permission parameters): +`BRITIVE_USER_EMAIL`, `TRX`, `BRITIVE_REMOTE_HOST`, `BRIDGE_URL`, `EXPIRATION`, +and optional `REMOTE_USER`, `PROVISION_HOST/PORT/KEY`, `BRITIVE_SUDO`. + +### Aurora MySQL — `scripts/aurora-mysql/` + +- `temp-user/` — create a temp MySQL user with `ALL` on a database, drop on checkin. +- `role-member/` — create a temp user and `GRANT`/`REVOKE` a named role on a + specific `database.table`. + +Both pull the master DB credentials from **AWS Secrets Manager** (`jq`-parsed), +write a short-lived `--defaults-extra-file` so the password never hits the +process args, run the SQL via the `mysql` client, then clean up. + +**Broker container needs:** `bash`, `mysql`, `aws`, `jq`, `/dev/urandom`. + +Script parameters (set during Britive profile setup): `user`, `host`, `dburl`, +`secret`, plus `table`, `role`, `database_name` for the role-member variant, and +optional `AWS_REGION` (default `us-west-2`). + +> The example SQL targets a database literally named `systemdb` (temp-user) and +> uses `us-west-2` as the default region — change these to your environment. +> The Secrets Manager secret must be JSON: `{"username": "...", "password": "..."}`. + +--- + +## Security notes + +- **Least privilege.** The MySQL master secret can create/drop users — scope the + broker's IAM/secret access tightly and prefer specific MySQL host patterns + over `%`. The SSH provisioning account should be a dedicated, minimal-rights + user, not a shared admin. +- **No secrets in the image.** Keys and DB creds are injected at runtime + (Secrets Manager / K8s Secret), never baked into the image or committed here. +- **Ephemeral by design.** Both examples create credentials on checkout and + destroy them on checkin; verify checkin runs (and consider the optional + user-deletion block in the SSH checkin for full teardown). +- **Pin versions for production.** Replace `:latest` (base and your image) with + immutable tags/digests so deployments are reproducible. + +--- + +## Customizing for other access types + +To support a new resource type, add its tools to the `Dockerfile`'s package +list (and an AWS-CLI-style block if it needs a special installer), rebuild, and +push. The deployment doesn't change — only the image does. Use the sanity-check +loop at the end of the `Dockerfile`'s `RUN` to fail the build early if a tool is +missing. diff --git a/Britive Bridge/custom-image/build-and-push.sh b/Britive Bridge/custom-image/build-and-push.sh new file mode 100755 index 0000000..ff31c49 --- /dev/null +++ b/Britive Bridge/custom-image/build-and-push.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# build-and-push.sh — Build the custom Britive Bridge image and push it to your +# container registry (Docker Hub or Amazon ECR). Multi-arch by default so the +# same tag runs on Fargate ARM64 and x86 / mixed Kubernetes nodes. +# +# Usage: +# REGISTRY=docker.io/yourorg ./build-and-push.sh # Docker Hub +# REGISTRY=.dkr.ecr.us-west-2.amazonaws.com ./build-and-push.sh # ECR +# +# Env overrides: +# REGISTRY (required) target registry/namespace, no trailing slash +# IMAGE_NAME image repo name (default: britive-bridge-custom) +# TAG image tag (default: latest) +# BASE_IMAGE base to extend (default: britive/bridge:latest) +# PLATFORMS buildx platforms (default: linux/amd64,linux/arm64) + +set -euo pipefail + +REGISTRY="${REGISTRY:?Set REGISTRY, e.g. docker.io/yourorg or .dkr.ecr..amazonaws.com}" +IMAGE_NAME="${IMAGE_NAME:-britive-bridge-custom}" +TAG="${TAG:-latest}" +BASE_IMAGE="${BASE_IMAGE:-britive/bridge:latest}" +PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" + +FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}" + +echo "==> Building ${FULL_IMAGE}" +echo " base: ${BASE_IMAGE}" +echo " platforms: ${PLATFORMS}" + +# ECR repos must exist before push; create on demand if this is an ECR target. +if echo "$REGISTRY" | grep -q 'dkr.ecr'; then + REGION=$(echo "$REGISTRY" | sed -E 's/.*\.ecr\.([^.]+)\.amazonaws\.com/\1/') + echo "==> ECR detected (region ${REGION}). Ensuring repo + login..." + aws ecr describe-repositories --repository-names "$IMAGE_NAME" --region "$REGION" >/dev/null 2>&1 \ + || aws ecr create-repository --repository-name "$IMAGE_NAME" --region "$REGION" >/dev/null + aws ecr get-login-password --region "$REGION" \ + | docker login --username AWS --password-stdin "${REGISTRY%%/*}" +else + echo "==> Ensure you are logged in: docker login ${REGISTRY%%/*}" +fi + +# buildx handles multi-arch + push in one step. Create a builder if missing. +docker buildx inspect bridge-builder >/dev/null 2>&1 || docker buildx create --name bridge-builder --use +docker buildx use bridge-builder + +docker buildx build \ + --platform "$PLATFORMS" \ + --build-arg "BASE_IMAGE=${BASE_IMAGE}" \ + -t "$FULL_IMAGE" \ + --push \ + . + +echo "==> Pushed ${FULL_IMAGE}" +echo " Set this as the image in your ECS task definition (ImageUri) or" +echo " Kubernetes Deployment (image:)." diff --git a/Britive Bridge/custom-image/k8s-overlays/README.md b/Britive Bridge/custom-image/k8s-overlays/README.md new file mode 100644 index 0000000..ddc80bd --- /dev/null +++ b/Britive Bridge/custom-image/k8s-overlays/README.md @@ -0,0 +1,45 @@ +# Kubernetes overlays for the custom image + +Ready-to-apply manifests that wire the [custom image](../) into the base +[Kubernetes deployment](../../kubernetes/) for the two examples. + +| File | Purpose | +|------|---------| +| `serviceaccount-irsa.yaml` | IRSA service account → AWS Secrets Manager (Aurora MySQL) | +| `deployment-patch.yaml` | Strategic-merge patch: custom image + SA + SSH-key mount | + +## Apply order + +```bash +# 0. Base manifests already applied (namespace, broker secret, pvc, deployment, service, ingress) +# from ../../kubernetes/manifests/ — see that README. + +# 1. SSH provisioning key (Linux SSH example) — the broker's private key +kubectl -n britive-bridge create secret generic bridge-ssh-key \ + --from-file=id_ed25519=./bridge_ed25519 + +# 2. IRSA service account (Aurora MySQL example) — edit the role ARN first. +# Skip if you used `eksctl create iamserviceaccount` (it makes this SA for you). +kubectl apply -f serviceaccount-irsa.yaml + +# 3. Patch the running Deployment: set your image, SA, and SSH mount +kubectl -n britive-bridge patch deployment britive-bridge \ + --type merge --patch-file deployment-patch.yaml +``` + +## Before applying — edit these placeholders + +- `deployment-patch.yaml` → `image:` → your pushed image + (`docker.io/yourorg/britive-bridge-custom:latest` or your ECR URI). +- `serviceaccount-irsa.yaml` → `eks.amazonaws.com/role-arn` → the IAM role with + `secretsmanager:GetSecretValue` on your DB master secret. + +## Notes + +- **Only need one example?** Apply just the parts you use — the SSH-key Secret + + mount for SSH, the IRSA SA for MySQL. The patch includes both; trim the patch + if you only want one. +- **Egress:** the MySQL example needs the pod to reach the Aurora endpoint on + 3306 — allow it in your network policy / security group. +- **Non-EKS clusters:** swap the IRSA SA for your platform's workload identity + (GKE Workload Identity, AKS workload identity) or mount AWS creds via a Secret. diff --git a/Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml b/Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml new file mode 100644 index 0000000..89447b8 --- /dev/null +++ b/Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml @@ -0,0 +1,47 @@ +# Strategic-merge patch over the base Deployment +# (../../kubernetes/manifests/deployment.yaml). +# +# Applies three changes for the custom image + the two examples: +# 1. Swap the image to your custom build. +# 2. Bind the IRSA service account (Aurora MySQL → Secrets Manager). +# 3. Mount the broker's SSH provisioning key (Linux SSH example) at +# /home/bridge/.ssh from a Secret, read-only, mode 0600. +# +# Lists merge by key: containers by `name`, volumes/volumeMounts by `name`, so +# only these fields are touched — everything else in the base Deployment stays. +# +# Create the SSH key Secret first (private key the broker uses to reach targets): +# kubectl -n britive-bridge create secret generic bridge-ssh-key \ +# --from-file=id_ed25519=./bridge_ed25519 +# +# Apply: +# kubectl -n britive-bridge patch deployment britive-bridge \ +# --type merge --patch-file deployment-patch.yaml +# +# Or fold these fields directly into kubernetes/manifests/deployment.yaml. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: britive-bridge + namespace: britive-bridge +spec: + template: + spec: + # IRSA: pod assumes the IAM role bound to this SA (see serviceaccount-irsa.yaml) + serviceAccountName: britive-bridge + containers: + - name: bridge + # Replace with YOUR pushed custom image (Docker Hub or ECR). + image: docker.io/yourorg/britive-bridge-custom:latest + volumeMounts: + - name: ssh-key + mountPath: /home/bridge/.ssh + readOnly: true + volumes: + - name: ssh-key + secret: + secretName: bridge-ssh-key + defaultMode: 0600 # ssh refuses world/group-readable keys + items: + - key: id_ed25519 + path: id_ed25519 diff --git a/Britive Bridge/custom-image/k8s-overlays/serviceaccount-irsa.yaml b/Britive Bridge/custom-image/k8s-overlays/serviceaccount-irsa.yaml new file mode 100644 index 0000000..b8229c1 --- /dev/null +++ b/Britive Bridge/custom-image/k8s-overlays/serviceaccount-irsa.yaml @@ -0,0 +1,27 @@ +# IRSA service account for the Bridge broker (Aurora MySQL example). +# Lets the pod call AWS Secrets Manager WITHOUT static AWS keys — the EKS OIDC +# provider exchanges this SA's projected token for the IAM role's credentials. +# +# Prereqs (one-time, per cluster): +# 1. Associate an OIDC provider with the cluster: +# eksctl utils associate-iam-oidc-provider --cluster --approve +# 2. Create an IAM role whose trust policy allows this SA +# (system:serviceaccount:britive-bridge:britive-bridge) and which grants +# secretsmanager:GetSecretValue on your DB master secret. Easiest: +# eksctl create iamserviceaccount \ +# --cluster --namespace britive-bridge \ +# --name britive-bridge \ +# --attach-policy-arn arn:aws:iam:::policy/ \ +# --approve --override-existing-serviceaccounts +# (eksctl will create/annotate this same SA for you — in that case you can +# skip applying this file and just reference serviceAccountName below.) +# +# If you create the role by hand, fill in the ARN annotation and apply this file. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: britive-bridge + namespace: britive-bridge + annotations: + # IAM role with secretsmanager:GetSecretValue on the DB master secret. + eks.amazonaws.com/role-arn: arn:aws:iam:::role/ diff --git a/Britive Bridge/docker-compose/README.md b/Britive Bridge/docker-compose/README.md new file mode 100644 index 0000000..deb95a0 --- /dev/null +++ b/Britive Bridge/docker-compose/README.md @@ -0,0 +1,98 @@ +# Britive Bridge — Docker Compose + +Run Bridge as a single container on any Docker host or VM. Simplest option — +ideal for local trials, POCs, and single-VM deployments. + +## Prerequisites + +- Docker Engine 20.10+ with the Compose plugin (`docker compose`) +- Outbound network access from the host to your Britive tenant (Broker/MQTT) +- Completed [platform setup](../platform-setup/) — you need: + - `BRITIVE_BROKER_TENANT_SUBDOMAIN` + - `BRITIVE_BROKER_AUTH_TOKEN` + +## What it deploys + +- One `britive/bridge` container +- Port `8080` published on the host (HTTPS API + WebSocket + browser protocols) +- A named Docker volume `bridge-data` mounted at `/data` for persistence +- A health check against `https://127.0.0.1:8080/api/health` + +## Install + +1. Provide the broker credentials. Either export them in your shell: + + ```bash + export BRITIVE_BROKER_TENANT_SUBDOMAIN="" + export BRITIVE_BROKER_AUTH_TOKEN="" + ``` + + …or create a `.env` file next to `docker-compose.yaml`: + + ```dotenv + BRITIVE_BROKER_TENANT_SUBDOMAIN= + BRITIVE_BROKER_AUTH_TOKEN= + ``` + + > Do not commit `.env` — add it to `.gitignore`. + +2. Start it: + + ```bash + docker compose up -d + ``` + +3. Check health and logs: + + ```bash + docker compose ps + docker compose logs -f bridge + curl -sfk https://localhost:8080/api/health + ``` + +## Equivalent `docker run` + +```bash +docker run -d --name bridge -p 8080:8080 \ + -e BRITIVE_BROKER_TENANT_SUBDOMAIN="" \ + -e BRITIVE_BROKER_AUTH_TOKEN="" \ + -v bridge-data:/data \ + britive/bridge:latest +``` + +## TLS + +By default the container serves an **auto-generated self-signed certificate**. +To use your own certificate, uncomment the `TLS_CERT_FILE` / `TLS_KEY_FILE` +environment variables and the matching bind mounts in `docker-compose.yaml`: + +```yaml + environment: + TLS_CERT_FILE: "/custom-certs/cert.pem" + TLS_KEY_FILE: "/custom-certs/key.pem" + volumes: + - ./certs/cert.pem:/custom-certs/cert.pem:ro + - ./certs/key.pem:/custom-certs/key.pem:ro +``` + +## Persistent storage options + +The `volumes:` block in `docker-compose.yaml` includes commented examples for +binding the data volume to a host path or an NFS export. Use those for backups +or shared storage; the default named volume is fine for single-host use. + +## External URL + +For browser users to reach Bridge, the host's `8080` must be reachable at the +URL you registered during platform setup (`BRIDGE_URL`). On a cloud VM, open the +security group / firewall for `8080` (or front the host with your own reverse +proxy / load balancer terminating TLS). + +## Lifecycle + +```bash +docker compose pull # get a newer image (update the tag in the file first) +docker compose up -d # apply changes +docker compose down # stop and remove the container (keeps the volume) +docker compose down -v # also delete the data volume (destroys session state) +``` diff --git a/Britive Bridge/docker-compose/docker-compose.yaml b/Britive Bridge/docker-compose/docker-compose.yaml new file mode 100644 index 0000000..f84207e --- /dev/null +++ b/Britive Bridge/docker-compose/docker-compose.yaml @@ -0,0 +1,60 @@ +# Britive Bridge - Single-Container Deployment +# =================================================== +# Optional convenience file. Equivalent to: +# docker run -d --name bridge -p 8080:8080 \ +# -e BRITIVE_BROKER_TENANT_SUBDOMAIN="..." \ +# -e BRITIVE_BROKER_AUTH_TOKEN="..." \ +# -v bridge-data:/data \ +# britive/bridge:latest +# +# Start: docker compose up -d --build +# Stop: docker compose down + +services: + bridge: + image: britive/bridge:latest + restart: unless-stopped + ports: + - "8080:8080" # HTTPS API + WebSocket + browser protocols + environment: + # BRIDGE_ADMIN_TOKEN: "${BRIDGE_ADMIN_TOKEN:-}" + # BRIDGE_JWT_SECRET: "${BRIDGE_JWT_SECRET:-}" + + # Britive broker - connects to platform via MQTT + BRITIVE_BROKER_TENANT_SUBDOMAIN: "${BRITIVE_BROKER_TENANT_SUBDOMAIN:-}" + BRITIVE_BROKER_AUTH_TOKEN: "${BRITIVE_BROKER_AUTH_TOKEN:-}" + + # Custom TLS certificate (optional - omit for auto-generated self-signed) + TLS_CERT_FILE: "${TLS_CERT_FILE:-}" + TLS_KEY_FILE: "${TLS_KEY_FILE:-}" + volumes: + - bridge-data:/data + # Uncomment to bind-mount your own TLS certificate: + # - ./certs/cert.pem:/custom-certs/cert.pem:ro + # - ./certs/key.pem:/custom-certs/key.pem:ro + # + # Uncomment to bind-mount a custom broker config + # - ./broker-config.yml:/opt/britive-broker/config/broker-config.yml:ro + healthcheck: + test: ["CMD", "curl", "-sfk", "https://127.0.0.1:8080/api/health"] + interval: 15s + timeout: 5s + retries: 20 + +volumes: + bridge-data: + # EXAMPLES: + # - local + # bridge-data: + # driver: local + # driver_opts: + # type: none + # o: bind + # device: /srv/britive-bridge/data + # - nfs + # bridge-data: + # driver: local + # driver_opts: + # type: nfs + # o: addr=10.0.0.20,nfsvers=4,rw + # device: :/exports/britive-bridge diff --git a/Britive Bridge/kubernetes/README.md b/Britive Bridge/kubernetes/README.md new file mode 100644 index 0000000..9cfce48 --- /dev/null +++ b/Britive Bridge/kubernetes/README.md @@ -0,0 +1,244 @@ +# Britive Bridge — Kubernetes + +Run Bridge on any Kubernetes cluster (EKS, AKS, GKE, or on-prem) using plain +manifests. Use this when your team is standardized on Kubernetes. + +> **Helm chart on the roadmap** — see [Roadmap](#roadmap-public-helm-chart) +> below. The manifests here map 1:1 to the chart's templates, so migrating later +> is straightforward. + +## What it deploys + +Manifests in [`manifests/`](manifests/): + +| File | Purpose | +|------|---------| +| `namespace.yaml` | `britive-bridge` namespace | +| `secret.example.yaml` | Broker credentials (template — **don't commit a filled copy**) | +| `pvc.yaml` | PersistentVolumeClaim for `/data` (5Gi, RWO) | +| `deployment.yaml` | The `britive/bridge` Deployment (1 replica, HTTPS probes) | +| `service.yaml` | ClusterIP service (443 → container 8080) | +| `ingress.yaml` | ingress-nginx Ingress with HTTPS backend + WebSocket support | +| `external-secret.example.yaml` | *(optional)* Source broker creds from an external store via the External Secrets Operator | +| `ha-overlay.example.yaml` | *(optional)* Multi-replica HA: RWM PVC + rolling-update + anti-affinity | + +Traffic path: `client → Ingress (TLS) → Service:443 → pod:8080 (HTTPS, self-signed)`. + +## Prerequisites + +- A Kubernetes cluster (1.25+) and `kubectl` context pointing at it +- An **Ingress controller** (examples use [ingress-nginx](https://kubernetes.github.io/ingress-nginx/)) +- A **StorageClass** for the PVC (RWO is fine for a single replica; use RWM for HA) +- A way to get a TLS cert for your domain — e.g. + [cert-manager](https://cert-manager.io/) — or bring your own cert +- Completed [platform setup](../platform-setup/) — you need + `BRITIVE_BROKER_TENANT_SUBDOMAIN` and `BRITIVE_BROKER_AUTH_TOKEN` + +## Install + +1. **Namespace** + + ```bash + kubectl apply -f manifests/namespace.yaml + ``` + +2. **Broker credentials secret** (preferred: create imperatively, no plaintext on disk): + + ```bash + kubectl -n britive-bridge create secret generic bridge-broker \ + --from-literal=BRITIVE_BROKER_TENANT_SUBDOMAIN='' \ + --from-literal=BRITIVE_BROKER_AUTH_TOKEN='' + ``` + + (`manifests/secret.example.yaml` shows the equivalent declarative form. For + production, prefer External Secrets Operator / Sealed Secrets / Vault.) + +3. **Storage, workload, service** + + ```bash + kubectl apply -f manifests/pvc.yaml + kubectl apply -f manifests/deployment.yaml + kubectl apply -f manifests/service.yaml + ``` + +4. **Ingress** — edit `manifests/ingress.yaml` first: + - Replace `bridge.example.com` with your hostname (two places). + - Adjust `ingressClassName` / annotations for your controller. + - For automatic certs, uncomment the `cert-manager.io/cluster-issuer` + annotation and set your issuer. + + ```bash + kubectl apply -f manifests/ingress.yaml + ``` + +## Verify + +```bash +kubectl -n britive-bridge get pods,svc,ingress +kubectl -n britive-bridge logs deploy/britive-bridge -f + +# in-cluster health check +kubectl -n britive-bridge exec deploy/britive-bridge -- \ + curl -sfk https://127.0.0.1:8080/api/health +``` + +Then point a DNS record for your host at the ingress controller's external +address, set `BRIDGE_URL` accordingly, and confirm the Bridge **resource** in +Britive uses that URL. + +## Key configuration notes + +- **HTTPS backend.** The container serves HTTPS with a self-signed cert on 8080. + Probes and the Service/Ingress all use the HTTPS scheme; the ingress is + configured with `backend-protocol: HTTPS` + `proxy-ssl-verify: off`. +- **WebSocket.** Bridge upgrades connections to WebSocket — the ingress sets + long read/send timeouts. Other controllers (ALB, Traefik, HAProxy) need their + own WebSocket/timeout settings. +- **Persistence.** `/data` is backed by a PVC. For `replicas > 1` you need a + `ReadWriteMany` volume (EFS CSI on AWS, Azure Files, Filestore on GCP) and + should change the Deployment strategy accordingly. +- **Ingress alternatives.** On EKS you can swap the Ingress for the AWS Load + Balancer Controller (ALB ingress class) with ACM annotations; on other clouds + use their respective controllers. + +## Controller-specific ingress hints + +- **ingress-nginx** (provided): `backend-protocol: HTTPS`, + `proxy-ssl-verify: off`, long `proxy-read/send-timeout` for WebSocket. +- **AWS Load Balancer Controller:** `alb.ingress.kubernetes.io/backend-protocol: HTTPS`, + `alb.ingress.kubernetes.io/certificate-arn: `, + `alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'`. +- **Traefik:** a `ServersTransport` with `insecureSkipVerify: true` plus an + HTTPS scheme on the service. + +## Optional: production hardening + +Both of these work with the plain manifests today — no Helm needed. When the +chart ships they become `values.yaml` toggles, but the behavior is identical. + +### External Secrets Operator (ESO) + +Instead of a raw Kubernetes Secret, pull the broker credentials from AWS Secrets +Manager / Vault / GCP / Azure. See `manifests/external-secret.example.yaml`. + +1. Install ESO and grant it read access to your backend secret (IRSA on EKS, + workload identity on GKE, etc.). +2. Edit the example for your provider/region and backend secret name. +3. Apply it **instead of** `secret.example.yaml` — ESO creates the same + `bridge-broker` Secret, so the Deployment is unchanged. + +### High availability (multiple replicas) + +Bridge persists session state to `/data`, so running `replicas > 1` requires a +**ReadWriteMany** volume shared across pods. See `manifests/ha-overlay.example.yaml`. + +1. Provision an RWM storage class (EFS CSI on AWS, Azure Files, Filestore on GCP). +2. Apply the RWM PVC from the overlay **instead of** `pvc.yaml`. +3. Bump `replicas` and switch the Deployment to `RollingUpdate` (the overlay + includes pod anti-affinity to spread replicas across nodes). + +> Confirm with your Britive team that your Bridge version supports active-active +> across replicas before relying on HA for production traffic. + +## Teardown + +```bash +kubectl delete -f manifests/ingress.yaml -f manifests/service.yaml \ + -f manifests/deployment.yaml -f manifests/pvc.yaml +kubectl -n britive-bridge delete secret bridge-broker +kubectl delete -f manifests/namespace.yaml +``` + +> Deleting the PVC destroys persisted session state. + +--- + +## Roadmap: public Helm chart + +A first-class Helm chart is planned so Bridge can be installed in one command +and configured declaratively via `values.yaml`. The chart will be published as +an **OCI artifact on Docker Hub**, alongside the `britive/bridge` container +image — one registry, one set of credentials, one `britive/` namespace. No +`helm repo add` needed. Target shape: + +```bash +helm install bridge oci://registry-1.docker.io/britive/britive-bridge \ + --namespace britive-bridge --create-namespace \ + --set broker.tenantSubdomain= \ + --set broker.existingSecret=bridge-broker \ + --set ingress.host=bridge.example.com +``` + +> The chart lives at `britive/britive-bridge` — a **sibling** Docker Hub repo to +> the `britive/bridge` image (OCI charts can't nest under an image repo). Pin a +> version in production: append `--version X.Y.Z`. + +**Planned `values.yaml` surface (subject to change):** + +```yaml +image: + repository: britive/bridge + tag: latest + pullPolicy: IfNotPresent + +replicaCount: 1 + +broker: + tenantSubdomain: "" # BRITIVE_BROKER_TENANT_SUBDOMAIN + authToken: "" # inline (dev only) ... + existingSecret: "" # ... or reference an existing Secret (preferred) + +persistence: + enabled: true + size: 5Gi + storageClass: "" + accessMode: ReadWriteOnce + +service: + type: ClusterIP + port: 443 + +ingress: + enabled: true + className: nginx + host: bridge.example.com + tls: + enabled: true + secretName: bridge-tls + annotations: {} # controller-specific (backend HTTPS, WebSocket, cert ARN) + +resources: + requests: { cpu: 500m, memory: 1Gi } + limits: { cpu: "1", memory: 2Gi } + +probes: + scheme: HTTPS + path: /api/health +``` + +**Planned delivery / milestones** + +1. Package the manifests in this directory as a chart (`Chart.yaml`, + `templates/`, `values.yaml`). +2. Add chart-level docs, schema validation (`values.schema.json`), and a CI lint + + `helm template` test. +3. Publish as a public **OCI chart artifact on Docker Hub**, alongside the + container image (see push flow below). +4. Surface the existing **ESO** and **HA (RWM)** manifest options (already + shipped under `manifests/`) as `values.yaml` toggles in the chart. + +**Planned publish flow (Docker Hub OCI)** + +```bash +helm package . # -> britive-bridge-X.Y.Z.tgz +echo "$DOCKERHUB_TOKEN" | helm registry login registry-1.docker.io \ + -u britive --password-stdin +helm push britive-bridge-X.Y.Z.tgz oci://registry-1.docker.io/britive +``` + +Requires Helm 3.8+ (OCI support is GA). The chart appears as the +`britive/britive-bridge` repository on Docker Hub with the +`application/vnd.cncf.helm.*` media type. + +> Until the chart ships, the manifests above are the supported install path and +> are kept in sync with the planned chart values so migration is mechanical. diff --git a/Britive Bridge/kubernetes/manifests/deployment.yaml b/Britive Bridge/kubernetes/manifests/deployment.yaml new file mode 100644 index 0000000..3527422 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/deployment.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: britive-bridge + namespace: britive-bridge + labels: + app.kubernetes.io/name: britive-bridge +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: britive-bridge + strategy: + type: Recreate # single RWO volume — avoid two pods mounting at once + template: + metadata: + labels: + app.kubernetes.io/name: britive-bridge + spec: + securityContext: + fsGroup: 0 + containers: + - name: bridge + image: britive/bridge:latest + imagePullPolicy: IfNotPresent + ports: + - name: https + containerPort: 8080 # HTTPS API + WebSocket + browser protocols + protocol: TCP + envFrom: + - secretRef: + name: bridge-broker + volumeMounts: + - name: bridge-data + mountPath: /data + # Container serves HTTPS with a self-signed cert, so probes use the + # HTTPS scheme. The kubelet does not verify the cert for probes. + readinessProbe: + httpGet: + path: /api/health + port: https + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /api/health + port: https + scheme: HTTPS + initialDelaySeconds: 30 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1" + memory: "2Gi" + volumes: + - name: bridge-data + persistentVolumeClaim: + claimName: bridge-data diff --git a/Britive Bridge/kubernetes/manifests/external-secret.example.yaml b/Britive Bridge/kubernetes/manifests/external-secret.example.yaml new file mode 100644 index 0000000..1970090 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/external-secret.example.yaml @@ -0,0 +1,54 @@ +# Optional: source the broker credentials from an external secret store via the +# External Secrets Operator (ESO) instead of a raw Kubernetes Secret. +# +# Prerequisites: +# - ESO installed in the cluster: +# helm install external-secrets oci://registry-1.docker.io/externalsecrets/external-secrets \ +# -n external-secrets --create-namespace +# - A backend holding the values (AWS Secrets Manager shown; Vault/GCP/Azure +# work the same way with a different SecretStore provider). +# - The pod/IRSA/workload identity has read access to that backend secret. +# +# ESO writes a Secret named `bridge-broker` with the two keys the Deployment's +# envFrom expects — so deployment.yaml needs NO change. Apply this INSTEAD of +# secret.example.yaml. +--- +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: bridge-store + namespace: britive-bridge +spec: + provider: + # AWS Secrets Manager example. Swap for vault/gcpsm/azurekv as needed. + aws: + service: SecretsManager + region: + auth: + # Preferred on EKS: IRSA on the ESO service account (no static keys). + jwt: + serviceAccountRef: + name: external-secrets # the ESO SA bound to an IAM role +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: bridge-broker + namespace: britive-bridge +spec: + refreshInterval: 1h + secretStoreRef: + name: bridge-store + kind: SecretStore + target: + name: bridge-broker # the Secret the Deployment consumes + creationPolicy: Owner + data: + - secretKey: BRITIVE_BROKER_TENANT_SUBDOMAIN + remoteRef: + key: britive/bridge/broker # backend secret name + property: tenantSubdomain # JSON key within that secret + - secretKey: BRITIVE_BROKER_AUTH_TOKEN + remoteRef: + key: britive/bridge/broker + property: authToken diff --git a/Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml b/Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml new file mode 100644 index 0000000..fe39757 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml @@ -0,0 +1,58 @@ +# Optional: high-availability overlay. Apply AFTER the base manifests to run +# multiple Bridge replicas. Requires a ReadWriteMany (RWM) volume so all pods +# can share /data — RWO will NOT schedule a second pod. +# +# RWM storage by platform: +# - AWS : EFS CSI driver (storageClassName: efs-sc) +# - Azure: Azure Files (storageClassName: azurefile-csi) +# - GCP : Filestore (storageClassName: filestore-csi / standard-rwx) +# +# Apply order: +# 1. kubectl apply -f namespace.yaml +# 2. create the bridge-broker secret (secret.example.yaml or ESO) +# 3. kubectl apply -f ha-overlay.example.yaml # RWM PVC instead of pvc.yaml +# 4. kubectl apply -f deployment.yaml service.yaml ingress.yaml +# 5. patch the Deployment for replicas + rolling updates (see below) +--- +# RWM PVC — replaces pvc.yaml for HA. Do not apply both. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: bridge-data + namespace: britive-bridge +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: efs-sc # set to your RWM storage class +--- +# Patch the base Deployment for HA: more replicas + rolling updates instead of +# Recreate (safe now that the volume is RWM). Apply with: +# kubectl -n britive-bridge patch deployment britive-bridge \ +# --type merge --patch-file ha-overlay.example.yaml --dry-run=client +# or fold these fields into deployment.yaml directly. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: britive-bridge + namespace: britive-bridge +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + spec: + affinity: + podAntiAffinity: # spread replicas across nodes + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app.kubernetes.io/name: britive-bridge diff --git a/Britive Bridge/kubernetes/manifests/ingress.yaml b/Britive Bridge/kubernetes/manifests/ingress.yaml new file mode 100644 index 0000000..af83747 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/ingress.yaml @@ -0,0 +1,38 @@ +# Ingress for Bridge. Bridge speaks HTTPS end-to-end and upgrades connections +# to WebSocket, so the ingress controller must: +# 1. Talk to the backend over HTTPS (the backend uses a self-signed cert). +# 2. Pass through / support WebSocket upgrades. +# +# Annotations below are for the ingress-nginx controller. Adapt for your +# controller (AWS ALB, Traefik, HAProxy, etc.) — see this option's README. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: britive-bridge + namespace: britive-bridge + annotations: + # Backend is HTTPS with a self-signed cert + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + nginx.ingress.kubernetes.io/proxy-ssl-verify: "off" + # WebSocket / long-lived connections + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + # TLS via cert-manager (optional — remove if you manage certs another way) + # cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - bridge.example.com + secretName: bridge-tls + rules: + - host: bridge.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: britive-bridge + port: + number: 443 diff --git a/Britive Bridge/kubernetes/manifests/namespace.yaml b/Britive Bridge/kubernetes/manifests/namespace.yaml new file mode 100644 index 0000000..63a1bd2 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: britive-bridge + labels: + app.kubernetes.io/name: britive-bridge diff --git a/Britive Bridge/kubernetes/manifests/pvc.yaml b/Britive Bridge/kubernetes/manifests/pvc.yaml new file mode 100644 index 0000000..3f96de1 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/pvc.yaml @@ -0,0 +1,19 @@ +# Persistent storage for Bridge state (mounted at /data). +# Bridge keeps active session/checkout state here — persistence lets the pod +# survive restarts and rescheduling without losing live sessions. +# +# For multi-replica (HA) deployments you need ReadWriteMany. On AWS that means +# the EFS CSI driver; on Azure, Azure Files; on GCP, Filestore. For a single +# replica, ReadWriteOnce on any default block storage class is fine. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: bridge-data + namespace: britive-bridge +spec: + accessModes: + - ReadWriteOnce # use ReadWriteMany for replicas > 1 + resources: + requests: + storage: 5Gi + # storageClassName: gp3 # uncomment and set to your cluster's storage class diff --git a/Britive Bridge/kubernetes/manifests/secret.example.yaml b/Britive Bridge/kubernetes/manifests/secret.example.yaml new file mode 100644 index 0000000..4c15ce0 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/secret.example.yaml @@ -0,0 +1,20 @@ +# Britive Bridge broker credentials. +# DO NOT commit a filled-in copy of this file. Create the secret imperatively +# instead (preferred), or fill placeholders and apply, then delete locally. +# +# Preferred — create from the CLI, no plaintext on disk: +# kubectl -n britive-bridge create secret generic bridge-broker \ +# --from-literal=BRITIVE_BROKER_TENANT_SUBDOMAIN='' \ +# --from-literal=BRITIVE_BROKER_AUTH_TOKEN='' +# +# For production, prefer an external secret store (AWS Secrets Manager, +# HashiCorp Vault, Sealed Secrets, External Secrets Operator) over a raw Secret. +apiVersion: v1 +kind: Secret +metadata: + name: bridge-broker + namespace: britive-bridge +type: Opaque +stringData: + BRITIVE_BROKER_TENANT_SUBDOMAIN: "" + BRITIVE_BROKER_AUTH_TOKEN: "" diff --git a/Britive Bridge/kubernetes/manifests/service.yaml b/Britive Bridge/kubernetes/manifests/service.yaml new file mode 100644 index 0000000..2e45b24 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/service.yaml @@ -0,0 +1,19 @@ +# ClusterIP service fronting the Bridge pod(s). +# External exposure is handled by the Ingress (see ingress.yaml). If you prefer +# a cloud load balancer instead of an Ingress, change type to LoadBalancer. +apiVersion: v1 +kind: Service +metadata: + name: britive-bridge + namespace: britive-bridge + labels: + app.kubernetes.io/name: britive-bridge +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: britive-bridge + ports: + - name: https + port: 443 + targetPort: https # -> containerPort 8080 + protocol: TCP diff --git a/Britive Bridge/linux-vm-docker/README.md b/Britive Bridge/linux-vm-docker/README.md new file mode 100644 index 0000000..c05fa41 --- /dev/null +++ b/Britive Bridge/linux-vm-docker/README.md @@ -0,0 +1,242 @@ +# Britive Bridge — Linux VM (Docker) + +Run the Britive Bridge container (which hosts the broker process) on a single +Linux VM using Docker, pulling the image straight from **Docker Hub** +(`britive/bridge`). Good for on-prem hosts, a cloud EC2/Compute instance, or any +long-lived Linux server. + +> Want zero scripting and don't care about per-distro detail? The +> [Docker Compose option](../docker-compose/) runs the same container with a +> single file. This guide is the **standalone-VM, fully-explained** path with an +> install helper and OS-specific gotchas. + +--- + +## Prerequisites + +- A 64-bit Linux VM (x86_64 or arm64) you have **root / sudo** on. Supported + families: Ubuntu, Debian, RHEL, Rocky, AlmaLinux, Amazon Linux 2/2023, Fedora. +- **Outbound** internet from the VM to your Britive tenant (Broker/MQTT) and to + Docker Hub (to pull the image). +- **Inbound** TCP on the Bridge port (default `8080`) reachable by your users — + open both the host firewall **and** any cloud security group / network ACL. +- Sizing: start with **2 vCPU / 4 GiB RAM** and ~5 GiB free disk for the image + + data volume. Scale up for heavier session loads. +- Completed [platform setup](../platform-setup/) — you need + `BRITIVE_BROKER_TENANT_SUBDOMAIN` and `BRITIVE_BROKER_AUTH_TOKEN`. + +--- + +## Quick start (scripted) + +```bash +# 1. Provide credentials +cp bridge.env.example bridge.env +chmod 600 bridge.env +# edit bridge.env — set tenant subdomain + broker token + +# 2. Install Docker + run the container (auto-detects your distro) +sudo ./install.sh + +# 3. Verify +curl -sfk https://127.0.0.1:8080/api/health +docker logs -f bridge +``` + +`install.sh` detects the distro, installs Docker Engine, enables it on boot, +opens the firewall, pulls `britive/bridge:latest`, and starts the container with +a persistent data volume and `--restart unless-stopped`. Re-run it any time to +update the image / recreate the container. + +Override defaults via env vars, e.g.: + +```bash +sudo IMAGE=britive/bridge:latest PORT=9443 CONTAINER_NAME=bridge ./install.sh +``` + +--- + +## Manual steps (if you prefer to do it by hand) + +### 1. Install Docker Engine + +**Ubuntu / Debian** (apt, official docker-ce repo): + +```bash +sudo apt-get update +sudo apt-get install -y ca-certificates curl gnupg +sudo install -m 0755 -d /etc/apt/keyrings +# Use "ubuntu" or "debian" in the URL to match your distro: +curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ +https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin +``` + +**RHEL / Rocky / AlmaLinux / Fedora** (dnf, docker-ce repo): + +```bash +sudo dnf install -y dnf-plugins-core +# CentOS repo works for RHEL/Rocky/Alma; use the fedora repo on Fedora: +sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin +``` + +**Amazon Linux** (do **not** use the docker-ce repo — use the bundled package): + +```bash +# Amazon Linux 2: +sudo amazon-linux-extras install -y docker +# Amazon Linux 2023: +sudo dnf install -y docker +``` + +### 2. Enable and start Docker + +```bash +sudo systemctl enable --now docker +docker --version +``` + +### 3. (Optional) run docker without sudo + +```bash +sudo usermod -aG docker "$USER" +# log out and back in for the group change to take effect +``` + +### 4. Open the firewall + +```bash +# RHEL / Amazon (firewalld): +sudo firewall-cmd --permanent --add-port=8080/tcp && sudo firewall-cmd --reload +# Ubuntu / Debian (ufw, if active): +sudo ufw allow 8080/tcp +``` + +Plus: open inbound TCP 8080 in your **cloud security group / network ACL**. + +### 5. Run the container + +```bash +cp bridge.env.example bridge.env && chmod 600 bridge.env # then edit it +docker pull britive/bridge:latest +docker run -d \ + --name bridge \ + --restart unless-stopped \ + -p 8080:8080 \ + --env-file bridge.env \ + -v bridge-data:/data \ + britive/bridge:latest +``` + +### 6. Verify + +```bash +docker ps +curl -sfk https://127.0.0.1:8080/api/health +docker logs -f bridge +``` + +--- + +## Distro-specific nuances + +| Topic | Debian / Ubuntu | RHEL / Rocky / Alma / Fedora | Amazon Linux | +|-------|-----------------|------------------------------|--------------| +| Package source | `docker-ce` apt repo | `docker-ce` dnf repo | **bundled** `docker` pkg (NOT docker-ce) | +| Installer | `apt-get` | `dnf` | `amazon-linux-extras` (AL2) / `dnf` (AL2023) | +| Default firewall | ufw (often inactive) | **firewalld** (often active) | firewalld (often active) | +| SELinux | usually disabled | **Enforcing** by default | Enforcing by default | +| Default login user | `ubuntu` / admin user | `cloud-user` / root | `ec2-user` | + +### SELinux + bind mounts (RHEL / Amazon family) + +When SELinux is **Enforcing**, a host **bind mount** must be relabeled or the +container can't read it. This guide uses a **named volume** (`bridge-data`), +which Docker labels correctly — so no action needed. If you switch to a host +path, add the `:Z` (private) or `:z` (shared) label: + +```bash +docker run -d --name bridge --restart unless-stopped \ + -p 8080:8080 --env-file bridge.env \ + -v /srv/britive-bridge/data:/data:Z \ + britive/bridge:latest +``` + +### Image architecture + +`britive/bridge` is multi-arch; Docker pulls the variant matching your VM +(`uname -m` → `x86_64`/amd64 or `aarch64`/arm64) automatically. If you pin a +single-arch tag, make sure it matches the VM's architecture. + +### Rootless / Podman + +On RHEL/Fedora you may have **Podman** instead of Docker. The same commands work +via `podman` (install `podman-docker` to alias `docker`). Note: rootless Podman +**cannot bind to ports < 1024** — fine here since we use 8080. For auto-start +under rootless Podman, generate a systemd unit with `podman generate systemd`. + +--- + +## Custom TLS certificate + +By default the container serves a **self-signed cert** on 8080 (browsers warn). +To present your own cert, mount it and point the env vars at the in-container +paths: + +```bash +docker run -d --name bridge --restart unless-stopped \ + -p 8080:8080 --env-file bridge.env \ + -v bridge-data:/data \ + -v /etc/ssl/bridge/cert.pem:/custom-certs/cert.pem:ro \ + -v /etc/ssl/bridge/key.pem:/custom-certs/key.pem:ro \ + -e TLS_CERT_FILE=/custom-certs/cert.pem \ + -e TLS_KEY_FILE=/custom-certs/key.pem \ + britive/bridge:latest +``` + +(On SELinux-enforcing hosts add `:Z` to those `-v` mounts, e.g. `...cert.pem:ro,Z`.) + +--- + +## External URL + +Users reach Bridge at the `BRIDGE_URL` you registered in platform setup. Point a +DNS record at the VM's public address (or front it with your own reverse proxy / +load balancer that terminates TLS) and confirm the Britive Bridge **resource** +uses that URL. + +--- + +## Lifecycle / operations + +```bash +docker logs -f bridge # follow logs +docker restart bridge # restart +docker pull britive/bridge:latest && \ + docker rm -f bridge && \ + ./... (re-run install.sh) # update to a newer image +docker rm -f bridge # stop + remove (keeps data volume) +docker volume rm bridge-data # delete persisted state (destructive) +``` + +**Auto-start on reboot** is handled by `--restart unless-stopped` plus +`systemctl enable docker`. Verify with `sudo reboot` then `docker ps`. + +--- + +## Troubleshooting + +| Symptom | Likely cause / fix | +|---------|--------------------| +| `curl` to `/api/health` hangs from another host | Firewall/security group not open on 8080 | +| Container restarts in a loop | Bad/missing broker token — check `docker logs bridge` | +| `permission denied` on `docker` | Add user to `docker` group (step 3) or use `sudo` | +| Can't read mounted cert (RHEL) | SELinux — add `:Z` to the `-v` mount | +| Image won't pull | No outbound to Docker Hub, or Docker Hub rate limit (log in: `docker login`) | +| Lost sessions after restart | Data volume not mounted — ensure `-v bridge-data:/data` | diff --git a/Britive Bridge/linux-vm-docker/bridge.env.example b/Britive Bridge/linux-vm-docker/bridge.env.example new file mode 100644 index 0000000..664d768 --- /dev/null +++ b/Britive Bridge/linux-vm-docker/bridge.env.example @@ -0,0 +1,17 @@ +# Britive Bridge environment file (Linux VM). +# Copy to "bridge.env", fill in real values, keep it OUT of source control: +# cp bridge.env.example bridge.env && chmod 600 bridge.env +# +# Loaded by: docker run --env-file bridge.env ... + +# ── Required: broker connection (from platform-setup) ────────────────────── +# Tenant subdomain only — e.g. "acme" for https://acme.britive-app.com +BRITIVE_BROKER_TENANT_SUBDOMAIN= +# Broker pool token created during platform setup. Treat as a secret. +BRITIVE_BROKER_AUTH_TOKEN= + +# ── Optional: custom TLS certificate ─────────────────────────────────────── +# Omit both to use the container's auto-generated self-signed cert. +# If set, they must point at paths INSIDE the container (mount them with -v). +# TLS_CERT_FILE=/custom-certs/cert.pem +# TLS_KEY_FILE=/custom-certs/key.pem diff --git a/Britive Bridge/linux-vm-docker/install.sh b/Britive Bridge/linux-vm-docker/install.sh new file mode 100755 index 0000000..93a8186 --- /dev/null +++ b/Britive Bridge/linux-vm-docker/install.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# +# install.sh — Install Docker and run the Britive Bridge container on a Linux VM. +# +# Supports the common server distros and handles their differences: +# - Debian / Ubuntu (apt, docker-ce repo) +# - RHEL / Rocky / AlmaLinux (dnf, docker-ce repo, firewalld, SELinux) +# - Amazon Linux 2 / 2023 (amazon-linux-extras vs dnf) +# - Fedora (dnf, docker-ce repo) +# +# Usage: +# cp bridge.env.example bridge.env # then edit bridge.env +# sudo ./install.sh +# +# Idempotent: re-running re-pulls the image and recreates the container. +# +# This script intentionally does ONE thing per step and prints what it does, so +# you can also follow it by hand from the README if you prefer. + +set -euo pipefail + +# ── Tunables (override via env: IMAGE=... PORT=... ./install.sh) ──────────── +IMAGE="${IMAGE:-britive/bridge:latest}" # Docker Hub image +CONTAINER_NAME="${CONTAINER_NAME:-bridge}" +PORT="${PORT:-8080}" # host:container port for HTTPS +DATA_VOLUME="${DATA_VOLUME:-bridge-data}" # named volume for /data persistence +ENV_FILE="${ENV_FILE:-bridge.env}" + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33mWARN:\033[0m %s\n' "$*"; } +die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } + +[ "$(id -u)" -eq 0 ] || die "Run as root (sudo ./install.sh)." +[ -f "$ENV_FILE" ] || die "Missing $ENV_FILE. Run: cp bridge.env.example bridge.env then edit it." + +# ── 1. Detect the distro ──────────────────────────────────────────────────── +# /etc/os-release is the portable source of truth across modern Linux. +[ -r /etc/os-release ] || die "Cannot read /etc/os-release; unsupported OS." +# shellcheck disable=SC1091 +. /etc/os-release +log "Detected: ${PRETTY_NAME:-$ID $VERSION_ID} ($(uname -m))" + +# ── 2. Install Docker Engine (skip if already present) ────────────────────── +install_docker_debian() { + # Ubuntu and Debian share the apt-based docker-ce repo flow. + apt-get update -y + apt-get install -y ca-certificates curl gnupg + install -m 0755 -d /etc/apt/keyrings + # $ID is "ubuntu" or "debian" — the repo path differs by distro. + curl -fsSL "https://download.docker.com/linux/${ID}/gpg" \ + | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ +https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" \ + > /etc/apt/sources.list.d/docker.list + apt-get update -y + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +} + +install_docker_rhel() { + # RHEL / Rocky / AlmaLinux / Fedora via dnf + docker-ce repo. + dnf install -y dnf-plugins-core + # Fedora has its own repo path; the rest use the centos repo. + if [ "$ID" = "fedora" ]; then + dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo + else + dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + fi + dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +} + +install_docker_amazon() { + # Amazon Linux ships its own docker package — do NOT use the docker-ce repo. + if [ "${VERSION_ID%%.*}" = "2" ]; then + # Amazon Linux 2: docker lives in amazon-linux-extras. + amazon-linux-extras install -y docker + else + # Amazon Linux 2023: plain dnf package named "docker". + dnf install -y docker + fi +} + +if command -v docker >/dev/null 2>&1; then + log "Docker already installed: $(docker --version)" +else + log "Installing Docker Engine..." + case "$ID" in + ubuntu|debian) install_docker_debian ;; + rhel|rocky|almalinux|centos|fedora) install_docker_rhel ;; + amzn) install_docker_amazon ;; + *) die "Unsupported distro '$ID'. Install Docker manually, then re-run." ;; + esac +fi + +# ── 3. Enable + start the Docker service ──────────────────────────────────── +# systemd is standard on all supported distros. Enabling makes it survive reboot. +log "Enabling and starting the docker service..." +systemctl enable --now docker + +# ── 4. Open the firewall (distro-dependent) ───────────────────────────────── +# Ubuntu/Debian: ufw (often inactive). RHEL/Amazon: firewalld (often active). +if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then + log "firewalld active — opening ${PORT}/tcp..." + firewall-cmd --permanent --add-port="${PORT}/tcp" + firewall-cmd --reload +elif command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; then + log "ufw active — opening ${PORT}/tcp..." + ufw allow "${PORT}/tcp" +else + warn "No active host firewall detected. Ensure your CLOUD security group / " + warn "network ACL allows inbound TCP ${PORT} from your users." +fi + +# ── 5. SELinux note (RHEL/Amazon family) ──────────────────────────────────── +# When SELinux is enforcing, bind-mounted host paths need a :z/:Z label or the +# container can't read them. We use a NAMED volume (managed by Docker), which is +# labeled correctly automatically — so no relabeling needed here. If you switch +# to a host bind mount, append :Z to the -v flag (see README). +if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then + log "SELinux is Enforcing. Using a named volume (no relabel needed)." +fi + +# ── 6. Pull the image and (re)create the container ────────────────────────── +log "Pulling ${IMAGE}..." +docker pull "$IMAGE" + +# Remove any previous container so this script is safely re-runnable. +if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then + log "Removing existing container '${CONTAINER_NAME}'..." + docker rm -f "$CONTAINER_NAME" +fi + +log "Starting container '${CONTAINER_NAME}'..." +# --restart unless-stopped : auto-restart on crash and on VM reboot +# --env-file : load broker creds without putting them on the cmdline +# -v ${DATA_VOLUME}:/data : persist session/checkout state across restarts +docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p "${PORT}:8080" \ + --env-file "$ENV_FILE" \ + -v "${DATA_VOLUME}:/data" \ + "$IMAGE" + +# ── 7. Health check ───────────────────────────────────────────────────────── +log "Waiting for Bridge to report healthy..." +for i in $(seq 1 20); do + # -k: accept the self-signed cert. -s/-f: quiet + fail on non-2xx. + if curl -sfk "https://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then + log "Bridge is healthy at https://127.0.0.1:${PORT}/api/health" + break + fi + [ "$i" -eq 20 ] && warn "Health check did not pass yet. Check: docker logs ${CONTAINER_NAME}" + sleep 3 +done + +log "Done. Logs: docker logs -f ${CONTAINER_NAME}" diff --git a/Britive Bridge/platform-setup/README.md b/Britive Bridge/platform-setup/README.md new file mode 100644 index 0000000..3e02b39 --- /dev/null +++ b/Britive Bridge/platform-setup/README.md @@ -0,0 +1,67 @@ +# Britive Bridge — Platform Setup + +Run this **before** deploying Bridge with any of the deployment options. It +creates the Britive platform objects Bridge needs and prints the environment +variables the container expects. + +## What it creates + +`quick-setup.py` is an interactive script (Britive Python SDK) that creates: + +- A **Broker Pool** with an active **token** (used as `BRITIVE_BROKER_AUTH_TOKEN`) +- A **Bridge Resource** associated with that pool +- A **Response Template** for clickable session URLs +- An **admin Profile** with the required script parameters (checkout/checkin) + +At the end it prints the two env vars every deployment needs: + +``` +BRITIVE_BROKER_TENANT_SUBDOMAIN= +BRITIVE_BROKER_AUTH_TOKEN= +``` + +## Prerequisites + +- Python 3.8+ +- An API token for your Britive tenant with permission to manage the Access + Broker (pools, resources, response templates, profiles) +- The Britive SDK: + + ```bash + pip install britive requests + ``` + +## Usage + +```bash +python3 quick-setup.py +``` + +You'll be prompted for: + +1. **Tenant name** — the subdomain of your Britive URL (e.g. `acme` for + `https://acme.britive-app.com`). Can also be supplied via `BRITIVE_TENANT`. +2. **API token** — entered hidden. Can also be supplied via `BRITIVE_API_TOKEN`. +3. **Bridge external URL** — the URL users will hit (e.g. + `https://bridge.example.com`). This is the load balancer / ingress address + from your chosen deployment option, so you may want to deploy infra first, + grab the DNS name, then re-run or update the resource. +4. **Resource name** — defaults to `Admin`. + +The script is **idempotent on names**: if a pool / resource type already exists +it reuses it rather than creating a duplicate. + +## After it finishes + +1. Save the printed `BRITIVE_BROKER_TENANT_SUBDOMAIN` and + `BRITIVE_BROKER_AUTH_TOKEN` — you'll plug them into your deployment. +2. Assign users or groups to the **admin profile** via Britive policies so they + can check out Bridge access. +3. Deploy Bridge using one of the options in the parent directory. + +## Notes + +- The broker token is shown **once** at creation time. If you lose it, generate + a new one from the Britive UI (the script won't reprint an existing pool's + token). +- Treat the token like a credential — do not commit it. diff --git a/Britive Bridge/platform-setup/quick-setup.py b/Britive Bridge/platform-setup/quick-setup.py new file mode 100644 index 0000000..1a7c797 --- /dev/null +++ b/Britive Bridge/platform-setup/quick-setup.py @@ -0,0 +1,607 @@ +#!/usr/bin/env python3 +"""Britive Bridge - First-Time Setup Script + +Interactive script that creates the required Britive platform objects for a new +Bridge deployment using the Britive Python SDK. + +Prerequisites: + pip install britive + +Usage: + python3 quick-setup.py + +The script will prompt for: + 1. Britive tenant name and API token + 2. Bridge external URL + 3. Resource name + +It then creates: + - A Broker Pool with an active token + - A Bridge Resource associated with the pool + - A Response Template for clickable session URLs + - An admin Profile with the required script parameters +""" + +import getpass +import os +import sys +import tempfile +import time + +import requests + +# --------------------------------------------------------------------------- +# Dependency check +# --------------------------------------------------------------------------- + +try: + from britive.britive import Britive +except ImportError: + print("Error: the 'britive' Python package is required.") + print("Install it with: pip install britive") + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def prompt(label, default=None, secret=False, required=True): + """Prompt the user for input with an optional default.""" + suffix = f" [{default[:16] + '...' if secret else default}]" if default else "" + while True: + value = getpass.getpass(f" {label}{suffix}: ").strip() if secret else input(f" {label}{suffix}: ").strip() + if not value and default: + return default + if value: + return value + if not required: + return "" + print(" (required)") + + +def confirm(message): + """Ask for y/n confirmation.""" + while True: + answer = input(f" {message} [y/n]: ").strip().lower() + if answer in ("y", "yes"): + return True + if answer in ("n", "no"): + return False + + +def header(step, title): + print(f"\n{'=' * 60}") + print(f" Step {step}: {title}") + print(f"{'=' * 60}\n") + + +def paragraph(*lines): + for line in lines: + print(f" {line}") + print() + + +def section_block(title): + print(f" {'=' * 50}") + print(f" {title}") + print(f" {'=' * 50}") + print() + + +def print_env_block(tenant_subdomain, auth_token): + section_block("Environment Variables for Bridge") + print(f" BRITIVE_BROKER_TENANT_SUBDOMAIN={tenant_subdomain}") + if auth_token: + print(f" BRITIVE_BROKER_AUTH_TOKEN={auth_token}") + else: + print(" BRITIVE_BROKER_AUTH_TOKEN=") + print() + + +def info(message): + print(f" -> {message}") + + +def success(message): + print(f" OK: {message}") + + +def warn(message): + print(f" WARNING: {message}") + + +def error(message): + print(f" ERROR: {message}") + + +# --------------------------------------------------------------------------- +# Protocol definitions +# --------------------------------------------------------------------------- + +ADMIN_PROFILE = { + "name": "admin", + "display": "Admin", + "description": "Bridge admin UI access.", +} + +# Bridge resource type icon (SVG). +BRIDGE_ICON_SVG = ( + '' + "" + '' + '' + '' + '' + '' + "" + "" + '' + "" +) + +# Resource type fields added to Bridge after broker registration. +BRIDGE_RESOURCE_TYPE_FIELDS = [ + {"name": "protocol", "paramType": "string", "isMandatory": True}, + {"name": "url", "paramType": "string", "isMandatory": True}, +] + +# Permission-level variable names (flat list of strings for the permission definition). +ADMIN_PERMISSION_VARIABLE_NAMES = [ + "PROTOCOL", + "USERNAME", + "BRIDGE_URL", + "EXPIRATION", + "TRANSACTION_ID", +] + +# Profile-level variable mappings (system-defined values resolved at checkout time). +ADMIN_PROFILE_VARIABLES = [ + {"name": "PROTOCOL", "value": "resource.protocol", "isSystemDefined": True}, + {"name": "USERNAME", "value": "user.username", "isSystemDefined": True}, + {"name": "BRIDGE_URL", "value": "resource.url", "isSystemDefined": True}, + {"name": "EXPIRATION", "value": "profile.timeout", "isSystemDefined": True}, + {"name": "TRANSACTION_ID", "value": "profile.transactionId", "isSystemDefined": True}, +] + +ADMIN_CHECKOUT_SCRIPT = """\ +#!/bin/sh +set -eu + +TOKEN=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 43) +NOW_EPOCH=$(date +%s) +EXPIRES_AT=$((NOW_EPOCH + EXPIRATION)) +cat </dev/null +{ + "transaction_id": "${TRANSACTION_ID}", + "protocol": "admin", + "username": "${USERNAME}", + "expires_at": ${EXPIRES_AT}, + "token": "${TOKEN}" +} +EOF +URL="${BRIDGE_URL}/admin#token=${TOKEN}" +printf '{"token": "%s", "url": "%s"}\\n' "${TOKEN}" "${URL}" +""" + +ADMIN_CHECKIN_SCRIPT = """\ +#!/bin/sh +set -eu + +/opt/britive-broker/scripts/bridge.sh checkout-delete "${TRANSACTION_ID}" +""" + + +def collect_tenant_credentials(): + header(1, "Britive Tenant Credentials") + paragraph( + "Provide your Britive tenant name and an API token.", + "The tenant name is the subdomain of your Britive URL.", + "Example: if your URL is https://acme.britive-app.com,", + "the tenant name is 'acme'.", + ) + + tenant = prompt("Tenant name", default=os.environ.get("BRITIVE_TENANT")) + token = prompt("API token", default=os.environ.get("BRITIVE_API_TOKEN"), secret=True) + + info("Connecting to Britive...") + try: + client = Britive(tenant=tenant, token=token) + client.access_broker.pools.list() + success(f"Connected to {tenant}.britive-app.com") + except Exception as exc: + error(f"Failed to connect: {exc}") + sys.exit(1) + + return client, tenant + + +def create_broker_pool_and_token(client): + header(2, "Create Broker Pool") + + pool_name = prompt("Broker pool name", default="Bridge") + + # Check if a pool with this name already exists + pool_id = None + auth_token = None + try: + existing_pools = client.access_broker.pools.list(name_filter=pool_name) + for p in existing_pools: + if p.get("name") == pool_name: + pool_id = p.get("pool-id") + break + except Exception: + pass + + if pool_id: + success(f"Broker pool '{pool_name}' already exists: {pool_id}") + warn("No new token will be generated — use the existing token from the Britive UI.") + return pool_name, pool_id, None + + info(f"Creating broker pool '{pool_name}'...") + try: + pool = client.access_broker.pools.create(name=pool_name, description="Britive Bridge") + pool_id = pool.get("pool-id") + success(f"Broker pool created: {pool_id}") + except Exception as exc: + error(f"Failed to create broker pool: {exc}") + sys.exit(1) + + info("Creating broker token...") + try: + token_result = client.access_broker.pools.create_token( + pool_id, name="bridge-token", description="Auto-generated by Bridge setup script" + ) + auth_token = token_result.get("token") or token_result.get("secret") + success("Broker token created") + except Exception as exc: + error(f"Failed to create broker token: {exc}") + sys.exit(1) + + info("Activating broker token...") + try: + client.access_broker.pools.update_token(pool_id, name="bridge-token", status="ACTIVE") + success("Broker token activated") + except Exception as exc: + warn(f"Failed to activate token (you may need to activate it manually): {exc}") + + return pool_name, pool_id, auth_token + + +def collect_bridge_url(tenant_subdomain, auth_token): + print_env_block(tenant_subdomain, auth_token) + paragraph("Save these values. You will need them to start Bridge.") + + header(3, "Bridge External URL") + paragraph( + "This is the URL users will use to access Bridge.", + "It must be reachable from their browser.", + ) + + return prompt("Bridge URL", default="https://localhost:8080").split("://", maxsplit=1) + + +def create_bridge_resource_type(client): + header(4, "Create Bridge Resource Type") + + # Check if it already exists + try: + for rt in client.access_broker.resources.types.list(): + if rt.get("name") == "Bridge": + bridge_type_id = rt.get("resourceTypeId") + success(f"Resource type 'Bridge' already exists: {bridge_type_id}") + return bridge_type_id + except Exception: + pass + + info("Creating 'Bridge' resource type...") + try: + rt = client.access_broker.resources.types.create( + name="Bridge", + fields=BRIDGE_RESOURCE_TYPE_FIELDS, + ) + bridge_type_id = rt.get("resourceTypeId") + success(f"Resource type created: {bridge_type_id}") + except Exception as exc: + error(f"Failed to create resource type: {exc}") + sys.exit(1) + + # Upload icon (retry briefly — the platform may need a moment after creation) + info("Setting Bridge resource type icon...") + icon_url = f"{client.base_url}/resource-manager/resource-types/{bridge_type_id}/icon-data" + for attempt in range(5): + try: + client.put( + icon_url, + data=BRIDGE_ICON_SVG, + headers={"Content-Type": "text/xml"}, + ) + success("Resource type icon set") + break + except Exception as exc: + if attempt < 4: + time.sleep(2) + else: + warn(f"Could not set icon: {exc}") + + return bridge_type_id + + +def create_admin_permission(client, bridge_type_id, template_id): + header(5, "Create Admin Permission") + + perms = client.access_broker.resources.permissions + + # Step 1: Create permission as draft (SDK) + info("Creating draft permission...") + try: + perm = perms.create( + resource_type_id=bridge_type_id, + name=ADMIN_PROFILE["name"], + description=ADMIN_PROFILE["description"], + ) + perm_id = perm["permissionId"] + success(f"Draft permission created: {perm_id}") + except Exception as exc: + warn(f"Failed to create draft permission: {exc}") + return None + + # Step 2: Get presigned upload URLs (SDK, with retry for propagation delay) + info("Getting script upload URLs...") + urls = None + for _attempt in range(5): + try: + urls = perms.get_urls(perm_id) + if isinstance(urls, dict) and "checkinURL" in urls: + success("Upload URLs retrieved") + break + urls = None + except Exception: + pass + time.sleep(2) + + if not urls: + warn("Could not get script upload URLs.") + return {"id": perm_id, "version": "", "resource_type_id": bridge_type_id} + + # Step 3: Upload scripts to presigned S3 URLs via temp files + info("Uploading checkout script...") + checkout_tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) # noqa: SIM115 + checkin_tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) # noqa: SIM115 + try: + checkout_tmp.write(ADMIN_CHECKOUT_SCRIPT) + checkout_tmp.close() + checkin_tmp.write(ADMIN_CHECKIN_SCRIPT) + checkin_tmp.close() + + with open(checkout_tmp.name, "rb") as f: + requests.put(urls["checkoutURL"], data=f, timeout=30) + success("Checkout script uploaded") + + info("Uploading checkin script...") + with open(checkin_tmp.name, "rb") as f: + requests.put(urls["checkinURL"], data=f, timeout=30) + success("Checkin script uploaded") + except Exception as exc: + warn(f"Failed to upload scripts: {exc}") + finally: + os.unlink(checkout_tmp.name) + os.unlink(checkin_tmp.name) + + # Step 4: Finalize permission with file metadata, variables, and response template. + # SDK's update() filters out responseTemplates via valid_fields, so we use a direct PUT. + info("Finalizing permission...") + finalize_body = { + "permissionId": perm_id, + "name": ADMIN_PROFILE["name"], + "resourceTypeId": bridge_type_id, + "description": ADMIN_PROFILE["description"], + "checkinFileName": f"{perm_id}_checkin", + "checkinTimeLimit": 60, + "checkoutFileName": f"{perm_id}_checkout", + "checkoutTimeLimit": 60, + "isDraft": False, + "inlineFileExists": True, + "editorType": "shell", + "variables": ADMIN_PERMISSION_VARIABLE_NAMES, + } + if template_id: + finalize_body["responseTemplates"] = [ + {"name": "Bridge", "templateId": template_id}, + ] + + try: + perm_url = f"{client.base_url}/resource-manager/permissions/{perm_id}" + result = client.put(perm_url, json=finalize_body) + perm_version = (result or {}).get("version", "") + success(f"Permission 'admin' finalized: {perm_id}") + except Exception as exc: + warn(f"Failed to finalize permission: {exc}") + perm_version = "" + + return { + "id": perm_id, + "version": perm_version, + "resource_type_id": bridge_type_id, + } + + +def create_bridge_resource(client, bridge_type_id, bridge_url, pool_id): + header(6, "Create Bridge Resource") + resource_name = prompt("Resource name", default="Admin") + + info(f"Creating resource '{resource_name}'...") + try: + resource = client.access_broker.resources.create( + name=resource_name, + resource_type_id=bridge_type_id, + description=f"Bridge deployment at {bridge_url}", + param_values={"protocol": "admin", "url": bridge_url}, + ) + resource_id = resource.get("resourceId") + success(f"Resource created: {resource_id}") + except Exception as exc: + error(f"Failed to create resource: {exc}") + sys.exit(1) + + info("Associating resource with broker pool...") + for attempt in range(5): + try: + client.access_broker.resources.add_broker_pools(resource_id, [pool_id]) + success("Resource associated with broker pool") + break + except Exception as exc: + if attempt < 4: + time.sleep(2) + else: + warn(f"Failed to associate resource with pool: {exc}") + + return resource_name, resource_id + + +def create_response_template(client, schema): + header(7, "Create Response Template") + info("Creating response template for clickable session URLs...") + try: + template = client.access_broker.response_templates.create( + name="Bridge", + description="Displays the Bridge session URL as a clickable link", + template_data=f"{schema}://{{{{url}}}}", + is_console_enabled=True, + ) + template_id = template.get("templateId") + success(f"Response template created: {template_id}") + return template_id + except Exception as exc: + warn(f"Failed to create response template: {exc}") + return None + + +def create_admin_profile(client, admin_permission): + header(8, "Create Admin Profile") + profile_name = f"Bridge {ADMIN_PROFILE['display']}" + + info(f"Creating profile '{profile_name}'...") + try: + profile = client.access_broker.profiles.create( + name=profile_name, + description=ADMIN_PROFILE["description"], + expiration_duration=3600000, + ) + profile_id = profile.get("profileId") or profile.get("profileId") + success(f" Profile created: {profile_id}") + except Exception as exc: + warn(f" Failed to create profile: {exc}") + return None + + try: + client.access_broker.profiles.add_association( + profile_id=profile_id, + associations={"Resource-Type": ["Bridge"]}, + ) + success(" Resource type 'Bridge' associated") + except Exception as exc: + warn(f" Failed to add resource association: {exc}") + + if admin_permission: + try: + client.access_broker.profiles.permissions.add_permissions( + profile_id=profile_id, + permission_id=admin_permission["id"], + version="latest", + resource_type_id=admin_permission["resource_type_id"], + variables=ADMIN_PROFILE_VARIABLES, + ) + success(f" Permission 'admin' attached with {len(ADMIN_PROFILE_VARIABLES)} script parameters") + except Exception as exc: + warn(f" Failed to add permission: {exc}") + + return {"name": profile_name, "id": profile_id, "protocol": ADMIN_PROFILE["name"]} + + +def print_summary(state): + header(9, "Setup Complete") + + print(" Created objects:") + print(f" Broker pool: {state['pool_name']} ({state['pool_id']})") + print(f" Resource type: Bridge ({state['bridge_type_id']})") + print(f" Resource: {state['resource_name']} ({state['resource_id']})") + if state["template_id"]: + print(f" Response template: Bridge ({state['template_id']})") + print(f" Admin profile: {1 if state['created_profile'] else 0}") + if state["created_profile"]: + print(f" - {state['created_profile']['name']} ({state['created_profile']['protocol']})") + + print() + print_env_block(state["tenant_subdomain"], state["auth_token"]) + + section_block("Next Steps") + print(" 1. Assign users or groups to the admin profile via") + print(" policies so they can check out Bridge access.") + print() + paragraph("Done! Users can now check out Bridge admin access at", state["bridge_url"]) + + +# --------------------------------------------------------------------------- +# Main setup flow +# --------------------------------------------------------------------------- + + +def main(): + client, tenant = collect_tenant_credentials() + pool_name, pool_id, auth_token = create_broker_pool_and_token(client) + tenant_subdomain = tenant + bridge_schema, bridge_url = collect_bridge_url(tenant_subdomain, auth_token) + template_id = create_response_template(client, bridge_schema) + bridge_type_id = create_bridge_resource_type(client) + admin_permission = create_admin_permission(client, bridge_type_id, template_id) + resource_name, resource_id = create_bridge_resource(client, bridge_type_id, bridge_url, pool_id) + created_profile = create_admin_profile(client, admin_permission) + print_summary( + { + "pool_name": pool_name, + "pool_id": pool_id, + "bridge_type_id": bridge_type_id, + "resource_name": resource_name, + "resource_id": resource_id, + "template_id": template_id, + "created_profile": created_profile, + "tenant_subdomain": tenant_subdomain, + "auth_token": auth_token, + "bridge_url": bridge_url, + } + ) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\n Setup cancelled.\n") + sys.exit(1) diff --git a/Britive Bridge/windows-vm-docker/README.md b/Britive Bridge/windows-vm-docker/README.md new file mode 100644 index 0000000..ef23716 --- /dev/null +++ b/Britive Bridge/windows-vm-docker/README.md @@ -0,0 +1,208 @@ +# Britive Bridge — Windows VM (Docker) + +Run the Britive Bridge container (which hosts the broker process) on a Windows VM +using Docker, pulling the image from **Docker Hub** (`britive/bridge`). + +> **Read this first — the key Windows nuance:** `britive/bridge` is a **Linux** +> container image. Windows can only run Linux images through a Linux runtime, +> i.e. the **WSL 2 backend**. Native "Windows containers" mode **cannot** run +> this image. Everything below assumes Linux-container mode via WSL 2. + +--- + +## Which path fits your VM? + +| Your VM | Recommended runtime | +|---------|---------------------| +| Windows 10/11 Pro/Enterprise | **Docker Desktop** with WSL 2 backend | +| Windows Server 2022 / 2025 | **Docker Desktop** (supported on Server 2022+) with WSL 2, **or** Docker CE inside a WSL 2 distro | +| Windows Server 2019 | No WSL 2 GA → run Docker CE in a Linux VM/WSL 1 is not supported. **Prefer a Linux VM** ([Linux guide](../linux-vm-docker/)) or upgrade to Server 2022+ | + +If you end up running Docker **inside a WSL 2 Linux distro**, follow the +[Linux VM guide](../linux-vm-docker/) from that distro's shell instead — it's +simpler and the same image. + +**Licensing note:** Docker Desktop requires a paid subscription for larger +businesses. If that's a blocker, use a Linux VM with Docker Engine (no Desktop +license) per the Linux guide. + +--- + +## Prerequisites + +- Windows 10/11 (Pro/Enterprise) or Windows Server 2022+, 64-bit, with + **virtualization enabled** in BIOS/hypervisor (required for WSL 2). +- **WSL 2** installed: in an elevated PowerShell run `wsl --install` then reboot. +- **Docker Desktop** installed and set to **Linux containers** (tray icon → + *Switch to Linux containers…* if currently on Windows containers). +- **Outbound** internet to your Britive tenant (Broker/MQTT) and Docker Hub. +- **Inbound** TCP on the Bridge port (default `8080`) — open the Windows Firewall + **and** any cloud security group / network ACL. +- PowerShell 5.1+ (built in). PowerShell 7+ recommended for the scripted health + check (`-SkipCertificateCheck`). +- Completed [platform setup](../platform-setup/) — you need + `BRITIVE_BROKER_TENANT_SUBDOMAIN` and `BRITIVE_BROKER_AUTH_TOKEN`. + +--- + +## Install WSL 2 + Docker Desktop (one-time) + +```powershell +# Elevated PowerShell: +wsl --install # installs WSL 2 + a default Linux distro; reboot after +wsl --status # confirm "Default Version: 2" +``` + +Then install **Docker Desktop** (download the MSI from Docker, or via winget): + +```powershell +winget install -e --id Docker.DockerDesktop +``` + +Launch Docker Desktop → Settings → **General**: ensure *Use the WSL 2 based +engine* is checked. Confirm you're in Linux mode: + +```powershell +docker info --format '{{.OSType}}' # must print: linux +``` + +--- + +## Quick start (scripted) + +```powershell +# 1. Provide credentials +Copy-Item bridge.env.example bridge.env +notepad bridge.env # set tenant subdomain + broker token, save + +# 2. Run (elevated PowerShell — needed for the firewall rule) +.\install.ps1 + +# 3. Verify (PowerShell 7+) +Invoke-WebRequest https://127.0.0.1:8080/api/health -SkipCertificateCheck +docker logs -f bridge +``` + +`install.ps1` verifies Docker is present and in **Linux** mode, opens the +firewall, pulls `britive/bridge:latest`, and runs the container with a persistent +volume and `--restart unless-stopped`. Re-run it to update / recreate. + +Override defaults: + +```powershell +.\install.ps1 -Port 9443 -ContainerName bridge -Image britive/bridge:latest +``` + +> **Execution policy:** if the script is blocked, run it for this session only: +> `powershell -ExecutionPolicy Bypass -File .\install.ps1` + +--- + +## Manual steps + +```powershell +# 1. Confirm Linux-container mode +docker info --format '{{.OSType}}' # -> linux + +# 2. Open the Windows Firewall (elevated) +New-NetFirewallRule -DisplayName "Britive Bridge 8080" -Direction Inbound ` + -Action Allow -Protocol TCP -LocalPort 8080 + +# 3. Credentials +Copy-Item bridge.env.example bridge.env # then edit bridge.env + +# 4. Pull + run. Backtick (`) is the PowerShell line-continuation character. +docker pull britive/bridge:latest +docker run -d ` + --name bridge ` + --restart unless-stopped ` + -p 8080:8080 ` + --env-file bridge.env ` + -v bridge-data:/data ` + britive/bridge:latest + +# 5. Verify +docker ps +docker logs -f bridge +``` + +--- + +## Windows-specific nuances + +- **Line continuation.** PowerShell uses a backtick `` ` `` at end-of-line, not + the shell's `\`. Copy-pasting Linux commands with `\` will break. +- **Container mode matters.** `docker info --format '{{.OSType}}'` must say + `linux`. If it says `windows`, switch via the Docker tray icon. The image will + not run otherwise (`no matching manifest for windows/amd64`). +- **WSL 2 file/volume location.** With the WSL 2 backend, the `bridge-data` + **named volume** lives inside the WSL distro's virtual disk (managed by + Docker). Prefer named volumes over Windows-path bind mounts — bind-mounting + `C:\...` into a Linux container crosses the WSL boundary and is slower and + permission-fiddly. +- **`--env-file` formatting.** Plain `KEY=VALUE` lines, no quotes, no `$env:` or + `set` prefixes. Save `bridge.env` as UTF-8 **without BOM** (Notepad's default + ANSI/UTF-8 is fine; avoid "UTF-8 with BOM"). +- **Health check TLS.** The container's cert is self-signed. + `Invoke-WebRequest -SkipCertificateCheck` requires **PowerShell 7+**. On + Windows PowerShell 5.1, check health from inside the container instead: + `docker exec bridge curl -sfk https://127.0.0.1:8080/api/health`. +- **Auto-start on reboot.** `--restart unless-stopped` restarts the container, + but only once **Docker Desktop itself starts**. In Docker Desktop Settings → + General, enable *Start Docker Desktop when you log in*. For an unattended + server that should run without an interactive login, a **Linux VM with Docker + Engine** is the more robust choice. + +--- + +## Custom TLS certificate + +```powershell +docker run -d --name bridge --restart unless-stopped ` + -p 8080:8080 --env-file bridge.env ` + -v bridge-data:/data ` + -v C:\certs\cert.pem:/custom-certs/cert.pem:ro ` + -v C:\certs\key.pem:/custom-certs/key.pem:ro ` + -e TLS_CERT_FILE=/custom-certs/cert.pem ` + -e TLS_KEY_FILE=/custom-certs/key.pem ` + britive/bridge:latest +``` + +(Windows-path bind mounts are passed through WSL 2; ensure the drive is shared +in Docker Desktop → Settings → Resources → File sharing.) + +--- + +## External URL + +Users reach Bridge at the `BRIDGE_URL` you registered in platform setup. Point a +DNS record at the VM's public address (or front it with a reverse proxy / load +balancer that terminates TLS) and confirm the Britive Bridge **resource** uses +that URL. + +--- + +## Lifecycle / operations + +```powershell +docker logs -f bridge # follow logs +docker restart bridge # restart +docker pull britive/bridge:latest; docker rm -f bridge; .\install.ps1 # update +docker rm -f bridge # stop + remove (keeps volume) +docker volume rm bridge-data # delete persisted state (destructive) +``` + +--- + +## Troubleshooting + +| Symptom | Likely cause / fix | +|---------|--------------------| +| `no matching manifest for windows/amd64` | Docker in Windows-container mode — switch to Linux containers | +| `docker: error during connect` / daemon unreachable | Docker Desktop not started, or WSL 2 not running | +| Script "cannot be loaded because running scripts is disabled" | `powershell -ExecutionPolicy Bypass -File .\install.ps1` | +| Health check fails on PS 5.1 | `-SkipCertificateCheck` needs PS 7+; use `docker exec ... curl -k` | +| Container restarts in a loop | Bad/missing broker token — check `docker logs bridge` | +| Reachable locally but not from other hosts | Firewall rule and/or cloud security group not open on 8080 | +| Bind-mounted `C:\` cert not found | Share the drive in Docker Desktop → Resources → File sharing | +| Lost sessions after reboot | Data volume not mounted, or Docker Desktop didn't auto-start | diff --git a/Britive Bridge/windows-vm-docker/bridge.env.example b/Britive Bridge/windows-vm-docker/bridge.env.example new file mode 100644 index 0000000..dbfd5a2 --- /dev/null +++ b/Britive Bridge/windows-vm-docker/bridge.env.example @@ -0,0 +1,19 @@ +# Britive Bridge environment file (Windows VM). +# Copy to "bridge.env", fill in real values, keep it OUT of source control. +# Copy-Item bridge.env.example bridge.env +# +# Loaded by: docker run --env-file bridge.env ... +# NOTE: use plain KEY=VALUE lines, no quotes, no "set"/"$env:" prefixes. +# Docker --env-file does not do shell-style quoting. + +# ── Required: broker connection (from platform-setup) ────────────────────── +# Tenant subdomain only — e.g. "acme" for https://acme.britive-app.com +BRITIVE_BROKER_TENANT_SUBDOMAIN= +# Broker pool token created during platform setup. Treat as a secret. +BRITIVE_BROKER_AUTH_TOKEN= + +# ── Optional: custom TLS certificate ─────────────────────────────────────── +# Omit both to use the container's auto-generated self-signed cert. +# Paths are INSIDE the container (mount them with -v). +# TLS_CERT_FILE=/custom-certs/cert.pem +# TLS_KEY_FILE=/custom-certs/key.pem diff --git a/Britive Bridge/windows-vm-docker/install.ps1 b/Britive Bridge/windows-vm-docker/install.ps1 new file mode 100644 index 0000000..a7d1c50 --- /dev/null +++ b/Britive Bridge/windows-vm-docker/install.ps1 @@ -0,0 +1,132 @@ +<# +.SYNOPSIS + Run the Britive Bridge container on a Windows VM using Docker (Docker Hub image). + +.DESCRIPTION + britive/bridge is a LINUX container image. Windows can only run it through a + Linux container runtime, which means one of: + - Docker Desktop with the WSL 2 backend (Windows 10/11, Server 2022+), OR + - Docker CE installed INSIDE a WSL 2 distro (run this from that distro's + shell instead — see the Linux guide). + Native Windows containers (the default "Windows containers" mode) CANNOT run + this image. This script verifies you are in Linux-container mode and refuses + to continue otherwise. + + The script does NOT install Docker (Docker Desktop is a GUI/MSI install and + WSL 2 setup is interactive). It checks prerequisites, opens the firewall, + pulls the image, and runs the container. + +.PARAMETER Image + Docker Hub image. Default: britive/bridge:latest + +.PARAMETER Port + Host port published for HTTPS. Default: 8080 + +.EXAMPLE + Copy-Item bridge.env.example bridge.env # then edit bridge.env + .\install.ps1 + +.NOTES + Run from an ELEVATED PowerShell (Run as Administrator) so the firewall rule + can be created. +#> + +[CmdletBinding()] +param( + [string]$Image = "britive/bridge:latest", + [string]$ContainerName = "bridge", + [int] $Port = 8080, + [string]$DataVolume = "bridge-data", + [string]$EnvFile = "bridge.env" +) + +$ErrorActionPreference = "Stop" + +function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } +function Write-Warn($msg) { Write-Host "WARN: $msg" -ForegroundColor Yellow } +function Die($msg) { Write-Host "ERROR: $msg" -ForegroundColor Red; exit 1 } + +# ── 1. Must be elevated (to add the firewall rule) ────────────────────────── +$principal = New-Object Security.Principal.WindowsPrincipal( + [Security.Principal.WindowsIdentity]::GetCurrent()) +if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Die "Run this from an elevated PowerShell (Run as Administrator)." +} + +# ── 2. Docker must be installed and the daemon reachable ──────────────────── +if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + Die "Docker not found. Install Docker Desktop (WSL 2 backend) first — see README." +} +try { docker info *> $null } catch { + Die "Docker daemon not reachable. Start Docker Desktop and wait for it to be running." +} + +# ── 3. Must be in LINUX container mode (not Windows containers) ───────────── +# 'docker info' reports the daemon OS. britive/bridge is a Linux image. +$daemonOS = (docker info --format '{{.OSType}}').Trim() +Write-Step "Docker daemon OSType: $daemonOS" +if ($daemonOS -ne "linux") { + Die @" +Docker is in '$daemonOS' container mode. britive/bridge is a LINUX image. +Switch Docker Desktop to Linux containers: + Right-click the Docker tray icon > 'Switch to Linux containers...' +Then re-run this script. +"@ +} + +# ── 4. Env file must exist ────────────────────────────────────────────────── +if (-not (Test-Path $EnvFile)) { + Die "Missing $EnvFile. Run: Copy-Item bridge.env.example bridge.env then edit it." +} + +# ── 5. Open the Windows Firewall for the port ─────────────────────────────── +$ruleName = "Britive Bridge $Port" +if (-not (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue)) { + Write-Step "Adding firewall rule '$ruleName' (inbound TCP $Port)..." + New-NetFirewallRule -DisplayName $ruleName -Direction Inbound ` + -Action Allow -Protocol TCP -LocalPort $Port | Out-Null +} else { + Write-Step "Firewall rule '$ruleName' already exists." +} +Write-Warn "Also open inbound TCP $Port in any CLOUD security group / network ACL." + +# ── 6. Pull the image and (re)create the container ────────────────────────── +Write-Step "Pulling $Image ..." +docker pull $Image + +$exists = docker ps -a --format '{{.Names}}' | Select-String -SimpleMatch $ContainerName +if ($exists) { + Write-Step "Removing existing container '$ContainerName'..." + docker rm -f $ContainerName | Out-Null +} + +Write-Step "Starting container '$ContainerName'..." +# --restart unless-stopped : restart on crash and when the VM/Docker restarts +# --env-file : load broker creds without exposing them on cmdline +# -v ${DataVolume}:/data : persist session state in a Docker-managed volume +docker run -d ` + --name $ContainerName ` + --restart unless-stopped ` + -p "$($Port):8080" ` + --env-file $EnvFile ` + -v "$($DataVolume):/data" ` + $Image | Out-Null + +# ── 7. Health check ───────────────────────────────────────────────────────── +Write-Step "Waiting for Bridge to report healthy..." +$healthy = $false +for ($i = 0; $i -lt 20; $i++) { + try { + # -SkipCertificateCheck accepts the container's self-signed cert (PS 6+). + $resp = Invoke-WebRequest -Uri "https://127.0.0.1:$Port/api/health" ` + -SkipCertificateCheck -TimeoutSec 5 -UseBasicParsing + if ($resp.StatusCode -eq 200) { $healthy = $true; break } + } catch { Start-Sleep -Seconds 3 } +} +if ($healthy) { + Write-Step "Bridge is healthy at https://127.0.0.1:$Port/api/health" +} else { + Write-Warn "Health check not passing yet. Inspect: docker logs $ContainerName" +} + +Write-Step "Done. Logs: docker logs -f $ContainerName" From 180df34fc9436133132647ef51eea7eaf14b6f90 Mon Sep 17 00:00:00 2001 From: palakchheda <42711310+palakchheda@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:35:19 -0700 Subject: [PATCH 2/5] pylinter fix --- Britive Bridge/README.md | 1 + Britive Bridge/platform-setup/README.md | 10 ++++++++-- Britive Bridge/platform-setup/requirements.txt | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 Britive Bridge/platform-setup/requirements.txt diff --git a/Britive Bridge/README.md b/Britive Bridge/README.md index 0163920..62f5920 100644 --- a/Britive Bridge/README.md +++ b/Britive Bridge/README.md @@ -92,6 +92,7 @@ Britive Bridge/ ├── README.md # you are here ├── platform-setup/ # run FIRST — creates Britive platform objects │ ├── quick-setup.py +│ ├── requirements.txt │ └── README.md ├── custom-image/ # extend britive/bridge with extra utilities │ ├── Dockerfile # base-distro & arch agnostic diff --git a/Britive Bridge/platform-setup/README.md b/Britive Bridge/platform-setup/README.md index 3e02b39..312f57f 100644 --- a/Britive Bridge/platform-setup/README.md +++ b/Britive Bridge/platform-setup/README.md @@ -25,12 +25,18 @@ BRITIVE_BROKER_AUTH_TOKEN= - Python 3.8+ - An API token for your Britive tenant with permission to manage the Access Broker (pools, resources, response templates, profiles) -- The Britive SDK: +- The Britive SDK + `requests`: ```bash - pip install britive requests + pip install -r requirements.txt + # or: pip install britive requests ``` + > VS Code showing "could not be resolved" on `import requests` / `import + > britive`? That's the editor pointing at an interpreter without these + > installed — run *Python: Select Interpreter* and pick the env where you ran + > the install above. It is not a code error. + ## Usage ```bash diff --git a/Britive Bridge/platform-setup/requirements.txt b/Britive Bridge/platform-setup/requirements.txt new file mode 100644 index 0000000..0a94f3a --- /dev/null +++ b/Britive Bridge/platform-setup/requirements.txt @@ -0,0 +1,4 @@ +# Dependencies for quick-setup.py +# Install: pip install -r requirements.txt +britive>=4.0.0 +requests>=2.31.0 From ec2fd5f89e3d28e91ff3f26bae072df58841475f Mon Sep 17 00:00:00 2001 From: palakchheda <42711310+palakchheda@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:05:51 -0700 Subject: [PATCH 3/5] fix(bridge): resolve HIGH-severity review findings, mark docs beta --- Britive Bridge/README.md | 7 +- .../aws-ecs-fargate-alb-ssh/README.md | 30 +++-- .../ecs-fargate-alb-ssh.yaml | 45 ++++--- Britive Bridge/aws-ecs-fargate-alb/README.md | 4 + .../aws-ecs-fargate-alb/ecs-fargate-alb.yaml | 24 +++- Britive Bridge/aws-ecs-fargate-nlb/README.md | 8 +- .../aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml | 45 ++++++- Britive Bridge/custom-image/README.md | 5 +- .../custom-image/k8s-overlays/README.md | 9 +- .../k8s-overlays/deployment-patch.yaml | 52 +++++++-- Britive Bridge/kubernetes/README.md | 21 +++- .../ha-deployment-patch.example.yaml | 39 +++++++ .../manifests/ha-overlay.example.yaml | 58 --------- .../kubernetes/manifests/pvc-rwm.example.yaml | 29 +++++ Britive Bridge/platform-setup/README.md | 11 +- Britive Bridge/platform-setup/quick-setup.py | 110 +++++++++++++++--- Britive Bridge/windows-vm-docker/README.md | 13 ++- Britive Bridge/windows-vm-docker/install.ps1 | 12 +- 18 files changed, 383 insertions(+), 139 deletions(-) create mode 100644 Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml delete mode 100644 Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml create mode 100644 Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml diff --git a/Britive Bridge/README.md b/Britive Bridge/README.md index 62f5920..0ffffcd 100644 --- a/Britive Bridge/README.md +++ b/Britive Bridge/README.md @@ -1,5 +1,9 @@ # Britive Bridge — Deployment Options +> **Note:** This directory targets **Britive Bridge v1.x**. The deployment +> scripts, templates, and examples here are in **BETA** — validate them in a +> non-production environment first, and expect changes between releases. + Britive Bridge is a self-hosted container that connects to the Britive platform through the Britive Broker and brokers clientless, browser-based sessions to your internal resources. This directory collects **ready-to-use deployment @@ -132,7 +136,8 @@ Britive Bridge/ │ ├── service.yaml │ ├── ingress.yaml │ ├── external-secret.example.yaml # optional: External Secrets Operator - │ └── ha-overlay.example.yaml # optional: multi-replica HA (RWM) + │ ├── pvc-rwm.example.yaml # optional HA: RWX PVC (instead of pvc.yaml) + │ └── ha-deployment-patch.example.yaml # optional HA: replicas + rolling update └── README.md # includes the public Helm chart roadmap ``` diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md b/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md index b609dd9..624e510 100644 --- a/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md @@ -12,8 +12,9 @@ private key to authenticate. CloudFormation template `ecs-fargate-alb-ssh.yaml` adds: -- A **Secrets Manager secret** holding the PEM-encoded private key -- Execution-role permission to **read that secret** at task start +- A **Secrets Manager secret** holding the PEM-encoded private key (alongside + the broker auth token secret that all variants create) +- Execution-role permission to **read those secrets** at task start - A container `Secrets` mapping that injects the key as `SSH_PRIVATE_KEY` - A container entrypoint that writes the key to `/home/bridge/.ssh/id_ed25519` (mode `600`) before starting Bridge @@ -38,18 +39,24 @@ Same parameters as the ALB option, **plus**: | Parameter | Notes | |-----------|-------| -| `BrokerSSHPrivateKey` | PEM-encoded private key (ed25519 or RSA). Newlines as `\n`. **Highly sensitive.** | +| `BrokerSSHPrivateKey` | PEM-encoded private key (ed25519 or RSA). **Highly sensitive.** | ### Filling in the private key safely -Avoid pasting the key into a file by hand. Convert it to a single JSON-escaped -line and write `params.json` programmatically, e.g.: +Avoid pasting the key into a file by hand — JSON requires the newlines to be +escaped, and doing that manually corrupts the key. Let `jq` do the escaping: ```bash -KEY=$(awk '{printf "%s\\n", $0}' ~/.ssh/bridge_ed25519) # escape newlines -# then inject "$KEY" into params.json with jq, and delete params.json after deploy +jq --arg key "$(cat ~/.ssh/bridge_ed25519)" \ + 'map(if .ParameterKey == "BrokerSSHPrivateKey" then .ParameterValue = $key else . end)' \ + params.example.json > params.json +# fill in the remaining placeholder values, then delete params.json after deploy ``` +Do **not** pre-escape the key (e.g. with `awk`/`sed`) before passing it to +`jq` — it gets escaped twice and the container receives literal `\n` text +instead of newlines, breaking SSH authentication. + > **Never commit `params.json`** with a real key. Delete it locally once the > stack is up — the key lives in Secrets Manager from then on. To rotate, update > the secret value (or redeploy with a new key) and restart the service. @@ -94,5 +101,10 @@ aws ecs execute-command --cluster --task \ aws cloudformation delete-stack --stack-name britive-bridge ``` -> The Secrets Manager secret may be retained with a recovery window depending on -> account settings — delete it explicitly if you need immediate removal. +> The Secrets Manager secrets are retained with a recovery window after stack +> deletion. To redeploy the same stack name immediately, force-delete them first: +> +> ```bash +> aws secretsmanager delete-secret --secret-id britive-bridge/broker/ssh-private-key --force-delete-without-recovery +> aws secretsmanager delete-secret --secret-id britive-bridge/broker/auth-token --force-delete-without-recovery +> ``` diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml index 4b01e32..6a9d6db 100644 --- a/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml @@ -105,6 +105,13 @@ Resources: Description: Private key used by the broker ECS task to SSH into EC2 instances SecretString: !Ref BrokerSSHPrivateKey + BrokerAuthTokenSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub ${StackNamePrefix}/broker/auth-token + Description: Britive broker pool authentication token for the bridge ECS task + SecretString: !Ref BrokerAuthToken + # ── IAM Roles ───────────────────────────────────────────────────────────── BridgeExecutionRole: Type: AWS::IAM::Role @@ -120,14 +127,16 @@ Resources: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy Policies: - - PolicyName: ReadBrokerSSHKey + - PolicyName: ReadBrokerSecrets PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue - Resource: !Ref BrokerSSHKeySecret + Resource: + - !Ref BrokerSSHKeySecret + - !Ref BrokerAuthTokenSecret BridgeTaskRole: Type: AWS::IAM::Role @@ -158,18 +167,9 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: !GetAtt BridgeLogGroup.Arn - - PolicyName: SecretsManagerReadOnly - PolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - - secretsmanager:DescribeSecret - - secretsmanager:GetRandomPassword - - secretsmanager:ListSecrets - - secretsmanager:ListSecretVersionIds - Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:*" + # If your checkout/checkin scripts read additional Secrets Manager + # secrets (e.g. database credentials), grant the task role + # secretsmanager:GetSecretValue scoped to those specific secret ARNs. # ── Security Groups ─────────────────────────────────────────────────────── @@ -362,9 +362,14 @@ Resources: PortMappings: - ContainerPort: 8080 Protocol: tcp - # Write SSH key to /root/.ssh before starting the bridge process. - # SSH_PRIVATE_KEY is injected from Secrets Manager by the ECS agent - # before this command runs, so it is available as an env var. + # Write SSH key to /home/bridge/.ssh before starting the bridge + # process. SSH_PRIVATE_KEY is injected from Secrets Manager by the + # ECS agent before this command runs, so it is available as an env + # var. The stock britive/bridge image config is: + # Entrypoint=["/entrypoint.sh"], Cmd=none, User=bridge + # so exec'ing /entrypoint.sh reproduces the default startup. If you + # use a custom ImageUri, confirm its entrypoint matches: + # docker inspect --format '{{json .Config.Entrypoint}}' EntryPoint: - "/bin/sh" - "-c" @@ -373,9 +378,11 @@ Resources: Environment: - Name: BRITIVE_BROKER_TENANT_SUBDOMAIN Value: !Ref BrokerTenantSubdomain - - Name: BRITIVE_BROKER_AUTH_TOKEN - Value: !Ref BrokerAuthToken Secrets: + # Injected by the ECS agent at task start; kept out of the task + # definition so ecs:DescribeTaskDefinition does not expose them. + - Name: BRITIVE_BROKER_AUTH_TOKEN + ValueFrom: !Ref BrokerAuthTokenSecret - Name: SSH_PRIVATE_KEY ValueFrom: !Ref BrokerSSHKeySecret MountPoints: diff --git a/Britive Bridge/aws-ecs-fargate-alb/README.md b/Britive Bridge/aws-ecs-fargate-alb/README.md index b5ce4df..8110483 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/README.md +++ b/Britive Bridge/aws-ecs-fargate-alb/README.md @@ -87,3 +87,7 @@ aws cloudformation describe-stacks --stack-name britive-bridge \ ```bash aws cloudformation delete-stack --stack-name britive-bridge ``` + +> The broker-token secret is retained with a recovery window after stack +> deletion. To redeploy the same stack name immediately, force-delete it first: +> `aws secretsmanager delete-secret --secret-id britive-bridge/broker/auth-token --force-delete-without-recovery` diff --git a/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml index 24a65c2..9735252 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml +++ b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml @@ -89,6 +89,14 @@ Resources: Properties: ClusterName: !Sub ${StackNamePrefix}-cluster + # ── Secrets Manager ─────────────────────────────────────────────────────── + BrokerAuthTokenSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub ${StackNamePrefix}/broker/auth-token + Description: Britive broker pool authentication token for the bridge ECS task + SecretString: !Ref BrokerAuthToken + # ── IAM Roles ───────────────────────────────────────────────────────────── BridgeExecutionRole: Type: AWS::IAM::Role @@ -103,6 +111,15 @@ Resources: Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: ReadBrokerSecrets + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref BrokerAuthTokenSecret BridgeTaskRole: Type: AWS::IAM::Role @@ -310,10 +327,13 @@ Resources: Environment: - Name: BRITIVE_BROKER_TENANT_SUBDOMAIN Value: !Ref BrokerTenantSubdomain - - Name: BRITIVE_BROKER_AUTH_TOKEN - Value: !Ref BrokerAuthToken # No TLS_CERT_FILE / TLS_KEY_FILE set → container uses its # auto-generated self-signed cert. ALB accepts it regardless. + Secrets: + # Injected by the ECS agent at task start; kept out of the task + # definition so ecs:DescribeTaskDefinition does not expose it. + - Name: BRITIVE_BROKER_AUTH_TOKEN + ValueFrom: !Ref BrokerAuthTokenSecret MountPoints: - SourceVolume: bridge-data ContainerPath: /data diff --git a/Britive Bridge/aws-ecs-fargate-nlb/README.md b/Britive Bridge/aws-ecs-fargate-nlb/README.md index 0fe2c7b..70ab984 100644 --- a/Britive Bridge/aws-ecs-fargate-nlb/README.md +++ b/Britive Bridge/aws-ecs-fargate-nlb/README.md @@ -16,7 +16,9 @@ CloudFormation template `ecs-fargate-nlb.yaml` creates: - ECS **cluster**, **task definition**, and **service** (Fargate, ARM64 by default) - An internet-facing **NLB** with a TCP:443 listener → target group TCP:8080 - **EFS** file system + access point + mount targets, mounted at `/data` (encrypted, transit encryption on) -- **Security groups** (task allows 8080 from your `IngressCidr`; EFS allows 2049 from the task) +- **Security groups** (NLB allows 443 from your `IngressCidr`; task allows 8080 + only from the NLB security group — covers forwarded traffic and NLB health + checks; EFS allows 2049 from the task) - **IAM** execution + task roles - A **CloudWatch** log group (30-day retention) - ECS Exec enabled for debugging @@ -105,3 +107,7 @@ curl -sfk https:///api/health ```bash aws cloudformation delete-stack --stack-name britive-bridge ``` + +> The broker-token secret is retained with a recovery window after stack +> deletion. To redeploy the same stack name immediately, force-delete it first: +> `aws secretsmanager delete-secret --secret-id britive-bridge/broker/auth-token --force-delete-without-recovery` diff --git a/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml index e98d6d4..cc3a96a 100644 --- a/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml +++ b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml @@ -74,6 +74,12 @@ Resources: Type: AWS::ECS::Cluster Properties: ClusterName: !Sub ${StackNamePrefix}-cluster + BrokerAuthTokenSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub ${StackNamePrefix}/broker/auth-token + Description: Britive broker pool authentication token for the bridge ECS task + SecretString: !Ref BrokerAuthToken BridgeExecutionRole: Type: AWS::IAM::Role Properties: @@ -87,6 +93,15 @@ Resources: Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: ReadBrokerSecrets + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref BrokerAuthTokenSecret BridgeTaskRole: Type: AWS::IAM::Role Properties: @@ -98,6 +113,27 @@ Resources: Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole + # NLB: enforces IngressCidr at the load balancer. Tightening IngressCidr is + # safe here — NLB health checks reach the tasks via the NLB security group + # reference below, not via this CIDR rule. + BridgeNlbSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Bridge NLB security group + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !Ref IngressCidr + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: !Sub ${StackNamePrefix}-nlb-sg + # ECS task: only accepts traffic (forwarded client traffic AND NLB health + # checks) from the NLB security group on port 8080. BridgeTaskSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: @@ -107,7 +143,7 @@ Resources: - IpProtocol: tcp FromPort: 8080 ToPort: 8080 - CidrIp: !Ref IngressCidr + SourceSecurityGroupId: !Ref BridgeNlbSecurityGroup SecurityGroupEgress: - IpProtocol: -1 CidrIp: 0.0.0.0/0 @@ -173,6 +209,8 @@ Resources: Scheme: internet-facing Type: network Subnets: !Ref PublicSubnetIds + SecurityGroups: + - !Ref BridgeNlbSecurityGroup BridgeApiTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: @@ -224,8 +262,11 @@ Resources: Environment: - Name: BRITIVE_BROKER_TENANT_SUBDOMAIN Value: !Ref BrokerTenantSubdomain + Secrets: + # Injected by the ECS agent at task start; kept out of the task + # definition so ecs:DescribeTaskDefinition does not expose it. - Name: BRITIVE_BROKER_AUTH_TOKEN - Value: !Ref BrokerAuthToken + ValueFrom: !Ref BrokerAuthTokenSecret MountPoints: - SourceVolume: bridge-data ContainerPath: /data diff --git a/Britive Bridge/custom-image/README.md b/Britive Bridge/custom-image/README.md index 4cd1de6..25fa35d 100644 --- a/Britive Bridge/custom-image/README.md +++ b/Britive Bridge/custom-image/README.md @@ -132,9 +132,10 @@ kubectl -n britive-bridge create secret generic bridge-ssh-key \ # 2. IRSA service account (Aurora MySQL example) — edit the role ARN first kubectl apply -f k8s-overlays/serviceaccount-irsa.yaml -# 3. Patch the Deployment: your image + SA + SSH mount +# 3. Patch the Deployment: your image + SA + SSH mount (strategic merge — +# do not use --type merge, it wipes list fields from the base Deployment) kubectl -n britive-bridge patch deployment britive-bridge \ - --type merge --patch-file k8s-overlays/deployment-patch.yaml + --type strategic --patch-file k8s-overlays/deployment-patch.yaml ``` Edit the placeholders first (`image:` → your build, diff --git a/Britive Bridge/custom-image/k8s-overlays/README.md b/Britive Bridge/custom-image/k8s-overlays/README.md index ddc80bd..58cb95f 100644 --- a/Britive Bridge/custom-image/k8s-overlays/README.md +++ b/Britive Bridge/custom-image/k8s-overlays/README.md @@ -6,7 +6,7 @@ Ready-to-apply manifests that wire the [custom image](../) into the base | File | Purpose | |------|---------| | `serviceaccount-irsa.yaml` | IRSA service account → AWS Secrets Manager (Aurora MySQL) | -| `deployment-patch.yaml` | Strategic-merge patch: custom image + SA + SSH-key mount | +| `deployment-patch.yaml` | Strategic-merge patch: custom image + SA + SSH key (init container chowns it to `bridge`, mode 0600, in-memory volume) | ## Apply order @@ -22,9 +22,12 @@ kubectl -n britive-bridge create secret generic bridge-ssh-key \ # Skip if you used `eksctl create iamserviceaccount` (it makes this SA for you). kubectl apply -f serviceaccount-irsa.yaml -# 3. Patch the running Deployment: set your image, SA, and SSH mount +# 3. Patch the running Deployment: set your image, SA, and SSH mount. +# Must be --type strategic: a plain merge patch would replace the +# containers/volumes lists and wipe the base ports, envFrom, probes, and +# PVC mount. kubectl -n britive-bridge patch deployment britive-bridge \ - --type merge --patch-file deployment-patch.yaml + --type strategic --patch-file deployment-patch.yaml ``` ## Before applying — edit these placeholders diff --git a/Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml b/Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml index 89447b8..f4cc504 100644 --- a/Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml +++ b/Britive Bridge/custom-image/k8s-overlays/deployment-patch.yaml @@ -4,19 +4,32 @@ # Applies three changes for the custom image + the two examples: # 1. Swap the image to your custom build. # 2. Bind the IRSA service account (Aurora MySQL → Secrets Manager). -# 3. Mount the broker's SSH provisioning key (Linux SSH example) at -# /home/bridge/.ssh from a Secret, read-only, mode 0600. +# 3. Deliver the broker's SSH provisioning key (Linux SSH example) to +# /home/bridge/.ssh/id_ed25519, owned by the `bridge` user, mode 0600. # -# Lists merge by key: containers by `name`, volumes/volumeMounts by `name`, so -# only these fields are touched — everything else in the base Deployment stays. +# Why the init container instead of mounting the Secret at /home/bridge/.ssh: +# - Secret volume files are root-owned; with mode 0600 the non-root `bridge` +# user (the image's runtime user) cannot read the key, and group-readable +# modes trip ssh's strict permission check. +# - Mounting a Secret over /home/bridge/.ssh shadows the directory read-only, +# so ssh cannot write known_hosts. +# The init container copies the key from the Secret into an in-memory +# emptyDir, chowns it to bridge, and the main container mounts that emptyDir +# at /home/bridge/.ssh (writable → known_hosts works). +# +# Lists merge by key: containers/initContainers by `name`, volumes/volumeMounts +# by `name`, so only these fields are touched — everything else in the base +# Deployment stays. # # Create the SSH key Secret first (private key the broker uses to reach targets): # kubectl -n britive-bridge create secret generic bridge-ssh-key \ # --from-file=id_ed25519=./bridge_ed25519 # -# Apply: +# Apply (must be a STRATEGIC merge — `--type merge` would replace the +# containers/volumes lists wholesale and wipe the base ports, envFrom, +# probes, and PVC mount): # kubectl -n britive-bridge patch deployment britive-bridge \ -# --type merge --patch-file deployment-patch.yaml +# --type strategic --patch-file deployment-patch.yaml # # Or fold these fields directly into kubernetes/manifests/deployment.yaml. apiVersion: apps/v1 @@ -29,19 +42,38 @@ spec: spec: # IRSA: pod assumes the IAM role bound to this SA (see serviceaccount-irsa.yaml) serviceAccountName: britive-bridge + initContainers: + # Same image as the main container, so the `bridge` user exists. + - name: ssh-key-init + image: docker.io/yourorg/britive-bridge-custom:latest + command: + - sh + - -c + - cp /ssh-secret/id_ed25519 /ssh/id_ed25519 && + chown bridge:bridge /ssh/id_ed25519 && + chmod 600 /ssh/id_ed25519 + securityContext: + runAsUser: 0 # root only to chown; main container runs as `bridge` + volumeMounts: + - name: ssh-key-secret + mountPath: /ssh-secret + readOnly: true + - name: ssh-dir + mountPath: /ssh containers: - name: bridge # Replace with YOUR pushed custom image (Docker Hub or ECR). image: docker.io/yourorg/britive-bridge-custom:latest volumeMounts: - - name: ssh-key + - name: ssh-dir mountPath: /home/bridge/.ssh - readOnly: true volumes: - - name: ssh-key + - name: ssh-key-secret secret: secretName: bridge-ssh-key - defaultMode: 0600 # ssh refuses world/group-readable keys items: - key: id_ed25519 path: id_ed25519 + - name: ssh-dir + emptyDir: + medium: Memory # key never touches node disk diff --git a/Britive Bridge/kubernetes/README.md b/Britive Bridge/kubernetes/README.md index 9cfce48..1031f0a 100644 --- a/Britive Bridge/kubernetes/README.md +++ b/Britive Bridge/kubernetes/README.md @@ -20,7 +20,8 @@ Manifests in [`manifests/`](manifests/): | `service.yaml` | ClusterIP service (443 → container 8080) | | `ingress.yaml` | ingress-nginx Ingress with HTTPS backend + WebSocket support | | `external-secret.example.yaml` | *(optional)* Source broker creds from an external store via the External Secrets Operator | -| `ha-overlay.example.yaml` | *(optional)* Multi-replica HA: RWM PVC + rolling-update + anti-affinity | +| `pvc-rwm.example.yaml` | *(optional, HA)* ReadWriteMany PVC — use **instead of** `pvc.yaml` | +| `ha-deployment-patch.example.yaml` | *(optional, HA)* Patch: replicas + rolling-update + anti-affinity | Traffic path: `client → Ingress (TLS) → Service:443 → pod:8080 (HTTPS, self-signed)`. @@ -130,12 +131,20 @@ Manager / Vault / GCP / Azure. See `manifests/external-secret.example.yaml`. ### High availability (multiple replicas) Bridge persists session state to `/data`, so running `replicas > 1` requires a -**ReadWriteMany** volume shared across pods. See `manifests/ha-overlay.example.yaml`. +**ReadWriteMany** (RWX) volume shared across pods. Choose HA **at install +time** — PVC access modes are immutable, so switching later means deleting the +RWO PVC and losing `/data`. -1. Provision an RWM storage class (EFS CSI on AWS, Azure Files, Filestore on GCP). -2. Apply the RWM PVC from the overlay **instead of** `pvc.yaml`. -3. Bump `replicas` and switch the Deployment to `RollingUpdate` (the overlay - includes pod anti-affinity to spread replicas across nodes). +1. Provision an RWX storage class (EFS CSI on AWS, Azure Files, Filestore on GCP). +2. Apply `manifests/pvc-rwm.example.yaml` **instead of** `pvc.yaml` (step 3 of + the install), then apply the remaining manifests as usual. +3. Patch the Deployment for replicas + rolling updates (includes pod + anti-affinity to spread replicas across nodes): + + ```bash + kubectl -n britive-bridge patch deployment britive-bridge \ + --type strategic --patch-file manifests/ha-deployment-patch.example.yaml + ``` > Confirm with your Britive team that your Bridge version supports active-active > across replicas before relying on HA for production traffic. diff --git a/Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml b/Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml new file mode 100644 index 0000000..28084f6 --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml @@ -0,0 +1,39 @@ +# Strategic-merge patch over the base Deployment for HA: more replicas and +# rolling updates instead of Recreate. Only safe once /data is on a +# ReadWriteMany volume — see pvc-rwm.example.yaml for the full install order. +# +# This is a PATCH, not a full manifest — do not `kubectl apply` it. +# +# Preview the result first (optional): +# kubectl -n britive-bridge patch deployment britive-bridge \ +# --type strategic --patch-file ha-deployment-patch.example.yaml \ +# --dry-run=server -o yaml +# +# Apply: +# kubectl -n britive-bridge patch deployment britive-bridge \ +# --type strategic --patch-file ha-deployment-patch.example.yaml +# +# Or fold these fields into deployment.yaml directly. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: britive-bridge + namespace: britive-bridge +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + spec: + affinity: + podAntiAffinity: # spread replicas across nodes + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app.kubernetes.io/name: britive-bridge diff --git a/Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml b/Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml deleted file mode 100644 index fe39757..0000000 --- a/Britive Bridge/kubernetes/manifests/ha-overlay.example.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# Optional: high-availability overlay. Apply AFTER the base manifests to run -# multiple Bridge replicas. Requires a ReadWriteMany (RWM) volume so all pods -# can share /data — RWO will NOT schedule a second pod. -# -# RWM storage by platform: -# - AWS : EFS CSI driver (storageClassName: efs-sc) -# - Azure: Azure Files (storageClassName: azurefile-csi) -# - GCP : Filestore (storageClassName: filestore-csi / standard-rwx) -# -# Apply order: -# 1. kubectl apply -f namespace.yaml -# 2. create the bridge-broker secret (secret.example.yaml or ESO) -# 3. kubectl apply -f ha-overlay.example.yaml # RWM PVC instead of pvc.yaml -# 4. kubectl apply -f deployment.yaml service.yaml ingress.yaml -# 5. patch the Deployment for replicas + rolling updates (see below) ---- -# RWM PVC — replaces pvc.yaml for HA. Do not apply both. -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: bridge-data - namespace: britive-bridge -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 5Gi - storageClassName: efs-sc # set to your RWM storage class ---- -# Patch the base Deployment for HA: more replicas + rolling updates instead of -# Recreate (safe now that the volume is RWM). Apply with: -# kubectl -n britive-bridge patch deployment britive-bridge \ -# --type merge --patch-file ha-overlay.example.yaml --dry-run=client -# or fold these fields into deployment.yaml directly. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: britive-bridge - namespace: britive-bridge -spec: - replicas: 2 - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 0 - maxSurge: 1 - template: - spec: - affinity: - podAntiAffinity: # spread replicas across nodes - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - topologyKey: kubernetes.io/hostname - labelSelector: - matchLabels: - app.kubernetes.io/name: britive-bridge diff --git a/Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml b/Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml new file mode 100644 index 0000000..3914dac --- /dev/null +++ b/Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml @@ -0,0 +1,29 @@ +# Optional: high-availability storage. Use INSTEAD of pvc.yaml — choose HA at +# install time. PVC accessModes are immutable: if the RWO pvc.yaml is already +# applied, you must delete that PVC first (DATA LOSS — back up /data) before +# applying this one. +# +# Requires a ReadWriteMany (RWX) storage class so all replicas share /data: +# - AWS : EFS CSI driver (storageClassName: efs-sc) +# - Azure: Azure Files (storageClassName: azurefile-csi) +# - GCP : Filestore (storageClassName: filestore-csi / standard-rwx) +# +# HA install order: +# 1. kubectl apply -f namespace.yaml +# 2. create the bridge-broker secret (secret.example.yaml or ESO) +# 3. kubectl apply -f pvc-rwm.example.yaml # instead of pvc.yaml +# 4. kubectl apply -f deployment.yaml -f service.yaml -f ingress.yaml +# 5. patch the Deployment for replicas + rolling updates +# (see ha-deployment-patch.example.yaml) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: bridge-data + namespace: britive-bridge +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: efs-sc # set to your RWX storage class diff --git a/Britive Bridge/platform-setup/README.md b/Britive Bridge/platform-setup/README.md index 312f57f..31c691f 100644 --- a/Britive Bridge/platform-setup/README.md +++ b/Britive Bridge/platform-setup/README.md @@ -22,7 +22,7 @@ BRITIVE_BROKER_AUTH_TOKEN= ## Prerequisites -- Python 3.8+ +- Python 3.10+ (required by the `britive` SDK) - An API token for your Britive tenant with permission to manage the Access Broker (pools, resources, response templates, profiles) - The Britive SDK + `requests`: @@ -54,8 +54,13 @@ You'll be prompted for: grab the DNS name, then re-run or update the resource. 4. **Resource name** — defaults to `Admin`. -The script is **idempotent on names**: if a pool / resource type already exists -it reuses it rather than creating a duplicate. +The script is **idempotent on names**: if a pool, resource type, response +template, permission, resource, or profile with the expected name already +exists, it is reused rather than duplicated. Re-running after infrastructure +deploy also **updates the existing resource's URL** to the one you enter — the +intended flow for replacing a placeholder URL with the real DNS name. Two +caveats: an existing pool's token is never reprinted (get it from the Britive +UI), and an existing permission's scripts/variables are left untouched. ## After it finishes diff --git a/Britive Bridge/platform-setup/quick-setup.py b/Britive Bridge/platform-setup/quick-setup.py index 1a7c797..3e54687 100644 --- a/Britive Bridge/platform-setup/quick-setup.py +++ b/Britive Bridge/platform-setup/quick-setup.py @@ -49,7 +49,7 @@ def prompt(label, default=None, secret=False, required=True): """Prompt the user for input with an optional default.""" - suffix = f" [{default[:16] + '...' if secret else default}]" if default else "" + suffix = f" [{'****' if secret else default}]" if default else "" while True: value = getpass.getpass(f" {label}{suffix}: ").strip() if secret else input(f" {label}{suffix}: ").strip() if not value and default: @@ -304,7 +304,13 @@ def collect_bridge_url(tenant_subdomain, auth_token): "It must be reachable from their browser.", ) - return prompt("Bridge URL", default="https://localhost:8080").split("://", maxsplit=1) + # The scheme is stripped here because the resource's `url` param is stored + # scheme-less; the response template re-adds it as `://{{url}}`. + while True: + url = prompt("Bridge URL", default="https://localhost:8080") + if "://" in url: + return url.split("://", maxsplit=1) + print(" (must include a scheme, e.g. https://bridge.example.com)") def create_bridge_resource_type(client): @@ -358,6 +364,21 @@ def create_admin_permission(client, bridge_type_id, template_id): perms = client.access_broker.resources.permissions + # Re-run safety: reuse an existing 'admin' permission for this resource type. + try: + for existing in perms.list(bridge_type_id) or []: + if existing.get("name") == ADMIN_PROFILE["name"]: + perm_id = existing.get("permissionId") + success(f"Permission 'admin' already exists: {perm_id}") + info("Scripts/variables left as-is — edit them in the Britive UI if needed.") + return { + "id": perm_id, + "version": existing.get("version", ""), + "resource_type_id": bridge_type_id, + } + except Exception: + pass + # Step 1: Create permission as draft (SDK) info("Creating draft permission...") try: @@ -401,15 +422,17 @@ def create_admin_permission(client, bridge_type_id, template_id): checkin_tmp.close() with open(checkout_tmp.name, "rb") as f: - requests.put(urls["checkoutURL"], data=f, timeout=30) + requests.put(urls["checkoutURL"], data=f, timeout=30).raise_for_status() success("Checkout script uploaded") info("Uploading checkin script...") with open(checkin_tmp.name, "rb") as f: - requests.put(urls["checkinURL"], data=f, timeout=30) + requests.put(urls["checkinURL"], data=f, timeout=30).raise_for_status() success("Checkin script uploaded") except Exception as exc: warn(f"Failed to upload scripts: {exc}") + warn("Permission left as a draft — re-run this script or upload scripts in the UI.") + return {"id": perm_id, "version": "", "resource_type_id": bridge_type_id} finally: os.unlink(checkout_tmp.name) os.unlink(checkin_tmp.name) @@ -456,19 +479,50 @@ def create_bridge_resource(client, bridge_type_id, bridge_url, pool_id): header(6, "Create Bridge Resource") resource_name = prompt("Resource name", default="Admin") - info(f"Creating resource '{resource_name}'...") + # Re-run safety: reuse an existing resource and refresh its URL — this is + # the documented flow for deploying infra first and re-running with the + # real DNS name. + resource_id = None try: - resource = client.access_broker.resources.create( - name=resource_name, - resource_type_id=bridge_type_id, - description=f"Bridge deployment at {bridge_url}", - param_values={"protocol": "admin", "url": bridge_url}, - ) - resource_id = resource.get("resourceId") - success(f"Resource created: {resource_id}") - except Exception as exc: - error(f"Failed to create resource: {exc}") - sys.exit(1) + result = client.access_broker.resources.list(search_text=resource_name) + items = result.get("data", result) if isinstance(result, dict) else result + for res in items or []: + if res.get("name") == resource_name: + resource_id = res.get("resourceId") + break + except Exception: + pass + + if resource_id: + success(f"Resource '{resource_name}' already exists: {resource_id}") + info(f"Updating its URL parameter to '{bridge_url}'...") + try: + client.put( + f"{client.base_url}/resource-manager/resources/{resource_id}", + json={ + "name": resource_name, + "resourceType": {"id": bridge_type_id}, + "description": f"Bridge deployment at {bridge_url}", + "paramValues": {"protocol": "admin", "url": bridge_url}, + }, + ) + success("Resource URL updated") + except Exception as exc: + warn(f"Could not update the resource URL ({exc}) — update it in the Britive UI.") + else: + info(f"Creating resource '{resource_name}'...") + try: + resource = client.access_broker.resources.create( + name=resource_name, + resource_type_id=bridge_type_id, + description=f"Bridge deployment at {bridge_url}", + param_values={"protocol": "admin", "url": bridge_url}, + ) + resource_id = resource.get("resourceId") + success(f"Resource created: {resource_id}") + except Exception as exc: + error(f"Failed to create resource: {exc}") + sys.exit(1) info("Associating resource with broker pool...") for attempt in range(5): @@ -487,6 +541,17 @@ def create_bridge_resource(client, bridge_type_id, bridge_url, pool_id): def create_response_template(client, schema): header(7, "Create Response Template") + + # Re-run safety: reuse an existing template instead of duplicating it. + try: + for tmpl in client.access_broker.response_templates.list(search_text="Bridge") or []: + if tmpl.get("name") == "Bridge": + template_id = tmpl.get("templateId") + success(f"Response template 'Bridge' already exists: {template_id}") + return template_id + except Exception: + pass + info("Creating response template for clickable session URLs...") try: template = client.access_broker.response_templates.create( @@ -507,6 +572,17 @@ def create_admin_profile(client, admin_permission): header(8, "Create Admin Profile") profile_name = f"Bridge {ADMIN_PROFILE['display']}" + # Re-run safety: do not duplicate the profile. + try: + for prof in client.access_broker.profiles.list() or []: + if prof.get("name") == profile_name: + profile_id = prof.get("profileId") or prof.get("id") + success(f"Profile '{profile_name}' already exists: {profile_id}") + info("Association/permission left as-is — manage them in the Britive UI.") + return {"name": profile_name, "id": profile_id, "protocol": ADMIN_PROFILE["name"]} + except Exception: + pass + info(f"Creating profile '{profile_name}'...") try: profile = client.access_broker.profiles.create( @@ -514,7 +590,7 @@ def create_admin_profile(client, admin_permission): description=ADMIN_PROFILE["description"], expiration_duration=3600000, ) - profile_id = profile.get("profileId") or profile.get("profileId") + profile_id = profile.get("profileId") or profile.get("id") success(f" Profile created: {profile_id}") except Exception as exc: warn(f" Failed to create profile: {exc}") diff --git a/Britive Bridge/windows-vm-docker/README.md b/Britive Bridge/windows-vm-docker/README.md index ef23716..a163046 100644 --- a/Britive Bridge/windows-vm-docker/README.md +++ b/Britive Bridge/windows-vm-docker/README.md @@ -15,8 +15,8 @@ using Docker, pulling the image from **Docker Hub** (`britive/bridge`). | Your VM | Recommended runtime | |---------|---------------------| | Windows 10/11 Pro/Enterprise | **Docker Desktop** with WSL 2 backend | -| Windows Server 2022 / 2025 | **Docker Desktop** (supported on Server 2022+) with WSL 2, **or** Docker CE inside a WSL 2 distro | -| Windows Server 2019 | No WSL 2 GA → run Docker CE in a Linux VM/WSL 1 is not supported. **Prefer a Linux VM** ([Linux guide](../linux-vm-docker/)) or upgrade to Server 2022+ | +| Windows Server 2022 / 2025 | **Docker CE inside a WSL 2 distro** — follow the [Linux guide](../linux-vm-docker/) from that distro's shell. Docker does **not support Docker Desktop on any Windows Server edition** | +| Windows Server 2019 | WSL 2 is not generally available, and WSL 1 cannot run Docker. **Use a Linux VM** ([Linux guide](../linux-vm-docker/)) or upgrade to Server 2022+ | If you end up running Docker **inside a WSL 2 Linux distro**, follow the [Linux VM guide](../linux-vm-docker/) from that distro's shell instead — it's @@ -33,8 +33,13 @@ license) per the Linux guide. - Windows 10/11 (Pro/Enterprise) or Windows Server 2022+, 64-bit, with **virtualization enabled** in BIOS/hypervisor (required for WSL 2). - **WSL 2** installed: in an elevated PowerShell run `wsl --install` then reboot. -- **Docker Desktop** installed and set to **Linux containers** (tray icon → - *Switch to Linux containers…* if currently on Windows containers). +- A Linux-container runtime: + - Windows 10/11: **Docker Desktop** set to **Linux containers** (tray icon → + *Switch to Linux containers…* if currently on Windows containers). + - Windows Server 2022+: **Docker CE inside your WSL 2 distro** (Docker + Desktop is not supported on Windows Server) — in that case follow the + [Linux guide](../linux-vm-docker/) from the distro's shell instead of this + script. - **Outbound** internet to your Britive tenant (Broker/MQTT) and Docker Hub. - **Inbound** TCP on the Bridge port (default `8080`) — open the Windows Firewall **and** any cloud security group / network ACL. diff --git a/Britive Bridge/windows-vm-docker/install.ps1 b/Britive Bridge/windows-vm-docker/install.ps1 index a7d1c50..8e41c66 100644 --- a/Britive Bridge/windows-vm-docker/install.ps1 +++ b/Britive Bridge/windows-vm-docker/install.ps1 @@ -57,7 +57,9 @@ if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administra if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { Die "Docker not found. Install Docker Desktop (WSL 2 backend) first — see README." } -try { docker info *> $null } catch { +# Native commands don't throw on failure in PowerShell — check the exit code. +docker info *> $null +if ($LASTEXITCODE -ne 0) { Die "Docker daemon not reachable. Start Docker Desktop and wait for it to be running." } @@ -93,8 +95,11 @@ Write-Warn "Also open inbound TCP $Port in any CLOUD security group / network AC # ── 6. Pull the image and (re)create the container ────────────────────────── Write-Step "Pulling $Image ..." docker pull $Image +if ($LASTEXITCODE -ne 0) { + Die "docker pull failed. Check internet access to Docker Hub and the image name '$Image'." +} -$exists = docker ps -a --format '{{.Names}}' | Select-String -SimpleMatch $ContainerName +$exists = docker ps -a --format '{{.Names}}' | Where-Object { $_ -eq $ContainerName } if ($exists) { Write-Step "Removing existing container '$ContainerName'..." docker rm -f $ContainerName | Out-Null @@ -111,6 +116,9 @@ docker run -d ` --env-file $EnvFile ` -v "$($DataVolume):/data" ` $Image | Out-Null +if ($LASTEXITCODE -ne 0) { + Die "docker run failed. Check the output above and your $EnvFile contents." +} # ── 7. Health check ───────────────────────────────────────────────────────── Write-Step "Waiting for Bridge to report healthy..." From 2f5aeeed9ebefd5bf4385fe38573f5b883f36bea Mon Sep 17 00:00:00 2001 From: palakchheda <42711310+palakchheda@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:38:22 -0700 Subject: [PATCH 4/5] fix(bridge): resolve review findings, improve guide flow --- Britive Bridge/README.md | 3 ++ .../aws-ecs-fargate-alb-ssh/README.md | 13 ++++- .../ecs-fargate-alb-ssh.yaml | 47 ++++++++++------ .../params.example.json | 1 + Britive Bridge/aws-ecs-fargate-alb/README.md | 5 +- .../aws-ecs-fargate-alb/ecs-fargate-alb.yaml | 54 +++++++++++++++---- .../aws-ecs-fargate-alb/params.example.json | 3 +- Britive Bridge/aws-ecs-fargate-nlb/README.md | 6 ++- .../aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml | 21 +++++++- Britive Bridge/custom-image/Dockerfile | 3 ++ Britive Bridge/custom-image/README.md | 13 +++-- .../custom-image/k8s-overlays/README.md | 4 +- Britive Bridge/docker-compose/.env.example | 14 +++++ Britive Bridge/docker-compose/README.md | 22 ++++++-- .../docker-compose/docker-compose.yaml | 10 ++-- Britive Bridge/kubernetes/README.md | 15 ++++-- .../kubernetes/manifests/deployment.yaml | 5 +- .../manifests/external-secret.example.yaml | 22 +++++--- Britive Bridge/linux-vm-docker/README.md | 12 +++-- Britive Bridge/linux-vm-docker/install.sh | 23 ++++---- Britive Bridge/platform-setup/README.md | 15 ++++-- Britive Bridge/platform-setup/quick-setup.py | 46 +++++++++------- Britive Bridge/windows-vm-docker/README.md | 14 ++++- .../windows-vm-docker/bridge.env.example | 2 + Britive Bridge/windows-vm-docker/install.ps1 | 20 ++++--- 25 files changed, 291 insertions(+), 102 deletions(-) create mode 100644 Britive Bridge/docker-compose/.env.example diff --git a/Britive Bridge/README.md b/Britive Bridge/README.md index 0ffffcd..14cbff1 100644 --- a/Britive Bridge/README.md +++ b/Britive Bridge/README.md @@ -43,6 +43,9 @@ pool, a token, a Bridge resource, and an admin profile. The that creates all of these for you and prints the env vars Bridge needs. Run it **first** — see [platform-setup/README.md](platform-setup/README.md). +You'll typically run it **twice**: once before deploying (with a placeholder +Bridge URL) and once after, to set the real URL — re-running updates the +existing resource in place. --- diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md b/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md index 624e510..a97b9b2 100644 --- a/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/README.md @@ -82,6 +82,9 @@ Confirm the broker can reach a target host by checking out an SSH session through Bridge, or inspect the running task with ECS Exec: ```bash +# find the task ID first: +aws ecs list-tasks --cluster --service-name + aws ecs execute-command --cluster --task \ --container bridge --interactive --command "/bin/sh" # inside: ls -l /home/bridge/.ssh/id_ed25519 (should be mode 600) @@ -93,7 +96,15 @@ aws ecs execute-command --cluster --task \ Secrets Manager; it is not written to CloudWatch. - Scope the target instances' `authorized_keys` to the minimum needed; prefer a dedicated, low-privilege broker user. -- Rotate the key periodically by updating the secret and restarting the service. +- Rotate the key periodically by updating the secret and restarting the service: + + ```bash + aws secretsmanager put-secret-value \ + --secret-id britive-bridge/broker/ssh-private-key \ + --secret-string "$(cat ~/.ssh/new_bridge_ed25519)" + aws ecs update-service --cluster --service \ + --force-new-deployment + ``` ## Teardown diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml index 6a9d6db..6cc8b37 100644 --- a/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml @@ -2,16 +2,19 @@ AWSTemplateFormatVersion: "2010-09-09" Description: > Britive Bridge ECS Fargate deployment. ALB terminates TLS with an ACM certificate on port 443. - Backend target group uses HTTPS on port 8080 with certificate - verification disabled (container uses its own self-signed cert). + Backend target group uses HTTPS on port 8080; ALBs do not validate + backend certificates, so the container's self-signed cert is accepted. SSH private key injected from Secrets Manager for broker → EC2 access. Parameters: StackNamePrefix: Type: String Default: britive-bridge - AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,30}" - Description: Prefix used for named AWS resources. + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,20}" + Description: > + Prefix used for named AWS resources. Max 21 characters so derived + load-balancer and target-group names stay within the 32-character + ELB limit. Must be unique per deployment in an account/region. VpcId: Type: AWS::EC2::VPC::Id Description: VPC where Bridge will run. @@ -82,6 +85,16 @@ Parameters: Description: > ARN of the ACM certificate for your domain (e.g. bridge.example.com or the wildcard *.example.com). Must be in the same region as the ALB. + DomainName: + Type: String + Default: "" + Description: > + DNS name users will reach Bridge on (e.g. bridge.example.com — the name + your ACM certificate covers). Used only for the BridgeUrl output; leave + empty to output the raw ALB DNS name instead. + +Conditions: + HasDomainName: !Not [!Equals [!Ref DomainName, ""]] Resources: # ── Logging ─────────────────────────────────────────────────────────────── @@ -161,12 +174,6 @@ Resources: - ssmmessages:OpenControlChannel - ssmmessages:OpenDataChannel Resource: "*" - - Effect: Allow - Action: - - logs:DescribeLogGroups - - logs:CreateLogStream - - logs:PutLogEvents - Resource: !GetAtt BridgeLogGroup.Arn # If your checkout/checkin scripts read additional Secrets Manager # secrets (e.g. database credentials), grant the task role # secretsmanager:GetSecretValue scoped to those specific secret ARNs. @@ -184,6 +191,11 @@ Resources: FromPort: 443 ToPort: 443 CidrIp: !Ref IngressCidr + # Port 80 only serves the HTTP → HTTPS redirect listener. + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: !Ref IngressCidr SecurityGroupEgress: - IpProtocol: -1 CidrIp: 0.0.0.0/0 @@ -280,8 +292,8 @@ Resources: - !Ref BridgeAlbSecurityGroup # Target group uses HTTPS so traffic to the container stays encrypted. - # TargetGroupAttributes disables cert verification so the container's - # self-signed cert is accepted without issue. + # ALBs never validate backend certificates, so the container's self-signed + # cert is accepted — no extra configuration needed for that. BridgeApiTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: @@ -290,7 +302,6 @@ Resources: Protocol: HTTPS # encrypted ALB → container Port: 8080 TargetType: ip - # Accept the container's self-signed cert — no CA verification TargetGroupAttributes: - Key: load_balancing.cross_zone.enabled Value: "true" @@ -430,8 +441,14 @@ Outputs: Value: !GetAtt BridgeLoadBalancer.DNSName Description: Point your Route 53 ALIAS record at this value. BridgeUrl: - Description: External URL for Britive BRIDGE_URL script parameter. - Value: !Sub https://${BridgeLoadBalancer.DNSName} + Description: >- + External URL for the Britive Bridge resource (BRIDGE_URL). If DomainName + was left empty this is the raw ALB DNS name, which does NOT match the + ACM certificate — point your DNS record at the ALB and use that name. + Value: !If + - HasDomainName + - !Sub https://${DomainName} + - !Sub https://${BridgeLoadBalancer.DNSName} DataFileSystemId: Value: !Ref BridgeFileSystem BrokerSSHKeySecretArn: diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json b/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json index 3657802..aa7bbd9 100644 --- a/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json @@ -11,5 +11,6 @@ { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" }, { "ParameterKey": "AcmCertificateArn", "ParameterValue": "arn:aws:acm:::certificate/" }, + { "ParameterKey": "DomainName", "ParameterValue": "bridge.example.com" }, { "ParameterKey": "BrokerSSHPrivateKey", "ParameterValue": "-----BEGIN OPENSSH PRIVATE KEY-----\n\n-----END OPENSSH PRIVATE KEY-----" } ] diff --git a/Britive Bridge/aws-ecs-fargate-alb/README.md b/Britive Bridge/aws-ecs-fargate-alb/README.md index 8110483..588b5ee 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/README.md +++ b/Britive Bridge/aws-ecs-fargate-alb/README.md @@ -42,6 +42,7 @@ Edit `params.json` — same parameters as the NLB option, **plus**: | Parameter | Notes | |-----------|-------| | `AcmCertificateArn` | ARN of your ACM cert, same region as the ALB: `arn:aws:acm:::certificate/` | +| `DomainName` | DNS name users will use (e.g. `bridge.example.com`) — makes the `BridgeUrl` output ready to paste. Optional. | > Keep `params.json` out of source control (broker token + cert ARN). @@ -65,7 +66,9 @@ aws cloudformation describe-stacks --stack-name britive-bridge \ 1. Take `LoadBalancerDnsName` from the outputs and create a **DNS ALIAS/CNAME** record for your domain (e.g. `bridge.example.com`) pointing at it. 2. Use `https://bridge.example.com` as your `BRIDGE_URL` and make sure the - Britive Bridge **resource** uses that URL. + Britive Bridge **resource** uses that URL — re-run + `platform-setup/quick-setup.py` with it (updates the resource in place), + or edit the resource in the Britive UI. 3. Verify: ```bash diff --git a/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml index 9735252..01a2328 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml +++ b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml @@ -2,15 +2,18 @@ AWSTemplateFormatVersion: "2010-09-09" Description: > Britive Bridge ECS Fargate deployment. ALB terminates TLS with an ACM certificate on port 443. - Backend target group uses HTTPS on port 8080 with certificate - verification disabled (container uses its own self-signed cert). + Backend target group uses HTTPS on port 8080; ALBs do not validate + backend certificates, so the container's self-signed cert is accepted. Parameters: StackNamePrefix: Type: String Default: britive-bridge - AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,30}" - Description: Prefix used for named AWS resources. + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,20}" + Description: > + Prefix used for named AWS resources. Max 21 characters so derived + load-balancer and target-group names stay within the 32-character + ELB limit. Must be unique per deployment in an account/region. VpcId: Type: AWS::EC2::VPC::Id Description: VPC where Bridge will run. @@ -74,6 +77,16 @@ Parameters: Description: > ARN of the ACM certificate for your domain (e.g. bridge.example.com or the wildcard *.example.com). Must be in the same region as the ALB. + DomainName: + Type: String + Default: "" + Description: > + DNS name users will reach Bridge on (e.g. bridge.example.com — the name + your ACM certificate covers). Used only for the BridgeUrl output; leave + empty to output the raw ALB DNS name instead. + +Conditions: + HasDomainName: !Not [!Equals [!Ref DomainName, ""]] Resources: # ── Logging ─────────────────────────────────────────────────────────────── @@ -132,6 +145,19 @@ Resources: Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole + Policies: + # Required for ECS Exec (EnableExecuteCommand on the service). + - PolicyName: ECSExecSSM + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - ssmmessages:CreateControlChannel + - ssmmessages:CreateDataChannel + - ssmmessages:OpenControlChannel + - ssmmessages:OpenDataChannel + Resource: "*" # ── Security Groups ─────────────────────────────────────────────────────── @@ -146,6 +172,11 @@ Resources: FromPort: 443 ToPort: 443 CidrIp: !Ref IngressCidr + # Port 80 only serves the HTTP → HTTPS redirect listener. + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: !Ref IngressCidr SecurityGroupEgress: - IpProtocol: -1 CidrIp: 0.0.0.0/0 @@ -242,8 +273,8 @@ Resources: - !Ref BridgeAlbSecurityGroup # Target group uses HTTPS so traffic to the container stays encrypted. - # TargetGroupAttributes disables cert verification so the container's - # self-signed cert is accepted without issue. + # ALBs never validate backend certificates, so the container's self-signed + # cert is accepted — no extra configuration needed for that. BridgeApiTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: @@ -252,7 +283,6 @@ Resources: Protocol: HTTPS # encrypted ALB → container Port: 8080 TargetType: ip - # Accept the container's self-signed cert — no CA verification TargetGroupAttributes: - Key: load_balancing.cross_zone.enabled Value: "true" @@ -379,7 +409,13 @@ Outputs: Value: !GetAtt BridgeLoadBalancer.DNSName Description: Point your Route 53 ALIAS record at this value. BridgeUrl: - Description: External URL for Britive BRIDGE_URL script parameter. - Value: !Sub https://${BridgeLoadBalancer.DNSName} + Description: >- + External URL for the Britive Bridge resource (BRIDGE_URL). If DomainName + was left empty this is the raw ALB DNS name, which does NOT match the + ACM certificate — point your DNS record at the ALB and use that name. + Value: !If + - HasDomainName + - !Sub https://${DomainName} + - !Sub https://${BridgeLoadBalancer.DNSName} DataFileSystemId: Value: !Ref BridgeFileSystem diff --git a/Britive Bridge/aws-ecs-fargate-alb/params.example.json b/Britive Bridge/aws-ecs-fargate-alb/params.example.json index 9a34161..40dba67 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/params.example.json +++ b/Britive Bridge/aws-ecs-fargate-alb/params.example.json @@ -10,5 +10,6 @@ { "ParameterKey": "DesiredCount", "ParameterValue": "1" }, { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" }, - { "ParameterKey": "AcmCertificateArn", "ParameterValue": "arn:aws:acm:::certificate/" } + { "ParameterKey": "AcmCertificateArn", "ParameterValue": "arn:aws:acm:::certificate/" }, + { "ParameterKey": "DomainName", "ParameterValue": "bridge.example.com" } ] diff --git a/Britive Bridge/aws-ecs-fargate-nlb/README.md b/Britive Bridge/aws-ecs-fargate-nlb/README.md index 70ab984..2affae7 100644 --- a/Britive Bridge/aws-ecs-fargate-nlb/README.md +++ b/Britive Bridge/aws-ecs-fargate-nlb/README.md @@ -20,6 +20,8 @@ CloudFormation template `ecs-fargate-nlb.yaml` creates: only from the NLB security group — covers forwarded traffic and NLB health checks; EFS allows 2049 from the task) - **IAM** execution + task roles +- A **Secrets Manager secret** for the broker token (`/broker/auth-token`), + injected into the container at task start - A **CloudWatch** log group (30-day retention) - ECS Exec enabled for debugging @@ -88,7 +90,9 @@ Key outputs: - `LoadBalancerDnsName`, `ClusterName`, `ServiceName`, `DataFileSystemId` Make sure the Bridge **resource** in Britive (from platform setup) uses this -URL. Then verify: +URL — re-run `platform-setup/quick-setup.py` with the new URL (it updates the +resource in place), or edit the resource in the Britive UI. Then verify +(`` is the output from the table above): ```bash curl -sfk https:///api/health diff --git a/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml index cc3a96a..07078d5 100644 --- a/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml +++ b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml @@ -4,8 +4,11 @@ Parameters: StackNamePrefix: Type: String Default: britive-bridge - AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,30}" - Description: Prefix used for named AWS resources. + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-]{1,20}" + Description: > + Prefix used for named AWS resources. Max 21 characters so derived + load-balancer and target-group names stay within the 32-character + ELB limit. Must be unique per deployment in an account/region. VpcId: Type: AWS::EC2::VPC::Id Description: VPC where Bridge will run. @@ -113,6 +116,19 @@ Resources: Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole + Policies: + # Required for ECS Exec (EnableExecuteCommand on the service). + - PolicyName: ECSExecSSM + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - ssmmessages:CreateControlChannel + - ssmmessages:CreateDataChannel + - ssmmessages:OpenControlChannel + - ssmmessages:OpenDataChannel + Resource: "*" # NLB: enforces IngressCidr at the load balancer. Tightening IngressCidr is # safe here — NLB health checks reach the tasks via the NLB security group # reference below, not via this CIDR rule. @@ -281,6 +297,7 @@ Resources: DependsOn: - BridgeApiListener - BridgeMountTargetA + - BridgeMountTargetB Properties: ServiceName: !Sub ${StackNamePrefix}-service Cluster: !Ref BridgeCluster diff --git a/Britive Bridge/custom-image/Dockerfile b/Britive Bridge/custom-image/Dockerfile index cd41305..eb8fd3e 100644 --- a/Britive Bridge/custom-image/Dockerfile +++ b/Britive Bridge/custom-image/Dockerfile @@ -89,6 +89,9 @@ RUN set -eu; \ for c in ssh ssh-keygen base64 python3 jq aws mysql bash; do \ command -v "$c" >/dev/null 2>&1 || { echo "MISSING: $c" >&2; exit 1; }; \ done; \ + # The final USER directive below assumes the base image defines a 'bridge' + # user — fail the build here, not at container start, if it doesn't. + id bridge >/dev/null 2>&1 || { echo "MISSING: 'bridge' user in base image — adjust the USER directive" >&2; exit 1; }; \ echo "All required utilities present." # ── Optional: bake the example scripts in for reference / local testing ────── diff --git a/Britive Bridge/custom-image/README.md b/Britive Bridge/custom-image/README.md index 25fa35d..9d0e93d 100644 --- a/Britive Bridge/custom-image/README.md +++ b/Britive Bridge/custom-image/README.md @@ -69,7 +69,7 @@ both Fargate ARM64 and mixed Kubernetes nodes. ## 1. Build & push ```bash -# Docker Hub +# Docker Hub (run `docker login` first) REGISTRY=docker.io/yourorg ./build-and-push.sh # Amazon ECR (script auto-creates the repo and logs in) @@ -114,9 +114,10 @@ Secrets Manager to `/home/bridge/.ssh/id_ed25519` — use that template with you custom `ImageUri`. For the **MySQL example**, the broker calls AWS Secrets Manager and reaches -Aurora. Grant the ECS **task role** `secretsmanager:GetSecretValue` on the DB -secret, and make sure the task's security group can reach the Aurora endpoint -(3306). +Aurora. Grant the ECS **task role** (named `-task-role-` +by the templates) `secretsmanager:GetSecretValue` via an inline policy scoped +to the DB secret's ARN, and make sure the task's security group can reach the +Aurora endpoint (3306). ### Kubernetes @@ -125,6 +126,10 @@ the base [Kubernetes deployment](../kubernetes/) with your custom image, the SSH-key mount, and the IRSA service account. After the base manifests are up: ```bash +# 0. Generate the broker's provisioning keypair (once) and install the PUBLIC +# key in the provisioning user's authorized_keys on each target host: +# ssh-keygen -t ed25519 -f ./bridge_ed25519 -N '' + # 1. Broker SSH provisioning key (Linux SSH example) kubectl -n britive-bridge create secret generic bridge-ssh-key \ --from-file=id_ed25519=./bridge_ed25519 diff --git a/Britive Bridge/custom-image/k8s-overlays/README.md b/Britive Bridge/custom-image/k8s-overlays/README.md index 58cb95f..e7eb15c 100644 --- a/Britive Bridge/custom-image/k8s-overlays/README.md +++ b/Britive Bridge/custom-image/k8s-overlays/README.md @@ -14,7 +14,9 @@ Ready-to-apply manifests that wire the [custom image](../) into the base # 0. Base manifests already applied (namespace, broker secret, pvc, deployment, service, ingress) # from ../../kubernetes/manifests/ — see that README. -# 1. SSH provisioning key (Linux SSH example) — the broker's private key +# 1. SSH provisioning key (Linux SSH example) — the broker's private key. +# Generate it once with: ssh-keygen -t ed25519 -f ./bridge_ed25519 -N '' +# and install bridge_ed25519.pub on each target host's provisioning user. kubectl -n britive-bridge create secret generic bridge-ssh-key \ --from-file=id_ed25519=./bridge_ed25519 diff --git a/Britive Bridge/docker-compose/.env.example b/Britive Bridge/docker-compose/.env.example new file mode 100644 index 0000000..e34ba5f --- /dev/null +++ b/Britive Bridge/docker-compose/.env.example @@ -0,0 +1,14 @@ +# Britive Bridge docker-compose variables. +# Copy to ".env" (docker compose reads it automatically from this directory), +# fill in real values, keep it OUT of source control: +# cp .env.example .env +# +# NOTE: this file feeds compose *variable interpolation* (values are +# substituted into docker-compose.yaml) — it is not docker's --env-file. +# Plain KEY=VALUE lines, no quotes needed. + +# ── Required: broker connection (from platform-setup) ────────────────────── +# Tenant subdomain only — e.g. "acme" for https://acme.britive-app.com +BRITIVE_BROKER_TENANT_SUBDOMAIN= +# Broker pool token created during platform setup. Treat as a secret. +BRITIVE_BROKER_AUTH_TOKEN= diff --git a/Britive Bridge/docker-compose/README.md b/Britive Bridge/docker-compose/README.md index deb95a0..9bd398a 100644 --- a/Britive Bridge/docker-compose/README.md +++ b/Britive Bridge/docker-compose/README.md @@ -27,11 +27,11 @@ ideal for local trials, POCs, and single-VM deployments. export BRITIVE_BROKER_AUTH_TOKEN="" ``` - …or create a `.env` file next to `docker-compose.yaml`: + …or copy the template and fill it in (compose reads `.env` automatically): - ```dotenv - BRITIVE_BROKER_TENANT_SUBDOMAIN= - BRITIVE_BROKER_AUTH_TOKEN= + ```bash + cp .env.example .env + # then edit .env with your real values ``` > Do not commit `.env` — add it to `.gitignore`. @@ -88,11 +88,23 @@ URL you registered during platform setup (`BRIDGE_URL`). On a cloud VM, open the security group / firewall for `8080` (or front the host with your own reverse proxy / load balancer terminating TLS). +Once you know the final external URL, make the Bridge **resource** in Britive +use it — re-run `platform-setup/quick-setup.py` with that URL (updates the +resource in place), or edit the resource in the Britive UI — then verify: + +```bash +curl -sfk https:///api/health +``` + ## Lifecycle ```bash -docker compose pull # get a newer image (update the tag in the file first) +docker compose pull # get a newer image (on :latest this is all you need; + # on a pinned tag, update the tag in the file first) docker compose up -d # apply changes docker compose down # stop and remove the container (keeps the volume) docker compose down -v # also delete the data volume (destroys session state) ``` + +> **Production:** pin a specific image tag in `docker-compose.yaml` (see Docker +> Hub `britive/bridge` tags) instead of `latest`, so updates are deliberate. diff --git a/Britive Bridge/docker-compose/docker-compose.yaml b/Britive Bridge/docker-compose/docker-compose.yaml index f84207e..a114133 100644 --- a/Britive Bridge/docker-compose/docker-compose.yaml +++ b/Britive Bridge/docker-compose/docker-compose.yaml @@ -17,16 +17,14 @@ services: ports: - "8080:8080" # HTTPS API + WebSocket + browser protocols environment: - # BRIDGE_ADMIN_TOKEN: "${BRIDGE_ADMIN_TOKEN:-}" - # BRIDGE_JWT_SECRET: "${BRIDGE_JWT_SECRET:-}" - # Britive broker - connects to platform via MQTT BRITIVE_BROKER_TENANT_SUBDOMAIN: "${BRITIVE_BROKER_TENANT_SUBDOMAIN:-}" BRITIVE_BROKER_AUTH_TOKEN: "${BRITIVE_BROKER_AUTH_TOKEN:-}" - # Custom TLS certificate (optional - omit for auto-generated self-signed) - TLS_CERT_FILE: "${TLS_CERT_FILE:-}" - TLS_KEY_FILE: "${TLS_KEY_FILE:-}" + # Custom TLS certificate (optional). Keep commented for the default + # auto-generated self-signed cert — even an empty value counts as "set". + # TLS_CERT_FILE: "/custom-certs/cert.pem" + # TLS_KEY_FILE: "/custom-certs/key.pem" volumes: - bridge-data:/data # Uncomment to bind-mount your own TLS certificate: diff --git a/Britive Bridge/kubernetes/README.md b/Britive Bridge/kubernetes/README.md index 1031f0a..8f530b2 100644 --- a/Britive Bridge/kubernetes/README.md +++ b/Britive Bridge/kubernetes/README.md @@ -64,7 +64,8 @@ Traffic path: `client → Ingress (TLS) → Service:443 → pod:8080 (HTTPS, sel 4. **Ingress** — edit `manifests/ingress.yaml` first: - Replace `bridge.example.com` with your hostname (two places). - - Adjust `ingressClassName` / annotations for your controller. + - Adjust `ingressClassName` / annotations for your controller (see + "Controller-specific ingress hints" below). - For automatic certs, uncomment the `cert-manager.io/cluster-issuer` annotation and set your issuer. @@ -84,11 +85,19 @@ kubectl -n britive-bridge exec deploy/britive-bridge -- \ ``` Then point a DNS record for your host at the ingress controller's external -address, set `BRIDGE_URL` accordingly, and confirm the Bridge **resource** in -Britive uses that URL. +address and make the Bridge **resource** in Britive use that URL — re-run +`platform-setup/quick-setup.py` with it (updates the resource in place), or +edit the resource in the Britive UI. Finally verify from outside the cluster: + +```bash +curl -sf https://bridge.example.com/api/health +``` ## Key configuration notes +- **Image tag.** The manifest ships `britive/bridge:latest` with + `imagePullPolicy: Always` so rollouts pick up new images. For production, + pin a versioned tag and switch to `IfNotPresent`. - **HTTPS backend.** The container serves HTTPS with a self-signed cert on 8080. Probes and the Service/Ingress all use the HTTPS scheme; the ingress is configured with `backend-protocol: HTTPS` + `proxy-ssl-verify: off`. diff --git a/Britive Bridge/kubernetes/manifests/deployment.yaml b/Britive Bridge/kubernetes/manifests/deployment.yaml index 3527422..f830d4a 100644 --- a/Britive Bridge/kubernetes/manifests/deployment.yaml +++ b/Britive Bridge/kubernetes/manifests/deployment.yaml @@ -22,7 +22,10 @@ spec: containers: - name: bridge image: britive/bridge:latest - imagePullPolicy: IfNotPresent + # Always re-check the registry while on :latest, otherwise nodes run + # whatever they cached first. For production, pin a versioned tag + # and switch to IfNotPresent. + imagePullPolicy: Always ports: - name: https containerPort: 8080 # HTTPS API + WebSocket + browser protocols diff --git a/Britive Bridge/kubernetes/manifests/external-secret.example.yaml b/Britive Bridge/kubernetes/manifests/external-secret.example.yaml index 1970090..cd14844 100644 --- a/Britive Bridge/kubernetes/manifests/external-secret.example.yaml +++ b/Britive Bridge/kubernetes/manifests/external-secret.example.yaml @@ -6,18 +6,22 @@ # helm install external-secrets oci://registry-1.docker.io/externalsecrets/external-secrets \ # -n external-secrets --create-namespace # - A backend holding the values (AWS Secrets Manager shown; Vault/GCP/Azure -# work the same way with a different SecretStore provider). -# - The pod/IRSA/workload identity has read access to that backend secret. +# work the same way with a different provider block). +# - The ESO controller's service account has read access to that backend +# secret (IRSA on EKS, workload identity on GKE/AKS). +# +# A ClusterSecretStore is used (not a namespaced SecretStore) because the +# referenced service account lives in the `external-secrets` namespace — +# cross-namespace serviceAccountRef is only valid on ClusterSecretStore. # # ESO writes a Secret named `bridge-broker` with the two keys the Deployment's # envFrom expects — so deployment.yaml needs NO change. Apply this INSTEAD of # secret.example.yaml. --- -apiVersion: external-secrets.io/v1beta1 -kind: SecretStore +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore metadata: name: bridge-store - namespace: britive-bridge spec: provider: # AWS Secrets Manager example. Swap for vault/gcpsm/azurekv as needed. @@ -25,12 +29,14 @@ spec: service: SecretsManager region: auth: - # Preferred on EKS: IRSA on the ESO service account (no static keys). + # Preferred on EKS: IRSA on the ESO controller's service account + # (no static keys). jwt: serviceAccountRef: name: external-secrets # the ESO SA bound to an IAM role + namespace: external-secrets # ESO's install namespace --- -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: bridge-broker @@ -39,7 +45,7 @@ spec: refreshInterval: 1h secretStoreRef: name: bridge-store - kind: SecretStore + kind: ClusterSecretStore target: name: bridge-broker # the Secret the Deployment consumes creationPolicy: Owner diff --git a/Britive Bridge/linux-vm-docker/README.md b/Britive Bridge/linux-vm-docker/README.md index c05fa41..e0dc6aa 100644 --- a/Britive Bridge/linux-vm-docker/README.md +++ b/Britive Bridge/linux-vm-docker/README.md @@ -30,6 +30,10 @@ long-lived Linux server. ## Quick start (scripted) ```bash +# 0. Get these files onto the VM and cd into the directory, e.g.: +# git clone && cd "onboarding/Britive Bridge/linux-vm-docker" +# (or scp install.sh bridge.env.example user@vm:) + # 1. Provide credentials cp bridge.env.example bridge.env chmod 600 bridge.env @@ -54,6 +58,10 @@ Override defaults via env vars, e.g.: sudo IMAGE=britive/bridge:latest PORT=9443 CONTAINER_NAME=bridge ./install.sh ``` +> **Production:** pin a specific image tag (see Docker Hub `britive/bridge` +> tags) instead of `latest`, so reinstalls don't pull an unplanned version: +> `sudo IMAGE=britive/bridge: ./install.sh` + --- ## Manual steps (if you prefer to do it by hand) @@ -218,9 +226,7 @@ uses that URL. ```bash docker logs -f bridge # follow logs docker restart bridge # restart -docker pull britive/bridge:latest && \ - docker rm -f bridge && \ - ./... (re-run install.sh) # update to a newer image +sudo ./install.sh # update: re-pulls image, recreates container docker rm -f bridge # stop + remove (keeps data volume) docker volume rm bridge-data # delete persisted state (destructive) ``` diff --git a/Britive Bridge/linux-vm-docker/install.sh b/Britive Bridge/linux-vm-docker/install.sh index 93a8186..1588794 100755 --- a/Britive Bridge/linux-vm-docker/install.sh +++ b/Britive Bridge/linux-vm-docker/install.sh @@ -24,14 +24,17 @@ IMAGE="${IMAGE:-britive/bridge:latest}" # Docker Hub image CONTAINER_NAME="${CONTAINER_NAME:-bridge}" PORT="${PORT:-8080}" # host:container port for HTTPS DATA_VOLUME="${DATA_VOLUME:-bridge-data}" # named volume for /data persistence -ENV_FILE="${ENV_FILE:-bridge.env}" +# Default bridge.env to the script's own directory so the script works when +# invoked from anywhere (docker --env-file resolves relative to the cwd). +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ENV_FILE="${ENV_FILE:-${SCRIPT_DIR}/bridge.env}" log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33mWARN:\033[0m %s\n' "$*"; } die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } [ "$(id -u)" -eq 0 ] || die "Run as root (sudo ./install.sh)." -[ -f "$ENV_FILE" ] || die "Missing $ENV_FILE. Run: cp bridge.env.example bridge.env then edit it." +[ -f "$ENV_FILE" ] || die "Missing ${ENV_FILE}. Run: cp ${SCRIPT_DIR}/bridge.env.example ${ENV_FILE} then edit it." # ── 1. Detect the distro ──────────────────────────────────────────────────── # /etc/os-release is the portable source of truth across modern Linux. @@ -47,8 +50,9 @@ install_docker_debian() { apt-get install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings # $ID is "ubuntu" or "debian" — the repo path differs by distro. + # --yes: overwrite a keyring left by a previous partial run (re-run safety) curl -fsSL "https://download.docker.com/linux/${ID}/gpg" \ - | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" \ @@ -59,13 +63,12 @@ https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" \ install_docker_rhel() { # RHEL / Rocky / AlmaLinux / Fedora via dnf + docker-ce repo. - dnf install -y dnf-plugins-core - # Fedora has its own repo path; the rest use the centos repo. - if [ "$ID" = "fedora" ]; then - dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo - else - dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo - fi + # Write the repo file directly instead of `dnf config-manager --add-repo`: + # dnf5 (Fedora 41+) changed that command's syntax, this works on both. + local repo_distro="centos" + [ "$ID" = "fedora" ] && repo_distro="fedora" # Fedora has its own repo path + curl -fsSL "https://download.docker.com/linux/${repo_distro}/docker-ce.repo" \ + -o /etc/yum.repos.d/docker-ce.repo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin } diff --git a/Britive Bridge/platform-setup/README.md b/Britive Bridge/platform-setup/README.md index 31c691f..d4ced40 100644 --- a/Britive Bridge/platform-setup/README.md +++ b/Britive Bridge/platform-setup/README.md @@ -24,7 +24,9 @@ BRITIVE_BROKER_AUTH_TOKEN= - Python 3.10+ (required by the `britive` SDK) - An API token for your Britive tenant with permission to manage the Access - Broker (pools, resources, response templates, profiles) + Broker (pools, resources, response templates, profiles). Create one in the + Britive console under **your profile menu → My Settings → API Tokens** (or + ask a tenant admin for one with Access Broker admin rights). - The Britive SDK + `requests`: ```bash @@ -50,8 +52,10 @@ You'll be prompted for: 2. **API token** — entered hidden. Can also be supplied via `BRITIVE_API_TOKEN`. 3. **Bridge external URL** — the URL users will hit (e.g. `https://bridge.example.com`). This is the load balancer / ingress address - from your chosen deployment option, so you may want to deploy infra first, - grab the DNS name, then re-run or update the resource. + from your chosen deployment option. **First run:** if the infra doesn't + exist yet, enter a placeholder (e.g. `https://bridge.example.com`); after + deploying, re-run the script with the real URL — it updates the existing + resource in place. 4. **Resource name** — defaults to `Admin`. The script is **idempotent on names**: if a pool, resource type, response @@ -66,8 +70,9 @@ UI), and an existing permission's scripts/variables are left untouched. 1. Save the printed `BRITIVE_BROKER_TENANT_SUBDOMAIN` and `BRITIVE_BROKER_AUTH_TOKEN` — you'll plug them into your deployment. -2. Assign users or groups to the **admin profile** via Britive policies so they - can check out Bridge access. +2. Assign users or groups to the **admin profile** so they can check out + Bridge access: in the Britive console open **Resource Manager → Profiles → + Bridge Admin → Policies** and add a policy listing your users or tags. 3. Deploy Bridge using one of the options in the parent directory. ## Notes diff --git a/Britive Bridge/platform-setup/quick-setup.py b/Britive Bridge/platform-setup/quick-setup.py index 3e54687..a2c634d 100644 --- a/Britive Bridge/platform-setup/quick-setup.py +++ b/Britive Bridge/platform-setup/quick-setup.py @@ -108,7 +108,12 @@ def success(message): print(f" OK: {message}") +WARNING_COUNT = 0 + + def warn(message): + global WARNING_COUNT + WARNING_COUNT += 1 print(f" WARNING: {message}") @@ -256,8 +261,8 @@ def create_broker_pool_and_token(client): if p.get("name") == pool_name: pool_id = p.get("pool-id") break - except Exception: - pass + except Exception as exc: + warn(f"Could not check for an existing pool ({exc}); attempting to create one.") if pool_id: success(f"Broker pool '{pool_name}' already exists: {pool_id}") @@ -314,7 +319,7 @@ def collect_bridge_url(tenant_subdomain, auth_token): def create_bridge_resource_type(client): - header(4, "Create Bridge Resource Type") + header(5, "Create Bridge Resource Type") # Check if it already exists try: @@ -323,8 +328,8 @@ def create_bridge_resource_type(client): bridge_type_id = rt.get("resourceTypeId") success(f"Resource type 'Bridge' already exists: {bridge_type_id}") return bridge_type_id - except Exception: - pass + except Exception as exc: + warn(f"Could not check for an existing resource type ({exc}); attempting to create one.") info("Creating 'Bridge' resource type...") try: @@ -360,7 +365,7 @@ def create_bridge_resource_type(client): def create_admin_permission(client, bridge_type_id, template_id): - header(5, "Create Admin Permission") + header(6, "Create Admin Permission") perms = client.access_broker.resources.permissions @@ -376,8 +381,8 @@ def create_admin_permission(client, bridge_type_id, template_id): "version": existing.get("version", ""), "resource_type_id": bridge_type_id, } - except Exception: - pass + except Exception as exc: + warn(f"Could not check for an existing permission ({exc}); attempting to create one.") # Step 1: Create permission as draft (SDK) info("Creating draft permission...") @@ -476,7 +481,7 @@ def create_admin_permission(client, bridge_type_id, template_id): def create_bridge_resource(client, bridge_type_id, bridge_url, pool_id): - header(6, "Create Bridge Resource") + header(7, "Create Bridge Resource") resource_name = prompt("Resource name", default="Admin") # Re-run safety: reuse an existing resource and refresh its URL — this is @@ -490,8 +495,8 @@ def create_bridge_resource(client, bridge_type_id, bridge_url, pool_id): if res.get("name") == resource_name: resource_id = res.get("resourceId") break - except Exception: - pass + except Exception as exc: + warn(f"Could not check for an existing resource ({exc}); attempting to create one.") if resource_id: success(f"Resource '{resource_name}' already exists: {resource_id}") @@ -540,7 +545,7 @@ def create_bridge_resource(client, bridge_type_id, bridge_url, pool_id): def create_response_template(client, schema): - header(7, "Create Response Template") + header(4, "Create Response Template") # Re-run safety: reuse an existing template instead of duplicating it. try: @@ -549,8 +554,8 @@ def create_response_template(client, schema): template_id = tmpl.get("templateId") success(f"Response template 'Bridge' already exists: {template_id}") return template_id - except Exception: - pass + except Exception as exc: + warn(f"Could not check for an existing template ({exc}); attempting to create one.") info("Creating response template for clickable session URLs...") try: @@ -580,8 +585,8 @@ def create_admin_profile(client, admin_permission): success(f"Profile '{profile_name}' already exists: {profile_id}") info("Association/permission left as-is — manage them in the Britive UI.") return {"name": profile_name, "id": profile_id, "protocol": ADMIN_PROFILE["name"]} - except Exception: - pass + except Exception as exc: + warn(f"Could not check for an existing profile ({exc}); attempting to create one.") info(f"Creating profile '{profile_name}'...") try: @@ -622,7 +627,7 @@ def create_admin_profile(client, admin_permission): def print_summary(state): - header(9, "Setup Complete") + header(9, "Summary") print(" Created objects:") print(f" Broker pool: {state['pool_name']} ({state['pool_id']})") @@ -641,7 +646,12 @@ def print_summary(state): print(" 1. Assign users or groups to the admin profile via") print(" policies so they can check out Bridge access.") print() - paragraph("Done! Users can now check out Bridge admin access at", state["bridge_url"]) + if WARNING_COUNT: + print(f" Setup finished with {WARNING_COUNT} warning(s) — review the output") + print(" above and resolve them before users check out Bridge access.") + print() + else: + paragraph("Done! Users can now check out Bridge admin access at", state["bridge_url"]) # --------------------------------------------------------------------------- diff --git a/Britive Bridge/windows-vm-docker/README.md b/Britive Bridge/windows-vm-docker/README.md index a163046..809a955 100644 --- a/Britive Bridge/windows-vm-docker/README.md +++ b/Britive Bridge/windows-vm-docker/README.md @@ -43,8 +43,9 @@ license) per the Linux guide. - **Outbound** internet to your Britive tenant (Broker/MQTT) and Docker Hub. - **Inbound** TCP on the Bridge port (default `8080`) — open the Windows Firewall **and** any cloud security group / network ACL. -- PowerShell 5.1+ (built in). PowerShell 7+ recommended for the scripted health - check (`-SkipCertificateCheck`). +- PowerShell 5.1+ (built in). `install.ps1` works on both: on 7+ it probes + health with `Invoke-WebRequest -SkipCertificateCheck`, on 5.1 it falls back + to `docker exec ... curl` automatically. - Completed [platform setup](../platform-setup/) — you need `BRITIVE_BROKER_TENANT_SUBDOMAIN` and `BRITIVE_BROKER_AUTH_TOKEN`. @@ -76,6 +77,9 @@ docker info --format '{{.OSType}}' # must print: linux ## Quick start (scripted) ```powershell +# 0. Get install.ps1 + bridge.env.example onto the VM (git clone this repo or +# copy the two files) and run from that folder. + # 1. Provide credentials Copy-Item bridge.env.example bridge.env notepad bridge.env # set tenant subdomain + broker token, save @@ -85,6 +89,8 @@ notepad bridge.env # set tenant subdomain + broker token, save # 3. Verify (PowerShell 7+) Invoke-WebRequest https://127.0.0.1:8080/api/health -SkipCertificateCheck +# PowerShell 5.1 instead: +# docker exec bridge curl -sfk https://127.0.0.1:8080/api/health docker logs -f bridge ``` @@ -98,6 +104,10 @@ Override defaults: .\install.ps1 -Port 9443 -ContainerName bridge -Image britive/bridge:latest ``` +> **Production:** pin a specific image tag (see Docker Hub `britive/bridge` +> tags) instead of `latest`, so reinstalls don't pull an unplanned version: +> `.\install.ps1 -Image britive/bridge:` + > **Execution policy:** if the script is blocked, run it for this session only: > `powershell -ExecutionPolicy Bypass -File .\install.ps1` diff --git a/Britive Bridge/windows-vm-docker/bridge.env.example b/Britive Bridge/windows-vm-docker/bridge.env.example index dbfd5a2..3cc5e86 100644 --- a/Britive Bridge/windows-vm-docker/bridge.env.example +++ b/Britive Bridge/windows-vm-docker/bridge.env.example @@ -1,6 +1,8 @@ # Britive Bridge environment file (Windows VM). # Copy to "bridge.env", fill in real values, keep it OUT of source control. # Copy-Item bridge.env.example bridge.env +# It contains a secret — restrict access to admins: +# icacls bridge.env /inheritance:r /grant:r "Administrators:F" "SYSTEM:F" # # Loaded by: docker run --env-file bridge.env ... # NOTE: use plain KEY=VALUE lines, no quotes, no "set"/"$env:" prefixes. diff --git a/Britive Bridge/windows-vm-docker/install.ps1 b/Britive Bridge/windows-vm-docker/install.ps1 index 8e41c66..ca7c514 100644 --- a/Britive Bridge/windows-vm-docker/install.ps1 +++ b/Britive Bridge/windows-vm-docker/install.ps1 @@ -123,13 +123,21 @@ if ($LASTEXITCODE -ne 0) { # ── 7. Health check ───────────────────────────────────────────────────────── Write-Step "Waiting for Bridge to report healthy..." $healthy = $false +# -SkipCertificateCheck (accepts the self-signed cert) needs PS 6+. On +# Windows PowerShell 5.1, probe from inside the container with curl instead. +$useInvokeWebRequest = $PSVersionTable.PSVersion.Major -ge 6 for ($i = 0; $i -lt 20; $i++) { - try { - # -SkipCertificateCheck accepts the container's self-signed cert (PS 6+). - $resp = Invoke-WebRequest -Uri "https://127.0.0.1:$Port/api/health" ` - -SkipCertificateCheck -TimeoutSec 5 -UseBasicParsing - if ($resp.StatusCode -eq 200) { $healthy = $true; break } - } catch { Start-Sleep -Seconds 3 } + if ($useInvokeWebRequest) { + try { + $resp = Invoke-WebRequest -Uri "https://127.0.0.1:$Port/api/health" ` + -SkipCertificateCheck -TimeoutSec 5 -UseBasicParsing + if ($resp.StatusCode -eq 200) { $healthy = $true; break } + } catch { } + } else { + docker exec $ContainerName curl -sfk "https://127.0.0.1:8080/api/health" *> $null + if ($LASTEXITCODE -eq 0) { $healthy = $true; break } + } + Start-Sleep -Seconds 3 } if ($healthy) { Write-Step "Bridge is healthy at https://127.0.0.1:$Port/api/health" From b5c89d273a49e1f25aee3d86f1f14dcbc5d10ab3 Mon Sep 17 00:00:00 2001 From: palakchheda <42711310+palakchheda@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:06:31 -0700 Subject: [PATCH 5/5] NLB HTTPS health check, CIDR/Fargate-combo param validation, placeholder + BOM preflight checks in installers, health-check fallbacks and non-zero exit, dnf5-safe manual steps, Helm roadmap trimmed from customer docs, compose healthcheck start_period, firewall cleanup docs, dead code removal. --- Britive Bridge/README.md | 2 +- .../ecs-fargate-alb-ssh.yaml | 22 +++- .../params.example.json | 4 +- Britive Bridge/aws-ecs-fargate-alb/README.md | 9 +- .../aws-ecs-fargate-alb/ecs-fargate-alb.yaml | 18 ++- .../aws-ecs-fargate-alb/params.example.json | 4 +- .../aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml | 22 +++- .../aws-ecs-fargate-nlb/params.example.json | 4 +- Britive Bridge/custom-image/.dockerignore | 1 + Britive Bridge/custom-image/Dockerfile | 4 +- Britive Bridge/custom-image/README.md | 2 +- .../custom-image/k8s-overlays/README.md | 4 + Britive Bridge/docker-compose/README.md | 1 + .../docker-compose/docker-compose.yaml | 10 +- Britive Bridge/kubernetes/README.md | 103 ++---------------- .../kubernetes/manifests/deployment.yaml | 14 ++- .../ha-deployment-patch.example.yaml | 3 +- .../kubernetes/manifests/pvc-rwm.example.yaml | 6 +- Britive Bridge/linux-vm-docker/README.md | 9 +- Britive Bridge/linux-vm-docker/install.sh | 36 ++++-- Britive Bridge/platform-setup/quick-setup.py | 47 +++----- Britive Bridge/windows-vm-docker/README.md | 5 +- Britive Bridge/windows-vm-docker/install.ps1 | 19 +++- 23 files changed, 170 insertions(+), 179 deletions(-) diff --git a/Britive Bridge/README.md b/Britive Bridge/README.md index 14cbff1..9b1af33 100644 --- a/Britive Bridge/README.md +++ b/Britive Bridge/README.md @@ -141,7 +141,7 @@ Britive Bridge/ │ ├── external-secret.example.yaml # optional: External Secrets Operator │ ├── pvc-rwm.example.yaml # optional HA: RWX PVC (instead of pvc.yaml) │ └── ha-deployment-patch.example.yaml # optional HA: replicas + rolling update - └── README.md # includes the public Helm chart roadmap + └── README.md ``` --- diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml index 6cc8b37..b15aa9d 100644 --- a/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/ecs-fargate-alb-ssh.yaml @@ -20,10 +20,14 @@ Parameters: Description: VPC where Bridge will run. PublicSubnetIds: Type: List - Description: Two public subnets for the internet-facing ALB, Fargate task, and EFS mount targets. + Description: > + Exactly two public subnets in different AZs for the internet-facing ALB, + Fargate task, and EFS mount targets (the template creates mount targets + for the first two subnets only). IngressCidr: Type: String Default: 0.0.0.0/0 + AllowedPattern: '(\d{1,3}\.){3}\d{1,3}/(\d|[12]\d|3[0-2])' Description: CIDR allowed to reach Bridge HTTPS on the ALB. ImageUri: Type: String @@ -38,7 +42,7 @@ Parameters: Description: ECS task CPU architecture. TaskCpu: Type: String - Default: "1024" + Default: "2048" AllowedValues: - "512" - "1024" @@ -47,7 +51,7 @@ Parameters: Description: Fargate task CPU units. TaskMemory: Type: String - Default: "2048" + Default: "4096" AllowedValues: - "1024" - "2048" @@ -59,7 +63,10 @@ Parameters: - "8192" - "16384" - "30720" - Description: Fargate task memory in MiB. + Description: > + Fargate task memory in MiB. Must be a valid combination for TaskCpu + (512 CPU: 1024-4096; 1024 CPU: 2048-8192; 2048 CPU: 4096-16384; + 4096 CPU: 8192-30720). DesiredCount: Type: Number Default: 1 @@ -253,6 +260,8 @@ Resources: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref BridgeFileSystem + # The access point forces all NFS operations to uid/gid 0, so the + # container's non-root user can write /data without matching ownership. PosixUser: Uid: "0" Gid: "0" @@ -283,7 +292,6 @@ Resources: BridgeLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: - Name: !Sub ${StackNamePrefix}-alb Scheme: internet-facing Type: application # ALB, not NLB @@ -385,7 +393,9 @@ Resources: - "/bin/sh" - "-c" Command: - - "mkdir -p /home/bridge/.ssh && echo \"$SSH_PRIVATE_KEY\" > /home/bridge/.ssh/id_ed25519 && chmod 700 /home/bridge/.ssh && chmod 600 /home/bridge/.ssh/id_ed25519 && exec /entrypoint.sh" + # unset drops the key from the bridge process env (it lives only + # in the key file from here on). + - "mkdir -p /home/bridge/.ssh && echo \"$SSH_PRIVATE_KEY\" > /home/bridge/.ssh/id_ed25519 && chmod 700 /home/bridge/.ssh && chmod 600 /home/bridge/.ssh/id_ed25519 && unset SSH_PRIVATE_KEY && exec /entrypoint.sh" Environment: - Name: BRITIVE_BROKER_TENANT_SUBDOMAIN Value: !Ref BrokerTenantSubdomain diff --git a/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json b/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json index aa7bbd9..3aa89a9 100644 --- a/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json +++ b/Britive Bridge/aws-ecs-fargate-alb-ssh/params.example.json @@ -5,8 +5,8 @@ { "ParameterKey": "IngressCidr", "ParameterValue": "0.0.0.0/0" }, { "ParameterKey": "ImageUri", "ParameterValue": "britive/bridge:latest" }, { "ParameterKey": "CpuArchitecture", "ParameterValue": "ARM64" }, - { "ParameterKey": "TaskCpu", "ParameterValue": "1024" }, - { "ParameterKey": "TaskMemory", "ParameterValue": "2048" }, + { "ParameterKey": "TaskCpu", "ParameterValue": "2048" }, + { "ParameterKey": "TaskMemory", "ParameterValue": "4096" }, { "ParameterKey": "DesiredCount", "ParameterValue": "1" }, { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" }, diff --git a/Britive Bridge/aws-ecs-fargate-alb/README.md b/Britive Bridge/aws-ecs-fargate-alb/README.md index 588b5ee..d0481c7 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/README.md +++ b/Britive Bridge/aws-ecs-fargate-alb/README.md @@ -10,15 +10,16 @@ self-signed cert and the ALB target group is configured to **not verify** it. ## What it deploys -CloudFormation template `ecs-fargate-alb.yaml` creates everything the -[NLB option](../aws-ecs-fargate-nlb/) does, plus: +CloudFormation template `ecs-fargate-alb.yaml` creates the same core stack as +the [NLB option](../aws-ecs-fargate-nlb/) (ECS cluster/task/service, EFS, IAM, +CloudWatch logs, broker-token secret), with an **ALB instead of an NLB**: - An **ALB** with: - HTTPS:443 listener using your **ACM certificate** (TLS 1.2/1.3 policy) - HTTP:80 listener that **redirects to 443** - An HTTPS target group on 8080 with health checks on `/api/health` -- A dedicated **ALB security group** (443 from your `IngressCidr`); the task SG - only accepts 8080 **from the ALB SG** +- A dedicated **ALB security group** (443 + 80 from your `IngressCidr`); the + task SG only accepts 8080 **from the ALB SG** Traffic path: `client → ALB:443 (ACM TLS) → task:8080 (HTTPS, cert not verified)`. diff --git a/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml index 01a2328..26e7066 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml +++ b/Britive Bridge/aws-ecs-fargate-alb/ecs-fargate-alb.yaml @@ -19,10 +19,14 @@ Parameters: Description: VPC where Bridge will run. PublicSubnetIds: Type: List - Description: Two public subnets for the internet-facing ALB, Fargate task, and EFS mount targets. + Description: > + Exactly two public subnets in different AZs for the internet-facing ALB, + Fargate task, and EFS mount targets (the template creates mount targets + for the first two subnets only). IngressCidr: Type: String Default: 0.0.0.0/0 + AllowedPattern: '(\d{1,3}\.){3}\d{1,3}/(\d|[12]\d|3[0-2])' Description: CIDR allowed to reach Bridge HTTPS on the ALB. ImageUri: Type: String @@ -37,7 +41,7 @@ Parameters: Description: ECS task CPU architecture. TaskCpu: Type: String - Default: "1024" + Default: "2048" AllowedValues: - "512" - "1024" @@ -46,7 +50,7 @@ Parameters: Description: Fargate task CPU units. TaskMemory: Type: String - Default: "2048" + Default: "4096" AllowedValues: - "1024" - "2048" @@ -58,7 +62,10 @@ Parameters: - "8192" - "16384" - "30720" - Description: Fargate task memory in MiB. + Description: > + Fargate task memory in MiB. Must be a valid combination for TaskCpu + (512 CPU: 1024-4096; 1024 CPU: 2048-8192; 2048 CPU: 4096-16384; + 4096 CPU: 8192-30720). DesiredCount: Type: Number Default: 1 @@ -234,6 +241,8 @@ Resources: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref BridgeFileSystem + # The access point forces all NFS operations to uid/gid 0, so the + # container's non-root user can write /data without matching ownership. PosixUser: Uid: "0" Gid: "0" @@ -264,7 +273,6 @@ Resources: BridgeLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: - Name: !Sub ${StackNamePrefix}-alb Scheme: internet-facing Type: application # ALB, not NLB diff --git a/Britive Bridge/aws-ecs-fargate-alb/params.example.json b/Britive Bridge/aws-ecs-fargate-alb/params.example.json index 40dba67..cbcd26d 100644 --- a/Britive Bridge/aws-ecs-fargate-alb/params.example.json +++ b/Britive Bridge/aws-ecs-fargate-alb/params.example.json @@ -5,8 +5,8 @@ { "ParameterKey": "IngressCidr", "ParameterValue": "0.0.0.0/0" }, { "ParameterKey": "ImageUri", "ParameterValue": "britive/bridge:latest" }, { "ParameterKey": "CpuArchitecture", "ParameterValue": "ARM64" }, - { "ParameterKey": "TaskCpu", "ParameterValue": "1024" }, - { "ParameterKey": "TaskMemory", "ParameterValue": "2048" }, + { "ParameterKey": "TaskCpu", "ParameterValue": "2048" }, + { "ParameterKey": "TaskMemory", "ParameterValue": "4096" }, { "ParameterKey": "DesiredCount", "ParameterValue": "1" }, { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" }, diff --git a/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml index 07078d5..cfdcdb0 100644 --- a/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml +++ b/Britive Bridge/aws-ecs-fargate-nlb/ecs-fargate-nlb.yaml @@ -14,10 +14,14 @@ Parameters: Description: VPC where Bridge will run. PublicSubnetIds: Type: List - Description: Two public subnets for the internet-facing NLB, Fargate task, and EFS mount targets. + Description: > + Exactly two public subnets in different AZs for the internet-facing NLB, + Fargate task, and EFS mount targets (the template creates mount targets + for the first two subnets only). IngressCidr: Type: String Default: 0.0.0.0/0 + AllowedPattern: '(\d{1,3}\.){3}\d{1,3}/(\d|[12]\d|3[0-2])' Description: CIDR allowed to reach Bridge HTTPS. ImageUri: Type: String @@ -32,7 +36,7 @@ Parameters: Description: ECS task CPU architecture. TaskCpu: Type: String - Default: "1024" + Default: "2048" AllowedValues: - "512" - "1024" @@ -41,7 +45,7 @@ Parameters: Description: Fargate task CPU units. TaskMemory: Type: String - Default: "2048" + Default: "4096" AllowedValues: - "1024" - "2048" @@ -53,7 +57,10 @@ Parameters: - "8192" - "16384" - "30720" - Description: Fargate task memory in MiB. + Description: > + Fargate task memory in MiB. Must be a valid combination for TaskCpu + (512 CPU: 1024-4096; 1024 CPU: 2048-8192; 2048 CPU: 4096-16384; + 4096 CPU: 8192-30720). DesiredCount: Type: Number Default: 1 @@ -195,6 +202,8 @@ Resources: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref BridgeFileSystem + # The access point forces all NFS operations to uid/gid 0, so the + # container's non-root user can write /data without matching ownership. PosixUser: Uid: "0" Gid: "0" @@ -235,8 +244,11 @@ Resources: Protocol: TCP Port: 8080 TargetType: ip - HealthCheckProtocol: TCP + # HTTPS health check against the bridge health endpoint — a process + # that accepts TCP but is unhealthy gets taken out of service. + HealthCheckProtocol: HTTPS HealthCheckPort: "8080" + HealthCheckPath: /api/health BridgeApiListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: diff --git a/Britive Bridge/aws-ecs-fargate-nlb/params.example.json b/Britive Bridge/aws-ecs-fargate-nlb/params.example.json index a633557..161cf37 100644 --- a/Britive Bridge/aws-ecs-fargate-nlb/params.example.json +++ b/Britive Bridge/aws-ecs-fargate-nlb/params.example.json @@ -5,8 +5,8 @@ { "ParameterKey": "IngressCidr", "ParameterValue": "0.0.0.0/0" }, { "ParameterKey": "ImageUri", "ParameterValue": "britive/bridge:latest" }, { "ParameterKey": "CpuArchitecture", "ParameterValue": "ARM64" }, - { "ParameterKey": "TaskCpu", "ParameterValue": "1024" }, - { "ParameterKey": "TaskMemory", "ParameterValue": "2048" }, + { "ParameterKey": "TaskCpu", "ParameterValue": "2048" }, + { "ParameterKey": "TaskMemory", "ParameterValue": "4096" }, { "ParameterKey": "DesiredCount", "ParameterValue": "1" }, { "ParameterKey": "BrokerTenantSubdomain", "ParameterValue": "" }, { "ParameterKey": "BrokerAuthToken", "ParameterValue": "" } diff --git a/Britive Bridge/custom-image/.dockerignore b/Britive Bridge/custom-image/.dockerignore index 657fe86..173d5ec 100644 --- a/Britive Bridge/custom-image/.dockerignore +++ b/Britive Bridge/custom-image/.dockerignore @@ -2,3 +2,4 @@ README.md build-and-push.sh .dockerignore **/*.md +k8s-overlays/ diff --git a/Britive Bridge/custom-image/Dockerfile b/Britive Bridge/custom-image/Dockerfile index eb8fd3e..47a23bc 100644 --- a/Britive Bridge/custom-image/Dockerfile +++ b/Britive Bridge/custom-image/Dockerfile @@ -60,7 +60,9 @@ RUN set -eu; \ PKG=apk; \ elif command -v microdnf >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1; then \ DNF=$(command -v dnf || command -v microdnf); \ - $DNF install -y \ + # --allowerasing: ubi-minimal ships coreutils-single, which conflicts + # with the full coreutils package. + $DNF install -y --allowerasing \ ca-certificates curl unzip bash coreutils \ openssh-clients python3 jq \ mariadb; \ diff --git a/Britive Bridge/custom-image/README.md b/Britive Bridge/custom-image/README.md index 9d0e93d..03e7086 100644 --- a/Britive Bridge/custom-image/README.md +++ b/Britive Bridge/custom-image/README.md @@ -90,7 +90,7 @@ Local single-arch build (no push) for testing: ```bash docker build -t britive-bridge-custom:latest . docker run --rm britive-bridge-custom:latest \ - sh -c 'for c in ssh ssh-keygen mysql aws jq python3; do command -v $c; done' + sh -c 'for c in ssh ssh-keygen mysql aws jq python3; do command -v $c || exit 1; done' ``` --- diff --git a/Britive Bridge/custom-image/k8s-overlays/README.md b/Britive Bridge/custom-image/k8s-overlays/README.md index e7eb15c..8d7056a 100644 --- a/Britive Bridge/custom-image/k8s-overlays/README.md +++ b/Britive Bridge/custom-image/k8s-overlays/README.md @@ -48,3 +48,7 @@ kubectl -n britive-bridge patch deployment britive-bridge \ 3306 — allow it in your network policy / security group. - **Non-EKS clusters:** swap the IRSA SA for your platform's workload identity (GKE Workload Identity, AKS workload identity) or mount AWS creds via a Secret. +- **Private registries:** a private Docker Hub repo (or ECR from a non-EKS + cluster) needs an image pull secret — `kubectl create secret docker-registry` + and reference it via `imagePullSecrets` in the patch. EKS nodes pull from ECR + via their node role without one. diff --git a/Britive Bridge/docker-compose/README.md b/Britive Bridge/docker-compose/README.md index 9bd398a..6e7321f 100644 --- a/Britive Bridge/docker-compose/README.md +++ b/Britive Bridge/docker-compose/README.md @@ -54,6 +54,7 @@ ideal for local trials, POCs, and single-VM deployments. ```bash docker run -d --name bridge -p 8080:8080 \ + --restart unless-stopped \ -e BRITIVE_BROKER_TENANT_SUBDOMAIN="" \ -e BRITIVE_BROKER_AUTH_TOKEN="" \ -v bridge-data:/data \ diff --git a/Britive Bridge/docker-compose/docker-compose.yaml b/Britive Bridge/docker-compose/docker-compose.yaml index a114133..10af1f1 100644 --- a/Britive Bridge/docker-compose/docker-compose.yaml +++ b/Britive Bridge/docker-compose/docker-compose.yaml @@ -2,12 +2,13 @@ # =================================================== # Optional convenience file. Equivalent to: # docker run -d --name bridge -p 8080:8080 \ +# --restart unless-stopped \ # -e BRITIVE_BROKER_TENANT_SUBDOMAIN="..." \ # -e BRITIVE_BROKER_AUTH_TOKEN="..." \ # -v bridge-data:/data \ # britive/bridge:latest # -# Start: docker compose up -d --build +# Start: docker compose up -d # Stop: docker compose down services: @@ -37,12 +38,15 @@ services: test: ["CMD", "curl", "-sfk", "https://127.0.0.1:8080/api/health"] interval: 15s timeout: 5s - retries: 20 + retries: 5 + start_period: 30s # startup failures don't count against retries volumes: bridge-data: # EXAMPLES: - # - local + # - local bind mount: create the directory FIRST (the none/bind driver does + # not create it) — mkdir -p /srv/britive-bridge/data. On SELinux-enforcing + # hosts (RHEL family), label it: chcon -Rt svirt_sandbox_file_t # bridge-data: # driver: local # driver_opts: diff --git a/Britive Bridge/kubernetes/README.md b/Britive Bridge/kubernetes/README.md index 8f530b2..439c668 100644 --- a/Britive Bridge/kubernetes/README.md +++ b/Britive Bridge/kubernetes/README.md @@ -20,8 +20,8 @@ Manifests in [`manifests/`](manifests/): | `service.yaml` | ClusterIP service (443 → container 8080) | | `ingress.yaml` | ingress-nginx Ingress with HTTPS backend + WebSocket support | | `external-secret.example.yaml` | *(optional)* Source broker creds from an external store via the External Secrets Operator | -| `pvc-rwm.example.yaml` | *(optional, HA)* ReadWriteMany PVC — use **instead of** `pvc.yaml` | -| `ha-deployment-patch.example.yaml` | *(optional, HA)* Patch: replicas + rolling-update + anti-affinity | +| `pvc-rwm.example.yaml` | *(optional, HA — not officially supported)* ReadWriteMany PVC — use **instead of** `pvc.yaml` | +| `ha-deployment-patch.example.yaml` | *(optional, HA — not officially supported)* Patch: replicas + rolling-update + anti-affinity | Traffic path: `client → Ingress (TLS) → Service:443 → pod:8080 (HTTPS, self-signed)`. @@ -29,7 +29,7 @@ Traffic path: `client → Ingress (TLS) → Service:443 → pod:8080 (HTTPS, sel - A Kubernetes cluster (1.25+) and `kubectl` context pointing at it - An **Ingress controller** (examples use [ingress-nginx](https://kubernetes.github.io/ingress-nginx/)) -- A **StorageClass** for the PVC (RWO is fine for a single replica; use RWM for HA) +- A **StorageClass** for the PVC (RWO is fine for a single replica; use RWX for HA) - A way to get a TLS cert for your domain — e.g. [cert-manager](https://cert-manager.io/) — or bring your own cert - Completed [platform setup](../platform-setup/) — you need @@ -155,8 +155,9 @@ RWO PVC and losing `/data`. --type strategic --patch-file manifests/ha-deployment-patch.example.yaml ``` -> Confirm with your Britive team that your Bridge version supports active-active -> across replicas before relying on HA for production traffic. +> **Not officially supported yet.** Multiple replicas sharing `/data` work on +> Bridge v1.x, but the configuration is not covered by support — run a single +> replica for production until Britive announces HA support. ## Teardown @@ -171,92 +172,8 @@ kubectl delete -f manifests/namespace.yaml --- -## Roadmap: public Helm chart +## Helm chart -A first-class Helm chart is planned so Bridge can be installed in one command -and configured declaratively via `values.yaml`. The chart will be published as -an **OCI artifact on Docker Hub**, alongside the `britive/bridge` container -image — one registry, one set of credentials, one `britive/` namespace. No -`helm repo add` needed. Target shape: - -```bash -helm install bridge oci://registry-1.docker.io/britive/britive-bridge \ - --namespace britive-bridge --create-namespace \ - --set broker.tenantSubdomain= \ - --set broker.existingSecret=bridge-broker \ - --set ingress.host=bridge.example.com -``` - -> The chart lives at `britive/britive-bridge` — a **sibling** Docker Hub repo to -> the `britive/bridge` image (OCI charts can't nest under an image repo). Pin a -> version in production: append `--version X.Y.Z`. - -**Planned `values.yaml` surface (subject to change):** - -```yaml -image: - repository: britive/bridge - tag: latest - pullPolicy: IfNotPresent - -replicaCount: 1 - -broker: - tenantSubdomain: "" # BRITIVE_BROKER_TENANT_SUBDOMAIN - authToken: "" # inline (dev only) ... - existingSecret: "" # ... or reference an existing Secret (preferred) - -persistence: - enabled: true - size: 5Gi - storageClass: "" - accessMode: ReadWriteOnce - -service: - type: ClusterIP - port: 443 - -ingress: - enabled: true - className: nginx - host: bridge.example.com - tls: - enabled: true - secretName: bridge-tls - annotations: {} # controller-specific (backend HTTPS, WebSocket, cert ARN) - -resources: - requests: { cpu: 500m, memory: 1Gi } - limits: { cpu: "1", memory: 2Gi } - -probes: - scheme: HTTPS - path: /api/health -``` - -**Planned delivery / milestones** - -1. Package the manifests in this directory as a chart (`Chart.yaml`, - `templates/`, `values.yaml`). -2. Add chart-level docs, schema validation (`values.schema.json`), and a CI lint - + `helm template` test. -3. Publish as a public **OCI chart artifact on Docker Hub**, alongside the - container image (see push flow below). -4. Surface the existing **ESO** and **HA (RWM)** manifest options (already - shipped under `manifests/`) as `values.yaml` toggles in the chart. - -**Planned publish flow (Docker Hub OCI)** - -```bash -helm package . # -> britive-bridge-X.Y.Z.tgz -echo "$DOCKERHUB_TOKEN" | helm registry login registry-1.docker.io \ - -u britive --password-stdin -helm push britive-bridge-X.Y.Z.tgz oci://registry-1.docker.io/britive -``` - -Requires Helm 3.8+ (OCI support is GA). The chart appears as the -`britive/britive-bridge` repository on Docker Hub with the -`application/vnd.cncf.helm.*` media type. - -> Until the chart ships, the manifests above are the supported install path and -> are kept in sync with the planned chart values so migration is mechanical. +A public Helm chart (OCI artifact on Docker Hub) is planned. Until it ships, +the manifests in this directory are the supported install path; they map 1:1 +to the planned chart values, so migrating later is mechanical. diff --git a/Britive Bridge/kubernetes/manifests/deployment.yaml b/Britive Bridge/kubernetes/manifests/deployment.yaml index f830d4a..15514a9 100644 --- a/Britive Bridge/kubernetes/manifests/deployment.yaml +++ b/Britive Bridge/kubernetes/manifests/deployment.yaml @@ -18,7 +18,9 @@ spec: app.kubernetes.io/name: britive-bridge spec: securityContext: - fsGroup: 0 + fsGroup: 0 # volumes group-owned by GID 0 — the stock image's + # non-root `bridge` user is in group 0, so it can + # write /data containers: - name: bridge image: britive/bridge:latest @@ -56,13 +58,15 @@ spec: periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 3 + # Recommended baseline for v1.x: 2 vCPU / 4 GiB (matches the VM and + # ECS guidance). resources: requests: - cpu: "500m" - memory: "1Gi" + cpu: "2" + memory: "4Gi" limits: - cpu: "1" - memory: "2Gi" + cpu: "2" + memory: "4Gi" volumes: - name: bridge-data persistentVolumeClaim: diff --git a/Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml b/Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml index 28084f6..ce6dad2 100644 --- a/Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml +++ b/Britive Bridge/kubernetes/manifests/ha-deployment-patch.example.yaml @@ -1,5 +1,6 @@ # Strategic-merge patch over the base Deployment for HA: more replicas and -# rolling updates instead of Recreate. Only safe once /data is on a +# rolling updates instead of Recreate. NOT OFFICIALLY SUPPORTED yet — works on +# Bridge v1.x but is not covered by support; single replica for production. Only safe once /data is on a # ReadWriteMany volume — see pvc-rwm.example.yaml for the full install order. # # This is a PATCH, not a full manifest — do not `kubectl apply` it. diff --git a/Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml b/Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml index 3914dac..4b2b425 100644 --- a/Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml +++ b/Britive Bridge/kubernetes/manifests/pvc-rwm.example.yaml @@ -1,5 +1,7 @@ -# Optional: high-availability storage. Use INSTEAD of pvc.yaml — choose HA at -# install time. PVC accessModes are immutable: if the RWO pvc.yaml is already +# Optional: high-availability storage. NOT OFFICIALLY SUPPORTED yet — multiple +# replicas sharing /data work on Bridge v1.x but are not covered by support; +# run a single replica for production. +# Use INSTEAD of pvc.yaml — choose HA at install time. PVC accessModes are immutable: if the RWO pvc.yaml is already # applied, you must delete that PVC first (DATA LOSS — back up /data) before # applying this one. # diff --git a/Britive Bridge/linux-vm-docker/README.md b/Britive Bridge/linux-vm-docker/README.md index e0dc6aa..df1436c 100644 --- a/Britive Bridge/linux-vm-docker/README.md +++ b/Britive Bridge/linux-vm-docker/README.md @@ -88,9 +88,10 @@ sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plu **RHEL / Rocky / AlmaLinux / Fedora** (dnf, docker-ce repo): ```bash -sudo dnf install -y dnf-plugins-core -# CentOS repo works for RHEL/Rocky/Alma; use the fedora repo on Fedora: -sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +# CentOS repo works for RHEL/Rocky/Alma; use the fedora repo on Fedora. +# (Written directly — `dnf config-manager --add-repo` changed syntax in dnf5.) +sudo curl -fsSL https://download.docker.com/linux/centos/docker-ce.repo \ + -o /etc/yum.repos.d/docker-ce.repo sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin ``` @@ -229,6 +230,8 @@ docker restart bridge # restart sudo ./install.sh # update: re-pulls image, recreates container docker rm -f bridge # stop + remove (keeps data volume) docker volume rm bridge-data # delete persisted state (destructive) +sudo firewall-cmd --permanent --remove-port=8080/tcp && sudo firewall-cmd --reload + # close the port on uninstall (firewalld) ``` **Auto-start on reboot** is handled by `--restart unless-stopped` plus diff --git a/Britive Bridge/linux-vm-docker/install.sh b/Britive Bridge/linux-vm-docker/install.sh index 1588794..aa80133 100755 --- a/Britive Bridge/linux-vm-docker/install.sh +++ b/Britive Bridge/linux-vm-docker/install.sh @@ -35,13 +35,17 @@ die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } [ "$(id -u)" -eq 0 ] || die "Run as root (sudo ./install.sh)." [ -f "$ENV_FILE" ] || die "Missing ${ENV_FILE}. Run: cp ${SCRIPT_DIR}/bridge.env.example ${ENV_FILE} then edit it." +# Catch unedited template placeholders before the container crash-loops on them. +if grep -q '). Edit it with your real tenant subdomain and broker token." +fi # ── 1. Detect the distro ──────────────────────────────────────────────────── # /etc/os-release is the portable source of truth across modern Linux. [ -r /etc/os-release ] || die "Cannot read /etc/os-release; unsupported OS." # shellcheck disable=SC1091 . /etc/os-release -log "Detected: ${PRETTY_NAME:-$ID $VERSION_ID} ($(uname -m))" +log "Detected: ${PRETTY_NAME:-$ID ${VERSION_ID:-}} ($(uname -m))" # ── 2. Install Docker Engine (skip if already present) ────────────────────── install_docker_debian() { @@ -58,7 +62,7 @@ install_docker_debian() { https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -y - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin } install_docker_rhel() { @@ -69,7 +73,7 @@ install_docker_rhel() { [ "$ID" = "fedora" ] && repo_distro="fedora" # Fedora has its own repo path curl -fsSL "https://download.docker.com/linux/${repo_distro}/docker-ce.repo" \ -o /etc/yum.repos.d/docker-ce.repo - dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin } install_docker_amazon() { @@ -89,6 +93,7 @@ else log "Installing Docker Engine..." case "$ID" in ubuntu|debian) install_docker_debian ;; + # centos = CentOS Stream 9+ (dnf); CentOS 7 (yum-only) is EOL/unsupported. rhel|rocky|almalinux|centos|fedora) install_docker_rhel ;; amzn) install_docker_amazon ;; *) die "Unsupported distro '$ID'. Install Docker manually, then re-run." ;; @@ -110,8 +115,7 @@ elif command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; th log "ufw active — opening ${PORT}/tcp..." ufw allow "${PORT}/tcp" else - warn "No active host firewall detected. Ensure your CLOUD security group / " - warn "network ACL allows inbound TCP ${PORT} from your users." + warn "No active host firewall detected. Ensure your CLOUD security group / network ACL allows inbound TCP ${PORT} from your users." fi # ── 5. SELinux note (RHEL/Amazon family) ──────────────────────────────────── @@ -147,14 +151,28 @@ docker run -d \ # ── 7. Health check ───────────────────────────────────────────────────────── log "Waiting for Bridge to report healthy..." +# Probe with host curl if present, else with curl inside the container. +health_probe() { + if command -v curl >/dev/null 2>&1; then + # -k: accept the self-signed cert. -s/-f: quiet + fail on non-2xx. + curl -sfk "https://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1 + else + docker exec "$CONTAINER_NAME" curl -sfk "https://127.0.0.1:8080/api/health" >/dev/null 2>&1 + fi +} +HEALTHY=0 for i in $(seq 1 20); do - # -k: accept the self-signed cert. -s/-f: quiet + fail on non-2xx. - if curl -sfk "https://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then + if health_probe; then + HEALTHY=1 log "Bridge is healthy at https://127.0.0.1:${PORT}/api/health" break fi - [ "$i" -eq 20 ] && warn "Health check did not pass yet. Check: docker logs ${CONTAINER_NAME}" sleep 3 done -log "Done. Logs: docker logs -f ${CONTAINER_NAME}" +if [ "$HEALTHY" -eq 1 ]; then + log "Done. Logs: docker logs -f ${CONTAINER_NAME}" +else + warn "Bridge did not report healthy within 60s. Inspect: docker logs ${CONTAINER_NAME}" + exit 1 +fi diff --git a/Britive Bridge/platform-setup/quick-setup.py b/Britive Bridge/platform-setup/quick-setup.py index a2c634d..3da2120 100644 --- a/Britive Bridge/platform-setup/quick-setup.py +++ b/Britive Bridge/platform-setup/quick-setup.py @@ -47,7 +47,7 @@ # --------------------------------------------------------------------------- -def prompt(label, default=None, secret=False, required=True): +def prompt(label, default=None, secret=False): """Prompt the user for input with an optional default.""" suffix = f" [{'****' if secret else default}]" if default else "" while True: @@ -56,21 +56,9 @@ def prompt(label, default=None, secret=False, required=True): return default if value: return value - if not required: - return "" print(" (required)") -def confirm(message): - """Ask for y/n confirmation.""" - while True: - answer = input(f" {message} [y/n]: ").strip().lower() - if answer in ("y", "yes"): - return True - if answer in ("n", "no"): - return False - - def header(step, title): print(f"\n{'=' * 60}") print(f" Step {step}: {title}") @@ -95,6 +83,9 @@ def print_env_block(tenant_subdomain, auth_token): print(f" BRITIVE_BROKER_TENANT_SUBDOMAIN={tenant_subdomain}") if auth_token: print(f" BRITIVE_BROKER_AUTH_TOKEN={auth_token}") + print() + print(" The token is a secret: clear your terminal scrollback after") + print(" saving it, and avoid running this script where output is logged.") else: print(" BRITIVE_BROKER_AUTH_TOKEN=") print() @@ -201,7 +192,8 @@ def error(message): TOKEN=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 43) NOW_EPOCH=$(date +%s) -EXPIRES_AT=$((NOW_EPOCH + EXPIRATION)) +# EXPIRATION (profile.timeout) is in milliseconds; expires_at is epoch seconds. +EXPIRES_AT=$((NOW_EPOCH + EXPIRATION / 1000)) cat </dev/null { "transaction_id": "${TRANSACTION_ID}", @@ -351,7 +343,7 @@ def create_bridge_resource_type(client): client.put( icon_url, data=BRIDGE_ICON_SVG, - headers={"Content-Type": "text/xml"}, + headers={"Content-Type": "image/svg+xml"}, ) success("Resource type icon set") break @@ -376,11 +368,7 @@ def create_admin_permission(client, bridge_type_id, template_id): perm_id = existing.get("permissionId") success(f"Permission 'admin' already exists: {perm_id}") info("Scripts/variables left as-is — edit them in the Britive UI if needed.") - return { - "id": perm_id, - "version": existing.get("version", ""), - "resource_type_id": bridge_type_id, - } + return {"id": perm_id, "resource_type_id": bridge_type_id} except Exception as exc: warn(f"Could not check for an existing permission ({exc}); attempting to create one.") @@ -401,7 +389,7 @@ def create_admin_permission(client, bridge_type_id, template_id): # Step 2: Get presigned upload URLs (SDK, with retry for propagation delay) info("Getting script upload URLs...") urls = None - for _attempt in range(5): + for attempt in range(5): try: urls = perms.get_urls(perm_id) if isinstance(urls, dict) and "checkinURL" in urls: @@ -410,11 +398,12 @@ def create_admin_permission(client, bridge_type_id, template_id): urls = None except Exception: pass - time.sleep(2) + if attempt < 4: + time.sleep(2) if not urls: warn("Could not get script upload URLs.") - return {"id": perm_id, "version": "", "resource_type_id": bridge_type_id} + return {"id": perm_id, "resource_type_id": bridge_type_id} # Step 3: Upload scripts to presigned S3 URLs via temp files info("Uploading checkout script...") @@ -437,7 +426,7 @@ def create_admin_permission(client, bridge_type_id, template_id): except Exception as exc: warn(f"Failed to upload scripts: {exc}") warn("Permission left as a draft — re-run this script or upload scripts in the UI.") - return {"id": perm_id, "version": "", "resource_type_id": bridge_type_id} + return {"id": perm_id, "resource_type_id": bridge_type_id} finally: os.unlink(checkout_tmp.name) os.unlink(checkin_tmp.name) @@ -466,18 +455,12 @@ def create_admin_permission(client, bridge_type_id, template_id): try: perm_url = f"{client.base_url}/resource-manager/permissions/{perm_id}" - result = client.put(perm_url, json=finalize_body) - perm_version = (result or {}).get("version", "") + client.put(perm_url, json=finalize_body) success(f"Permission 'admin' finalized: {perm_id}") except Exception as exc: warn(f"Failed to finalize permission: {exc}") - perm_version = "" - return { - "id": perm_id, - "version": perm_version, - "resource_type_id": bridge_type_id, - } + return {"id": perm_id, "resource_type_id": bridge_type_id} def create_bridge_resource(client, bridge_type_id, bridge_url, pool_id): diff --git a/Britive Bridge/windows-vm-docker/README.md b/Britive Bridge/windows-vm-docker/README.md index 809a955..8d38fb1 100644 --- a/Britive Bridge/windows-vm-docker/README.md +++ b/Britive Bridge/windows-vm-docker/README.md @@ -32,6 +32,8 @@ license) per the Linux guide. - Windows 10/11 (Pro/Enterprise) or Windows Server 2022+, 64-bit, with **virtualization enabled** in BIOS/hypervisor (required for WSL 2). +- Sizing: start with **2 vCPU / 4 GiB RAM** for the Bridge container (plus + headroom for Windows + WSL 2 itself). - **WSL 2** installed: in an elevated PowerShell run `wsl --install` then reboot. - A Linux-container runtime: - Windows 10/11: **Docker Desktop** set to **Linux containers** (tray icon → @@ -202,9 +204,10 @@ that URL. ```powershell docker logs -f bridge # follow logs docker restart bridge # restart -docker pull britive/bridge:latest; docker rm -f bridge; .\install.ps1 # update +.\install.ps1 # update: re-pulls image, recreates container docker rm -f bridge # stop + remove (keeps volume) docker volume rm bridge-data # delete persisted state (destructive) +Remove-NetFirewallRule -DisplayName "Britive Bridge 8080" # close the port on uninstall ``` --- diff --git a/Britive Bridge/windows-vm-docker/install.ps1 b/Britive Bridge/windows-vm-docker/install.ps1 index ca7c514..9f81802 100644 --- a/Britive Bridge/windows-vm-docker/install.ps1 +++ b/Britive Bridge/windows-vm-docker/install.ps1 @@ -19,9 +19,18 @@ .PARAMETER Image Docker Hub image. Default: britive/bridge:latest +.PARAMETER ContainerName + Name for the container. Default: bridge + .PARAMETER Port Host port published for HTTPS. Default: 8080 +.PARAMETER DataVolume + Docker named volume mounted at /data. Default: bridge-data + +.PARAMETER EnvFile + Env file with the broker credentials. Default: bridge.env + .EXAMPLE Copy-Item bridge.env.example bridge.env # then edit bridge.env .\install.ps1 @@ -76,10 +85,18 @@ Then re-run this script. "@ } -# ── 4. Env file must exist ────────────────────────────────────────────────── +# ── 4. Env file must exist, be edited, and be BOM-free ────────────────────── if (-not (Test-Path $EnvFile)) { Die "Missing $EnvFile. Run: Copy-Item bridge.env.example bridge.env then edit it." } +if (Select-String -Path $EnvFile -Pattern '). Edit it with your real tenant subdomain and broker token." +} +# A UTF-8 BOM corrupts the first variable name when docker parses --env-file. +$firstBytes = [System.IO.File]::ReadAllBytes((Resolve-Path $EnvFile))[0..2] +if ($firstBytes.Count -ge 3 -and $firstBytes[0] -eq 0xEF -and $firstBytes[1] -eq 0xBB -and $firstBytes[2] -eq 0xBF) { + Die "$EnvFile starts with a UTF-8 BOM, which breaks docker --env-file. Re-save it as UTF-8 WITHOUT BOM (in VS Code: 'Save with Encoding' > 'UTF-8')." +} # ── 5. Open the Windows Firewall for the port ─────────────────────────────── $ruleName = "Britive Bridge $Port"