added CORS logging and Agent max auth retry logic via config

This commit is contained in:
Oliver Kieselbach
2026-02-16 10:09:10 +01:00
parent 63b9458806
commit f0465e76a1
7 changed files with 146 additions and 3 deletions
@@ -138,6 +138,19 @@ namespace AutopilotMonitor.Agent.Core.Configuration
/// </summary>
public int HelloWaitTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Maximum consecutive authentication failures (401/403) before the agent shuts down.
/// Prevents endless retry traffic when the device is not authorized.
/// 0 = disabled (retry forever). Default: 5.
/// </summary>
public int MaxAuthFailures { get; set; } = 5;
/// <summary>
/// Maximum time in minutes the agent keeps retrying after the first auth failure.
/// 0 = disabled (no time limit, only MaxAuthFailures applies). Default: 0.
/// </summary>
public int AuthFailureTimeoutMinutes { get; set; } = 0;
/// <summary>
/// Validates the configuration
/// </summary>
@@ -48,6 +48,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
// Cleanup/self-destruct
private readonly CleanupService _cleanupService;
// Auth failure circuit breaker
private int _consecutiveAuthFailures = 0;
private DateTime? _firstAuthFailureTime = null;
public MonitoringService(AgentConfiguration configuration, AgentLogger logger, string agentVersion)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
@@ -242,9 +246,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_configuration.ImeMatchLogPath = config.ImeMatchLogPath;
}
_configuration.MaxAuthFailures = config.MaxAuthFailures;
_configuration.AuthFailureTimeoutMinutes = config.AuthFailureTimeoutMinutes;
_logger.Info("Applied runtime settings from remote config");
_logger.Info($" uploadIntervalSeconds={_configuration.UploadIntervalSeconds}, cleanupOnExit={_configuration.CleanupOnExit}, selfDestructOnComplete={_configuration.SelfDestructOnComplete}, keepLogFile={_configuration.KeepLogFile}");
_logger.Info($" imeMatchLogPath={_configuration.ImeMatchLogPath ?? "(none)"}");
_logger.Info($" maxAuthFailures={_configuration.MaxAuthFailures}, authFailureTimeoutMinutes={_configuration.AuthFailureTimeoutMinutes}");
}
/// <summary>
@@ -377,6 +385,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_logger.Warning($"Session registration failed: {response.Message}");
}
}
catch (BackendAuthException ex)
{
_logger.Error($"Session registration authentication failed: {ex.Message}");
HandleAuthFailure();
}
catch (Exception ex)
{
_logger.Error("Failed to register session", ex);
@@ -623,18 +636,66 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
{
_spool.RemoveEvents(events);
_logger.Debug($"Successfully uploaded {response.EventsProcessed} events");
// Reset auth failure counter on success
_consecutiveAuthFailures = 0;
_firstAuthFailureTime = null;
}
else
{
_logger.Warning($"Upload failed: {response.Message}");
}
}
catch (BackendAuthException ex)
{
_logger.Error($"Upload authentication failed: {ex.Message}");
HandleAuthFailure();
}
catch (Exception ex)
{
_logger.Error("Error uploading events", ex);
}
}
/// <summary>
/// Handles an authentication failure by incrementing the counter and shutting down
/// the agent if the configured threshold is reached.
/// </summary>
private void HandleAuthFailure()
{
_consecutiveAuthFailures++;
if (_firstAuthFailureTime == null)
_firstAuthFailureTime = DateTime.UtcNow;
_logger.Warning($"Authentication failure {_consecutiveAuthFailures}" +
(_configuration.MaxAuthFailures > 0 ? $"/{_configuration.MaxAuthFailures}" : "") +
$" (first failure at {_firstAuthFailureTime.Value:HH:mm:ss})");
// Check max attempts (0 = disabled)
if (_configuration.MaxAuthFailures > 0 && _consecutiveAuthFailures >= _configuration.MaxAuthFailures)
{
_logger.Error($"=== AGENT SHUTDOWN: {_consecutiveAuthFailures} consecutive authentication failures (401/403). " +
"The device is not authorized to send data to Autopilot Monitor. " +
"Check client certificate and serial number validation in your tenant configuration. ===");
Environment.Exit(1);
}
// Check timeout (0 = disabled)
if (_configuration.AuthFailureTimeoutMinutes > 0 && _firstAuthFailureTime.HasValue)
{
var elapsed = DateTime.UtcNow - _firstAuthFailureTime.Value;
if (elapsed.TotalMinutes >= _configuration.AuthFailureTimeoutMinutes)
{
_logger.Error($"=== AGENT SHUTDOWN: Authentication failures persisted for {elapsed.TotalMinutes:F0} minutes " +
$"(timeout: {_configuration.AuthFailureTimeoutMinutes} min). " +
"The device is not authorized to send data to Autopilot Monitor. " +
"Check client certificate and serial number validation in your tenant configuration. ===");
Environment.Exit(1);
}
}
}
/// <summary>
/// Deletes the persisted session ID file
/// Should be called when enrollment is complete/failed
@@ -108,6 +108,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
AddClientCertificateHeader(httpRequest);
var httpResponse = await _httpClient.SendAsync(httpRequest);
ThrowOnAuthFailure(httpResponse);
httpResponse.EnsureSuccessStatusCode();
var responseJson = await httpResponse.Content.ReadAsStringAsync();
@@ -157,6 +158,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
AddClientCertificateHeader(httpRequest);
var response = await _httpClient.SendAsync(httpRequest);
ThrowOnAuthFailure(response);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
@@ -165,6 +167,21 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
return result;
}
/// <summary>
/// Throws BackendAuthException for 401/403 responses so callers can distinguish
/// authentication failures from transient server errors.
/// </summary>
private static void ThrowOnAuthFailure(HttpResponseMessage response)
{
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
response.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
throw new BackendAuthException(
$"Backend returned {(int)response.StatusCode} {response.StatusCode}. " +
"The device is not authorized. Check client certificate and serial number validation.");
}
}
/// <summary>
/// Creates NDJSON (newline-delimited JSON) from IngestEventsRequest
/// Format: metadata line + one event per line
@@ -254,4 +271,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
_httpClient?.Dispose();
}
}
/// <summary>
/// Thrown when the backend returns 401 or 403, indicating the device is not authorized.
/// </summary>
public class BackendAuthException : Exception
{
public BackendAuthException(string message) : base(message) { }
}
}
@@ -90,7 +90,7 @@ namespace AutopilotMonitor.Functions.Functions
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new AgentConfigResponse
{
ConfigVersion = 2,
ConfigVersion = 3,
UploadIntervalSeconds = 30,
CleanupOnExit = false,
SelfDestructOnComplete = false,
@@ -98,7 +98,9 @@ namespace AutopilotMonitor.Functions.Functions
ImeMatchLogPath = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log",
Collectors = collectors,
GatherRules = gatherRules,
ImeLogPatterns = imeLogPatterns
ImeLogPatterns = imeLogPatterns,
MaxAuthFailures = tenantConfig.MaxAuthFailures ?? 5,
AuthFailureTimeoutMinutes = tenantConfig.AuthFailureTimeoutMinutes ?? 0
});
return response;
@@ -107,4 +107,20 @@ builder.Services.AddSingleton<AnalyzeRuleService>();
builder.Services.AddSingleton<MaintenanceService>();
builder.Services.AddSingleton<AutopilotMonitor.Functions.Security.SerialNumberValidator>();
builder.Build().Run();
var app = builder.Build();
// Log CORS configuration at startup so misconfigured origins are immediately visible
// in the log stream. CORS is enforced by Azure infrastructure, not by function code,
// so a blocked preflight never reaches the function worker and leaves no trace.
var startupLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Startup");
var corsOrigins = builder.Configuration["Host:CORS"] // local.settings.json
?? builder.Configuration["WEBSITE_CORS_ALLOWED_ORIGINS"] // Azure App Settings
?? "(not configured - all cross-origin requests will be blocked!)";
var corsCredentials = builder.Configuration["Host:CORSCredentials"]
?? builder.Configuration["WEBSITE_CORS_SUPPORT_CREDENTIALS"]
?? "unknown";
startupLogger.LogInformation(
"=== CORS CONFIG: AllowedOrigins={CorsOrigins} | SupportCredentials={CorsCredentials} ===",
corsOrigins, corsCredentials);
app.Run();
@@ -51,6 +51,19 @@ namespace AutopilotMonitor.Shared.Models
/// Delivered from backend so patterns can be updated without agent rebuild.
/// </summary>
public List<ImeLogPattern> ImeLogPatterns { get; set; } = new List<ImeLogPattern>();
/// <summary>
/// Maximum consecutive authentication failures (401/403) before the agent shuts down.
/// Prevents endless retry traffic when the device is not authorized.
/// 0 = disabled (retry forever). Default: 5.
/// </summary>
public int MaxAuthFailures { get; set; } = 5;
/// <summary>
/// Maximum time in minutes the agent keeps retrying after the first auth failure.
/// 0 = disabled (no time limit, only MaxAuthFailures applies). Default: 0.
/// </summary>
public int AuthFailureTimeoutMinutes { get; set; } = 0;
}
/// <summary>
@@ -138,6 +138,19 @@ namespace AutopilotMonitor.Shared.Models
/// </summary>
public int PerformanceCollectorIntervalSeconds { get; set; } = 60;
// ===== AGENT AUTH CIRCUIT BREAKER =====
/// <summary>
/// Maximum consecutive authentication failures (401/403) before the agent shuts down.
/// null = use default (5). 0 = disabled (retry forever).
/// </summary>
public int? MaxAuthFailures { get; set; }
/// <summary>
/// Maximum time in minutes the agent keeps retrying after the first auth failure.
/// null = use default (0 = disabled, only MaxAuthFailures applies).
/// </summary>
public int? AuthFailureTimeoutMinutes { get; set; }
// ===== HELPER METHODS =====