embedded Intune root + intermediate certs for chain validation

This commit is contained in:
Oliver Kieselbach
2026-02-11 11:51:30 +01:00
parent 05261e9082
commit 4591d05e65
8 changed files with 146 additions and 20 deletions
@@ -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"),
+6 -2
View File
@@ -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;
@@ -28,4 +28,9 @@
<ProjectReference Include="..\..\Shared\AutopilotMonitor.Shared\AutopilotMonitor.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Security\Certificates\intune-intermediate-ca.pem" />
<EmbeddedResource Include="Security\Certificates\intune-root-ca.pem" />
</ItemGroup>
</Project>
@@ -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)
/// </remarks>
public static class CertificateValidator
{
private static readonly ConcurrentDictionary<string, CachedValidationResult> _validationCache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
// Lazily loaded Intune CA certs from embedded resources
private static readonly Lazy<X509Certificate2[]> _intuneCaCerts = new(LoadIntuneCaCertificates);
private class CachedValidationResult
{
public CertificateValidationResult Result { get; set; } = null!;
public DateTime ExpiresAt { get; set; }
}
/// <summary>
/// Validates a client certificate from HTTP request header
/// </summary>
@@ -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<X509ChainElement>().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
};
}
}
/// <summary>
/// Loads Intune CA certificates from embedded PEM resources.
/// Files: Security/Certificates/intune-intermediate-ca.pem
/// Security/Certificates/intune-root-ca.pem
/// </summary>
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<X509Certificate2> 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();
}
}
/// <summary>
@@ -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-----
@@ -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-----
@@ -4,6 +4,7 @@
"logLevel": {
"default": "Warning",
"Function": "Information",
"AutopilotMonitor.Functions.Security": "Debug",
"Host": "Error",
"Host.Aggregator": "Error",
"Host.Results": "Error",