Auto: s7-e2 — PLC password / protection-level handling

Closes #303
This commit is contained in:
Joseph Doherty
2026-04-26 10:51:07 -04:00
parent e0f3d1c925
commit 30c3b10c94
9 changed files with 887 additions and 0 deletions

View File

@@ -25,6 +25,8 @@ dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli -- --help
| `--tsap-mode` | `Auto` | ISO-on-TCP connection class: `Auto` / `Pg` / `Op` / `S7Basic` / `Other`. Hardened S7-1500 / ET 200SP CPUs may require `Op` or `S7Basic`. See [s7.md TSAP / Connection Type](v2/s7.md#tsap--connection-type). |
| `--local-tsap` | (unset) | Optional 16-bit local TSAP override (e.g. `0x0200`). Required when `--tsap-mode Other`; wins over class default under Pg/Op/S7Basic. |
| `--remote-tsap` | (unset) | Optional 16-bit remote TSAP override. Required when `--tsap-mode Other`; wins over class default under Pg/Op/S7Basic. |
| `--password` | (unset) | Connection-level password sent right after `OpenAsync`. Used by hardened S7-300/400 (protection levels 1-3) and S7-1200/1500 (TIA Portal *Connection Mechanism* gate). Never logged. NB: S7netplus 0.20 doesn't expose `SendPassword`; the CLI prints a one-line warning and continues. See [s7.md "PLC password / protection levels"](v2/s7.md#plc-password--protection-levels). |
| `--protection-level` | `Auto` | Declarative hint: `Auto` / `None` / `Level1` / `Level2` / `Level3` (S7-300/400) / `ConnectionMechanism` (S7-1200/1500). Diagnostic only — the wire-side unlock is driven by `--password`. |
| `--verbose` | off | Serilog debug output |
## PUT/GET must be enabled
@@ -139,6 +141,43 @@ wrong `--slot` produces also shows up when the CPU rejects PG class — try
endpoint config. See [s7.md TSAP / Connection Type](v2/s7.md#tsap--connection-type)
for the byte table and motivation.
### Hardened CPU — supplying a connection-level password
```powershell
# S7-300 protection-level 2 — read+write protected without unlock.
otopcua-s7-cli read -h 192.168.1.31 -c S7300 --slot 2 `
--password "tia-portal-set-password" `
--protection-level Level2 `
-a DB1.DBW0 -t Int16
# S7-1500 ConnectionMechanism — TIA Portal Protection & Security pane gate.
otopcua-s7-cli probe -h 10.50.12.30 `
--tsap-mode Op `
--password "tia-portal-set-password" `
--protection-level ConnectionMechanism
```
The password is emitted to the PLC immediately after `OpenAsync` succeeds and
before the pre-flight PUT/GET probe runs (the same probe that would otherwise
be the first operation a hardened CPU refuses). Never logged in any form;
identifier-only success line is `S7 password sent for {Host}`.
**S7netplus 0.20 does not yet expose a public `SendPassword`** — the driver
discovers the method reflectively, so a future minor release will be picked
up automatically. Until then, configuring `--password` on a hardened CPU
emits this warning at Init:
```
[Warning] S7 password is set on driver '<id>' against host '<host>', but
the linked S7netplus library does not expose SendPassword; password is
being ignored at the wire.
```
Init still completes (the COTP handshake itself doesn't require the
password) but the first read against a hardened CPU will surface
`BadDeviceFailure`. See [s7.md "PLC password / protection levels"](v2/s7.md#plc-password--protection-levels)
for the full motivation, the no-log invariant, and the workaround matrix.
### `subscribe`
```powershell

View File

@@ -109,6 +109,27 @@ or we ship a raw S7comm PDU helper. See
[`docs/v2/s7.md` "CPU diagnostics (SZL)"](../v2/s7.md#cpu-diagnostics-szl)
for the wire-status detail.
### 7. Password / protection levels — not modelled by snap7
PR-S7-E2 / [#303](https://github.com/dohertj2/lmxopcua/issues/303) adds
`Password` + `ProtectionLevel` options that emit a connection-level password
right after `OpenAsync`. **snap7 does not model S7 protection levels** — the
simulator accepts every connection regardless of the password set on the
client, so the integration profile cannot distinguish "password sent
correctly" from "password ignored". Coverage stays at the unit-test seam:
`S7PasswordOptionsTests` injects a fake `IS7PlcAuthGate` to assert the
dispatch contract (Password=null skips the call; Password+SupportsSendPassword
calls the gate; auth-failed wraps to a clean `InvalidOperationException`),
plus the no-log invariant on `S7DriverOptions.ToString()`.
The wire path is also fundamentally limited until S7netplus 0.20 exposes a
public `SendPassword` — the driver currently logs a warning and continues
when the API is missing. See
[`docs/v2/s7.md` "PLC password / protection levels"](../v2/s7.md#plc-password--protection-levels)
for the library-limitation note. Live-firmware coverage of the unlock path
requires a hardened S7-1500 lab rig with TIA Portal "Protection & Security"
configured, which is parked as a follow-up.
## When to trust the S7 tests, when to reach for a rig
| Question | Unit tests | Real PLC |

View File

@@ -1156,6 +1156,123 @@ diagnostics tests where you want every read to hit the wire.
}
```
## PLC password / protection levels
PR-S7-E2 (issue #303) adds a connection-level password option for hardened
deployments. The driver emits the password to the PLC immediately after
`OpenAsync` succeeds and before the pre-flight PUT/GET probe runs (the same
pre-flight read that would otherwise be the first operation a hardened CPU
refuses).
### Options
| Option | Default | Purpose |
| ----------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Password` | `null` | Connection-level password. Secret — never logged. `null` or empty = no password is sent. |
| `ProtectionLevel` | `Auto` | Declarative hint about the PLC's protection scheme. One of `Auto`, `None`, `Level1`, `Level2`, `Level3` (S7-300/400 SFC 109/110 levels), or `ConnectionMechanism` (S7-1200/1500 TIA Portal "Protection & Security" pane). |
### S7-300 / S7-400 protection levels (1, 2, 3)
S7-300/400 firmware exposes three CPU-side protection levels:
* **Level 1** — write protection. Reads work without a password; writes
(parameter, DB, M/Q changes) require an unlock.
* **Level 2** — read and write protection. Both kinds of operation require
the password.
* **Level 3** — full protection. Even online presence detection / status
list reads require the password.
Set `ProtectionLevel = Level1` / `Level2` / `Level3` and supply
`Password` to match the level configured in the CPU's HW Config dialog.
The level value is descriptive — the driver doesn't switch behaviour
between Level1/2/3, since the wire-side `SendPassword` is the same call
in all three cases. The hint surfaces in the driver-diagnostics RPC so a
"PLC said Level 3 but config says Level 1" mismatch is spottable from the
Admin UI.
### S7-1200 / S7-1500 connection mechanism
S7-1200/1500 firmware uses a different gate: TIA Portal's "Protection &
Security" pane has a single **Connection Mechanism** dropdown that, when
set to anything stricter than "No access", requires every PG/HMI/SCADA
connection to authenticate after the COTP handshake. The wire-level
exchange is the same `SendPassword` call but the diagnostic flag is
distinct, so set `ProtectionLevel = ConnectionMechanism` for these
families.
### No-log invariant
`Password` is a secret. The driver MUST NOT include the password value in
log lines, exception messages, or diagnostic surfaces. Specifically:
* `S7DriverOptions.ToString()` redacts the field as `***`.
* `S7Driver`'s success log line is `S7 password sent for {Host}`
identifier-only, no value.
* The "S7netplus does not expose SendPassword" warning logs the host name
and driver instance ID only, never the password.
* Authentication-failure exceptions wrap the inner `S7.Net.PlcException`
but their own message says only "S7 password authentication failed for
host '{Host}'" — no password value.
Any new logging surface that flows an `S7DriverOptions` value MUST
continue to redact. See the FOCAS-F4-d
`docs/v2/focas-deployment.md` § "FOCAS password handling" entry for the
sister no-log discipline on the FOCAS driver.
### Library limitation — S7netplus 0.20
**S7netplus 0.20.0 (the pinned dependency) does not expose a public
`SendPassword` method.** The driver discovers the method reflectively
(checking for `SendPasswordAsync(string, CancellationToken)` first, then
`SendPassword(string)`) so a future minor release that ships the API
will be picked up automatically without a code change here.
Until the upstream lands, configuring `Password` on a hardened CPU
produces this Init-time warning:
```
[Warning] S7 password is set on driver '<DriverInstanceId>' against
host '<Host>', but the linked S7netplus library does not expose
SendPassword; password is being ignored at the wire. Hardened-CPU
connect may fail at first read.
```
Init still completes — the COTP/S7comm handshake itself doesn't require
the password — but the first read against a hardened CPU will surface
`BadDeviceFailure` because PUT/GET-disabled and "level-3 protection"
return identical "function not allowed" PDUs at the wire layer.
If your S7-1200/1500 deployment requires `ConnectionMechanism`, the
near-term workarounds are:
1. **Lower the protection setting** in TIA Portal's Protection & Security
pane to "Full access (no protection)" for the duration of the
evaluation.
2. **Configure a separate non-hardened connection** on a CP module that
the driver can target while keeping the production endpoint hardened.
3. **Track upstream S7netplus** for a `SendPassword` PR (the package owner
has discussed adding it; see issue
<https://github.com/S7NetPlus/s7netplus/issues>).
For S7-300/400 CPUs, levels 1 and 2 leave at least *read* access open
without a password, so most monitoring use cases work without
`SendPassword` until the library catches up — only Level 3 and the
S7-1200/1500 ConnectionMechanism require the wire-level unlock.
### JSON config example
```json
{
"DriverConfig": {
"Host": "192.168.10.50",
"Port": 102,
"CpuType": "S71500",
"Password": "tia-portal-set-password",
"ProtectionLevel": "ConnectionMechanism"
}
}
```
## References
1. Siemens Industry Online Support, *Modbus/TCP Communication between SIMATIC S7-1500 / S7-1200 and Modbus/TCP Controllers with Instructions `MB_CLIENT` and `MB_SERVER`*, Entry ID 102020340, V6 (Feb 2021). https://cache.industry.siemens.com/dl/files/340/102020340/att_118119/v6/net_modbus_tcp_s7-1500_s7-1200_en.pdf