From 4591d05e65c144523f96223cf4fbfee08e7fe8b7 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 11 Feb 2026 11:51:30 +0100 Subject: [PATCH] embedded Intune root + intermediate certs for chain validation --- .../AutopilotMonitorService.cs | 3 +- src/Agent/AutopilotMonitor.Agent/Program.cs | 8 +- .../AutopilotMonitor.Functions.csproj | 5 ++ .../Security/CertificateValidator.cs | 88 +++++++++++++++---- .../Certificates/intune-intermediate-ca.pem | 28 ++++++ .../Security/Certificates/intune-root-ca.pem | 33 +++++++ .../AutopilotMonitor.Functions/host.json | 1 + ...on => staticwebapp.config.json.README.txt} | 0 8 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-intermediate-ca.pem create mode 100644 src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-root-ca.pem rename src/Web/autopilot-monitor-web/{staticwebapp.config.README.json => staticwebapp.config.json.README.txt} (100%) diff --git a/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs b/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs index 1062e045..58f21115 100644 --- a/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs +++ b/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs @@ -68,7 +68,8 @@ namespace AutopilotMonitor.Agent return new AgentConfiguration { - ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "http://localhost:7071", + //ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "http://localhost:7071", + ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "https://autopilotmonitor-func.azurewebsites.net", SessionId = sessionId, TenantId = "default-tenant", SpoolDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Spool"), diff --git a/src/Agent/AutopilotMonitor.Agent/Program.cs b/src/Agent/AutopilotMonitor.Agent/Program.cs index 0a08e082..d1d1b96e 100644 --- a/src/Agent/AutopilotMonitor.Agent/Program.cs +++ b/src/Agent/AutopilotMonitor.Agent/Program.cs @@ -45,7 +45,10 @@ namespace AutopilotMonitor.Agent var logDir = Environment.ExpandEnvironmentVariables(config.LogDirectory); var logger = new AgentLogger(logDir, enableDebug: true); - logger.Info("Agent starting in console mode"); + logger.Info(""); + logger.Info("============================================================================================================"); + logger.Info("====================================== Agent starting in console mode ======================================"); + logger.Info("============================================================================================================"); Console.WriteLine($"Session ID: {config.SessionId}"); Console.WriteLine($"Tenant ID: {config.TenantId}"); Console.WriteLine($"API URL: {config.ApiBaseUrl}"); @@ -334,7 +337,8 @@ namespace AutopilotMonitor.Agent } // Defaults (fallback values if no config file exists) - string apiBaseUrl = "http://localhost:7071"; + //string apiBaseUrl = "http://localhost:7071"; + string apiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net"; int uploadIntervalSeconds = 30; bool cleanupOnExit = true; bool selfDestructOnComplete = true; diff --git a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj index 5b47be84..abea1170 100644 --- a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj +++ b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj @@ -28,4 +28,9 @@ + + + + + diff --git a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs index 4800179b..b4502e49 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; @@ -18,17 +21,23 @@ namespace AutopilotMonitor.Functions.Security /// - Prevents self-signed certificate attacks /// - Validates Enhanced Key Usage for Client Authentication /// - In-memory cache for validated certificates (5 minute TTL) + /// - Intune CA certificates embedded as resources (required on Linux/Azure Functions flex plan + /// where the OS trust store does not contain Microsoft Intune CAs) /// public static class CertificateValidator { private static readonly ConcurrentDictionary _validationCache = new(); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); + // Lazily loaded Intune CA certs from embedded resources + private static readonly Lazy _intuneCaCerts = new(LoadIntuneCaCertificates); + private class CachedValidationResult { public CertificateValidationResult Result { get; set; } = null!; public DateTime ExpiresAt { get; set; } } + /// /// Validates a client certificate from HTTP request header /// @@ -77,25 +86,33 @@ namespace AutopilotMonitor.Functions.Security var now = DateTime.UtcNow; if (now < certificate.NotBefore || now > certificate.NotAfter) { - var result = new CertificateValidationResult + return new CertificateValidationResult { IsValid = false, ErrorMessage = $"Certificate expired or not yet valid (NotBefore={certificate.NotBefore}, NotAfter={certificate.NotAfter})", Thumbprint = thumbprint }; - - // Don't cache failures - return result; } // 2. Validate certificate chain (without revocation check - Intune certs have no CRL/OCSP) + // + // On Linux (Azure Functions flex consumption plan) the OS trust store does not contain + // Microsoft Intune CAs. We load them from embedded PEM resources into ExtraStore and set + // AllowUnknownCertificateAuthority so chain.Build() can resolve the issuer without the OS + // store. The issuer identity is still enforced in step 3. using var chain = new X509Chain(); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; - chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; chain.ChainPolicy.VerificationTime = DateTime.UtcNow; chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(10); + // Load embedded Intune CA certs into ExtraStore + var caCerts = _intuneCaCerts.Value; + logger?.LogDebug("Loading {Count} embedded Intune CA certificate(s) into chain ExtraStore", caCerts.Length); + foreach (var caCert in caCerts) + chain.ChainPolicy.ExtraStore.Add(caCert); + var chainValid = chain.Build(certificate); if (!chainValid) @@ -103,15 +120,12 @@ namespace AutopilotMonitor.Functions.Security var chainErrors = string.Join(", ", chain.ChainStatus.Select(s => s.StatusInformation)); logger?.LogWarning($"Certificate chain validation failed: {chainErrors}"); - var result = new CertificateValidationResult + return new CertificateValidationResult { IsValid = false, ErrorMessage = $"Certificate chain validation failed: {chainErrors}", Thumbprint = thumbprint }; - - // Don't cache failures - return result; } // 3. Verify issuer chain contains Microsoft Intune CA @@ -128,15 +142,12 @@ namespace AutopilotMonitor.Functions.Security var chainInfo = string.Join(" -> ", chain.ChainElements.Cast().Select(e => e.Certificate.Subject)); logger?.LogWarning($"Certificate chain does not contain Microsoft Intune CA: {chainInfo}"); - var result = new CertificateValidationResult + return new CertificateValidationResult { IsValid = false, ErrorMessage = $"Certificate not issued by Microsoft Intune CA", Thumbprint = thumbprint }; - - // Don't cache failures - return result; } // 4. Check Enhanced Key Usage for Client Authentication (1.3.6.1.5.5.7.3.2) @@ -148,15 +159,12 @@ namespace AutopilotMonitor.Functions.Security if (!hasClientAuth) { - var result = new CertificateValidationResult + return new CertificateValidationResult { IsValid = false, ErrorMessage = "Certificate does not have Client Authentication EKU", Thumbprint = thumbprint }; - - // Don't cache failures - return result; } // All checks passed - create and cache success result @@ -190,6 +198,52 @@ namespace AutopilotMonitor.Functions.Security }; } } + + /// + /// Loads Intune CA certificates from embedded PEM resources. + /// Files: Security/Certificates/intune-intermediate-ca.pem + /// Security/Certificates/intune-root-ca.pem + /// + private static X509Certificate2[] LoadIntuneCaCertificates() + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceNames = new[] + { + "AutopilotMonitor.Functions.Security.Certificates.intune-intermediate-ca.pem", + "AutopilotMonitor.Functions.Security.Certificates.intune-root-ca.pem" + }; + + List certs = []; + + foreach (var resourceName in resourceNames) + { + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + // Resource not found - not fatal, chain validation will fall back to OS store + continue; + } + + using var reader = new StreamReader(stream); + var pem = reader.ReadToEnd(); + + // Skip placeholder files that haven't been filled with a real cert yet + if (pem.Contains("PLACEHOLDER_REPLACE_WITH_BASE64")) + continue; + + try + { + var cert = X509Certificate2.CreateFromPem(pem); + certs.Add(cert); + } + catch + { + // Malformed PEM - skip silently, chain validation continues with OS store + } + } + + return certs.ToArray(); + } } /// diff --git a/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-intermediate-ca.pem b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-intermediate-ca.pem new file mode 100644 index 00000000..b822133f --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-intermediate-ca.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEfTCCAmWgAwIBAgIQQt4O1ZNyWaBPqw1mUq1F8jANBgkqhkiG9w0BAQsF +ADA4MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMjMwNTA1MDAwMDAwWhcNMjYwNTA1MDAwMDAw +WjApMScwJQYDVQQDEx5NaWNyb3NvZnQgSW50dW5lIE1ETSBEZXZpY2UgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYOy/IV8f+BmH5y9/z +JUv5oROn3MEQua39r03K7fgAqwEsAFtU/4ZWnFbriFSx0xZsDkZxxUy9BlOQ +Z+2sr/eJ2j6uI6CocR4woRktJR6pQ5EXdsJb9lZOfV9+q6PwLnAIBQTTpUq+ +XtE8T1MwwGQ5NWW1JqQMDPo3nR0Th14tDQNrLtCPe7c2f+qaG2x+uRvWGMgT +mxtGpYiipQB8upnInAZZ5wVdhu+/GMZgTSYJvn+3Px33NOB+0mpVanq7Cwzy +XA+dqGio8sZJDfOS1fPYDKqPj52l3rct3doa2o98gPHWaDs7Rj+GiAKeidUk +N3uhQUyNfi8+YJEHb8OVGjEFAgMBAAGjgZEwgY4wHQYDVR0OBBYEFC5aTr/l +v8IEGDqYYmHaaj3AsHw4MB8GA1UdIwQYMBaAFCtJzrEHJUyd5gqT85C5MPfm +RJ+SMBAGCSsGAQQBgjcVAQQDAgEAMA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUB +Af8EDDAKBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMA0GCSqGSIb3 +DQEBCwUAA4ICAQCoci1EyidjS6xqi3S37/TJdIGqM0OcvTsi12WPIVxAMFRa +F638dgu9FrRpTDCquc06lBHtIfBWCU7VFkGIGGIZ54L4en/dGi0ZBXjv4vfp +RQ+LsoJZnYtCF3MbvlPXfQr3nk1k6m7c1utsuSzL0VgeEJJ++5KXlMRHrFdl +XXm5N8MJNw78I/e7sbwNZxvA4UdxnEpqHeKDj1+nppANJDKrobIp9WzmYCFA +8Qu3bX/Ng9OnxLnpGi8mCIvPCkPu3oo1RtHxjf/tQtdSE4V1/8mggYLNNftj +Wk6ElqA+gRGYgJCssJrfG/bRYk+9ZQjavu3oG0B+SD7j3UeAOQqoOaPi8faF +95tpgpVfJqQ6bm9wqBdODWfE4TYIPd0QG5nsYr86JThEtsL36U9j74wC33+4 +YHV4UviUiycyQiQQCSIFxSY2u9cLpFO4rjM+Qe/n8KWQURTW8i3wUFReVrMp +9l+y70GklU/HHkXv8SDtFi6Q0+aK2duUhbHr+8P2nPuPNetfryVsdE+RVL4p +FWEfIvbOKOI5EQj3Bh4/XkbhQtZk+FR7VkUIm5A9Kp+OzsQADkYhWE1N4xL/ +ysfRWmOcjB1MT1m7Zfie3YsR7j+rY4NLb+snuHqqpYRDSZpngZAJgRYHI1xS +UxOk+zJ8XyHG+eyeZdfwlrWLKuMJEFw4pczs0Q== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-root-ca.pem b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-root-ca.pem new file mode 100644 index 00000000..4f637ee3 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-root-ca.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIQWDldDibkr6lFMz6rqUVDRDANBgkqhkiG9w0BAQsF +ADA4MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMjUwOTE1MDAwMDAwWhcNMzAwOTE1MDAwMDAw +WjA4MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDpcRMWOOO5oDbbLnOV2SnaqN5NuQoEaE7nIUuH/3dKtqL/ZSRqvpNgKUyI +SfM/hpuq8V8JlYW/cutwg+B80WGXLkFHMgYhJGYxgDiwNbfZ0tk64zUpidzY +gj1PeNWkKOVIspvVBiMj3co92LQlyK8CL0V4oSeD1LpR8fncdiZtYyWKZssD +E1fSG/TH/wTAQHywZJ4cwKV2itgVr1QwF5yShozUerrwmQ+QnEb/5gthJzKl +eb8lUW1FtHCaMWiEVhflUpQCDJzYgvZJOhFQ6l0CD+eR1Oz6scDj/UxJPBEv +NKB4G5m1b93gYKZ10mtK0a3Qi4df5A+8P44AhDI6zaG2GrmF7EX6vLFOVdPC +1d3MKFoApJ3ylMeknwqRlzZj0HB/SjhzyvVbLI+BPVDy8AvM+w8cJAtV3Ety +rflpmc+dEzS8I1RTO1soEuMZ6/8osg2iPAH7rV6KwbLitQvo4thwVSw0x+hE +Rl+M4vKnq7Vs789yC5Nb1zZ6MGTXp3tRiuG40BBOY+6FdBS0aQAQ+UmsnRxS +MTrOC9CKCYFuFYxwDn9J+VvzMNhJ4VlR6e/R7KoJNVV7KWF+OfDcsxxIqLw3 +RcD1dsKKW+kl33K5XW7Q0Ra7KcMiLsvqHffhrYsgmllTGtpCgDLGl1H1vP9/ +TM31bLEc6udQX5xOdcht0VmewQIDAQABo28wbTAdBgNVHQ4EFgQUsj/fiIB6 +Rv1I2uifIZIKWggJF+0wDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYB +Af8CAQEwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwEAYJKwYBBAGCNxUBBAMC +AQAwDQYJKoZIhvcNAQELBQADggIBAJjQPngeOpKjhd0eouyDSlWzYMyTLEgr +QwKc3XGL4el/AjB1bOTaSPs8Y7jLUQ62Za51qyijacWSHBbrOh77We5kUf4U +vFcCOKl7KdgBfgqEKJKM3/r6SIFMouc0DhWO82Yjs2IOwOuEoKXagZRHfhHG +MHFvnj6ebb1uGaPRvxdMVi70kEoPysCavQbDahVTKuFySo1iK2BtRDYKJYUZ +RLTON3NfuVb6AaIUCQpAB8+0qDdRVGV0nblmecLJ+pyCmfrWKFoRHiqV69Wu +AweiCX6qUxpokzwpM78fuaFnDFzihxzoFuLbv+79MiupYSsC73OrF9ABNyc2 +15jhmkM1NNNYtEH7Dj0JZz5o5GQ/ROH4Q2jU2mSbeB0EOpiW1tiUzt7/R/wR +cYvg6XSlwfapmE0EjrW2iUwywkU2+qGHvNQeuoIwILZ2eSBiaYZW97P49GNB +psliPGmpv9N5I35132b8CfwXpSBmqTTcN0UMBUKgZAV2AUqjNml8B7bZw8h++ +CybzMdHOy+8uNnwO/MWsozqXz5SIeRW6YeWqyHTwbfmkJzMVKuSBFrSgP9pe +UmzmpMFiqZ0yb9Rp5AhKx+kBEBAJAq4UEIV9Dxcw1hgiz00g7NewBokEfeVK +pYAyVLPiMvkT/HNZ0mnKeTES1xYONSV27UVzQHhQ6CxAKXiPS8Y +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/Backend/AutopilotMonitor.Functions/host.json b/src/Backend/AutopilotMonitor.Functions/host.json index 41d45508..ced44c96 100644 --- a/src/Backend/AutopilotMonitor.Functions/host.json +++ b/src/Backend/AutopilotMonitor.Functions/host.json @@ -4,6 +4,7 @@ "logLevel": { "default": "Warning", "Function": "Information", + "AutopilotMonitor.Functions.Security": "Debug", "Host": "Error", "Host.Aggregator": "Error", "Host.Results": "Error", diff --git a/src/Web/autopilot-monitor-web/staticwebapp.config.README.json b/src/Web/autopilot-monitor-web/staticwebapp.config.json.README.txt similarity index 100% rename from src/Web/autopilot-monitor-web/staticwebapp.config.README.json rename to src/Web/autopilot-monitor-web/staticwebapp.config.json.README.txt