SSL
HttpClient
Untrusted Certificates
Security
Networking

Allowing Untrusted SSL Certificates with HttpClient

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

To allow untrusted SSL certificates with HttpClient in C#, set a ServerCertificateCustomValidationCallback on HttpClientHandler that returns true. The simplest version uses the built-in DangerousAcceptAnyServerCertificateValidator. This bypasses all certificate validation, which is acceptable during local development but must never reach production. In production, the correct fix is trusting the certificate in the operating system's certificate store or using a properly issued certificate.

The Error You Are Seeing

When HttpClient connects to a server with an untrusted certificate (self-signed, expired, wrong hostname, or issued by an unknown CA), it throws:

csharp
1using var client = new HttpClient();
2var response = await client.GetAsync("https://localhost:5001/api/data");
3// System.Net.Http.HttpRequestException:
4//   The SSL connection could not be established, see inner exception.
5// Inner: AuthenticationException:
6//   The remote certificate is invalid according to the validation procedure.

This is the TLS handshake failing because the server's certificate chain cannot be validated against the system's trusted root store.

Quick Bypass for Development

The fastest way to suppress the error is DangerousAcceptAnyServerCertificateValidator:

csharp
1var handler = new HttpClientHandler
2{
3    ServerCertificateCustomValidationCallback =
4        HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
5};
6
7using var client = new HttpClient(handler);
8var response = await client.GetAsync("https://localhost:5001/api/data");
9Console.WriteLine(response.StatusCode); // OK

The name is intentionally alarming. This delegate returns true for every certificate regardless of errors, hostname mismatches, or expiration.

Custom Validation Logic

Instead of accepting everything, you can write a callback that accepts only specific certificates. This gives you a middle ground between full bypass and full validation.

csharp
1var handler = new HttpClientHandler
2{
3    ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
4    {
5        // No errors means the certificate is fully valid
6        if (errors == System.Net.Security.SslPolicyErrors.None)
7            return true;
8
9        // Accept a known self-signed certificate by thumbprint
10        if (cert?.GetCertHashString() == "A1B2C3D4E5F67890ABCDEF1234567890ABCDEF12")
11            return true;
12
13        // Accept any certificate when connecting to localhost
14        if (request.RequestUri?.Host == "localhost")
15            return true;
16
17        // Reject everything else
18        return false;
19    }
20};
21
22using var client = new HttpClient(handler);

The callback receives four parameters:

ParameterTypeDescription
requestHttpRequestMessageThe outgoing HTTP request
certX509Certificate2The server's certificate
chainX509ChainThe full certificate chain
errorsSslPolicyErrorsFlags indicating what validation failed

The SslPolicyErrors enum has three relevant values:

ValueMeaning
NoneCertificate is fully valid
RemoteCertificateChainErrorsChain validation failed (self-signed, unknown CA, expired)
RemoteCertificateNameMismatchCertificate hostname does not match the request URL

Using IHttpClientFactory in ASP.NET Core

In ASP.NET Core applications, you should not create HttpClient instances directly. Use IHttpClientFactory to manage handler lifetimes and configure SSL behavior per named or typed client:

csharp
1// Program.cs
2builder.Services.AddHttpClient("DevApi", client =>
3{
4    client.BaseAddress = new Uri("https://localhost:5001");
5})
6.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
7{
8    ServerCertificateCustomValidationCallback =
9        HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
10});

Then inject and use the factory:

csharp
1public class OrderService
2{
3    private readonly IHttpClientFactory _httpClientFactory;
4
5    public OrderService(IHttpClientFactory httpClientFactory)
6    {
7        _httpClientFactory = httpClientFactory;
8    }
9
10    public async Task<string> GetOrdersAsync()
11    {
12        var client = _httpClientFactory.CreateClient("DevApi");
13        var response = await client.GetAsync("/api/orders");
14        return await response.Content.ReadAsStringAsync();
15    }
16}

This approach keeps the SSL bypass scoped to a single named client rather than affecting all HTTP traffic in the application.

Environment-Conditional Bypass

The bypass should only activate in development. Use the hosting environment or a configuration flag to gate it:

csharp
1// Option 1: Check the hosting environment
2var handler = new HttpClientHandler();
3
4if (builder.Environment.IsDevelopment())
5{
6    handler.ServerCertificateCustomValidationCallback =
7        HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
8}
9
10builder.Services.AddHttpClient("ApiClient")
11    .ConfigurePrimaryHttpMessageHandler(() => handler);
csharp
1// Option 2: Use a configuration flag
2var bypassSsl = builder.Configuration.GetValue<bool>("BypassSslValidation");
3
4builder.Services.AddHttpClient("ApiClient")
5    .ConfigurePrimaryHttpMessageHandler(() =>
6    {
7        var handler = new HttpClientHandler();
8        if (bypassSsl)
9        {
10            handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
11        }
12        return handler;
13    });

In Option 2, the BypassSslValidation key would be set in appsettings.Development.json but not in appsettings.Production.json.

The Correct Fix: Trust the Certificate

Bypassing validation is a development shortcut. The production fix is adding the certificate to the trusted store so that HttpClient validates it normally:

bash
1# Windows: import to the Trusted Root Certification Authorities store
2certutil -addstore -f "ROOT" my-dev-cert.crt
3
4# macOS: add to the System keychain
5sudo security add-trusted-cert -d -r trustRoot \
6  -k /Library/Keychains/System.keychain my-dev-cert.crt
7
8# Linux (Debian/Ubuntu): add to system certificates
9sudo cp my-dev-cert.crt /usr/local/share/ca-certificates/
10sudo update-ca-certificates
11
12# Linux (RHEL/CentOS): use the trust command
13sudo cp my-dev-cert.crt /etc/pki/ca-trust/source/anchors/
14sudo update-ca-trust
15
16# ASP.NET Core development certificate
17dotnet dev-certs https --trust

After trusting the certificate, HttpClient accepts it without any custom callback. This is the approach that should be used in staging and production environments.

.NET Framework (Legacy Approach)

In .NET Framework (not .NET Core/.NET 5+), SSL validation is controlled globally through ServicePointManager:

csharp
// .NET Framework only: affects ALL HTTP connections in the entire process
ServicePointManager.ServerCertificateValidationCallback +=
    (sender, cert, chain, errors) => true;

This is even more dangerous than the per-handler approach because it disables validation for every HttpClient, WebClient, and HttpWebRequest in the process, including those created by third-party libraries. In .NET Core and .NET 5+, always use the per-handler ServerCertificateCustomValidationCallback instead.

Comparison: Bypass Approaches

ApproachScope.NET VersionRisk Level
HttpClientHandler callbackSingle client.NET Core+Moderate (scoped)
IHttpClientFactory handler configNamed/typed client.NET Core+Moderate (scoped)
ServicePointManager callbackEntire process.NET FrameworkHigh (global)
Trust certificate in OS storeSystem-wideAnyLow (proper fix)
dotnet dev-certs https --trustDev machine.NET Core+Low (dev only)

Common Pitfalls

  • Shipping the bypass to production: This is the single most important thing to get right. Disabling SSL validation in production exposes every request to man-in-the-middle attacks. An attacker on the network can intercept, read, and modify all traffic. Gate the bypass behind IsDevelopment() or a configuration flag that is never set in production.
  • Using ServicePointManager in .NET Core: It does not work in .NET Core. The per-handler callback is the only option, which is actually safer because it is scoped to a single client.
  • Forgetting that IHttpClientFactory reuses handlers: The ConfigurePrimaryHttpMessageHandler callback runs when a new handler is created, and handlers are pooled for two minutes by default. Changing configuration at runtime does not immediately affect existing pooled handlers.
  • Certificate pinning without rotation plans: If you pin a specific thumbprint in your custom callback, deploying a new certificate requires a code change and redeployment. Plan for certificate rotation by pinning the public key (SPKI) or the issuer instead.
  • Docker containers missing CA certificates: Containers built from minimal base images often lack the host's trusted certificates. Copy the CA certificate into the image during the Docker build (COPY ca.crt /usr/local/share/ca-certificates/ && RUN update-ca-certificates) rather than bypassing validation in the application code.
  • Singleton HttpClient with development handler: If you register HttpClient as a singleton with SSL bypass and it survives into a production deployment through misconfiguration, every request in the application is unprotected for the process lifetime.

Summary

  • Use ServerCertificateCustomValidationCallback on HttpClientHandler to bypass SSL validation during development.
  • DangerousAcceptAnyServerCertificateValidator accepts all certificates. Use it only for local testing.
  • Write custom validation to accept certificates by thumbprint or domain for more controlled bypass.
  • In ASP.NET Core, configure SSL bypass through IHttpClientFactory with named clients.
  • Gate the bypass behind IsDevelopment() or a configuration flag that is absent in production.
  • The proper production fix is trusting the certificate in the OS store or using dotnet dev-certs https --trust.
  • Never deploy SSL bypass to production. It completely negates the protection HTTPS provides.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.