Files
lmxopcua/scripts/validation/pair-gate
Joseph Doherty 0f001bccdb
v2-ci / unit-tests (push) Has been cancelled
test(validation): pair-gate helper for the #801 revocation/expiry/ACL checks on docker-dev
Console helper that mints and revokes OPC UA API keys through the same IApiKeyAdminService the AdminUI
/apikeys page calls, edits NodeAcl grants in the rig ConfigDb, and holds one OPC UA session open per node
(Client.Shared) with a subscription plus a read/write every 3 s, so a revocation, expiry or grant removal
can be observed landing on an ALREADY-OPEN session on both pair nodes next to a non-revoked control key.
Credentials go to a 0600 file and are never printed. LOCAL docker-dev only.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 10:22:33 -05:00
..
…

Pair-behaviour gate helper (lmxopcua#801)

PairGate is a small console tool for the local docker-dev rig only. Never point it at a wonder box. It drives the redundancy and revocation half of the #801 SDK 2 live gate against the central pair (central-1 opc.tcp://localhost:4840, central-2 :4841):

  • mint / set-enabled call the same IApiKeyAdminService that the AdminUI /apikeys page uses (Create key, Revoke / Re-enable). The rig's committed dev pepper is the default. Override it with PAIRGATE_PEPPER and the ConfigDb with PAIRGATE_CONFIGDB. The credential goes to <credDir>/<name>.key (mode 0600) and is never printed. Output names the key id only.
  • grant / ungrant add or remove a NodeAcl row in the rig ConfigDb.
  • probe opens a fresh session (a new activation), then reads and writes the probe tag (PAIRGATE_TAG, default the writable ns=2;s=pymodbus/plc/HR200).
  • watch holds one session open for N seconds. It subscribes to the given tags and reads and writes every 3 s. Output is JSON lines, so a revocation or expiry can be seen landing on an already-open session.
  • server reads Server.ServiceLevel (i=2267) and Server.ServerArray (i=2254).

DB edits only reach the nodes' data-plane snapshot through a deployment (docker-dev/deploy.sh </dev/null). That's the documented revocation contract in docs/security.md.

dotnet build scripts/validation/pair-gate -c Release -o /tmp/pairgate
g="dotnet /tmp/pairgate/PairGate.dll"
$g mint ./keys k-revoke p-revoke                 # plus a control key, and one with an expiry: ... p-exp 2026-11-12T15:00:00Z
$g grant p-revoke-main p-revoke MAIN Cluster - 159   # ReadOnly + WriteOperate
./docker-dev/deploy.sh </dev/null
$g watch opc.tcp://localhost:4840 @keys/k-revoke.key 600 ns=2\;s=pymodbus/plc/HR200 ns=2\;s=pymodbus/plc/HR100 > c1.log &
$g watch opc.tcp://localhost:4841 @keys/k-revoke.key 600 ns=2\;s=pymodbus/plc/HR200 ns=2\;s=pymodbus/plc/HR100 > c2.log &
$g set-enabled <keyId> false                     # new activations refused within StoreCacheSeconds
./docker-dev/deploy.sh </dev/null                # the open sessions on BOTH nodes lose write (ACL on: everything)

Always run a non-revoked control key next to the revoked one, so a denial can't be mistaken for an outage. The 2026-11-12 run and its results are on #801.