fix(opcua): failed sign-ins no longer lock a client out of anonymous sessions (#833)
OPC Foundation SDK 2.0.1's SessionManager lockout refused EVERY activation (Anonymous included, before the token is read) from a client with 5 failed identity validations in 60 s, for 5 minutes, keyed on cert thumbprint else peer IP. Disable it (MaxFailedAuthenticationAttempts=0 on built and loaded configs) and enforce FailedCredentialLockout in the identity-registry adapter instead: UserName activations only, keyed on thumbprint+peer together, same 5/60s/300s defaults, configurable via OpcUa:CredentialLockout.
This commit is contained in:
@@ -74,6 +74,9 @@ Until #678.6 the overlay never loaded on a node carrying a `cluster-*` role (the
|
||||
| `EnabledSecurityProfiles` | list of `OpcUaSecurityProfile` | `[None, Basic256Sha256Sign, Basic256Sha256SignAndEncrypt]` | Transport-security profiles, one endpoint per entry. Must contain ≥1. Profile detail in [`security.md`](security.md). |
|
||||
| `AutoAcceptUntrustedClientCertificates` | bool | `false` | Auto-trust unknown client certs on first connect (dev convenience). Not validated. See [`security.md`](security.md). |
|
||||
| `PeerApplicationUris` | list of string | `[]` (empty) | Partner node `ApplicationUri`s published in `Server.ServerArray` for redundancy discovery. See [`Redundancy.md`](Redundancy.md). |
|
||||
| `CredentialLockout:MaxFailedAttempts` | int | `5` | Failed UserName (API-key) activations from one client before its UserName activations are refused `BadUserAccessDenied` (#833). `≤ 0` disables. **Never refuses Anonymous.** See [`security.md`](security.md#failed-credential-lockout-opcuacredentiallockout). |
|
||||
| `CredentialLockout:FailureWindowSeconds` | int | `60` | A failure more than this long after the client's previous one starts a fresh count. |
|
||||
| `CredentialLockout:LockoutSeconds` | int | `300` | How long a locked-out client's UserName activations are refused. |
|
||||
|
||||
> **Transport security profiles** (the values in `EnabledSecurityProfiles` — `None`, `Basic256Sha256Sign`, `Basic256Sha256SignAndEncrypt`) and the PKI trust flow are documented in full in [`security.md`](security.md). This page does not duplicate them.
|
||||
|
||||
|
||||
@@ -313,6 +313,40 @@ charged process-wide only — never handed a fresh budget. Clients behind one NA
|
||||
guard applies to a driver-only node: its `ArtifactApiKeyStore` is two in-memory dictionary probes and is never
|
||||
wrapped.
|
||||
|
||||
### Failed-credential lockout (`OpcUa:CredentialLockout`)
|
||||
|
||||
**A failed sign-in never locks a client out of anonymous access (#833).** OPC Foundation SDK 2.0.1's
|
||||
`SessionManager` has its own lockout: after `ServerConfiguration.MaxFailedAuthenticationAttempts` (SDK
|
||||
default **5**) failed identity validations — each within 60 s of the previous — it refuses **every**
|
||||
`ActivateSession` from that client for **5 minutes** with `BadUserAccessDenied` ("Too many failed
|
||||
authentication attempts"). Its check runs before the identity token is looked at, so it refused
|
||||
**Anonymous** too (even a namespace-array read needs an activated session), a successful anonymous
|
||||
activation did not clear it, and the authenticator was never called (no "anonymous admitted" line). Its key
|
||||
is the client certificate thumbprint when one is presented, else the peer IP — so certificate-less clients
|
||||
behind one NAT share a key, and on a `None` endpoint a presented certificate is not proven. That is what
|
||||
the docker-dev live gate hit: four failed `multi-role` UserName activations against central-1, then every
|
||||
anonymous session refused for five minutes while central-2 (which never saw the failures) kept admitting.
|
||||
|
||||
The host now sets `MaxFailedAuthenticationAttempts = 0` on every configuration it builds or loads
|
||||
(`OpcUaApplicationHost.DisableSdkAuthenticationLockout`) and enforces a narrower replacement,
|
||||
`FailedCredentialLockout`, in its identity-registry adapter:
|
||||
|
||||
- **UserName activations only.** A failed one counts; at `MaxFailedAttempts` (5) within
|
||||
`FailureWindowSeconds` (60) of each other, that client's **UserName** activations are refused
|
||||
`BadUserAccessDenied` for `LockoutSeconds` (300) without the key store being consulted, and a Warning is
|
||||
logged once. A refusal during the lockout is not counted (it never extends itself); a successful UserName
|
||||
activation clears the count. **Anonymous activations never consult it**, from any client.
|
||||
- **Keyed on certificate thumbprint *and* peer address together**, so locking a client out needs both its
|
||||
certificate and its source address, and NAT neighbours with different certificates do not share a count.
|
||||
- Every denial counts, including a key-store timeout or miss-budget refusal (as the SDK's did) — a ConfigDb
|
||||
outage can therefore lock a keyed client out for up to `LockoutSeconds` after recovery.
|
||||
- Bounded at 4096 tracked clients (expired entries are pruned; a client arriving at a full table is not
|
||||
tracked). API-key secrets are not guessable, so this is defence in depth, not the brute-force control.
|
||||
|
||||
Pinned over the wire by `CredentialLockoutWireTests` (OpcUaServer.IntegrationTests — anonymous admitted after
|
||||
six failures; a valid key refused while locked out; `MaxFailedAttempts = 0` admits after seven failures,
|
||||
proving the SDK lockout is off) and unit-tested by `FailedCredentialLockoutTests`.
|
||||
|
||||
### LDAP bind flow (`OtOpcUaLdapAuthService`)
|
||||
|
||||
LDAP is configured under the `Security:Ldap` section (bound to `LdapOptions`, `src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/LdapOptions.cs`, `SectionName = "Security:Ldap"`). The app authenticator is `OtOpcUaLdapAuthService` — a thin wrapper around the shared `ZB.MOM.WW.Auth.Ldap` directory client that adds two app-only concerns the shared library deliberately does not model: the `Enabled` master switch and `DevStubMode`. Since #520 it serves the **Admin UI cookie login (`/auth/login`) only** — `LdapOpcUaUserAuthenticator` is deleted and the OPC UA UserName path authenticates API keys instead (see [API-key identity](#api-key-identity-opc-ua-data-plane)).
|
||||
|
||||
Reference in New Issue
Block a user