From 81c48060832c83dd2beff3fac33094b28ff5800a Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Thu, 26 Feb 2026 23:02:50 +0100 Subject: [PATCH] Hardening: re-architecture to use mTLS for complete agent communication (agent endpoints consolidated) --- .../Monitoring/Network/BackendApiClient.cs | 42 +++++++++---------- .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- .../Functions/GetAgentConfigFunction.cs | 2 +- .../GetDiagnosticsUploadUrlFunction.cs | 2 +- .../Functions/IngestEventsFunction.cs | 2 +- .../Functions/RegisterSessionFunction.cs | 2 +- .../Middleware/AuthenticationMiddleware.cs | 6 +-- .../Security/CertificateValidator.cs | 7 ++++ .../Security/SecurityValidator.cs | 12 ++++-- .../AutopilotMonitor.Shared/Constants.cs | 8 ++-- 10 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/BackendApiClient.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/BackendApiClient.cs index ff8f8667..7ba59bf5 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/BackendApiClient.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Network/BackendApiClient.cs @@ -67,7 +67,17 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network _model = hardwareInfo.Model; _serialNumber = hardwareInfo.SerialNumber; - _httpClient = new HttpClient + // Use HttpClientHandler with client certificate for TLS-level mTLS negotiation. + // Azure App Service extracts the cert from the TLS handshake and forwards it + // in the X-ARR-ClientCert header to the backend function. + var handler = new HttpClientHandler(); + if (_clientCertificate != null) + { + handler.ClientCertificates.Add(_clientCertificate); + logger?.Info("Client certificate attached to TLS handler for mTLS negotiation"); + } + + _httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) }; @@ -107,8 +117,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network // Add TenantId header so the backend can run security checks before parsing the body httpRequest.Headers.Add("X-Tenant-Id", request.TenantId); - // Add client certificate for device authentication (if available) - AddClientCertificateHeader(httpRequest); + // Add additional security headers for device authorization (client cert is at TLS layer, but we can add hardware info in headers for whitelist validation and Autopilot device verification) + AddSecurityHeaders(httpRequest); var httpResponse = await _httpClient.SendAsync(httpRequest); ThrowOnAuthFailure(httpResponse); @@ -128,7 +138,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network var url = $"{_baseUrl}{Constants.ApiEndpoints.GetAgentConfig}?tenantId={Uri.EscapeDataString(tenantId)}"; var httpRequest = new HttpRequestMessage(HttpMethod.Get, url); - AddClientCertificateHeader(httpRequest); + AddSecurityHeaders(httpRequest); var response = await _httpClient.SendAsync(httpRequest); response.EnsureSuccessStatusCode(); @@ -166,8 +176,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network Content = new StringContent(json, Encoding.UTF8, "application/json") }; - // Add device identification headers for all API calls - AddClientCertificateHeader(httpRequest); + // Add additional security headers for device authorization (client cert is at TLS layer, but we can add hardware info in headers for whitelist validation and Autopilot device verification) + AddSecurityHeaders(httpRequest); var response = await _httpClient.SendAsync(httpRequest); ThrowOnAuthFailure(response); @@ -237,25 +247,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network } /// - /// Adds device identification headers for authentication and authorization - /// Includes: Client certificate, manufacturer, model, serial number + /// Adds additional security headers for device authorization. + /// Client certificate is sent at TLS layer via HttpClientHandler.ClientCertificates. /// - private void AddClientCertificateHeader(HttpRequestMessage request) + private void AddSecurityHeaders(HttpRequestMessage request) { _logger?.Verbose("Adding security headers to request..."); - - // Add client certificate for device authentication - if (_clientCertificate != null) - { - var certBytes = _clientCertificate.Export(X509ContentType.Cert); - var certBase64 = Convert.ToBase64String(certBytes); - request.Headers.Add("X-Client-Certificate", certBase64); - _logger?.Verbose($" X-Client-Certificate: {_clientCertificate.Thumbprint.Substring(0, Math.Min(16, _clientCertificate.Thumbprint.Length))}..."); - } - else - { - _logger?.Verbose(" X-Client-Certificate: NOT SENT (no certificate available)"); - } + // Client certificate is sent at TLS layer via HttpClientHandler.ClientCertificates (mTLS) // Add hardware information for whitelist validation if (!string.IsNullOrEmpty(_manufacturer)) diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 8e14edce..d15a2cc4 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -78 +80 diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs index 2e451752..3f5d8d63 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs @@ -97,7 +97,7 @@ namespace AutopilotMonitor.Functions.Functions var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(new AgentConfigResponse { - ConfigVersion = 7, + ConfigVersion = 8, // mTLS migration: agent sends cert via TLS, routes moved under /api/agent/ UploadIntervalSeconds = 30, SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true, KeepLogFile = tenantConfig.KeepLogFile ?? false, diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/GetDiagnosticsUploadUrlFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/GetDiagnosticsUploadUrlFunction.cs index 212e1c15..06fe52bc 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/GetDiagnosticsUploadUrlFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/GetDiagnosticsUploadUrlFunction.cs @@ -36,7 +36,7 @@ namespace AutopilotMonitor.Functions.Functions [Function("GetDiagnosticsUploadUrl")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "diagnostics/upload-url")] HttpRequestData req) + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/upload-url")] HttpRequestData req) { try { diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs index 96c9184e..9cf006cf 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs @@ -47,7 +47,7 @@ namespace AutopilotMonitor.Functions.Functions [Function("IngestEvents")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "events/ingest")] HttpRequestData req) + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/ingest")] HttpRequestData req) { try { diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/RegisterSessionFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/RegisterSessionFunction.cs index f57341f2..e3bee32e 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/RegisterSessionFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/RegisterSessionFunction.cs @@ -34,7 +34,7 @@ namespace AutopilotMonitor.Functions.Functions [Function("RegisterSession")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "sessions/register")] HttpRequestData req) + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/register-session")] HttpRequestData req) { _logger.LogInformation("RegisterSession function processing request"); diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs index 98383a91..7af20805 100644 --- a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs +++ b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs @@ -42,10 +42,10 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware { "/api/health", "/api/platform-stats", - "/api/sessions/register", - "/api/events/ingest", + "/api/agent/register-session", + "/api/agent/ingest", "/api/agent/config", - "/api/diagnostics/upload-url" + "/api/agent/upload-url" }; public AuthenticationMiddleware( diff --git a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs index b4502e49..ebab0cac 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs @@ -57,6 +57,13 @@ namespace AutopilotMonitor.Functions.Security try { + // Azure App Service may URL-encode the X-ARR-ClientCert header value. + // Decode before Base64 parsing. + if (certificateBase64.Contains('%')) + { + certificateBase64 = Uri.UnescapeDataString(certificateBase64); + } + // Parse certificate from Base64 var certBytes = Convert.FromBase64String(certificateBase64); var certificate = new X509Certificate2(certBytes); diff --git a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs index 235db41e..0b07c42f 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs @@ -109,9 +109,15 @@ namespace AutopilotMonitor.Functions.Security } // 1. Validate client certificate - var certHeader = req.Headers.Contains("X-Client-Certificate") - ? req.Headers.GetValues("X-Client-Certificate").FirstOrDefault() - : null; + // Production: Azure App Service mTLS forwards client cert in X-ARR-ClientCert header. + // Local dev fallback: X-Client-Certificate header for testing with Azure Functions Core Tools + // (which does not support mTLS). This fallback never triggers in production because + // requests without a TLS client cert are rejected by Azure before reaching the function. + var certHeader = req.Headers.Contains("X-ARR-ClientCert") + ? req.Headers.GetValues("X-ARR-ClientCert").FirstOrDefault() + : req.Headers.Contains("X-Client-Certificate") + ? req.Headers.GetValues("X-Client-Certificate").FirstOrDefault() + : null; var certValidation = CertificateValidator.ValidateCertificate(certHeader, _logger); if (!certValidation.IsValid) diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index f4e0a507..bf6cec10 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -67,12 +67,12 @@ namespace AutopilotMonitor.Shared /// public static class ApiEndpoints { - public const string RegisterSession = "/api/sessions/register"; - public const string IngestEvents = "/api/events/ingest"; -public const string GetAgentConfig = "/api/agent/config"; + public const string RegisterSession = "/api/agent/register-session"; + public const string IngestEvents = "/api/agent/ingest"; + public const string GetAgentConfig = "/api/agent/config"; public const string GatherRules = "/api/gather-rules"; public const string AnalyzeRules = "/api/analyze-rules"; - public const string GetDiagnosticsUploadUrl = "/api/diagnostics/upload-url"; + public const string GetDiagnosticsUploadUrl = "/api/agent/upload-url"; public const string BlockDevice = "/api/devices/block"; public const string GetBlockedDevices = "/api/devices/blocked"; }