fix(security): validate the API-key pepper at start, finish JWT bearer, circuit-aware audit actor

- ApiKeyOptionsValidator (ValidateOnStart on any driver/admin node): a blank or placeholder
  Security:ApiKeys:Pepper fails host start instead of rejecting every key after a green boot (R03:2.3).
- JWT is finished and opt-in by key (R03:2.4): with Security:Jwt:SigningKey set, a Bearer scheme
  (JwtBearerAuthenticationHandler over JwtTokenService.TryValidate / BuildValidationParameters, fail
  closed) accepts the tokens /auth/token mints; the default scheme is a cookie-or-bearer policy scheme
  and the fallback policy names it, so /api/* and every RequireAuthorization endpoint take a browser
  session OR a machine token. JwtOptions are validated at start (JwtOptionsValidator). Without a key,
  /auth/token is not mapped (404, not the former constructor 500) and no bearer scheme exists.
  A direct AddJwtBearer needs Microsoft.AspNetCore.Authentication.JwtBearer in
  Directory.Packages.props (not this lane's file) — the handler is a drop-in to swap.
- AuthenticationStateAuditActorAccessor is the registered IAuditActorAccessor: the Blazor circuit's
  principal first, HttpContext as the fallback (R03:2.5). HttpAuditActorAccessor shares its resolver.
- CookieAuthenticationStateProvider (unregistered, fire-and-forget ping loop) is deleted (R03:1.5/2.5).
- /auth/login logs the LDAP exception at Warning before answering 503 (R03:2.7).
- Admin operations reply with a fixed message + correlation id instead of ex.Message (R03:4.1).

Refs #733

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 10:23:14 -04:00
parent f5579e6ef2
commit f72af76e99
17 changed files with 773 additions and 127 deletions
@@ -89,6 +89,18 @@ public sealed class AdminOperationsActor : ReceiveActor
Receive<ShelveAlarmCommand>(HandleShelveAlarm);
}
/// <summary>
/// The reply text for an unexpected server-side failure (R03:4.1): a fixed sentence plus the request's
/// correlation id, never <c>ex.Message</c> — a SqlException or UriFormatException message carries
/// connection-string fragments and host names, and the reply reaches any ConfigEditor browser session.
/// The detail is in the central log under the same correlation id.
/// </summary>
/// <param name="operation">The admin operation that failed.</param>
/// <param name="correlationId">The request's correlation token, as logged alongside the exception.</param>
/// <returns>The operator-facing message.</returns>
internal static string ServerFault(string operation, object correlationId) =>
$"{operation} failed on the server; see the central node log for correlation {correlationId}.";
/// <summary>
/// Hands a central→node command to the mesh transport router, or publishes it directly when
/// no router is wired.
@@ -136,8 +148,8 @@ public sealed class AdminOperationsActor : ReceiveActor
}
catch (Exception ex)
{
_log.Error(ex, "AdminOps: Acknowledge failed for alarm {AlarmId}", msg.AlarmId);
replyTo.Tell(new AcknowledgeAlarmResult(false, ex.Message, msg.CorrelationId));
_log.Error(ex, "AdminOps: Acknowledge failed for alarm {AlarmId} (correlation {CorrelationId})", msg.AlarmId, msg.CorrelationId);
replyTo.Tell(new AcknowledgeAlarmResult(false, ServerFault("Acknowledge", msg.CorrelationId), msg.CorrelationId));
}
}
@@ -183,8 +195,8 @@ public sealed class AdminOperationsActor : ReceiveActor
}
catch (Exception ex)
{
_log.Error(ex, "AdminOps: Shelve ({Kind}) failed for alarm {AlarmId}", msg.Kind, msg.AlarmId);
replyTo.Tell(new ShelveAlarmResult(false, ex.Message, msg.CorrelationId));
_log.Error(ex, "AdminOps: Shelve ({Kind}) failed for alarm {AlarmId} (correlation {CorrelationId})", msg.Kind, msg.AlarmId, msg.CorrelationId);
replyTo.Tell(new ShelveAlarmResult(false, ServerFault("Shelve", msg.CorrelationId), msg.CorrelationId));
}
}
@@ -237,12 +249,12 @@ public sealed class AdminOperationsActor : ReceiveActor
}
catch (Exception ex)
{
_log.Error(ex, "StartDeployment failed for {CreatedBy}", msg.CreatedBy);
_log.Error(ex, "StartDeployment failed for {CreatedBy} (correlation {CorrelationId})", msg.CreatedBy, msg.CorrelationId);
replyTo.Tell(new StartDeploymentResult(
StartDeploymentOutcome.Rejected,
DeploymentId: null,
RevisionHash: null,
Message: ex.Message,
Message: ServerFault("StartDeployment", msg.CorrelationId),
msg.CorrelationId));
return;
}
@@ -269,12 +281,12 @@ public sealed class AdminOperationsActor : ReceiveActor
private void HandleDeployComposeFailed(DeployComposeFailed msg)
{
_composeInFlight = null;
_log.Error(msg.Error, "StartDeployment failed for {CreatedBy}", msg.Request.CreatedBy);
_log.Error(msg.Error, "StartDeployment failed for {CreatedBy} (correlation {CorrelationId})", msg.Request.CreatedBy, msg.Request.CorrelationId);
msg.ReplyTo.Tell(new StartDeploymentResult(
StartDeploymentOutcome.Rejected,
DeploymentId: null,
RevisionHash: null,
Message: msg.Error.Message,
Message: ServerFault("StartDeployment", msg.Request.CorrelationId),
msg.Request.CorrelationId));
}
@@ -513,10 +525,10 @@ public sealed class AdminOperationsActor : ReceiveActor
}
catch (Exception ex)
{
_log.Error(ex, "Probe for {DriverType} threw", msg.DriverType);
_log.Error(ex, "Probe for {DriverType} threw (correlation {CorrelationId})", msg.DriverType, msg.CorrelationId);
return new TestDriverConnectResult(
false,
ex.Message,
ServerFault("Test Connect", msg.CorrelationId),
null,
msg.CorrelationId);
}
@@ -548,8 +560,8 @@ public sealed class AdminOperationsActor : ReceiveActor
}
catch (Exception ex)
{
_log.Error(ex, "AdminOps: RestartDriver failed for {DriverInstanceId}", msg.DriverInstanceId);
replyTo.Tell(new RestartDriverResult(false, ex.Message, msg.CorrelationId));
_log.Error(ex, "AdminOps: RestartDriver failed for {DriverInstanceId} (correlation {CorrelationId})", msg.DriverInstanceId, msg.CorrelationId);
replyTo.Tell(new RestartDriverResult(false, ServerFault("RestartDriver", msg.CorrelationId), msg.CorrelationId));
}
}
@@ -578,8 +590,8 @@ public sealed class AdminOperationsActor : ReceiveActor
}
catch (Exception ex)
{
_log.Error(ex, "AdminOps: ReconnectDriver failed for {DriverInstanceId}", msg.DriverInstanceId);
replyTo.Tell(new ReconnectDriverResult(false, ex.Message, msg.CorrelationId));
_log.Error(ex, "AdminOps: ReconnectDriver failed for {DriverInstanceId} (correlation {CorrelationId})", msg.DriverInstanceId, msg.CorrelationId);
replyTo.Tell(new ReconnectDriverResult(false, ServerFault("ReconnectDriver", msg.CorrelationId), msg.CorrelationId));
}
}
}