Architecture Decision · 2026-04-12

Mobile Operator Console

A mobile companion app for iGaming SREs and compliance ops — built on Cloudflare Access, OpenBao SSH CA, and YubiKey 5 NFC. Zero Teleport licences, zero new critical-path services, and a GLI-33 / SIGAP / LGPD audit trail that lands in Wazuh.

3
Auth layers
15m
SSH cert TTL
$0
Licence cost
~10d
To first release

The Problem

On-call at 03:00 from a phone is a reality for every iGaming SRE. The dashboard works. But when a payment provider throws 502s mid-tournament, or a geofence trips on a cruise-ship IP block, the operator still needs a real shell — not just a status page. SSH from a laptop at a petrol station with a YubiKey hanging off a keyring is not a workflow; it is a liability.

At the same time, regulators do not care about your operational romance. SIGAP (Brazil), GLI-33, MGA Class 4, UKGC LCCP and LGPD all ask the same three questions: who authenticated, with what factor, and what did they do? Any mobile console that cannot answer those three questions in a forensic log is a compliance liability, not an asset.

The naive answer is to buy Teleport and move on. That answer is wrong at our scale (1–5 operators, ≤ 3 SSH targets) because it introduces a new critical-path dependency, a new per-user licence, and duplicates capabilities we already run. The architecture on this page is the answer we actually chose.

The App, Five Screens

Built with Expo (React Native) — same TypeScript codebase ships to iOS and Android. Screens captured on iPhone 17 (iOS 26 simulator). Each tab pulls live data from /api/v2/dash/* on new.acmetocasino.com behind Cloudflare Access; SSH actions go through the OpenBao SSH CA flow described below. Hostnames in the Terminal tab are redacted.

SRE overview tab — environment health, open incidents, infrastructure, services, live web digest
Overview — environment-first SRE summary, badge counts the open P0/P1.
Incidents tab — severity filter chips and breached SLA badges
Incidents — live /incidents/list feed with severity filters and SLA countdowns.
Infra tab — CPU/memory/disk rings, DB pool, Redis, Kafka, services grid
Infra — real CPU from /proc/stat delta, DB pool bar, Redis & Kafka health.
Compliance tab — AI bias audits, self-exclusion registry, LGPD
Compliance — AI bias audits, self-exclusion registry, LGPD DSR queue.
Terminal tab — Cloudflare Access SSH connection details and recent commands
Terminal — Cloudflare Access tunnel state, copy-paste SSH commands, 15-min OpenBao cert.

Decision Matrix: Cloudflare Access vs Teleport

Ten weighted criteria, scored 1 (poor) to 5 (excellent), against the five credible paths: Teleport (spec default), Cloudflare Access + OpenBao SSH CA (winner), OpenBao + VPN, Tailscale, and "mobile-responsive web only".

Criterion Weight Teleport CF + OpenBao OpenBao + VPN Tailscale Mobile web
Security posture (FIDO2 + mTLS + short-lived)555433
Operational complexity (services to keep alive)424445
Compliance fit (GLI-33, MGA, SIGAP)554434
Team-size fit (1–5 operators)425555
Vendor lock-in323535
Cost (licence + infra + effort)425545
Mobile UX (terminal, push, biometric)454343
Time-to-first-release425445
Fits existing chapters (20, 20b, 24h)235523
Lock-out / single-point-of-failure exposure423534
Weighted total140176167131159

Option B wins on total score and on every "services to keep alive" and "time-to-release" criterion that matters at small team size.

Architecture

Everything in the diagram below is either (a) already running or (b) a small addition to an existing component. No new control-plane service is introduced.

phone_iphone
Tier 1

Mobile Client · iOS 16+ / Android 13+

smartphoneReact Native + Expo

XTerm.js terminal embedded in a hardened WebView.

securitySecure Enclave / StrongBox

Device-bound passkey; private keys never leave the secure element.

contactlessYubiKey 5 NFC

FIDO2 roaming authenticator via NFC tap for phishing-resistant step-up.

cloud
Tier 2

Cloudflare Edge · Zero Trust gate

shieldWAF → Cloudflare Access

Policy engine in front of every origin. Deny-by-default.

verified_userIdentity & posture checks
  • → OIDC via Google Workspace SSO
  • → FIDO2 hardware-key policy (YubiKey required)
  • → Device posture (OS version + mTLS client cert)
apiAPI route

api.example.com → Cloudflare Worker → PROD origin

vpn_lockSSH route

ssh.example.com → Cloudflare Tunnel → LAN

dns
Tier 3

On-Prem LAN · Ops VLAN

key_verticalOpenBao
  • • SSH CA (15m short-lived certs)
  • • PKI mobile role
  • • HSM-sealed at rest
memoryYubiHSM 2

PKCS#11 · root signing · seal wrap

terminalsshd on ops host

TrustedUserCAKeys verifies OpenBao-signed certs. Prod bastion on Ops VLAN.

monitor_heartWazuh SIEM

auth.log + sshd session + auditd · Cloudflare Logpush (Access events).

Three-Layer Auth Flow

The spec as written listed five auth layers. Three is enough to satisfy OWASP MASVS-AUTH-1/2 and GLI-33, and each layer defends a different threat model.

fingerprint

1 · Passkey

Biometric-unlocked Passkey bound to Secure Enclave / StrongBox via expo-passkeys. Defends against stolen phone + weak screen lock. Never leaves the TEE.

key

2 · YubiKey + CF Access

Cloudflare Access enforces a FIDO2 hardware-key policy. NFC tap with YubiKey 5 NFC produces a signed assertion; Google Workspace SSO supplies identity. Result: an 8h CF_Authorization JWT.

verified_user

3 · OpenBao SSH Cert

For privileged ops (SSH, drain, payout approve) the JWT is exchanged at OpenBao for a 15-minute SSH certificate signed by the SSH CA. Each target sshd trusts only that CA — no per-key authorized_keys.

Cloudflare API Token — Exact Scopes

Create one API token, used by the mobile backend Worker and by the deploy pipeline. Five permissions, no more:

ScopeWhy
Account.Access: Apps and Policies — EditCreate / update Access applications for api and ssh hostnames.
Account.Cloudflare Tunnel — EditCreate and rotate tunnel credentials for cloudflared on the ops host.
Zone.DNS — EditManage CNAME records that point to the tunnel hostname.
Zone.Zone — ReadLook up zone id at deploy time.
Account.Workers Scripts — EditDeploy the mobile API gateway Worker.
Never grant global account-level write. The token above cannot delete zones, cannot modify billing, and cannot touch R2 or D1. If it leaks, the blast radius is confined to the mobile stack.

Implementation Highlights

Five code snippets that carry the design. All tokens, IDs and hostnames are placeholders: replace with your own before running.

OpenBao SSH CA bootstrap

# On the OpenBao host — enable SSH secrets engine, generate CA key export VAULT_ADDR="https://openbao.internal:8200" export VAULT_TOKEN="<OPENBAO_ROOT_TOKEN>" bao secrets enable -path=ssh-client ssh bao write ssh-client/config/ca generate_signing_key=true # Role: 15-minute certs, user root@ops, key_type ed25519 bao write ssh-client/roles/mobile-admin \ algorithm_signer=default \ allow_user_certificates=true \ allowed_users="root,ops" \ default_user="ops" \ ttl="15m" max_ttl="15m" \ key_type=ca key_bits=0 \ allowed_extensions="permit-pty,permit-agent-forwarding" # Publish the CA public key — copy to every sshd host bao read -field=public_key ssh-client/config/ca > /etc/ssh/ca.pub

sshd configuration on targets

# /etc/ssh/sshd_config.d/10-openbao-ca.conf TrustedUserCAKeys /etc/ssh/ca.pub AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u PasswordAuthentication no PubkeyAuthentication yes # /etc/ssh/auth_principals/ops # (one principal per line — matches the "valid_principals" in the cert) mobile-admin

Cloudflare Access application (Terraform)

resource "cloudflare_access_application" "ssh" { account_id = "<CF_ACCOUNT_ID>" name = "ops-ssh" domain = "ssh.acmetocasino.com" type = "ssh" session_duration = "8h" } resource "cloudflare_access_policy" "ssh_hwk" { account_id = "<CF_ACCOUNT_ID>" application_id = cloudflare_access_application.ssh.id name = "hwk-only" decision = "allow" precedence = 1 include { email_domain = ["example.com"] } require { auth_method = "hwk" # hardware key device_posture = ["posture-os-version"] } }

cloudflared on the ops host

# /etc/cloudflared/config.yml tunnel: <TUNNEL_UUID> credentials-file: /etc/cloudflared/<TUNNEL_UUID>.json ingress: - hostname: ssh.acmetocasino.com service: ssh://127.0.0.1:22 - hostname: api.acmetocasino.com service: http://127.0.0.1:8000 - service: http_status:404 # systemd unit drops privileges to cloudflared user; runs on the LAN host only.

Mobile client — JWT → SSH cert exchange

// src/lib/openbao.ts — sign a local ed25519 pubkey, TTL 15m import { getItem } from "expo-secure-store"; export async function requestShortLivedCert(jwt: string, publicKey: string) { const res = await fetch("https://openbao.acmetocasino.com/v1/ssh-client/sign/mobile-admin", { method: "POST", headers: { "X-Vault-Token": jwt, // CF Access JWT, bridged by Worker "Content-Type": "application/json", }, body: JSON.stringify({ public_key: publicKey, valid_principals: "mobile-admin", ttl: "15m", }), }); if (!res.ok) throw new Error(`sign failed: ${res.status}`); const { data } = await res.json(); return data.signed_key as string; // OpenSSH certificate, 15m TTL }

Compliance Mapping

What each regulator asks for, and which piece of the stack answers it.

RegimeControlAnswered by
SIGAP (Brazil)Privileged access authentication & session recordCF Access FIDO2 log + Wazuh sshd session log
GLI-33Named operator, hardware-backed factor, audit trailGoogle SSO identity + YubiKey + Wazuh
LGPDWho accessed PII, when, purposeCF Logpush to Wazuh + application-level audit on /api/v2/*
MGA Class 4Key custody & short-lived credentialsOpenBao SSH CA (15m TTL) sealed by YubiHSM 2
UKGC LCCPSeparation of duties, least privilegeCF Access policies per application + OpenBao roles
OWASP MASVSAUTH-1/2, NETWORK-1/2, STORAGE-1Passkey + mTLS + cert pinning + MMKV encrypted store

Cost Comparison

Back-of-envelope annualised cost for a 5-operator team. "Verify with vendor" applies to every third-party number below — these are the right order of magnitude, not current quotes.

Line itemCF + OpenBaoTeleport (Team tier)
Control-plane licences$0 — CF Zero Trust free ≤ 50 seatsPer-user per month (verify with vendor)
Self-hosted servicesOpenBao already runningAuth + Proxy + agents + Device Trust
YubiKey 5 NFC × 5~$275 one-off~$275 one-off
Mobile app (Expo EAS)$99/mo team$99/mo team
Engineering time (setup)10–14 person-days15–20 person-days + ongoing upgrades
Migration cost if abandonedLow — OpenBao and CF are open standardsHigh — RBAC + recordings live in Teleport

Stack Summary

keyHardware

YubiKey 5 NFC · YubiHSM 2 · iPhone Secure Enclave / Android StrongBox

lockIdentity / secrets

Cloudflare Access · Google Workspace SSO · OpenBao (SSH CA + PKI) · cloudflared

phone_iphoneMobile app

React Native · Expo SDK 51 · TypeScript · Zustand · TanStack Query · XTerm.js · MMKV

shieldAudit / SIEM

Wazuh SIEM · Cloudflare Logpush · sshd session log · auditd

cloudEdge

Cloudflare Workers (mobile API) · Cloudflare Tunnel · Cloudflare WAF

notifications_activePush & alerts

Expo Push → FCM + APNs · critical-alert bypass for incident channels