The previous article introduced how to use Consul to store configuration of ASP.Net Core (or .Net also). However, it was missing an important thing : the security ! In this article, we will see how we can address this by using ACLs mechanism built into Consul with the previously developed code in order to secure the connection between Consul and ASP.Net Core.
What’s required on Consul
On a normal Consul installation, the cluster should be secured by TLS (see here) to at least verify the authenticity of the server and force the API to use HTTPS.
Going further, it’s possible to use an ACL (Access Control List) key to give rights to the different applications. For example, you can create an ACL to allow App1 to read its configuration key/values, declare itself in the service catalog, consume the service catalog and update its health. The ACL would prevent App1 from reading other apps configuration or declare another service in the catalog.
Declaring an ACL rule is easy once ACLs are activated (see here), it uses the following syntax in the Consul UI :
key "App1/Dev" {
policy = "read"
}
After creating the ACL, the UI gives a token which looks like a UUID, this token needs to be passed in the HTTP requests headers.
The default policy can be configured to deny everything for anonymous calls.
Adapting the code
Let’s adapt the code (some code hidden for brievety) :
public class ConsulConfigurationProvider : ConfigurationProvider
{
private const string ConsulIndexHeader = "X-Consul-Index";
private const string ConsulAclTokenHeader = "X-Consul-Token";
private readonly string _path;
private readonly string _consulAclToken;
private readonly HttpClient _httpClient;
/* ... */
public ConsulConfigurationProvider(IEnumerable<Uri> consulUrls, string path, string consulAclToken = null)
{
_path = path;
_consulAclToken = consulAclToken;
_consulUrls = consulUrls.Select(u => new Uri(u, $"v1/kv/{path}")).ToList();
/* ... */
}
private async Task<IDictionary<string, string>> ExecuteQueryAsync(bool isBlocking = false)
{
var requestUri = isBlocking ? $"?recurse=true&index={_consulConfigurationIndex}" : "?recurse=true";
using (var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_consulUrls[_consulUrlIndex], requestUri)))
{
if (!string.IsNullOrWhiteSpace(_consulAclToken))
request.Headers.Add(ConsulAclTokenHeader, _consulAclToken);
using (var response = await _httpClient.SendAsync(request))
{
/* ... */
}
}
}
}
The only change is to get a token through the constructor and pass it in the header of the request.
Of course, the methods in the ConfigurationSource and in the extension should be updated too.
Don’t forget to consider the token as a secret, therefore it should be handled properly (as a docker secret, a secret in Azure Key Vault, etc.)
Going further with client certificate
It’s even possible to use client certificate to authenticate the client. For this, a certificate must be installed on the machine certificate store. What is needed on a code perspective is a method to retrieve the certificate and use it with the HttpClient instance.
First of all, here’s a sample on how to retrieve a certificate by its thumbprint :
private static X509Certificate2Collection GetLocalMachineCertificateByThumbprint(string thumbprint)
{
using (var x509Store = new X509Store(StoreLocation.LocalMachine))
{
x509Store.Open(OpenFlags.OpenExistingOnly);
return x509Store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true);
}
}
We can now change the constructor of the ConfigurationProvider to use this.
public ConsulConfigurationProvider(IEnumerable<Uri> consulUrls, string path, string consulAclToken = null, string clientCertThumbprint = null)
{
_path = path;
_consulAclToken = consulAclToken;
_consulUrls = consulUrls.Select(u => new Uri(u, $"v1/kv/{path}")).ToList();
if (_consulUrls.Count <= 0)
{
throw new ArgumentOutOfRangeException(nameof(consulUrls));
}
var httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip };
if (!string.IsNullOrWhiteSpace(clientCertThumbprint))
httpClientHandler.ClientCertificates.AddRange(GetLocalMachineCertificateByThumbprint(clientCertThumbprint));
_httpClient = new HttpClient(httpClientHandler, true);
_configurationListeningTask = new Task(ListenToConfigurationChanges);
}
The changes here are the call to the method above if there’s a thumbprint provided and use the result in the HttpClientHandler
.
Final word
The ConfigurationProvider can now authenticates with its client certificate and declare an ACL token to authorize its action and access its private resources (read its configuration, update its health, etc.). Everything is now secured !