Correct the spec against the host as actually observed

Verified on lin001 over SSH on 2026-08-20, read-only:

- Port 502 is bound to 10.0.0.17, not 0.0.0.0, so unauthenticated Modbus is
  not internet-reachable at the Docker level. This was an open risk in §3 and
  an unchecked item in §15; it is now a confirmation. openplc-runtime also
  publishes 8443 (the Runtime web UI) on the same private address, which the
  spec did not mention.
- openplc-runtime is not the only published port on the host: caddy, wireguard,
  mosquitto and chirpstack-gateway-bridge all publish on 0.0.0.0. The
  no-published-ports rule still applies in full to what we build, but the
  "one deliberate exception" framing was wrong and invited over-reading.
- Port 22 is open to the internet. Added to §15 as an open item for the same
  NSG review.
- 21 containers running, not 22; no stopped containers.
- /datadisk is 46% used with InfluxDB at 55 GB, up from 43%/52 GB. Recorded
  the growth rate so it can be budgeted for.

CLAUDE.md restates two of these rules and is updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-08-20 14:29:41 +10:00
parent 60555e0885
commit 4826255672
2 changed files with 35 additions and 11 deletions

View file

@ -62,7 +62,7 @@ These are **code paths, not prompt instructions**. Prompts are advisory and mode
|---|---|---| |---|---|---|
| `yau-poc-cicore1` | SCADA server. Operator Chromium runs here. Holds the **raw historian**. Acts as **Modbus master**, polling the PLC on `lin001`. | **Built** | | `yau-poc-cicore1` | SCADA server. Operator Chromium runs here. Holds the **raw historian**. Acts as **Modbus master**, polling the PLC on `lin001`. | **Built** |
| `yau-sls-poc-imh` | SQL Server. Holds a **copy of the raw SCADA historian** — safe to query directly with no impact on the live system. | **⚠ Pending setup** | | `yau-sls-poc-imh` | SQL Server. Holds a **copy of the raw SCADA historian** — safe to query directly with no impact on the live system. | **⚠ Pending setup** |
| `yau-sls-poc-lin001` | Ubuntu 22.04 Docker host, `10.0.0.17`. 22 containers already running, **including `openplc-runtime`** — the PLC for this demo, serving Modbus TCP on host port 502. **Everything we build goes here.** | **Built** | | `yau-sls-poc-lin001` | Ubuntu 22.04 Docker host, `10.0.0.17`. 21 containers already running, **including `openplc-runtime`** — the PLC for this demo, serving Modbus TCP on port 502. **Everything we build goes here.** | **Built** |
### `openplc-runtime` — added after the host brief was written ### `openplc-runtime` — added after the host brief was written
@ -70,8 +70,27 @@ The PLC for this demo runs as a container on `lin001`. SCADA on `cicore1` polls
Consequences worth knowing: Consequences worth knowing:
- **It is the one deliberate exception to the no-published-ports rule** (host brief §10.6). Modbus is not HTTP and cannot go through Caddy. That exception is justified; it does not generalise to anything we build. - **It is not the only published port on this host** — an earlier draft of this
- **Port 502 has no authentication and no encryption.** Modbus never has. It must be reachable from the LAN and VPN only — confirm the Azure NSG does not expose it to the internet. This is worth checking now rather than assuming. document said it was. Verified on the host 2026-08-20: `caddy` publishes 80 and 443,
`wireguard` 443/udp, `mosquitto` 1883, and `chirpstack-gateway-bridge` 1700/udp, all on
`0.0.0.0`. The no-published-ports rule (host brief §10.6) is about **new web services**,
which belong on the `proxy` network behind Caddy. It still applies in full to
everything we build. Do not read the precedent more widely than that.
- **Port 502 is bound to `10.0.0.17`, not `0.0.0.0`** — verified on the host 2026-08-20:
```
ports=map[502/tcp:[{10.0.0.17 502}] 8443/tcp:[{10.0.0.17 8443}]]
ss -lntp → LISTEN 10.0.0.17:502
```
So it is published on the VNet interface only and is not internet-reachable at the
Docker level, whatever the NSG says. That is a stronger position than this document
originally assumed, and it is the reason the NSG item in §15 is now a confirmation
rather than an open risk. **`openplc-runtime` also publishes 8443** — the OpenPLC
Runtime web UI — on the same private address; earlier drafts did not mention it.
- **Port 502 still has no authentication and no encryption.** Modbus never has. The
binding above is what contains it, so anything that changes the binding to `0.0.0.0`,
or any NSG rule that exposes the VNet address, removes the only control on it.
- **`lin001` is now in the control path for the demo.** Restarting Caddy or Authelia doesn't touch Modbus, but a host-level problem — disk full, OOM, reboot — now stops the simulated plant as well as the web stack. Weigh that before any disruptive work, and announce it. - **`lin001` is now in the control path for the demo.** Restarting Caddy or Authelia doesn't touch Modbus, but a host-level problem — disk full, OOM, reboot — now stops the simulated plant as well as the web stack. Weigh that before any disruptive work, and announce it.
- **Do not add `openplc-runtime` to Watchtower's update list**, and do not restart it casually while a demo is running. - **Do not add `openplc-runtime` to Watchtower's update list**, and do not restart it casually while a demo is running.
@ -97,7 +116,7 @@ What local Postgres (`pg-ai`) is still for:
`lin001` is **shared and live** — it runs customer-facing demos. From the host brief, §10: `lin001` is **shared and live** — it runs customer-facing demos. From the host brief, §10:
1. **Growing data goes on `/datadisk`, never `/`.** Root is 62 GB and has hit 100% before, killing Grafana. 1. **Growing data goes on `/datadisk`, never `/`.** Root is 62 GB and has hit 100% before, killing Grafana.
2. **No published host ports.** New services join the external `proxy` network and are reached through Caddy. 2. **No published host ports for anything we build.** New services join the external `proxy` network and are reached through Caddy. Some existing containers do publish ports — `caddy`, `wireguard`, `mosquitto`, `chirpstack-gateway-bridge`, `openplc-runtime` — because they carry non-HTTP protocols that cannot go through a reverse proxy. Nothing in the AI stack is in that category.
3. **Never bypass Authelia.** Omitting `import authelia` silently makes a service public. 3. **Never bypass Authelia.** Omitting `import authelia` silently makes a service public.
4. **`~/authelia/configuration.yml` is root-owned.** Edit with `sudo`, back up first (`.bak-<purpose>-<date>`), and know that restarting Authelia **logs out every active user**. 4. **`~/authelia/configuration.yml` is root-owned.** Edit with `sudo`, back up first (`.bak-<purpose>-<date>`), and know that restarting Authelia **logs out every active user**.
5. **AD group membership must be DIRECT** — nested membership silently fails. 5. **AD group membership must be DIRECT** — nested membership silently fails.
@ -106,7 +125,7 @@ What local Postgres (`pg-ai`) is still for:
8. **Announce restarts of Caddy or Authelia** — they interrupt everyone. 8. **Announce restarts of Caddy or Authelia** — they interrupt everyone.
9. **No secrets in Git or in compose files.** The Grafana admin password sitting in `~/docker-compose.yml` is a known defect, not a pattern to copy. Use a `0600` env file, following `~/authelia/authelia.env`. 9. **No secrets in Git or in compose files.** The Grafana admin password sitting in `~/docker-compose.yml` is a known defect, not a pattern to copy. Use a `0600` env file, following `~/authelia/authelia.env`.
10. Orphan-container warnings are expected (shared Compose project name) — ignore them. 10. Orphan-container warnings are expected (shared Compose project name) — ignore them.
11. **`openplc-runtime` is live control for the demo.** Do not restart, update or reconfigure it as a side effect of AI work. Do not reuse its published-port pattern for anything we build. 11. **`openplc-runtime` is live control for the demo.** Do not restart, update or reconfigure it as a side effect of AI work. Do not reuse its published-port pattern for anything we build, and do not change its `10.0.0.17` binding to `0.0.0.0` — that binding is what keeps unauthenticated Modbus off the internet.
--- ---
@ -389,7 +408,7 @@ Each phase ends in a gate. Gates are not suggestions.
### Phase 1 — Compose scaffold and `pg-ai` ### Phase 1 — Compose scaffold and `pg-ai`
**Tasks** **Tasks**
1. `mkdir -p /datadisk/pg-ai /datadisk/ai-docs`**check `df -h /datadisk` first** (43% used, InfluxDB owns 52 GB). 1. `mkdir -p /datadisk/pg-ai /datadisk/ai-docs`**check `df -h /datadisk` first** (46% used as at 2026-08-20, InfluxDB owns 55 GB and is growing).
2. Write `~/ai-compose.yml` with `pg-ai` only. Internal network, no published ports, log rotation, healthcheck. 2. Write `~/ai-compose.yml` with `pg-ai` only. Internal network, no published ports, log rotation, healthcheck.
3. `~/ai/pg-ai.env` at `0600`. 3. `~/ai/pg-ai.env` at `0600`.
4. Apply `001_schema.sql`, `003_roles.sql`. Load `equipment.csv` and `tags.csv` with alias arrays. 4. Apply `001_schema.sql`, `003_roles.sql`. Load `equipment.csv` and `tags.csv` with alias arrays.
@ -569,7 +588,7 @@ Deployed early, deliberately: from here on, every experiment is traced.
## 13. Cost and capacity ## 13. Cost and capacity
- **`/datadisk` is 128 GB and 43% used**, with InfluxDB at 52 GB and growing. Check `df -h` before every phase that writes data. The Grafana disk alert is **UI-only — nobody gets notified.** - **`/datadisk` is 128 GB and 46% used** as at 2026-08-20 (65 GB free), with InfluxDB at 55 GB and growing — it is effectively the only consumer. It was 43%/52 GB when this document was first written, so budget for roughly 1 GB a week of InfluxDB growth on top of whatever the AI stack adds. `/` is 62 GB and 24% used (48 GB free). Check `df -h` before every phase that writes data. The Grafana disk alert is **UI-only — nobody gets notified.**
- `CHEAP_DEPLOYMENT` for the classifier, entity extraction and tool selection; `CHAT_DEPLOYMENT` for final prose only. - `CHEAP_DEPLOYMENT` for the classifier, entity extraction and tool selection; `CHAT_DEPLOYMENT` for final prose only.
- Cap output tokens — output bills several times higher than input. - Cap output tokens — output bills several times higher than input.
- Keep system prompts byte-identical between calls so prompt caching applies. - Keep system prompts byte-identical between calls so prompt caching applies.
@ -586,7 +605,7 @@ Deployed early, deliberately: from here on, every experiment is traced.
- No OT/IT firewall boundary — one flat `10.0.0.0/24` PoC network - No OT/IT firewall boundary — one flat `10.0.0.0/24` PoC network
- Public egress to Azure OpenAI, no private endpoint - Public egress to Azure OpenAI, no private endpoint
- Chromium running on the SCADA VM itself - Chromium running on the SCADA VM itself
- Modbus TCP on port 502 with no authentication or encryption — inherent to the protocol; contained by NSG/VPN scope only - Modbus TCP on port 502 with no authentication or encryption — inherent to the protocol; contained by its bind to `10.0.0.17` plus NSG/VPN scope. The OpenPLC Runtime web UI on 8443 is contained the same way and nothing else
- Single host, no HA — `lin001` is now a single point of failure for **both** the demo estate and the simulated plant's PLC - Single host, no HA — `lin001` is now a single point of failure for **both** the demo estate and the simulated plant's PLC
- Shared `azureuser` login; no per-person audit trail on the host - Shared `azureuser` login; no per-person audit trail on the host
- Shared service account to `imh`; no per-operator row-level security - Shared service account to `imh`; no per-operator row-level security
@ -605,7 +624,8 @@ Production closes these in the order: network segmentation → secrets → SQL g
- [ ] `pg-ai` included in a backup routine and a restore tested once - [ ] `pg-ai` included in a backup routine and a restore tested once
- [ ] Eval scorecard committed, broken down by question class - [ ] Eval scorecard committed, broken down by question class
- [ ] `/datadisk` headroom checked and recorded - [ ] `/datadisk` headroom checked and recorded
- [ ] Azure NSG confirmed to expose port 502 to LAN/VPN only, never the internet - [x] Port 502 confirmed bound to `10.0.0.17`, not `0.0.0.0` — verified on the host 2026-08-20, so it is not internet-reachable at the Docker level. Still worth confirming the NSG agrees, and worth re-checking after any change to `openplc-runtime`
- [ ] Port 22 on the VM is open to the internet (`20.211.144.151:22`). Key-only auth, but review it in the same NSG pass and restrict to office and VPN ranges
- [ ] **Section 2 reviewed with an OT/safety representative before any operator sees a demo** - [ ] **Section 2 reviewed with an OT/safety representative before any operator sees a demo**
- [ ] Section 14 reviewed and confirmed as still-accurate shortcuts - [ ] Section 14 reviewed and confirmed as still-accurate shortcuts
- [ ] The new services added to the host documentation, following the existing change-log convention - [ ] The new services added to the host documentation, following the existing change-log convention

View file

@ -22,14 +22,18 @@ Advisory beats Historical. Partly-advisory is advisory.
## Host rules — inherited, non-negotiable ## Host rules — inherited, non-negotiable
- Growing data goes on **`/datadisk`**, never `/`. Root is 62 GB and has hit 100% before. - Growing data goes on **`/datadisk`**, never `/`. Root is 62 GB and has hit 100% before.
- **No published host ports.** Join the external `proxy` network, reach it through Caddy. - **No published host ports for anything we build.** Join the external `proxy` network, reach it
through Caddy. Some existing containers do publish (`caddy`, `wireguard`, `mosquitto`,
`chirpstack-gateway-bridge`, `openplc-runtime`) — they carry non-HTTP protocols. We do not.
- **Never omit `import authelia`** from a Caddyfile block — it silently makes a service public. - **Never omit `import authelia`** from a Caddyfile block — it silently makes a service public.
- `~/authelia/configuration.yml` is **root-owned**: `sudo`, back up as `.bak-<purpose>-<date>`. - `~/authelia/configuration.yml` is **root-owned**: `sudo`, back up as `.bak-<purpose>-<date>`.
Restarting Authelia **logs out every active user** — announce it first. Restarting Authelia **logs out every active user** — announce it first.
- AD group membership must be **DIRECT**. Nested membership silently fails. - AD group membership must be **DIRECT**. Nested membership silently fails.
- `pg-ai` and `cube` are pinned — **do not add them to Watchtower's update list**. - `pg-ai` and `cube` are pinned — **do not add them to Watchtower's update list**.
- **`openplc-runtime` is live control for this demo.** Never restart, update or reconfigure it as - **`openplc-runtime` is live control for this demo.** Never restart, update or reconfigure it as
a side effect of AI work. Do not copy its published-port pattern. a side effect of AI work. Do not copy its published-port pattern, and never change its
`10.0.0.17` binding to `0.0.0.0` — that binding is what keeps unauthenticated Modbus off the
internet.
- Never install on, write to or restart `cicore1`. Never exceed read-only on `imh`. - Never install on, write to or restart `cicore1`. Never exceed read-only on `imh`.
- **No secrets in Git, compose files, logs or error messages.** `0600` env files under `~/ai/`. - **No secrets in Git, compose files, logs or error messages.** `0600` env files under `~/ai/`.