Laptop access to Dev and QA through a city relay¶
Status: City rollout complete; native automatic-client acceptance pending | Last reviewed: 2026-09-21
Use a dedicated WireGuard relay on a city EC2 host to carry laptop traffic to TrueServe Dev and QA. Keep the laptop's existing core VPC VPN connected: it carries the encrypted WireGuard connection to the city's private IP. The city then forwards selected application traffic through its existing network.
The toolkit creates private per-device
bundles with Mac and Windows helpers. Give developers the separate
Mac laptop guide or
Windows laptop guide. Both explain each step and
assume the host is ready. They ship in every bundle as MAC-SETUP.md and
WINDOWS-SETUP.md; LAPTOP-README.md is the shorter shared reference.
What has been verified¶
The September 17 city rollout report records
the installed hosts, exact runtime revision and current acceptance results.
During that rollout, Dev Studio timed out and Dev Dex/Grafana returned
503 no healthy upstream, including from the existing Vanguard path.
The city tunnels passed transport checks; QA passed the full probe suite.
Treat the earlier successful Dev checks below as historical evidence, not
confirmation that the Dev application outage has recovered.
On September 17, the original Vanguard/Mac test loaded Dev Studio, completed sign-in and carried audio in both directions. It used the host's ordinary network egress, independently of the experimental GlobalProtect container.
Earlier on September 17, Dev and QA both passed HTTPS, Janus WSS, STUN/UDP, TURN TLS, ArgoCD, Dex and
Grafana probes from Vanguard and from an authenticated, disposable WireGuard
client using the toolkit's relay, scoped DNS and combined destination routes.
Shared SSO HTTPS also passed. QA Studio returned 200; Janus upgraded with
101. This uses the ordinary host network and public application DNS.
QA initially timed out at its public ALB (52.38.231.248, 54.185.43.168).
The September 17 fix added the city NAT egress 54.92.142.217/32 to both
the QA ALB's TCP 443 security-group rule set and its existing WAF IP allowlist.
See the exact resources and ticket follow-up below.
The original Mac proof of concept is an end-to-end result; the new Mac and Windows helpers require native laptop acceptance testing. A relay-side probe does not prove browser sign-in, microphone permission, ICE selection or audio.
The disposable Docker integration test passes scoped Dev/QA DNS, IPv6-answer
filtering, TCP/UDP forwarding, restart/reconnection and live peer revocation.
PowerShell parsing and mocked DNS ownership, rollback and policy-conflict
checks pass. The live WireGuard probe additionally verifies the actual Dev
and QA network paths, but does not replace authenticated browser/audio tests.
The original StudioTest bundle was Dev-only. Its running relay was
subsequently extended with six QA forwarding destinations and 21 total DNS
names, and both environments were verified through a temporary authenticated
peer on that exact relay/endpoint. The original Mac peer was left connected.
Existing Mac users still need the local profile/DNS upgrade below; cloud
allowlist changes alone do not update a laptop.
Upgrade the original Mac StudioTest connection¶
Keep the core VPN and the Homebrew-managed StudioTest tunnel active. Download
upgrade-studio-test-mac.sh
onto the Mac, then run it from the existing StudioTest-mac folder:
sudo bash ~/Downloads/upgrade-studio-test-mac.sh ./StudioTest.conf
This specific migration contains no private keys. It verifies the active
interface and profile identity, backs up the old profile, adds QA routes to
the live WireGuard peer and saved config, verifies QA HTTPS through the
tunnel, and installs the scoped DNS names. It extends targets.txt so the
original studio-dns.sh disable command still removes all its DNS entries.
Version 3 tolerates a stale utun8 and live utun9 both retaining the
10.254.253.250 address: it selects the single WireGuard interface whose
client/server keys, endpoint and recent handshake match. It moves this
profile's routes from the stale interface and removes its duplicate address.
An old CLI-visible WireGuard peer is disabled only when both keys and the
endpoint match and its handshake is stale. Interfaces carrying the core
endpoint or a different WireGuard identity are preserved. Two matching
interfaces with recent handshakes are treated as ambiguous and left unchanged.
An address-only leftover interface may remain as an empty device until its owning application closes it; the script does not kill unknown VPN processes. It refuses unrelated conflicting routes or unmanaged resolver files. Mocked regression tests cover cleanup and these preservation boundaries; native Mac execution remains part of acceptance. Its address snapshot is dated September 17, 2026; new users should use the generated profiles and normal lifecycle helpers.
After the script succeeds, reload https://studio.qa.trueserve.deloitte.com/,
sign in and verify audio in both directions. Native QA acceptance remains
pending until that laptop test succeeds.
Connection model and scope¶
flowchart LR
Laptop["Mac / Windows<br/>TrueServe WireGuard tunnel"]
Core["Existing core VPC VPN<br/>UDP to city private IP"]
Relay["City EC2<br/>isolated WireGuard + scoped DNS"]
Dev["Dev applications, TURN<br/>and KMS tools"]
QA["QA applications, TURN<br/>and KMS tools"]
SSO["Shared SSO"]
Laptop --> Core --> Relay
Relay --> Dev
Relay --> QA
Relay --> SSO
Choose one relay that can reach both environments, and use the both
profile. Dev and QA then work side by side in ordinary browser tabs. dev and
qa profiles are alternatives for users who only need one environment.
Disconnect before switching. Do not activate separate Dev and QA tunnels
simultaneously: their shared SSO and possibly other destination routes overlap.
This is IP forwarding, so TCP and UDP to the selected destinations—including WebSockets, SSE and TURN media—can traverse the relay. It is not a universal browser proxy or a full-tunnel VPN. Only enumerated IPv4 destinations and any explicitly reviewed extra media routes are forwarded. WebRTC ICE candidates, third-party identity providers and integrations outside that list retain their normal laptop route. Dev-only/QA-only profiles are convenience filters, not server-side authorization boundaries; the relay permits its union of destinations.
The relay publishes one private UDP port and isolates its forwarding/NAT
rules in a Docker network namespace. It does not change host routes, global
DNS or host forwarding policy. Docker manages the ordinary published-port
rules. Kernel WireGuard and Docker NET_ADMIN are required. No Kubernetes
change, VPN password, shared Grafana token or GlobalProtect browser session
is included in the toolkit.
City prerequisites¶
- Use an existing city EC2, managed through SSM.
Run relay commands on the Ubuntu host, not its
vscodedevcontainer. The host checkout is/home/ubuntu/trueserve-framework. Updating it remains the normal city release workflow; the access installer never resets Git. - Ensure the laptop can route to that host's private IP over its existing core VPC VPN. An SSM or VS Code TCP port forward cannot carry WireGuard UDP. City HTTPS through Global Accelerator does not imply UDP relay reachability.
- Select an unused host UDP port (default
51828) and unused RFC1918 tunnel subnet (default10.254.252.0/24). Check both the host and laptop routing tables, including corporate VPN ranges. The original test uses a different port/subnet (51827,10.254.253.248/29) and can be retired after migration. - The host's security group must admit that UDP port from the actual core VPN client/source CIDRs. NACLs must permit it and return traffic. Inspect existing rules before proposing a narrowly scoped change; the scripts do not modify SGs, NACLs, TGWs or load balancers.
- The city must resolve and reach all required downstream names, including private KMS management addresses and the shared SSO host. Its egress must be allowed by the destination SG/WAF/network policy. A working Dev path does not establish a working QA path.
The September 17 EC2 inventory places all eight current cities in us-east-1,
VPC vpc-03baca9c704dd5749, subnet subnet-05f087a854a624070, alongside
Vanguard. Its default route uses NAT gateway nat-037bd395f735109df, with
public egress 54.92.142.217. This is separate from each city's private
WireGuard endpoint:
| City | Private endpoint IPv4 |
|---|---|
| richmond | 10.120.206.52 |
| istanbul | 10.120.206.24 |
| macau | 10.120.206.58 |
| shimla | 10.120.206.38 |
| rome | 10.120.206.10 |
| kyiv | 10.120.206.42 |
| seattle | 10.120.206.47 |
| athens | 10.120.206.59 |
Instance IDs are in city dev environments. Verify the selected instance's current private IP and egress during setup; these values are an inventory snapshot, not hardcoded installer defaults.
Current endpoint catalog:
environments.json. It includes
Studio, conversation API, surface-app, Janus, TURN, tools, code-server, ArgoCD,
Dex and Grafana for each environment, and sso.trueserve.deloitte.com once.
Platform hostnames intentionally retain QA's legacy truserve-qa-wus spelling.
Private application load-balancer path¶
If the public QA path fails but the city has approved private VPC connectivity, the operator can test the same application ALB at its private ENI addresses. Obtain those addresses from QA EC2 Network Interfaces using the ALB's name. Management gateway IPs and historical application NLB IPs are not substitutes. Never disable TLS verification to make an alternative address appear healthy.
Create a JSON mapping of exact application names to the confirmed ALB private IPv4 addresses. Include each applicable QA L7 name from the endpoint catalog (Studio, conversation API, surface-app, Janus, tools and code); leave TURN and management names on their own DNS records. Do not add guessed IPs.
sudo python3 scripts/studio-access/relay.py probe --environments both \
--dns-overrides /root/qa-private-dns.json
If the probe passes, supply the same --dns-overrides file to init. Overrides
change the socket destination while preserving the hostname, TLS SNI,
certificate checks and HTTP Host. They are validated against the selected
catalog, saved in private state, and used consistently for tunnel DNS and
client routes. They do not alter corporate or public DNS. Private ENI addresses
can change; update them through the coordinated refresh --dns-overrides FILE
workflow. An empty JSON object removes overrides.
QA source allowlist fix and ticket follow-up¶
The verified Vanguard/city public egress is 54.92.142.217 and
the QA application ALB DNS name is
k8s-skasyste-publicl7-4bbb54704b-2119879782.us-west-2.elb.amazonaws.com.
The initial 41 inbound source ranges did not include this address, and the
second attached group (the shared Kubernetes backend SG) had no inbound rule
allowing it either.
After the operator made the qa AWS CLI profile available, two targeted
updates were applied in account 836010056554, region us-west-2:
| Resource | Change |
|---|---|
Frontend SG sg-0e81233c66c1ab9c3 (for-alb-rules) |
Added TCP 443 from 54.92.142.217/32, rule sgr-0202a276efbb92963 |
WAF IP set waf-rules-westus_IPallowedlist_IPV4, ID cd2cb79b-d9cb-40d2-a582-ef97fa343786 |
Appended the same /32, retaining all 41 previous entries |
The IP set is referenced by IPallowedlist in regional web ACL
waf-rules-westus. Its existing allow rule and the other WAF rules were
retained. The SG update changed QA from a TCP timeout to HTTP 403; the WAF
update then produced Studio 200 and Janus 101. No Dev cloud resources,
shared backend SG, DNS records or application settings were changed.
The previously submitted ticket should record both updates in the KMS-managed configuration so reconciliation does not remove them. Ticket closure has not been confirmed. These were explicit operational updates; the reusable relay installer still does not mutate cloud security controls.
For rollback, revoke only SG rule sgr-0202a276efbb92963 and remove only
54.92.142.217/32 from the WAF IP set using its current lock token. Preserve
any intervening changes by other operators; do not restore an old full IP
set blindly. Rollback removes the newly enabled city-to-QA application path.
Install on a city¶
Tactical release installation¶
The city rollout installs only the committed access files under
/opt/trueserve-access/releases/<commit> and selects that release through
/opt/trueserve-access/current. install-city.py builds and starts the
dedicated relay, copies the access scripts and related guides into the
city's bind-mounted checkout, and adds a bounded instruction block to root
AGENTS.md, root CLAUDE.md and .devcontainer/CLAUDE.md. Other instructions
are preserved. It does not fetch, switch or reset application branches.
The rollout's per-city endpoint and tunnel subnets are recorded in
cities.json. Verify EC2 identity/IPs
before using this inventory for a later rollout. Each city gets separate
server keys and state; profiles belong to one city and one device.
After installation, run these commands on the host through SSM:
sudo trueserve-access status
sudo trueserve-access probe --environments both
sudo trueserve-access add-peer alex-windows
sudo trueserve-access add-peer eric-mac
The last two are examples: create a uniquely named peer only for the actual
device being onboarded. Deliver its private ZIP from
/var/lib/trueserve-access/exports/<device>.zip using the private transfer
procedure below. Do not give every developer a shared city key. The installed
relay can run with no enrolled peers while waiting for onboarding.
For city-side log access, follow Grafana credential setup. The relay itself does not supply Grafana authentication.
The active revision and preserved application HEAD are recorded in
/var/lib/trueserve-access/deployment.json. Root-only installation/build logs
are stored alongside it. The Docker restart policy keeps the relay running
across host restarts; application services are managed separately.
Manual installation¶
Connect using the AWS CLI and Session Manager plugin, with the correct account and region for the selected city:
aws ssm start-session --region YOUR_REGION --target YOUR_INSTANCE_ID
On the host:
cd /home/ubuntu/trueserve-framework
sudo python3 scripts/studio-access/relay.py build
sudo python3 scripts/studio-access/relay.py probe --environments both
The probe runs on a Docker bridge, matching the relay's egress topology. It
requires HTTPS responses, Janus upgrade 101, STUN binding success and
certificate-valid TURN TLS. If an endpoint fails, investigate it before
proceeding. Probe success does not authenticate to TURN or test a voice call.
After the selected environments pass:
# Replace the endpoint with this city's assigned private IPv4 address.
sudo python3 scripts/studio-access/relay.py init \
--endpoint YOUR_CITY_PRIVATE_IP --environments both \
--port 51828 --network 10.254.252.0/24
sudo python3 scripts/studio-access/relay.py add-peer eric-mac
sudo python3 scripts/studio-access/relay.py add-peer colleague-windows
sudo python3 scripts/studio-access/relay.py start
sudo python3 scripts/studio-access/relay.py status
init repeats the selected environment probes before creating state or keys;
it refuses setup when a required path fails. It resolves the selected
hostnames from the host's resolver and rejects
recursive endpoint routes, overlapping host tunnel ranges, metadata/loopback
destinations and broad extra routes. Each peer gets its own keypair and /32;
all three profiles for that peer intentionally reuse that device identity.
The container restarts with Docker unless explicitly stopped.
Private state is under /var/lib/trueserve-access (0700); keys, configs and
ZIPs are 0600. Peer bundles are generated at
/var/lib/trueserve-access/exports/<peer>.zip. Server-side key generation
simplifies onboarding, so the operator also holds client private keys.
Protect this state as credential material; do not commit it.
For repeatable remote administration from a checkout with Python 3 and the AWS CLI:
python3 scripts/studio-access/city.py \
--region YOUR_REGION --instance-id YOUR_INSTANCE_ID -- build
python3 scripts/studio-access/city.py \
--region YOUR_REGION --instance-id YOUR_INSTANCE_ID -- probe --environments both
python3 scripts/studio-access/city.py \
--region YOUR_REGION --instance-id YOUR_INSTANCE_ID -- \
init --endpoint YOUR_CITY_PRIVATE_IP --environments both
python3 scripts/studio-access/city.py \
--region YOUR_REGION --instance-id YOUR_INSTANCE_ID -- add-peer eric-mac
python3 scripts/studio-access/city.py \
--region YOUR_REGION --instance-id YOUR_INSTANCE_ID -- start
city.py executes the already-installed host script through SSM and reports
the remote exit code. It does not upload source, change cloud resources,
retrieve keys into SSM output or update application services.
Transfer a private bundle without SSH¶
In an interactive SSM host shell, start the single-download loopback server:
sudo python3 /home/ubuntu/trueserve-framework/scripts/studio-access/download.py \
/var/lib/trueserve-access/exports/eric-mac.zip
In a separate laptop terminal, forward its TCP port:
aws ssm start-session --region YOUR_REGION --target YOUR_INSTANCE_ID \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["18086"],"localPortNumber":["18086"]}'
For Windows PowerShell, avoid native JSON quoting differences:
'{"portNumber":["18086"],"localPortNumber":["18086"]}' |
Set-Content -Encoding ascii ssm-ports.json
aws ssm start-session --region YOUR_REGION --target YOUR_INSTANCE_ID `
--document-name AWS-StartPortForwardingSession `
--parameters file://ssm-ports.json
Open the random http://127.0.0.1:18086/<random>.zip URL printed by the host
script on the laptop. The server listens only on host loopback, serves only
that bundle, and exits after one download or ten minutes. Close the SSM port
forward afterward. This TCP tunnel is for bundle delivery, not WireGuard.
An approved private file-transfer channel is also suitable.
Extract the ZIP into a private folder and follow MAC-SETUP.md or
WINDOWS-SETUP.md for the developer's laptop.
On Windows, the active CLI config gets a restricted ACL, but the downloaded
ZIP and extraction folder still need normal private-profile storage.
Everyday use and switching¶
| Task | Mac CLI | Windows elevated PowerShell |
|---|---|---|
| Connect both | sudo bash mac.sh up both |
.\windows.ps1 up both |
| Check | sudo bash mac.sh status |
.\windows.ps1 status |
| Disconnect | sudo bash mac.sh down |
.\windows.ps1 down |
| Dev only, after disconnect | sudo bash mac.sh up dev |
.\windows.ps1 up dev |
| QA only, after disconnect | sudo bash mac.sh up qa |
.\windows.ps1 up qa |
| GUI tunnel DNS on | sudo bash mac.sh dns-on both |
.\windows.ps1 dns-on both |
| GUI tunnel DNS off | sudo bash mac.sh dns-off |
.\windows.ps1 dns-off |
DNS helpers verify the route to the relay endpoint stays outside the new
tunnel, all selected routes use its interface, and direct tunnel DNS answers.
Only then do they install exact-host scoped DNS. Mac uses /etc/resolver;
Windows uses NRPT. Unknown names are not forwarded by the relay resolver,
and AAAA answers are suppressed for the scoped names. Browser DoH and managed
DNS policy require separate review if the browser ignores the OS resolver.
Mac ownership state is retained under /var/db/trueserve-access so DNS
cleanup still works after a reboot.
Turn scoped DNS off before deactivating a GUI tunnel. A reboot, sleep or VPN
drop may leave DNS rules while connectivity is absent; use status and
down/dns-off to recover. Helpers do not install a background reconnect
agent. Windows CLI tunnel services may start at boot before the core VPN is
ready; keep the core connection available when using them.
DNS changes and maintenance¶
The relay intentionally pins a versioned IPv4 snapshot, with DNS and client routes generated from the same records. It does not resolve new ALB addresses at runtime, which could make DNS return addresses absent from laptop routes. Address changes require a coordinated refresh:
- Have clients disconnect and remove their scoped DNS.
- On the host, run:
bash
sudo python3 scripts/studio-access/relay.py stop
sudo python3 scripts/studio-access/relay.py refresh
sudo python3 scripts/studio-access/relay.py start
sudo python3 scripts/studio-access/relay.py probe --environments both
- Deliver the regenerated bundles to every device. Replace the entire old bundle; GUI users must replace their imported profile too. Reconnect and repeat browser and audio acceptance tests.
refresh refuses a running relay, requires the selected probes to pass,
preserves existing peer keys, and reissues
all bundles. Manual clients use snapshots. The optional
automatic maintenance mode refreshes host DNS
and laptop routes, starts at boot and checks HTTPS every 30 minutes. A new
catalog or extra-media-route change needs the same coordinated treatment;
the initialized catalog is stored in private state, so editing the repository
catalog alone does not update an existing relay. Use a new state directory
and explicit migration for catalog/subnet/port changes.
Add another device with add-peer while running; wg syncconf preserves
other peers. Revoke a device immediately with:
sudo python3 scripts/studio-access/relay.py revoke eric-mac
Revocation removes the live peer and its local exports. Previously distributed
bundles stop authenticating. remove stops/removes only the managed relay
container and retains private state. Remove laptop DNS first; private state
deletion is a separate operator decision.
Troubleshooting and acceptance¶
Application outages and relay faults need separate checks. The normal
probe and init commands require successful application responses.
For a confirmed upstream outage, probe --transport-only, init
--transport-only, and install-city.py --transport-only explicitly require
certificate-verified TLS and STUN reachability instead. This mode records
limited validation and does not establish HTTP, WebSocket, sign-in or call
health. Run the normal probe separately and record which environment remains
degraded. Never describe a transport-only pass as full application readiness.
| Observation | Next check |
|---|---|
| No handshake | Core VPN route to city IP; SG/NACL UDP rule; server status; matching peer keys |
| Handshake, direct DNS timeout | Route to tunnel DNS; stale Mac utun; peer address; resolver process |
| Direct DNS works, browser name fails | Scoped DNS installed; OS resolver cache; browser DoH; Windows effective NRPT policy |
Mac route: File exists |
Old TrueServe interface still owns routes; deactivate old GUI/CLI tunnel first |
| HTTPS times out only in QA | QA ALB reachability from city; downstream source allowlist; current DNS snapshot |
| HTTPS works, Janus fails | WSS /janus upgrade and certificate, then app-advertised signaling URL |
| Call connects, one-way/no audio | ICE selected pair, TURN allocation, browser mic permission, advertised candidates and allowed media destinations |
ArgoCD Unregistered redirect_uri |
Platform Dex client registration; this is not a WireGuard/port-forwarding issue |
For the Mac stale-route case, status shows the interface holding the peer
address and the interface used for tunnel DNS. The original failure had a
healthy utun9 handshake but destination routes still on utun8.
mac.sh repair-routes both utun8 is an explicit, narrowly scoped recovery
after verifying utun8 belonged to the previous TrueServe tunnel. It refuses
the interface carrying the core endpoint and routes on unexpected interfaces.
Never delete all utun interfaces or flush corporate VPN routes.
Acceptance on each OS and each environment:
- Record the core VPN route to the city before and after connection; it must remain outside WireGuard. Check a normal core VPC resource still works.
- Confirm handshake and direct DNS, then OS DNS and browser Studio sign-in.
- Confirm Janus WebSocket, place a real call, and verify sent/received audio.
Inspect
chrome://webrtc-internalsor the equivalent browser diagnostics for the selected candidate pair; record relay/direct, IPs, ports and both byte counters. Avoid exporting credentials or conversation payloads. - Open ArgoCD and Grafana with their own normal SSO.
- Test
bothwith Dev and QA tabs, then disconnect and verify scoped DNS and the test routes are gone while core VPC access remains. - Test reconnect, GUI/CLI switching, sleep/reboot recovery and a fresh bundle.
If direct media addresses are outside the endpoint list, add only reviewed
specific destinations at relay initialization with --extra-route IP/32
(up to /24). Do not add a default route or broad private network ranges.
Alternatively have the application owner review TURN-only policy; the
toolkit does not change app ICE settings.
ArgoCD CLI SSO¶
The CLI expects the hostname without https:// or a trailing slash. Browser
SSO can work while CLI SSO fails. The current Dev external Dex setup has
client ID argocd and did not accept http://localhost:8085/auth/callback.
The KMS team needs to register an appropriate public CLI client and configure
ArgoCD's oidc.config.cliClientID. A localhost tunnel cannot repair registration.
After the platform supports the CLI flow, run the CLI locally on the laptop where possible. For a headless city session, select a free remote port, disable automatic browser launch, and forward the callback to the laptop. Example only after the exact redirect is registered:
argocd login argocd.kms.dev.trueserve.deloitte.com \
--sso --grpc-web --sso-launch-browser=false \
--sso-port 18085 --callback http://localhost:8085
Forward laptop TCP 8085 to remote TCP 18085 through SSM or VS Code Ports,
and open the printed authorization URL on the laptop. Port 8085 is commonly
occupied by media-proxy in this dev environment. Neither a port change nor
--insecure fixes an unregistered redirect.
References¶
For Windows devices that need another client, see the OpenVPN companion relay pilot and its laptop guide.