feat(security): per-client pre-auth API-key miss budget; widen IOpcUaUserAuthenticator with the remote endpoint (lmxopcua#751)
The #733 miss budget (MissBurst / MissRefillPerSecond) was process-wide, because the impersonation seam did not carry the client's address, so one noisy client could drain it for everybody while the known-id set was unavailable. - IOpcUaUserAuthenticator gains AuthenticateUserNameAsync(username, password, IPEndPoint? remoteEndpoint, ct) (default: ignore it and delegate); the server calls it. IApiKeyStore gains the matching FindAsync(..., IPAddress?, ...). - The SDK (1.5.378) exposes no client address publicly. SecureChannelRemoteEndpoint reads it from the TCP listener's private channel map by reflection (through the protected TransportListeners, exposed by OtOpcUaSdkServer), keyed by the session's secure channel id. Fail-soft: null => process-wide accounting only. - BoundedApiKeyStore charges a miss to the client's own bucket (keyed by IP address) and then to the process-wide one, which stays as the backstop. A refusal by either takes from neither; a hit refunds both. New options Security:ApiKeys:MissBurstPerEndpoint (10) / MissRefillPerSecondPerEndpoint (2), 0 disables; telemetry outcome endpoint_miss_budget. At most 4096 addresses are tracked; idle buckets are pruned and an address beyond the cap is charged process-wide only. Tests: BoundedApiKeyStorePerEndpointTests (noisy client vs quiet one, process-wide backstop, refund + refill, off switch, authenticator forwards the address) and over the wire ApiKeyMissBudgetPerEndpointTests (activation hands the authenticator the loopback endpoint; real authenticator + bounded store refuse a client's 3rd and 4th unknown key without a store read). Each new behaviour test fails with its fix mutated out. Host.Tests 224/0, OpcUaServer.Tests 1019/1 (the one failure is the wall-clock NodeManagerLockTimeBenchmarkTests under suite load; passes alone). Docs: security.md. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -25,6 +25,26 @@ public interface IOpcUaUserAuthenticator
|
||||
/// <returns>An OpcUaUserAuthResult indicating success or failure.</returns>
|
||||
Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="AuthenticateUserNameAsync(string, string, CancellationToken)"/> with the activating client's
|
||||
/// remote endpoint (lmxopcua#751), so an implementation can account pre-authentication failures per
|
||||
/// client rather than process-wide. This is the overload the server calls; the default ignores the
|
||||
/// endpoint and delegates, so an implementation that does not need it is unchanged.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The endpoint is best effort (<see cref="SecureChannelRemoteEndpoint"/>) and null when unknown — an
|
||||
/// implementation must treat null as "no per-client information", never as a distinct client. Same
|
||||
/// must-not-throw contract as the three-argument overload.
|
||||
/// </remarks>
|
||||
/// <param name="username">The username to authenticate.</param>
|
||||
/// <param name="password">The cleartext password to authenticate.</param>
|
||||
/// <param name="remoteEndpoint">The client's IP endpoint, or null when it could not be resolved.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>An OpcUaUserAuthResult indicating success or failure.</returns>
|
||||
Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(
|
||||
string username, string password, System.Net.IPEndPoint? remoteEndpoint, CancellationToken ct) =>
|
||||
AuthenticateUserNameAsync(username, password, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves what an <b>anonymous</b> session is allowed to do. Anonymous is a normal
|
||||
/// principal — <see cref="OpcUaPrincipals.Anonymous"/> — not a special case, so it can be granted
|
||||
|
||||
Reference in New Issue
Block a user