Hardening: re-architecture to use mTLS for complete agent communication (agent endpoints consolidated)

This commit is contained in:
Oliver Kieselbach
2026-02-26 23:02:50 +01:00
parent 64d631376e
commit 81c4806083
10 changed files with 48 additions and 37 deletions
@@ -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
}
/// <summary>
/// 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.
/// </summary>
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))
@@ -1 +1 @@
78
80
@@ -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,
@@ -36,7 +36,7 @@ namespace AutopilotMonitor.Functions.Functions
[Function("GetDiagnosticsUploadUrl")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "diagnostics/upload-url")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/upload-url")] HttpRequestData req)
{
try
{
@@ -47,7 +47,7 @@ namespace AutopilotMonitor.Functions.Functions
[Function("IngestEvents")]
public async Task<IngestEventsOutput> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "events/ingest")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/ingest")] HttpRequestData req)
{
try
{
@@ -34,7 +34,7 @@ namespace AutopilotMonitor.Functions.Functions
[Function("RegisterSession")]
public async Task<RegisterSessionOutput> 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");
@@ -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(
@@ -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);
@@ -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)
@@ -67,12 +67,12 @@ namespace AutopilotMonitor.Shared
/// </summary>
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";
}