From 5f97c9d1ed8fcdf2fb4057cd4127c1cc3cd075b7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 4 Jun 2026 16:37:52 -0400 Subject: [PATCH] docs(glauth): point all dev/test LDAP at the shared GLAuth on 10.100.0.35 deployment.md / CLAUDE.md / env_vars.md: the per-app LDAP (scadabridge-ldap container, OtOpcUa DevStubMode, per-box C:\publish\glauth) is replaced by one shared zb-shared-glauth on 10.100.0.35:3893 (dc=zb,dc=local); source of truth infra/glauth/. Fixed stale baseDNs (dc=lmxopcua/dc=otopcua -> dc=zb). --- CLAUDE.md | 7 +- deployment.md | 341 ++++++++++++++++++++++++++++++++++++++++++++++++++ env_vars.md | 244 ++++++++++++++++++++++++++++++++++++ 3 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 deployment.md create mode 100644 env_vars.md diff --git a/CLAUDE.md b/CLAUDE.md index d9f3c31..a987ac6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -281,9 +281,14 @@ dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj # ScadaBridge (~/Desktop/ScadaBridge) dotnet build ZB.MOM.WW.ScadaBridge.slnx bash docker/deploy.sh # rebuild + redeploy the 8-node cluster -cd infra && docker compose up -d # local test services (LDAP, SQL, OPC UA, SMTP, REST, Traefik) +cd infra && docker compose up -d # local test services (SQL, OPC UA, SMTP, REST, Traefik) — LDAP is NOT here ``` +> **Shared GLAuth (all three apps):** LDAP auth for every local dev/test stack is provided by a +> single `zb-shared-glauth` container on the Linux fixture host **`10.100.0.35:3893`** +> (`baseDN dc=zb,dc=local`, Transport=None). Source of truth and deploy runbook: +> [`scadaproj/infra/glauth/`](infra/glauth/) (`config.toml` + `docker-compose.yml` + `README.md`). + ## Refreshing this index This file is meant to be re-scanned when `scadaproj` is opened in Claude Code: diff --git a/deployment.md b/deployment.md new file mode 100644 index 0000000..b6cd8c7 --- /dev/null +++ b/deployment.md @@ -0,0 +1,341 @@ +# Deployment & Environments — SCADA/OT family + +> How the sister projects are deployed: environments, hosts, SSH access, Docker/Traefik +> topology, databases, and the full service/port map. Compiled **2026-06-03** by reading the +> actual compose/Traefik/SSH files (not docs alone). For the per-service **environment +> variables** see the companion [`env_vars.md`](env_vars.md). +> +> **Source confidence:** container/port/Traefik/DB facts below are read straight from the +> compose + `traefik/*.yml` files. SSH facts are from `~/.ssh/config` + `~/.ssh/known_hosts`. +> Where a fact is referenced in repo docs but not pinned in a config/script on this machine, +> it's marked _(referenced, not scripted in-repo)_ — don't treat those as automated. + +--- + +## 1. Environment inventory + +| Environment | Where it runs | What it is | Entry point | +|---|---|---|---| +| **ScadaBridge `docker`** | This Mac (Docker Desktop/OrbStack) | Full hub-and-spoke: 2 Central + 3 sites ×2 nodes | `http://localhost:9000` | +| **ScadaBridge `docker-env2`** | This Mac | Second isolated cluster: 2 Central + 1 site ×2 nodes | `http://localhost:9100` | +| **ScadaBridge `infra`** | This Mac | Shared backing services (MSSQL, OPC-UA sims, SMTP, REST, Playwright) — **not** LDAP (see shared GLAuth below) | n/a (deps) | +| **OtOpcUa `otopcua-dev`** | This Mac | 3 independent Akka clusters (MAIN + SITE-A + SITE-B) sharing one ConfigDb | `http://localhost:9200` | +| **MxAccessGateway** | `windev` (10.100.0.48), Windows | Windows-native gRPC gateway + per-session x86 worker (no Docker) | `http://10.100.0.48:5120` (gRPC) | +| **Production (VD03)** | `wonder-app-vd03.zmr.zimmer.com` | Single-node ScadaBridge + MxGateway prod host | see `docs/operations/` runbooks | + +The three ScadaBridge stacks share one external Docker network **`scadabridge-net`**; the +OtOpcUa `otopcua-dev` stack runs on its **own** default network (`otopcua-dev_default`) and is +network-isolated from ScadaBridge. All local stacks can run simultaneously — host ports do not +collide (see [§7](#7-consolidated-host-port-map)). + +> On this Apple-Silicon Mac, MSSQL runs under amd64 emulation (slow first-ready; the "platform +> does not match" warning is expected/benign). See [[scadabridge-local-deploy-gotchas]]. + +--- + +## 2. Hosts & SSH connectivity + +### 2.1 Host inventory + +| Host | Address | OS | Role | SSH port | +|---|---|---|---|---| +| **This Mac** | local | macOS (darwin) | Dev workstation — runs all local Docker stacks | n/a | +| **windev** | `10.100.0.48` | Windows | OtOpcUa Windows-service host **+** MxAccessGateway (gRPC `5120` / dashboard `5130`) | 22 | +| **fixture host** | `10.100.0.35` | Debian/Linux + Docker | OtOpcUa driver **integration-test fixtures** + a test SQL Server | 22 | +| **VD03 (prod)** | `wonder-app-vd03.zmr.zimmer.com` | Windows | Production single-node ScadaBridge + MxGateway | **2222** | +| **gitea** | `gitea.dohertylan.com` (`10.100.0.228`) | Linux | Git remotes + NuGet feed (`/api/packages/dohertj2/nuget`) | 22 | + +All are on the private `10.x` lab network — a LAN/VPN connection is required. + +### 2.2 How to connect (passwordless SSH) + +Auth is **key-based (passwordless)** with `~/.ssh/id_ed25519` (a legacy `~/.ssh/id_rsa` exists +as fallback). Only **one** host alias is defined in `~/.ssh/config`: + +```sshconfig +# ~/.ssh/config (verified) +Include ~/.orbstack/ssh/config # OrbStack local Linux VMs — use `ssh orb` / `orb` CLI + +Host windev + HostName 10.100.0.48 + User dohertj2 + IdentityFile ~/.ssh/id_ed25519 + # Port 22 (default) +``` + +| Target | Command | Notes | +|---|---|---| +| **windev** (Win host) | `ssh windev` | Configured alias; user `dohertj2`, key `id_ed25519`, port 22 | +| **fixture host** | `ssh dohertj2@10.100.0.35` | In `known_hosts`; **no** config alias — pass user explicitly; port 22, key-based | +| **VD03 (prod)** | `ssh dohertj2@wonder-app-vd03.zmr.zimmer.com -p 2222` | In `known_hosts` on **port 2222** (the only non-standard SSH port); user/key not pinned in config — confirm before use | +| **local Linux VMs** | `ssh orb` / `orb` | OrbStack-managed | + +> ⚠️ `~/bin` is **empty** on this Mac. OtOpcUa's `CLAUDE.md` mentions an `lmxopcua-fix` helper "in +> `~/bin`" for controlling the `10.100.0.35` fixture containers — it is **not present here** (it's a +> Windows-side helper). On this machine, drive the fixture host with direct SSH, e.g. +> `ssh dohertj2@10.100.0.35 'docker compose -f /opt/otopcua-/docker-compose.yml up -d'`. +> Treat the exact remote paths/commands as _(referenced, not scripted in-repo)_ — verify on the host. + +--- + +## 3. ScadaBridge deployment + +.NET 10 + Akka.NET. One image `scadabridge:latest` (built by `docker/build.sh`) backs every node; +role is chosen by `SCADABRIDGE_CONFIG` (`Central`|`Site`) → `appsettings.{role}.json`. Central is a +2-node Akka cluster (split-brain resolver = `keep-oldest`); each Site is its **own** 2-node Akka +cluster reached from Central via ClusterClient. + +### 3.1 `docker/` — primary 3-site cluster (network `scadabridge-net`) + +| Service | Container | Host→container ports | Role | Volumes | +|---|---|---|---|---| +| central-a | `scadabridge-central-a` | `9001:5000` (UI+Inbound API), `9011:8081` (Akka) | Central | `central-node-a/appsettings.Central.json` (ro), `…/logs` | +| central-b | `scadabridge-central-b` | `9002:5000`, `9012:8081` | Central | `central-node-b/…` | +| site-a-a | `scadabridge-site-a-a` | `9021:8082` (Akka), `9023:8083` (gRPC) | Site | `site-a-node-a/{appsettings.Site.json,data,logs}` | +| site-a-b | `scadabridge-site-a-b` | `9022:8082`, `9024:8083` | Site | `site-a-node-b/…` | +| site-b-a | `scadabridge-site-b-a` | `9031:8082`, `9033:8083` | Site | `site-b-node-a/…` | +| site-b-b | `scadabridge-site-b-b` | `9032:8082`, `9034:8083` | Site | `site-b-node-b/…` | +| site-c-a | `scadabridge-site-c-a` | `9041:8082`, `9043:8083` | Site | `site-c-node-a/…` | +| site-c-b | `scadabridge-site-c-b` | `9042:8082`, `9044:8083` | Site | `site-c-node-b/…` | +| traefik | `scadabridge-traefik` | `9000:80` (Central LB), `8180:8080` (dashboard) | LB | `traefik/{traefik,dynamic}.yml` (ro) | + +All `restart: unless-stopped`; image `scadabridge:latest` (traefik `traefik:v3.4`). +**Access:** Central UI/API via LB `http://localhost:9000`; direct nodes `:9001`/`:9002`; Traefik +dashboard `http://localhost:8180`; Management API `http://localhost:9000/management`; health +`…/health/ready` + `…/health/active`. + +### 3.2 `docker-env2/` — secondary 1-site cluster (same `scadabridge-net`) + +| Service | Container | Host→container ports | Role | +|---|---|---|---| +| central-a | `scadabridge-env2-central-a` | `9101:5000`, `9111:8081` | Central | +| central-b | `scadabridge-env2-central-b` | `9102:5000`, `9112:8081` | Central | +| site-x-a | `scadabridge-env2-site-x-a` | `9121:8082`, `9123:8083` | Site | +| site-x-b | `scadabridge-env2-site-x-b` | `9122:8082`, `9124:8083` | Site | +| traefik | `scadabridge-env2-traefik` | `9100:80` (LB), `8181:8080` (dashboard) | LB | + +**Access:** LB `http://localhost:9100`; direct `:9101`/`:9102`; dashboard `http://localhost:8181`. +This cluster's DBs and **auth cookie name** are distinct from `docker/` so the two can run on +`localhost` at once — cookie `ZB.MOM.WW.ScadaBridge.Auth.env2` vs the default; see +[[scadabridge-local-deploy-gotchas]]. + +### 3.3 `infra/` — shared backing services (network `scadabridge-net`) + +| Service | Container | Image | Host ports | Purpose | +|---|---|---|---|---| +| mssql | `scadabridge-mssql` | `mcr.microsoft.com/mssql/server:2022-latest` | `1433:1433` | SQL Server — Central DBs for **both** clusters; named vol `scadabridge-mssql-data`; init via `/docker-entrypoint-initdb.d/{setup,machinedata_seed,setup-env2}.sql` | +| opcua | `scadabridge-opcua` | `mcr.microsoft.com/iotedge/opc-plc:latest` | `50000:50000`, `8080:8080` | OPC-UA simulator 1 (`--unsecuretransport --autoaccept`) | +| opcua2 | `scadabridge-opcua2` | `…/opc-plc:latest` | `50010:50010`, `8081:8080` | OPC-UA simulator 2 | +| smtp | `scadabridge-smtp` | `axllent/mailpit:latest` | `1025:1025`, `8025:8025` | SMTP sink + web UI (`http://localhost:8025`) | +| restapi | `scadabridge-restapi` | local build `./restapi` | `5200:5200` | Test REST endpoint | +| playwright | `scadabridge-playwright` | `mcr.microsoft.com/playwright:v1.58.2-noble` | `3000:3000` | Browser-automation server | + +> **LDAP is NOT started by `infra/`.** The per-app `scadabridge-ldap` container has been retired +> (commented out in `infra/docker-compose.yml`). All three apps (ScadaBridge, OtOpcUa, MxAccessGateway) +> now share a single **`zb-shared-glauth`** container on the Linux fixture host **`10.100.0.35:3893`** +> (`baseDN dc=zb,dc=local`, Transport=None). Source of truth and deploy/verify runbook: +> **`scadaproj/infra/glauth/`** (`config.toml` + `docker-compose.yml` + `README.md`); deploy by +> scp-ing those two files to `10.100.0.35` and running `docker compose up -d`. + +### 3.4 Traefik (ScadaBridge) + +Both clusters use a file provider + insecure API dashboard. `traefik.yml`: entrypoint `web:80`, +`api.dashboard: true / insecure: true`, file provider `dynamic.yml`. `dynamic.yml` router +`central` (`PathPrefix(/)` → service `central`) load-balances the two Central containers with an +**active health check** on `/health/active` (interval 5s, timeout 3s) — so traffic only routes to +the active leader (standby returns 503 and is dropped from rotation): + +```yaml +# docker/traefik/dynamic.yml (env2 points at scadabridge-env2-central-a/-b) +http: + routers: { central: { rule: "PathPrefix(`/`)", service: central, entryPoints: [web] } } + services: + central: + loadBalancer: + healthCheck: { path: /health/active, interval: 5s, timeout: 3s } + servers: [ {url: "http://scadabridge-central-a:5000"}, {url: "http://scadabridge-central-b:5000"} ] +``` + +### 3.5 Databases (ScadaBridge) + +- **Central → MSSQL** (`scadabridge-mssql:1433`), app login `scadabridge_app` / `ScadaBridge_Dev1#` 🔒(dev-only): + - `docker/`: `ScadaBridgeConfig` + `ScadaBridgeMachineData` + - `docker-env2/`: `ScadaBridgeConfig2` + `ScadaBridgeMachineData2` + - Created by `infra/mssql/setup.sql` + `setup-env2.sql` at MSSQL init; EF Core migrations run on Central startup; `docker-env2/init-db.sh` ensures the env2 DBs before deploy; `seed-sites.sh` seeds Site rows post-deploy. +- **Site → SQLite**, per node under the mounted `…/data` volume (`SiteDbPath`, plus a store-and-forward DB). Not networked, not replicated across hosts. + +### 3.6 Deploy commands (ScadaBridge) + +```bash +cd ~/Desktop/ScadaBridge +cd infra && docker compose up -d # 1) backing services (MSSQL, OPC-UA, SMTP, REST) — LDAP is shared glauth on 10.100.0.35 (scadaproj/infra/glauth/) +bash docker/build.sh # 2) create scadabridge-net (if missing) + build scadabridge:latest +bash docker/deploy.sh # 3) up -d --force-recreate; prints access points (9000/9001/9002/8180) +bash docker/seed-sites.sh # 4) seed sites + data-connections (optional) +# env2 cluster: +bash docker-env2/deploy.sh # reuses the image; runs init-db.sh; ports 9100/9101/9102/8181 +``` + +> **Caveat:** `deploy.sh` does `up -d --force-recreate`, starting both Central nodes at once — they +> can split-brain on a simultaneous start. Start Central **sequenced** (central-a → wait `/health/active` +> 200 → central-b). Central also requires `ScadaBridge__InboundApi__ApiKeyPepper` (dev value is inline in +> both composes). Full detail: [[scadabridge-local-deploy-gotchas]]. + +--- + +## 4. OtOpcUa deployment (`otopcua-dev`) + +.NET 10 OPC-UA server. **Three independent Akka clusters** share the single `OtOpcUa` ConfigDb +(multi-tenancy via the `ServerCluster` table); Akka isolation is by disjoint seed lists (same +system name `otopcua`, internal remoting port `4053`). Built locally from `docker-dev/Dockerfile` +→ image `otopcua-host:dev`. **No per-app LDAP container** — `docker-dev` is un-stubbed +(`Authentication__Ldap__DevStubMode` removed) and binds the **shared GLAuth** at +`10.100.0.35:3893` (`baseDN dc=zb,dc=local`, Transport=None). Start the shared glauth first via +`scadaproj/infra/glauth/` if it is not already running. + +| Service | Container | Host→container ports | Cluster / role | +|---|---|---|---| +| sql | (`otopcua-dev-sql-1`) | `14330:1433` | SQL Server 2022 — the shared `OtOpcUa` ConfigDb | +| cluster-seed | one-shot | — | `mssql-tools` running `/seed/entrypoint.sh` (idempotent ServerCluster/ClusterNode seed) | +| admin-a | host | _(none — internal `:9000` UI behind Traefik)_ | MAIN, role `admin` (seed) | +| admin-b | host | _(none)_ | MAIN, role `admin` (joins admin-a) | +| driver-a | host | `4840:4840` (OPC UA) | MAIN, role `driver` | +| driver-b | host | `4841:4840` | MAIN, role `driver` | +| site-a-1 | host | `4842:4840` | SITE-A, `admin,driver` (seed) | +| site-a-2 | host | `4843:4840` | SITE-A, `admin,driver` | +| site-b-1 | host | `4844:4840` | SITE-B, `admin,driver` (seed) | +| site-b-2 | host | `4845:4840` | SITE-B, `admin,driver` | +| traefik | host | `9200:80` (Admin UI LB), `8089:8080` (dashboard) | `traefik:v3.1` | + +- **OPC UA endpoints:** `opc.tcp://localhost:4840` (driver-a) … `:4845` (site-b-2). Admin nodes serve no OPC UA. +- **Admin UI (Traefik, sticky cookie `otopcua_lb`, health-checked on `/health/active`):** + - MAIN cluster: `http://localhost:9200` + - SITE-A: `http://site-a.localhost:9200` · SITE-B: `http://site-b.localhost:9200` (Host-header routing; macOS auto-resolves `*.localhost`) + - Traefik dashboard: `http://localhost:8089` +- **DB:** `sql` service, `14330:1433`, SA `OtOpcUa!Dev123` 🔒(dev-only), database `OtOpcUa`; EF auto-migrates on host start, then `cluster-seed` inserts the 3 ServerCluster + 6 ClusterNode rows. +- **Deploy:** `docker compose -f docker-dev/docker-compose.yml up -d --build` ; tear down with `… down -v`. +- **Galaxy link:** driver nodes resolve `GALAXY_MXGW_API_KEY` and connect out to MxAccessGateway (see §5). + +> **Integration-test fixtures (separate from this stack)** run on the Linux **fixture host +> `10.100.0.35`** (Modbus `:5020`, Allen-Bradley `:44818`, S7 `:102`, OPC-UA `:50000`, SQL `:14330`). +> Those are test endpoints, not the deployed app; per-fixture env defaults are in [`env_vars.md`](env_vars.md) §1.3. + +--- + +## 5. MxAccessGateway deployment (Windows-native, no Docker) + +Two processes: an **x64 .NET 10 Server** (ASP.NET Core gRPC + Blazor dashboard) and a **per-session +x86 .NET 4.8 Worker** that owns the 32-bit AVEVA MXAccess COM/STA. Windows-only. Deployed on +**`windev` (10.100.0.48)** and **VD03**, run as a **Windows Service via NSSM** (config delivered as +`Kestrel__Endpoints__…` environment variables, not `appsettings.json`). + +### 5.1 Endpoint/port map + +| Endpoint | Default URL | Protocol | Config key | Purpose | +|---|---|---|---|---| +| **Http (gRPC)** | `http://0.0.0.0:5120` (h2c) | HTTP/2 cleartext | `Kestrel__Endpoints__Http__Url` / `__Protocols=Http2` | Public gRPC: sessions, MxCommand/MxEvent, Galaxy browse | +| **Dashboard** | `http://0.0.0.0:5130` | HTTP/1.1 | `Kestrel__Endpoints__Dashboard__Url` | Blazor dashboard + SignalR hubs + `/login` | + +Local dev (`launchSettings.json`): gRPC `http://localhost:5120` (https dev profile adds `7121`). +TLS optional — set `…Http__Url=https://…`; the gateway auto-generates a self-signed cert if none is +supplied (`docs/GatewayConfiguration.md`). Dashboard cookie name is now configurable +(`MxGateway:Dashboard:CookieName`). + +### 5.2 Run / host + +```powershell +# local dev +dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj +# the x86 worker must be published first; path = MxGateway:Worker:ExecutablePath +dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86 +``` + +- **Worker model:** the Server spawns one `ZB.MOM.WW.MxGateway.Worker.exe` (x86) **per gRPC session**; + IPC over a named pipe (`\\.\pipe\mxgateway-` + a per-session `MXGATEWAY_WORKER_NONCE`); + heartbeat 5s / grace 15s; max 64 concurrent sessions. The worker exits when the session closes. +- **Production hosts:** both `10.100.0.48` and `wonder-app-vd03` serve gRPC on `:5120` (per + `docs/GatewayConfiguration.md`). + +### 5.3 Who connects to it + +| Client | Connects to | Auth | +|---|---|---| +| OtOpcUa `GalaxyDriver` | `http://10.100.0.48:5120` (gRPC) | API key via `GALAXY_MXGW_API_KEY` (`mxgw_…` bearer) 🔒 | +| ScadaBridge MxGateway adapter | same gRPC endpoint `:5120` | API key | + +--- + +## 6. Cross-project runtime data flow (deployed) + +``` +AVEVA Galaxy (Wonderware) ──MXAccess COM (32-bit)──► MxAccessGateway (windev:5120 gRPC / :5130 dashboard) + ▲ ▲ + OtOpcUa GalaxyDriver ───gRPC────┘ │ gRPC + (otopcua-dev: opc.tcp :4840–4845) │ + │ OPC UA │ + ▼ │ + ScadaBridge DCL ◄──OPC UA──┐ ┌──MxGateway adapter──┘ + (docker :9000 / env2 :9100) └───┘ +``` + +ScadaBridge reaches Wonderware data two ways: **(1)** OPC UA → OtOpcUa → gateway, or **(2)** its +MxGateway adapter → gateway directly. The break surface is the wire contracts (the gateway `.proto`s +and OtOpcUa's OPC-UA address space), not compile references. + +--- + +## 7. Consolidated host port map + +Every published host port across the local stacks (no collisions — all can run at once): + +| Port | → Container:port | Service | Stack | +|---|---|---|---| +| 1025 | `scadabridge-smtp`:1025 | SMTP submission | infra | +| 1433 | `scadabridge-mssql`:1433 | SQL Server (ScadaBridge Central DBs) | infra | +| 3000 | `scadabridge-playwright`:3000 | Playwright server | infra | +| 3893 | `zb-shared-glauth`:3893 on **10.100.0.35** | LDAP (shared GLAuth — remote fixture host, not a local container) | scadaproj/infra/glauth/ | +| 5200 | `scadabridge-restapi`:5200 | Test REST API | infra | +| 8025 | `scadabridge-smtp`:8025 | Mailpit web UI | infra | +| 8080 | `scadabridge-opcua`:8080 | OPC-UA sim 1 web UI | infra | +| 8081 | `scadabridge-opcua2`:8080 | OPC-UA sim 2 web UI | infra | +| 50000 | `scadabridge-opcua`:50000 | OPC-UA sim 1 endpoint | infra | +| 50010 | `scadabridge-opcua2`:50010 | OPC-UA sim 2 endpoint | infra | +| 9000 | `scadabridge-traefik`:80 | **Central UI/API (LB)** | docker | +| 8180 | `scadabridge-traefik`:8080 | Traefik dashboard | docker | +| 9001 / 9002 | central-a / central-b :5000 | Central UI+Inbound API (direct) | docker | +| 9011 / 9012 | central-a / central-b :8081 | Akka remoting | docker | +| 9021–9024 | site-a-a/b :8082 / :8083 | Site A Akka / gRPC | docker | +| 9031–9034 | site-b-a/b :8082 / :8083 | Site B Akka / gRPC | docker | +| 9041–9044 | site-c-a/b :8082 / :8083 | Site C Akka / gRPC | docker | +| 9100 | `scadabridge-env2-traefik`:80 | **Central UI/API (LB)** | docker-env2 | +| 8181 | `scadabridge-env2-traefik`:8080 | Traefik dashboard | docker-env2 | +| 9101 / 9102 | env2 central-a / central-b :5000 | Central (direct) | docker-env2 | +| 9111 / 9112 | env2 central-a / central-b :8081 | Akka remoting | docker-env2 | +| 9121–9124 | env2 site-x-a/b :8082 / :8083 | Site X Akka / gRPC | docker-env2 | +| 14330 | `otopcua-dev` sql :1433 | SQL Server (`OtOpcUa` DB) | otopcua-dev | +| 4840 / 4841 | driver-a / driver-b :4840 | OPC UA (MAIN) | otopcua-dev | +| 4842 / 4843 | site-a-1 / site-a-2 :4840 | OPC UA (SITE-A) | otopcua-dev | +| 4844 / 4845 | site-b-1 / site-b-2 :4840 | OPC UA (SITE-B) | otopcua-dev | +| 9200 | `otopcua-dev` traefik :80 | **Admin UI (LB)** | otopcua-dev | +| 8089 | `otopcua-dev` traefik :8080 | Traefik dashboard | otopcua-dev | + +**Remote (non-local) endpoints:** MxAccessGateway gRPC `10.100.0.48:5120` (h2c) / dashboard `:5130`; +production gRPC on `wonder-app-vd03:5120`. SSH: windev/fixture/gitea on `22`, **VD03 on `2222`**. + +--- + +## 8. Secrets & dev-only values + +Every credential shown above (`OtOpcUa!Dev123`, `ScadaBridge_Dev1#`, the inline API-key peppers, +the `docker-dev` JWT signing key, the `mxgw_…` API key) is a **dev-only placeholder** for the local +stacks — never reuse as a real secret. Production injects real secrets out-of-band (NSSM env / secret +store), per ScadaBridge `docs/operations/inbound-api-key-reissue.md` (the VD03 runbook). The full +🔒 secret inventory and the `__`-env-var override forms are in [`env_vars.md`](env_vars.md) §5. + +## 9. Production (VD03) — pointer + +`wonder-app-vd03.zmr.zimmer.com` (SSH `:2222`) runs the production single-node ScadaBridge and the +MxGateway (gRPC `:5120`). The production install is **not a scripted in-repo flow** here — the +operational procedures live in ScadaBridge `docs/operations/` (`failover-procedures.md`, +`maintenance-procedures.md`, `inbound-api-key-reissue.md`, `troubleshooting-guide.md`). Treat any +prod service/port specifics not in those runbooks as unverified. diff --git a/env_vars.md b/env_vars.md new file mode 100644 index 0000000..55515aa --- /dev/null +++ b/env_vars.md @@ -0,0 +1,244 @@ +# Environment Variables — SCADA/OT family + +> Cross-project audit of every environment variable used or read by the sister projects +> and the shared `ZB.MOM.WW.*` libraries. Compiled **2026-06-03** by sweeping C# reads, +> Docker/compose, `launchSettings.json`, shell/PowerShell scripts, and CI for each repo. +> This is a **summary index** — when a value matters operationally, confirm against the +> cited `file:line` in the owning repo (paths below are relative to each project root). + +## Scope + +| Project | Root | Covered | +|---|---|---| +| OtOpcUa | `~/Desktop/OtOpcUa` | Host, Galaxy/Historian drivers, docker-dev, tests, CI | +| MxAccessGateway | `~/Desktop/MxAccessGateway` | Server (x64), Worker (x86), client CLI, tests, pack script | +| ScadaBridge | `~/Desktop/ScadaBridge` | Host (Central/Site), CLI, `docker/`, `docker-env2/`, `infra/`, tests | +| Shared libs | `~/Desktop/scadaproj/ZB.MOM.WW.*` | Auth, Theme, Health, Telemetry, Configuration, Audit (code + build scripts) | + +## How env vars reach these apps + +All four .NET apps call `AddEnvironmentVariables()`, so **any** configuration key is overridable +from the environment using the **double-underscore (`__`) → colon (`:`)** convention +(`ScadaBridge__InboundApi__ApiKeyPepper` overrides `ScadaBridge:InboundApi:ApiKeyPepper`). Array +indices use a trailing `__0`, `__1` (`Cluster__SeedNodes__0`). Because *every* options key is +technically settable this way, the tables below split into: + +- **Direct reads / operationally-set** — `Environment.GetEnvironmentVariable(...)` in code, or + values actually set in compose/launchSettings/scripts. These are the ones you'll really touch. +- **Config keys overridable via `__`** — the validated/notable options that are normally in + `appsettings*.json` but are commonly (or required to be) supplied via environment in containers. + Not every options key is reproduced — only validated, secret, or container-set ones. + +> **Secrets:** rows marked 🔒 are secrets. Per the Auth/Config normalization, peppers/keys/passwords +> are **per-environment secrets injected out-of-band** (secret store / orchestrator), never committed. +> The dev-only values that *do* appear in compose are explicitly insecure placeholders for the local +> clusters — see `scadabridge-local-deploy-gotchas` and `docs/operations/inbound-api-key-reissue.md`. + +--- + +## 1. OtOpcUa + +### 1.1 Direct reads / operationally-set (runtime) + +| Variable | Where | Purpose | Req? / default | Process | +|---|---|---|---|---| +| `OTOPCUA_ROLES` | `src/Server/.../Host/Program.cs:31` | Comma-list of roles (admin, driver) for conditional wiring | optional | Host | +| `ASPNETCORE_ENVIRONMENT` | `Host/Properties/launchSettings.json:9` | ASP.NET Core environment | optional / `Production` | Host | +| `ASPNETCORE_URLS` | `docker-dev/docker-compose.yml` | Kestrel bind address/port | optional | Host | +| `GALAXY_MXGW_API_KEY` 🔒 | `docker-dev/docker-compose.yml`; resolved in `GalaxyDriver.cs:466` | mxaccessgw API key for the Galaxy driver | required if Galaxy driver deployed | Driver (Galaxy) | +| `OTOPCUA_CONFIG_CONNECTION` 🔒 | `Configuration/DesignTimeDbContextFactory.cs:25` | SQL connection for EF design-time (`dotnet ef`) | required for migrations tooling | build-time | +| `OTOPCUA_HISTORIAN_PIPE` | `Driver.Historian.Wonderware/Program.cs:32` | Named-pipe name for the Historian sidecar IPC | required | Historian sidecar | +| `OTOPCUA_ALLOWED_SID` | `…/Program.cs:34` | Windows SID allowed to connect to the sidecar | required | Historian sidecar | +| `OTOPCUA_HISTORIAN_SECRET` 🔒 | `…/Program.cs:36` | Shared secret for named-pipe auth | required | Historian sidecar | +| `OTOPCUA_HISTORIAN_ENABLED` | `…/Program.cs:48` | Init the Historian SDK (else pipe-only) | optional / `false` | Historian sidecar | +| `OTOPCUA_HISTORIAN_SERVER` | `…/Program.cs:89` | Wonderware Historian host | optional / `localhost` | Historian sidecar | +| `OTOPCUA_HISTORIAN_PORT` | `…/Program.cs:90` | Historian port | optional / `32568` | Historian sidecar | +| `OTOPCUA_HISTORIAN_INTEGRATED` | `…/Program.cs:91` | Use Windows Integrated Security | optional / `true` | Historian sidecar | +| `OTOPCUA_HISTORIAN_USER` | `…/Program.cs:92` | SQL user (when not integrated) | optional | Historian sidecar | +| `OTOPCUA_HISTORIAN_PASS` 🔒 | `…/Program.cs:93` | SQL password (when not integrated) | optional | Historian sidecar | +| `OTOPCUA_HISTORIAN_TIMEOUT_SEC` | `…/Program.cs:94` | SQL command timeout (s) | optional / `30` | Historian sidecar | +| `OTOPCUA_HISTORIAN_MAX_VALUES` | `…/Program.cs:95` | Max values per read query | optional / `10000` | Historian sidecar | +| `OTOPCUA_HISTORIAN_COOLDOWN_SEC` | `…/Program.cs:96` | Failure cooldown before retry (s) | optional / `60` | Historian sidecar | +| `OTOPCUA_HISTORIAN_SERVERS` | `…/Program.cs:99` | Comma-list of historian servers for failover | optional | Historian sidecar | +| `OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED` | `…/Program.cs:125` | Enable alarm-event writer to historian | optional / `true` when enabled | Historian sidecar | + +> The Galaxy API key supports prefix resolution (`GalaxyDriver.cs:466`): `env:VAR` (read another +> env var), `file:PATH` (read a file), `dev:LITERAL` (dev only), or an unprefixed literal. + +### 1.2 Config keys overridable via `__` (set in `docker-dev/docker-compose.yml`) + +| Variable | Purpose | Req? | +|---|---|---| +| `ConnectionStrings__ConfigDb` 🔒 | SQL connection for the OtOpcUa config DB | required | +| `Cluster__Hostname` / `Cluster__Port` / `Cluster__PublicHostname` | Akka remoting bind/advertise | required | +| `Cluster__SeedNodes__0` | Initial seed node URL | required | +| `Cluster__Roles__0` (`__1`) | Cluster role assignment (admin/driver) | required | +| `Security__Jwt__SigningKey` 🔒 | JWT signing key (≥32 bytes) | required | +| `Security__Jwt__Issuer` / `Security__Jwt__Audience` | JWT claims | required | +| `Security__Ldap__*` | LDAP host/bind (in `appsettings.admin*.json`) | per-deploy | +| `Authentication__Ldap__DevStubMode` | Dev LDAP stub (any user → FleetAdmin) — **removed from docker-dev**; `docker-dev` now binds the shared GLAuth on `10.100.0.35:3893` | optional / `false` | + +### 1.3 Docker infra & test fixtures (OtOpcUa) + +- **docker-dev SQL:** `ACCEPT_EULA=Y`, `SA_PASSWORD` 🔒 (`OtOpcUa!Dev123` dev-only), `MSSQL_PID=Developer`. +- **docker-dev seed (`seed/entrypoint.sh`):** `SQL_HOST` / `SQL_USER` / `SQL_PASSWORD` 🔒 / `SQL_DATABASE` (defaults `sql`/`sa`/`OtOpcUa!Dev123`/`OtOpcUa`). +- **Integration compose:** SQL (`SA_PASSWORD` 🔒 `OtOpcUa!Harness123`) + OpenLDAP (`LDAP_ROOT`, `LDAP_ADMIN_USERNAME`, `LDAP_ADMIN_PASSWORD` 🔒, `LDAP_USERS`, `LDAP_PASSWORDS` 🔒, `LDAP_USER_DC`) — this OpenLDAP instance is **integration-test-only**; the standard DEV auth is the shared GLAuth at `10.100.0.35:3893` (`dc=zb,dc=local`, see `scadaproj/infra/glauth/`). +- **Test-fixture overrides (all optional, default to the shared host `10.100.0.35`):** `OPCUA_SIM_ENDPOINT`, `MODBUS_SIM_ENDPOINT`, `MODBUS_SIM_PROFILE`, `AB_SERVER_ENDPOINT`, `AB_SERVER_PROFILE`, `S7_SIM_ENDPOINT`, `OTOPCUA_FOCAS_SIM_ENDPOINT`, `OTOPCUA_FOCAS_SIM_PROFILE`, `TWINCAT_HOST`, `TWINCAT_NETID`, `TWINCAT_PORT`, `AB_LEGACY_ENDPOINT`, `AB_LEGACY_CIP_PATH`, `AB_LEGACY_COMPOSE_PROFILE`, `OTOPCUA_CONFIG_TEST_SERVER`, `OTOPCUA_CONFIG_TEST_SA_PASSWORD` 🔒, `OTOPCUA_HARNESS_USE_SQL`, `OTOPCUA_HARNESS_USE_LDAP`, `MXGW_ENDPOINT`, `D1_SMOKE_OUT`. +- **CI (`.github/workflows/v2-*.yml`):** `DOTNET_NOLOGO=1`, `DOTNET_CLI_TELEMETRY_OPTOUT=1`. + +--- + +## 2. MxAccessGateway + +> Ships **no** Docker/compose assets — it's a Windows-native app (x64 .NET 10 Server + x86 .NET 4.8 Worker). +> The Server spawns the Worker via `ProcessStartInfo.Environment`, passing the two `MXGATEWAY_WORKER_*` vars below. + +### 2.1 Direct reads / operationally-set (runtime) + +| Variable | Where | Purpose | Req? / default | Process | +|---|---|---|---|---| +| `MXGATEWAY_WORKER_NONCE` 🔒 | Server `WorkerProcessLauncher.cs:180` → Worker `WorkerOptionsParser.cs:78` | Per-session handshake nonce (kept off the command line) | required (generated per session) | Server→Worker | +| `MXGATEWAY_WORKER_PIPE_CONNECT_ATTEMPT_TIMEOUT_MS` | Server `WorkerProcessLauncher.cs:181` → Worker `WorkerPipeClient.cs:255` | Per-attempt named-pipe connect timeout | optional / `2000` | Server→Worker | +| `ASPNETCORE_CONTENTROOT` | `GatewayApplication.cs:130` | Content-root override (logs/wwwroot) | optional / auto | Server | +| `ASPNETCORE_ENVIRONMENT` | `Server/Properties/launchSettings.json:10` | Environment selector | optional / `Production` | Server | + +### 2.2 Config keys overridable via `__` (notable) + +| Variable | Purpose | Req? | +|---|---|---| +| `MxGateway__ApiKeyPepper` 🔒 | HMAC pepper for API-key secrets in the SQLite auth DB | required when auth Mode=ApiKey | +| `MxGateway__Authentication__Mode` / `__SqlitePath` / `__PepperSecretName` | Auth mode, auth DB path, pepper config-key name | per-deploy | +| `MxGateway__Worker__ExecutablePath` / `__WorkingDirectory` / `__StartupTimeoutSeconds` / `__PipeConnectAttemptTimeoutMilliseconds` | x86 worker launch config | per-deploy | +| `MxGateway__Sessions__*` / `MxGateway__Events__QueueCapacity` | Session pool & event queue tuning | optional | +| `MxGateway__Galaxy__ConnectionString` 🔒 | SQL connection for Galaxy browse RPCs | per-deploy | +| `MxGateway__Alarms__*` / `MxGateway__Dashboard__*` | Alarm monitor & dashboard config | optional | +| `MxGateway__Telemetry__Exporter` / `__OtlpEndpoint` | OpenTelemetry exporter selection / OTLP endpoint | optional | +| `MxGateway__Tls__SelfSignedCertPath` | Self-signed PFX path | optional | +| `Kestrel__Endpoints__Http__Url` / `__Protocols`, `Kestrel__Endpoints__Dashboard__Url` | gRPC (h2c) + dashboard endpoints | per-deploy | + +### 2.3 Client CLI, tests & build script (MxGateway) + +- **Client CLI / smoke tests:** `MXGATEWAY_ENDPOINT` (default `http://localhost:5000`), `MXGATEWAY_API_KEY` 🔒 (`MxGatewayClientCli.cs:289`). +- **Live-test opt-in gates (set to `1` to enable; otherwise skipped):** `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS`, `MXGATEWAY_RUN_LIVE_LDAP_TESTS`. +- **Live-test params (optional):** `MXGATEWAY_LIVE_MXACCESS_WORKER_EXE`, `_ITEM`, `_CLIENT_NAME`, `_EVENT_TIMEOUT_SECONDS`, `_WRITE_SECURED_USER`, `_WRITE_SECURED_PASSWORD` 🔒, `MXGATEWAY_LIVE_GALAXY_CONN` 🔒. +- **Pack/publish (`scripts/pack-clients.ps1`):** `GITEA_USERNAME`, `GITEA_TOKEN` 🔒 (required with `-Publish`), `JAVA_HOME` (Java client build). + +--- + +## 3. ScadaBridge + +> Role is selected by `SCADABRIDGE_CONFIG` (`Central`|`Site`), which picks `appsettings.{role}.json` +> (falls back to `DOTNET_ENVIRONMENT`, then `Production`). The pre-host `StartupValidator` / +> `ConfigPreflight` enforces the **required** keys below and fails fast if any are missing/invalid. + +### 3.1 Direct reads (C#) + +| Variable | Where | Purpose | Req? / default | Scope | +|---|---|---|---|---| +| `SCADABRIDGE_CONFIG` | `Host/Program.cs:31` | Role selector → appsettings file | optional (→ `DOTNET_ENVIRONMENT` → `Production`) | both | +| `DOTNET_ENVIRONMENT` | `Host/Program.cs:32` | Fallback role/env selector | optional | both | +| `ASPNETCORE_ENVIRONMENT` | `Host/Program.cs:245` | Dev-mode check | optional | both | +| `SCADABRIDGE_DESIGNTIME_CONNECTIONSTRING` 🔒 | `ConfigurationDatabase/DesignTimeDbContextFactory.cs:48` | EF tooling connection (`dotnet ef`) | optional (build-time) | design-time | +| `SCADABRIDGE_MANAGEMENT_URL` | `CLI/CliConfig.cs:72` | Management API URL for the CLI | optional | CLI | +| `SCADABRIDGE_FORMAT` | `CLI/CliConfig.cs:76` | CLI default output format | optional / `json` | CLI | +| `SCADABRIDGE_USERNAME` | `CLI/CliConfig.cs:81` | CLI LDAP username (safer than `--password`) | optional | CLI | +| `SCADABRIDGE_PASSWORD` 🔒 | `CLI/CliConfig.cs:85` | CLI LDAP password | optional | CLI | + +### 3.2 Config keys required at startup (`__` form, enforced by `StartupValidator`) + +| Variable | Purpose | Constraint | Scope | +|---|---|---|---| +| `ScadaBridge__Node__Role` | `Central` or `Site` | required | all | +| `ScadaBridge__Node__NodeHostname` | Hostname advertised to cluster | required, non-empty | all | +| `ScadaBridge__Node__RemotingPort` | Akka remoting TCP port | required, 1–65535 (default 8081) | all | +| `ScadaBridge__Cluster__SeedNodes__0` / `__1` | Akka seed addresses | required, ≥2 entries | all | +| `ScadaBridge__Database__ConfigurationDb` 🔒 | SQL config-DB connection | required | Central | +| `ScadaBridge__Security__JwtSigningKey` 🔒 | Cookie-JWT HMAC key | required, ≥32 chars | Central | +| `ScadaBridge__Security__Ldap__Server` | LDAP host | required | Central | +| `ScadaBridge__InboundApi__ApiKeyPepper` 🔒 | Peppered-HMAC inbound API-key pepper | **required, ≥16 chars** | Central | +| `ScadaBridge__Node__SiteId` | Site identifier | required | Site | +| `ScadaBridge__Database__SiteDbPath` | Site-local SQLite path | required | Site | +| `ScadaBridge__Node__GrpcPort` | gRPC streaming port | default 8083; ≠ remoting/metrics | Site | +| `ScadaBridge__Node__MetricsPort` | Prometheus `/metrics` port | default 8084; ≠ remoting/grpc | Site | + +> LDAP service-account fields (`ScadaBridge__Security__Ldap__ServiceAccountDn`, +> `__ServiceAccountPassword` 🔒, `__SearchBase`) are validated **post-host** by `LdapOptionsValidator` +> for Central, not in the fail-fast pre-host pass. + +### 3.3 Config keys overridable via `__` (optional, not validated) + +Large surface — all bound from the role appsettings and overridable via env. Grouped by module +(each prefixed `ScadaBridge__`): + +- **Node:** `Node__NodeName`. **Database:** `Database__MachineDataDb` 🔒, `Database__SkipMigrations`. +- **Security:** `Security__Ldap__Port|Transport|AllowInsecure`, `Security__JwtExpiryMinutes`, `Security__IdleTimeoutMinutes`, `Security__JwtRefreshThresholdMinutes`, `Security__RequireHttpsCookie`. +- **Cluster (SBR):** `Cluster__SplitBrainResolverStrategy|StableAfter|HeartbeatInterval|FailureDetectionThreshold|MinNrOfMembers|DownIfAlone`. +- **Communication:** `Communication__DeploymentTimeout|LifecycleTimeout|QueryTimeout|TransportHeartbeatInterval|TransportFailureThreshold`, `Communication__CentralContactPoints__0…` (Site→Central ClusterClient). +- **HealthMonitoring:** `HealthMonitoring__ReportInterval|OfflineTimeout|CentralOfflineTimeout`. +- **InboundApi:** `InboundApi__DefaultMethodTimeout|MaxRequestBodyBytes`. +- **Notification (SMTP):** `Notification__SmtpServer|SmtpPort|AuthMode|FromAddress|ConnectionTimeoutSeconds|MaxConcurrentConnections`. +- **NotificationOutbox (Central):** `NotificationOutbox__DispatchInterval|DispatchBatchSize|StuckAgeThreshold|TerminalRetention|PurgeInterval|DeliveredKpiWindow`. +- **Transport (Central):** `Transport__SourceEnvironment|BundleSessionTtlMinutes|MaxBundleSizeMb|MaxBundleEntryDecompressedMb|MaxBundleEntryCount|MaxBundleEntryCompressionRatio|MaxUnlockAttemptsPerSession|MaxUnlockAttemptsPerIpPerHour|Pbkdf2Iterations`. +- **Logging:** `Logging__MinimumLevel`. +- **DataConnection (Site):** `DataConnection__ReconnectInterval|TagResolutionRetryInterval|WriteTimeout|StableConnectionThreshold`. +- **StoreAndForward (Site):** `StoreAndForward__SqliteDbPath|ReplicationEnabled|DefaultRetryInterval|DefaultMaxRetries`. +- **SiteEventLog (Site):** `SiteEventLog__RetentionDays|MaxStorageMb|DatabasePath|PurgeInterval`. +- **SiteRuntime (Site):** `SiteRuntime__StartupBatchSize|StartupBatchDelayMs|MaxScriptCallDepth|ScriptExecutionTimeoutSeconds|StreamBufferSize|ScriptExecutionThreadCount`. +- **Telemetry (Site):** `Telemetry__Exporter|OtlpEndpoint`. + +### 3.4 Docker / infra / build (ScadaBridge) + +- **`docker/docker-compose.yml`** (3-site cluster) — Central: `SCADABRIDGE_CONFIG=Central`, `ASPNETCORE_ENVIRONMENT=Development`, `ASPNETCORE_URLS=http://+:5000`, `ScadaBridge__InboundApi__ApiKeyPepper` 🔒 = `dev-only-insecure-pepper-docker-cluster-0001`; Sites: `SCADABRIDGE_CONFIG=Site`. +- **`docker-env2/docker-compose.yml`** (1-site cluster) — same shape, pepper 🔒 = `dev-only-insecure-pepper-env2-cluster-0001` (distinct per environment). +- **`infra/docker-compose.yml`** — MSSQL (`ACCEPT_EULA=Y`, `MSSQL_SA_PASSWORD` 🔒 `ScadaBridge_Dev1#`, `MSSQL_PID=Developer`), Mailpit SMTP (`MP_SMTP_AUTH_ACCEPT_ANY=1`, `MP_SMTP_AUTH_ALLOW_INSECURE=1`, `MP_MAX_MESSAGES=500`), REST API (`API_NO_AUTH=0`, `PORT=5200`). +- **`docker/Dockerfile` build args:** `NUGET_GITEA_USER` / `NUGET_GITEA_PASS` 🔒 → injected as `NuGetPackageSourceCredentials_dohertj2-gitea`. **`docker/build.sh`** reads `MXGW_NUGET_USER` / `MXGW_NUGET_PASS` 🔒 from host env (blank ⇒ anonymous feed). +- **Seed/init scripts** (`docker*/init-db.sh`, `seed-sites.sh`) hardcode the dev SA password 🔒 `ScadaBridge_Dev1#`. +- **`launchSettings.json`** profiles set `DOTNET_ENVIRONMENT`, `ASPNETCORE_ENVIRONMENT`, `SCADABRIDGE_CONFIG` (`Central`/`Site`). + +--- + +## 4. Shared libraries (`ZB.MOM.WW.*`) + +The libraries deliberately read almost nothing from the environment directly — config flows through +strongly-typed options bound by the **consuming** app. Notable exceptions: + +| Variable | Where | Purpose | Req? / default | +|---|---|---|---| +| `GITEA_NUGET_SOURCE` | `ZB.MOM.WW.{Auth,Theme,Audit}/build/push.sh:16` | Gitea NuGet feed URL for `dotnet nuget push` | required to publish | +| `GITEA_NUGET_KEY` 🔒 | `…/build/push.sh:17` | Gitea token (`package:write`) | required to publish | +| `ZB_LDAP_IT` | `ZB.MOM.WW.Auth/tests/.../GLAuthIntegrationTests.cs:48` | Gate flag (`1`) to run the live LDAP test | optional (skipped if unset) | +| `ZB_LDAP_SERVER` / `_PORT` / `_BASE` / `_SVC_DN` / `_SVC_PW` 🔒 / `_USER` / `_PW` 🔒 / `_USERATTR` | `…/GLAuthIntegrationTests.cs:52-59` | Live LDAP test connection params | optional (defaults: `localhost`/`3893`/`dc=zb,dc=local`/…); point at the shared GLAuth (`10.100.0.35:3893`, `dc=zb,dc=local`) for the live test | + +**Telemetry:** `ZB.MOM.WW.Telemetry` does **not** read standard `OTEL_*` env vars — OTel identity/exporter +come from `ZbTelemetryOptions` passed to `AddZbTelemetry()`. It only reads system properties +(`Environment.MachineName`, `Environment.ProcessId` in `ZbResource.cs:20`) to form +`service.instance.id` / `host.name`. Consuming apps that want OTLP wire it via their own +`…Telemetry__OtlpEndpoint` config key (see MxGateway §2.2, ScadaBridge §3.3). + +**Health, Configuration, Audit, Theme:** no direct environment-variable reads (code or build). + +--- + +## 5. Cross-cutting cheat sheet + +### Standard framework vars (honored everywhere) +`ASPNETCORE_ENVIRONMENT`, `ASPNETCORE_URLS`, `ASPNETCORE_CONTENTROOT`, `DOTNET_ENVIRONMENT`, +`DOTNET_NOLOGO`, `DOTNET_CLI_TELEMETRY_OPTOUT`. + +### Build/publish (Gitea NuGet feed) — naming differs per repo +| Repo | Vars | +|---|---| +| Shared libs | `GITEA_NUGET_SOURCE`, `GITEA_NUGET_KEY` 🔒 | +| MxGateway pack | `GITEA_USERNAME`, `GITEA_TOKEN` 🔒 | +| ScadaBridge image build | `MXGW_NUGET_USER`, `MXGW_NUGET_PASS` 🔒 (→ `NUGET_GITEA_USER`/`NUGET_GITEA_PASS` build args) | + +### Secrets inventory (🔒) — inject out-of-band, never commit real values +- **Peppers:** `ScadaBridge__InboundApi__ApiKeyPepper` (≥16, Central-only), `MxGateway__ApiKeyPepper`. +- **Signing/JWT keys:** `Security__Jwt__SigningKey` (OtOpcUa), `ScadaBridge__Security__JwtSigningKey`. +- **API keys / nonces:** `GALAXY_MXGW_API_KEY`, `MXGATEWAY_API_KEY`, `MXGATEWAY_WORKER_NONCE`, `OTOPCUA_HISTORIAN_SECRET`. +- **DB / LDAP / SMTP passwords:** all `*SA_PASSWORD`/`MSSQL_SA_PASSWORD`, `ConnectionStrings__ConfigDb`, `ScadaBridge__Database__ConfigurationDb`, `*Ldap*Password`, `SCADABRIDGE_PASSWORD`, `OTOPCUA_HISTORIAN_PASS`. +- **Feed tokens:** `GITEA_NUGET_KEY`, `GITEA_TOKEN`, `MXGW_NUGET_PASS`/`NUGET_GITEA_PASS`. + +> The only secret-typed values that legitimately appear in source are the **dev-only, insecure** +> local-cluster placeholders in `docker*/docker-compose.yml` and the dev SA passwords in the +> `infra`/seed scripts — usable for the local stacks only, never as real secrets.