@@ -183,3 +183,108 @@ granular kill switches are lightweight runtime toggles, not config-DB
|
||||
redeploys. PMC in particular should default OFF in production and only
|
||||
flip on for windows where the ladder team has signed off on the write
|
||||
path.
|
||||
|
||||
## FOCAS password handling — issue #271 (F4-d)
|
||||
|
||||
Some controllers (16i + certain 30i firmwares with parameter-protect on)
|
||||
gate `cnc_wrparam` and selected reads behind a connection-level password.
|
||||
The driver supports this via the `Password` field on `FocasDeviceOptions`
|
||||
which is emitted via `cnc_wrunlockparam` on connect and re-emitted on any
|
||||
`EW_PASSWD` read/write retry path. See
|
||||
[`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) § "FOCAS password" for the
|
||||
driver-side behaviour; this section covers the deployment side.
|
||||
|
||||
### Storage in `appsettings.json`
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"Drivers": {
|
||||
"Focas01": {
|
||||
"DriverConfigJson": {
|
||||
"Backend": "fwlib",
|
||||
"Series": "Sixteen_i",
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "focas://10.0.0.5:8193",
|
||||
"Password": "1234"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For dev environments, the password is materialised under
|
||||
`.local/focas-passwords.txt` (or whichever .local subkey the deployment
|
||||
team prefers); production deployments use the same secrets-store /
|
||||
KeyVault pattern the LDAP `Authentication.Ldap.Password` field follows.
|
||||
**The `.local/` directory is .gitignore'd** — this is the same posture
|
||||
as `.local/galaxy-host-secret.txt` and other dev secrets in this repo.
|
||||
|
||||
### No-log invariant
|
||||
|
||||
The driver guarantees the password is **never logged**:
|
||||
|
||||
1. **`FocasDeviceOptions` ToString redaction.** The record overrides
|
||||
`PrintMembers` so any Serilog destructure of the device options renders
|
||||
`Password = ***` when the field is non-null. This catches the most
|
||||
common leak path — a structured-log statement that included
|
||||
`{@Device}` for diagnostic context.
|
||||
2. **No password in exception messages.** `FwlibFocasClient.UnlockAsync`
|
||||
omits the password from its `InvalidOperationException` text — only
|
||||
the FWLIB error code (`EW_PASSWD`, `EW_HANDLE`, etc.) makes it through.
|
||||
3. **Driver log line uses host only.** When unlock succeeds the driver
|
||||
updates `DriverHealth.StatusText` to `"FOCAS unlock applied for
|
||||
{host}"` — no password.
|
||||
4. **CLI flag covered by the same choke point.** The
|
||||
`Driver.FOCAS.Cli --cnc-password` flag flows through
|
||||
`FocasDeviceOptions.Password`, so its redaction is identical to the
|
||||
server's. The PowerShell e2e harness (`scripts/e2e/test-focas.ps1
|
||||
-CncPassword`) follows the same path.
|
||||
|
||||
Any new logging surface that touches `FocasDeviceOptions` MUST continue
|
||||
to use the record's `ToString` (or otherwise omit `Password`). A code
|
||||
review checklist item: "no log statement contains `device.Options.Password`
|
||||
or `device.Password` directly."
|
||||
|
||||
### Password-rotation runbook
|
||||
|
||||
When the CNC password rotates (operator team flipped a parameter-protect
|
||||
gate, or your security policy requires periodic rotation):
|
||||
|
||||
1. **Update the password on the controller** (CNC pendant or vendor's
|
||||
admin tool). The exact path varies by series — Fanuc service manual
|
||||
page reference depends on the MTB.
|
||||
2. **Update `appsettings.json`** in place with the new value.
|
||||
- Production: bump the secrets-store entry that backs the
|
||||
`Devices[*].Password` config-DB column. Same workflow as rotating
|
||||
the LDAP service-account password.
|
||||
- Dev: update `.local/focas-passwords.txt` (or wherever the dev
|
||||
deployment sources the secret).
|
||||
3. **Restart the OtOpcUa server** (or trigger a config-DB bump that
|
||||
forces driver reinitialise). The driver picks up the new password
|
||||
on the next `EnsureConnectedAsync` call. **No need to manually
|
||||
reconnect each device** — `cnc_wrunlockparam` emits on the next
|
||||
wire-call boundary.
|
||||
4. **Verify**. The first wire call after restart logs
|
||||
`"FOCAS unlock applied for focas://{host}:{port}"` at info. A wrong
|
||||
password surfaces as `BadUserAccessDenied` on the next gated read or
|
||||
write.
|
||||
5. **Audit.** OPC UA wrote-event entries (per
|
||||
[`audit-log-rules.md`](audit-log-rules.md)) cover the
|
||||
parameter/macro write paths. Password rotation itself is NOT logged
|
||||
beyond "unlock applied" — same posture as LDAP service-account
|
||||
rotation, where the password change is logged out-of-band by the IAM
|
||||
system.
|
||||
|
||||
### Cross-references
|
||||
|
||||
- [`docs/Security.md`](../Security.md) — server-wide secrets handling +
|
||||
the same `.local/` pattern used for LDAP and the Galaxy.Host pipe
|
||||
secret. The FOCAS password follows the same posture.
|
||||
- [`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) § "FOCAS password" —
|
||||
driver-side behaviour, EW_PASSWD retry semantics, status-code
|
||||
surface.
|
||||
- [`docs/v2/implementation/focas-wire-protocol.md`](implementation/focas-wire-protocol.md)
|
||||
§ "cnc_wrunlockparam" — wire-frame layout for the password buffer.
|
||||
|
||||
Reference in New Issue
Block a user