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";
}