Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/Runner.Common/HttpClientHandlerFactory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using GitHub.Runner.Sdk;

namespace GitHub.Runner.Common
Expand All @@ -21,7 +23,60 @@ public HttpClientHandler CreateClientHandler(RunnerWebProxy webProxy)
client.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}

// Configure mTLS client certificate for proxy authentication
if (!string.IsNullOrEmpty(webProxy.HttpsProxyClientCert))
{
var clientCert = LoadClientCertificate(
webProxy.HttpsProxyClientCert,
webProxy.HttpsProxyClientKey);
if (clientCert != null)
{
client.ClientCertificates.Add(clientCert);
}
}

return client;
}

private X509Certificate2 LoadClientCertificate(string certPath, string keyPath)
{
try
{
if (!File.Exists(certPath))
{
Trace.Warning($"Client certificate file not found: {certPath}");
return null;
}

// If key path is provided separately, load cert and key from separate files
if (!string.IsNullOrEmpty(keyPath))
{
if (!File.Exists(keyPath))
{
Trace.Warning($"Client key file not found: {keyPath}");
return null;
}

// Load certificate and private key from separate PEM files
var certPem = File.ReadAllText(certPath);
var keyPem = File.ReadAllText(keyPath);
var cert = X509Certificate2.CreateFromPem(certPem, keyPem);

// On Windows, we need to export and re-import to make the certificate usable
// with SslStream/HttpClient
return new X509Certificate2(cert.Export(X509ContentType.Pfx));
}
else
{
// Assume the cert file contains both certificate and key (PFX/PKCS12 format)
return new X509Certificate2(certPath);
}
}
catch (Exception ex)
{
Trace.Warning($"Failed to load client certificate: {ex.Message}");
return null;
}
}
}
}
37 changes: 37 additions & 0 deletions src/Runner.Sdk/RunnerWebProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class RunnerWebProxy : IWebProxy
private string _httpsProxyUsername;
private string _httpsProxyPassword;
private string _noProxyString;
private string _httpsProxyClientCert;
private string _httpsProxyClientKey;
private string _httpsProxyCACert;

private readonly List<ByPassInfo> _noProxyList = new();
private readonly HashSet<string> _noProxyUnique = new(StringComparer.OrdinalIgnoreCase);
Expand All @@ -35,6 +38,9 @@ public class RunnerWebProxy : IWebProxy
public string HttpsProxyUsername => _httpsProxyUsername;
public string HttpsProxyPassword => _httpsProxyPassword;
public string NoProxyString => _noProxyString;
public string HttpsProxyClientCert => _httpsProxyClientCert;
public string HttpsProxyClientKey => _httpsProxyClientKey;
public string HttpsProxyCACert => _httpsProxyCACert;

public List<ByPassInfo> NoProxyList => _noProxyList;

Expand Down Expand Up @@ -137,6 +143,37 @@ public RunnerWebProxy()
}
}

// Load mTLS client certificate configuration for proxy connections
var httpsProxyClientCert = Environment.GetEnvironmentVariable("HTTPS_PROXY_CLIENT_CERT");
if (string.IsNullOrEmpty(httpsProxyClientCert))
{
httpsProxyClientCert = Environment.GetEnvironmentVariable("https_proxy_client_cert");
}
if (!string.IsNullOrEmpty(httpsProxyClientCert))
{
_httpsProxyClientCert = httpsProxyClientCert.Trim();
}

var httpsProxyClientKey = Environment.GetEnvironmentVariable("HTTPS_PROXY_CLIENT_KEY");
if (string.IsNullOrEmpty(httpsProxyClientKey))
{
httpsProxyClientKey = Environment.GetEnvironmentVariable("https_proxy_client_key");
}
if (!string.IsNullOrEmpty(httpsProxyClientKey))
{
_httpsProxyClientKey = httpsProxyClientKey.Trim();
}

var httpsProxyCACert = Environment.GetEnvironmentVariable("HTTPS_PROXY_CA_CERT");
if (string.IsNullOrEmpty(httpsProxyCACert))
{
httpsProxyCACert = Environment.GetEnvironmentVariable("https_proxy_ca_cert");
}
if (!string.IsNullOrEmpty(httpsProxyCACert))
{
_httpsProxyCACert = httpsProxyCACert.Trim();
}

if (!string.IsNullOrEmpty(noProxyList))
{
_noProxyString = noProxyList;
Expand Down
52 changes: 52 additions & 0 deletions src/Test/L0/RunnerWebProxyL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,52 @@ public void WebProxyFromEnvironmentVariablesWithPort80()
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void WebProxyClientCertificateFromEnvironmentVariables()
{
try
{
Environment.SetEnvironmentVariable("https_proxy", "http://127.0.0.1:9999");
Environment.SetEnvironmentVariable("HTTPS_PROXY_CLIENT_CERT", "/path/to/client.crt");
Environment.SetEnvironmentVariable("HTTPS_PROXY_CLIENT_KEY", "/path/to/client.key");
Environment.SetEnvironmentVariable("HTTPS_PROXY_CA_CERT", "/path/to/ca.crt");
var proxy = new RunnerWebProxy();

Assert.Equal("/path/to/client.crt", proxy.HttpsProxyClientCert);
Assert.Equal("/path/to/client.key", proxy.HttpsProxyClientKey);
Assert.Equal("/path/to/ca.crt", proxy.HttpsProxyCACert);
}
finally
{
CleanProxyEnv();
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void WebProxyClientCertificateLowerCaseFromEnvironmentVariables()
{
try
{
Environment.SetEnvironmentVariable("https_proxy", "http://127.0.0.1:9999");
Environment.SetEnvironmentVariable("https_proxy_client_cert", "/path/to/client.crt");
Environment.SetEnvironmentVariable("https_proxy_client_key", "/path/to/client.key");
Environment.SetEnvironmentVariable("https_proxy_ca_cert", "/path/to/ca.crt");
var proxy = new RunnerWebProxy();

Assert.Equal("/path/to/client.crt", proxy.HttpsProxyClientCert);
Assert.Equal("/path/to/client.key", proxy.HttpsProxyClientKey);
Assert.Equal("/path/to/ca.crt", proxy.HttpsProxyCACert);
}
finally
{
CleanProxyEnv();
}
}

private void CleanProxyEnv()
{
Environment.SetEnvironmentVariable("http_proxy", null);
Expand All @@ -589,6 +635,12 @@ private void CleanProxyEnv()
Environment.SetEnvironmentVariable("HTTPS_PROXY", null);
Environment.SetEnvironmentVariable("no_proxy", null);
Environment.SetEnvironmentVariable("NO_PROXY", null);
Environment.SetEnvironmentVariable("HTTPS_PROXY_CLIENT_CERT", null);
Environment.SetEnvironmentVariable("HTTPS_PROXY_CLIENT_KEY", null);
Environment.SetEnvironmentVariable("HTTPS_PROXY_CA_CERT", null);
Environment.SetEnvironmentVariable("https_proxy_client_cert", null);
Environment.SetEnvironmentVariable("https_proxy_client_key", null);
Environment.SetEnvironmentVariable("https_proxy_ca_cert", null);
}
}
}