The request was aborted: Could not create SSL/TLS secure channel

Since most Servers are moving towards TLS 1.3 and removing TLS 1.0/1.1 support, it is important to make note of certain Server configurations that might be required to make your .Net Framework Application compatible with new TLS versions like TLS 1.2.

Just upgrading the Application to latest .Net Framework like 4.8 version, which as per documentation states it automatically handles the compatibility with newer TLS versions when older TLS versions are disabled.

I have managed to resolve the issues on my server by updating the SSL Cipher Suite Order, I had mistakenly removed some of the suites that windows suggested was for TLS1.0 and 1.1 only when in actual fact they were needed for some TLS1.2 connections as well.

I resolved my issues by:

  1. Open Run Prompt and run gpedit.msc
  2. Navigate to “Administrative Templates > Network > SSL Configuration Settings”
  3. Open SSL Cipher Suite Order
  4. Select Enabled
  5. Paste the list of suites below into the text box (make sure there are no spaces)
  6. Click Apply
  7. Restart the server

SSL SUITES:

TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384_P384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384_P384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA_P256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA_P256,TLS_DHE_DSS_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_256_CBC_SHA

Note, these suites work for me but you may require other ones for different applications. You should be able to find a full list and more info on the suites here https://docs.microsoft.com/en-us/windows/win32/secauthn/cipher-suites-in-schannel?redirectedfrom=MSDN

You can also use a tool like IISCrypto to update the Cipher Suite order.

Advertisement

Retry and Circuit Breaker Policy example .Net 6 and Polly

In this example, we’ll implement the Wait and Retry and Circuit Breaker policy using .Net 6 Web API and Polly. For more details on what Circuit Breaker is, refer to the MSDN documentation.

Create a WebAPI with ValuesController in .Net 6 which will always return an Exception in the Get call.

using Microsoft.AspNetCore.Mvc;

namespace ExternalAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ValuesController : ControllerBase
    {

        private readonly ILogger<ValuesController> _logger;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            throw new Exception("Error");
        }

    }
}

Let’s call this ExternalAPI and running on https://localhost:7250/.

Create another .Net 6 WebAPI called CircuitBreaker.Demo and install Polly and Newtonsoft.json Nuget packages.

Configure Polly and HttpClient in Program.cs file as shown below:

using Polly;
using Polly.Extensions.Http;

var builder = WebApplication.CreateBuilder(args);
var httpRetryPolicy = Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt));

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient("errorApi", c => { c.BaseAddress = new Uri("https://localhost:7250"); });
builder.Services.AddSingleton<IAsyncPolicy<HttpResponseMessage>>(httpRetryPolicy);


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Now add the PollyController to call the ExternalAPI using the HttpClient:

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Polly;

namespace CircuitBreaker.Demo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PollyController : ControllerBase
    {

        private readonly ILogger<PollyController> _logger;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly IAsyncPolicy<HttpResponseMessage> _policy;

        public PollyController(ILogger<PollyController> logger, IHttpClientFactory httpClientFactory, IAsyncPolicy<HttpResponseMessage> policy)
        {
            _logger = logger;
            _httpClientFactory = httpClientFactory;
            _policy = policy;
        }

        [HttpGet(Name = "GetApiResult")]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            var client = _httpClientFactory.CreateClient("errorApi");
            var response = await _policy.ExecuteAsync(() => client.GetAsync("values"));
            response.EnsureSuccessStatusCode();
            return JsonConvert.DeserializeObject<string[]>(await response.Content.ReadAsStringAsync());
        }

    }
}

When you run both the APIs, the call to External APIs is automatically made using Wait and Retry policy after every retryAttempt number of seconds 3 times. After that, the calls are stopped as the ExternalAPI keeps giving 500 error.

CircuitBreaker example:

Now change the Http Retry Policy line in the Program.cs file to:

var httpRetryPolicy = Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .CircuitBreakerAsync(2, TimeSpan.FromSeconds(30));

The above line will open the circuit and will start giving Circuit Open Exception after 2 attempts and keep the circuit Open for 30 seconds, which means you cannot make further calls to the API for that duration. The Exception will now be shown as below:

Kubernetes ReplicaSet example on Mac using VS Code

A ReplicaSet helps load balance and scale our Application up or down when the demand for it changes. It makes sure the desired number of pods are always running for high availability.

I’m using VS Code on Mac to create the below yaml file.

Create the following yaml file:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-replicaset
  labels:
    app: myapp
spec:
  selector:
    matchLabels:
      app: myapp
  replicas: 3
  template:
    metadata:
      name: nginx-2
      labels:
        app: myapp
    spec:
      containers:
        - name: nginx
          image: nginx

The above yaml file has 4 main properties which are apiVersion, kind, metadata and spec. The spec contains the definition for the number of replicas and the containers to be created inside the pods.

Run the following command:

$ kubectl create -f ReplicaSetsDemo.yaml 

replicaset.apps/myapp-replicaset created

Now let’s check the status of the replicaSet and then the pods:

$ kubectl get replicaset
NAME               DESIRED   CURRENT   READY   AGE
myapp-replicaset   3         3         3       2m29s


$ kubectl get pods

NAME                     READY   STATUS    RESTARTS   AGE
myapp-replicaset-s7jm7   1/1     Running   0          33s
myapp-replicaset-svqvm   1/1     Running   0          33s
myapp-replicaset-xnbbq   1/1     Running   0          33s

The above command shows 3 pods created for nginx with the name of the replicaset prefixed. A replicaset ensures that sufficient number of replicas or pods are available at all times.

Now, let’s delete a pod:

$ kubectl delete pod myapp-replicaset-s7jm7

pod "myapp-replicaset-s7jm7" deleted

Check the status of the pods again:

$ kubectl get pods
                         
NAME                     READY   STATUS    RESTARTS   AGE
myapp-replicaset-d7f88   1/1     Running   0          36s
myapp-replicaset-svqvm   1/1     Running   0          5m53s
myapp-replicaset-xnbbq   1/1     Running   0          5m53s

Notice that there are still 3 pods running as one more pod was created to maintain the desired state. Also, if you try to create a pod with the same label app=myapp outside of the replicaset, it’ll still come under the purview of the replicaset we created and will terminate that pod to maintain the desired state of 3 replicas. This is where it differs from a Replication Controller.

Now, let’s edit the replicaset to scale it up as below:

$ kubectl edit replicaset myapp-replicaset

The above command let’s you edit the in-memory configuration file that Kubernetes creates. Search for the replicas section and edit it to 4 using the vi editor commands and save it with wq!

Upon saving you’ll see the output as:

replicaset.apps/myapp-replicaset edited

Check the pods status and you’ll see and additional pod created:

$ kubectl get pods
                            
NAME                     READY   STATUS    RESTARTS   AGE
myapp-replicaset-d7f88   1/1     Running   0          14m
myapp-replicaset-pnlvh   1/1     Running   0          55s
myapp-replicaset-svqvm   1/1     Running   0          19m
myapp-replicaset-xnbbq   1/1     Running   0          19m

Another way to scale the replicaset is to use the below command and check the status again:

$ kubectl scale replicaset myapp-replicaset --replicas=2

replicaset.apps/myapp-replicaset scaled


$ kubectl get pods

NAME                     READY   STATUS    RESTARTS   AGE
myapp-replicaset-svqvm   1/1     Running   0          22m
myapp-replicaset-xnbbq   1/1     Running   0          22m

Notice that the pods are scaled down to 2 only as other 2 got terminated.

Enable CORS in .Net Core WebAPI

It is a common scenario where a React front-end SPA is calling a .Net Core WebAPI to fetch data. In this case, suppose both Applications are using their respective domains shown below:

React App => http://myfrontend.com
API=> http://myapi.com

CORS (Cross-Origin Resource Sharing) is a standard that works by adding HTTP headers that allow servers to describe the set of origins that are permitted to fetch information using a web browser and the kind of requests that are allowed.
For the API to allow the React App to fetch the data, it has to allow the Origin of the React App. This is a common CORS problem. So if you’re using, say axios or Fetch, to fetch data from the .Net Core WebAPI, it will only succeed if the API allows the domain http://myfrontend.com to call itself.

Below I’ve used the technique of CORS with named policy and middleware:

public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}
	readonly string MyAllowedOrigins = "_myAllowedOrigins";
	public IConfiguration Configuration { get; }
	public IContainer ApplicationContainer { get; private set; }
	// This method gets called by the runtime. Use this method to add services to the container.
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();
		services.AddCors(options =>
		{
			options.AddPolicy(MyAllowedOrigins,
			builder =>
			{
				//Allowing both the localhost and hosted domains.
				builder.WithOrigins("http://localhost:3000",
									"http://www.myfrontend.com");
			});
		});
	}
	
	//Apply CORS policies to all endpoints via CORS Middleware:
	// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
	{
		//Omitted code
		app.UseCors(MyAllowedOrigins);
		app.UseEndpoints(endpoints =>
		{
			endpoints.MapControllers();
		});
		//Omitted code
	}
}

The example above is how you apply CORS policy globally to all endpoints in your API. You can also use the [EnableCors(“Policy String”)] attribute on your controllers/page-model/action method.

e.g. Applying CORS on Action method:

[EnableCors("_myAllowedOrigins")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
	return new string[] { "test1", "test2" };
}

It is recommended to enable CORS either locally or globally and not combine the two approaches.

We can also expose the allowed Headers and http methods the following way:

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllers();
	services.AddCors(options =>
	{
		options.AddPolicy(MyAllowSpecificOrigins,
		builder =>
		{
			builder.WithOrigins("http://myfrontend.com", "http://localhost:3000")
			.WithHeaders("token")
			.WithMethods("OPTIONS", "GET", "POST");
		});
	});
}

You can also allow credentials to be passed to the WebAPI from the Client by chaining the .AllowCredentials method.

Pre-flight requests for OPTIONS method call fails with Windows authentication and gives 401 Unauthorized. This can be worked around by enabling both Windows and Anonymous authentication.

If you are using Windows Authentication in the case of an Intranet Application and also enabled Anonymous Authentication for allowing OPTIONS pre-flight request, then make sure to use [Authorize] and [AllowAnonymous] attributes to the respective endpoints where required. Without using [Authorize], the name of the user is returned
null with context.HttpContext.User.Identity.Name where context is the ActionExecutingContext object. These attributes can be used on Controllers or Actions as required.
Also, make sure to setup the middleware in the correct order for using with UseCors, UseAuthentication and UseAuthorization in the Startup.cs file Configuration.