mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
embedded Intune root + intermediate certs for chain validation
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
+28
@@ -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-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
@@ -4,6 +4,7 @@
|
||||
"logLevel": {
|
||||
"default": "Warning",
|
||||
"Function": "Information",
|
||||
"AutopilotMonitor.Functions.Security": "Debug",
|
||||
"Host": "Error",
|
||||
"Host.Aggregator": "Error",
|
||||
"Host.Results": "Error",
|
||||
|
||||
Reference in New Issue
Block a user