Hantera dina känsliga konfigurationer

Intro

I introduktionen till den här mini-serien om konfiguration beskrev jag hur jag fick idén till den. Jag labbade, och håller fortfarande på, med docker och devops/CI/CD av containeriserade applikationer och då gled jag in på hur man hanterar konfigurationer i dom lägena. Vitsen med en containeriserad applikation är att den ska se exakt likadan ut oberoende av miljö, en container som snurrar på en utvecklarbox ska vara en exakt kopia av en som snurrar i produktion. Detta gör alltså att binärer och helst också andra filer som ingår i systemet ska vara identiska mellan miljöer. Det som gör det här svårt att uppnå är att just konfiguration är sådant som i dom flesta fallen skiljer sig mellan miljöerna. Det här går såklart att lösa på en mängd olika sätt. Jag vågar dock påstå att lösningarna kommer bli enklare om man skaffar sig kontroll över sin konfiguration, t.ex. på dom sätten som presenterades i förra posten, http://blog.headlight.se/hantera-configurationmanager-battre/.

Key management/vaults

En lösning på ovanstående, som jag tänkte presentera här, bygger på att man bryter loss sina konfigurerbara parametrar helt från applikationen och låter dess leva i ett system utanför. Parametrarna kommer att leva i säkra key-value-stores, där Azure Key Vault och Vault från HashiCorp är exempel på sådana system.

Får man ut miljöberoende parametrar utanför sin image så kan exakt samma image användas i alla miljöer för att skapa containrar.

Azure Key Vault och Vault från HashiCorp kan så mycket mer än att bara hålla en lista med nyckel-värde-par, såsom hantera kryptonyklar, certifikat etc.

I den här posten tittar vi bara på nyckel-värde-hanteringen i respektive och vi börjar med Azure Key Vault.

Azure Key Vault

Nedan syns två exempel på hur man kan använda Azure Key Vault från en .NET Core console app och från en ASP.NET Core web app. Båda exemplen bygger på att man använder den färdiga extension som finns i paketet Microsoft.Extensions.Configuration.AzureKeyVault för att läsa ut nycklar och värden. I ASP.NET Core webappen läser man i Key Vault i samband med uppstart och i Console-appen använder man det inbyggda IoC-ramverket och registrerar en konfiguration som en singleton för applikationen att använda. Exemplet nedan visar det sistnämnda.

class Program  
{
    ...
    static void Main(string[] args)
    {
        var serviceProvider = ServiceProviderConfiguration.CreateProvider(args[0]);
        ...
    }
}

public class ServiceProviderConfiguration  
{
    public static IServiceProvider CreateProvider(string keyVaultEndpoint)
    {
        var services = new ServiceCollection()
            .AddSingleton(service =>
            {
                IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
                configurationBuilder.AddAzureKeyVault(keyVaultEndpoint);

                return Configuration.Create(configurationBuilder.Build());
            })
            .AddSingleton<HttpClient>()
            .AddTransient<ThingSpeak>().BuildServiceProvider();

        return services;
    }
}

public class Configuration : IConfiguration  
{
    ... 
    public static IConfiguration Create(IConfigurationRoot configurationRoot) => 
        new Configuration(configurationRoot);

    private Configuration(IConfigurationRoot configurationRoot)
    {
        ...
    }        
}

Den uppmärksamme noterade nog i exemplet ovan att det inte finns någon autentisering eller auktorisering mot Azure Key Vault i koden?
När det gäller Azure Key Vault så kan man använda ett väldigt tilltalande sätt att autentisera sig. Istället för att hantera tokens, API-nycklar eller användarnamn och lösenord i konfigurationen till applikationen så låter man den få en identitet. Detta fungerar såklart bara om applikationen lever i Azure eller i en miljö med koppling till Azure och då lagras identiteten i Azure AD och det är den identiteten som har rättigheter att använda sig av Azure Key Vault. När en utvecklare kör applikationen lokalt på sin dev-box så använder man sin egen identitet, som även den finns i Azure AD. Man använder Azure CLI för att logga in och få tillgång till den identiteten.
På det här sättet kan man alltså få bort ALLA känsliga uppgifter från sin applikation, till och med uppstartsparametrar skulle kunna ha spelat ut sin roll.

Läs om Azure CLI här https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest och hur man loggar in mha 'az login' här https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest.

Koden för dom två exemplen som använder Azure Key Vault för att hålla konfigurationen finns här https://github.com/HeadlightAB/SecureSecretsStorage/tree/master/HelloAzureKeyVault och här https://github.com/HeadlightAB/SecureSecretsStorage/tree/master/HelloAzureKeyVaultWWW.

Vault by HashiCorp

Vault från HashiCorp är en plattform i ungefär samma stil som Azure Key Vault, även om det skiljer en del i respektive funktionsflora.

Att använda Vaults key-value-store är väldigt likt Azure Key Vault, men man måste skriva lite mer kod själv för att läsa ut värdena i storen. Även här finns två exempel, ett för en webbapplikation https://github.com/HeadlightAB/SecureSecretsStorage/tree/master/HelloVaultWWW och ett för en console-applikation https://github.com/HeadlightAB/SecureSecretsStorage/tree/master/HelloVault.

Under arbetet med exempelkoden kördes Vault i en docker-container:
docker run -d -p 81:8200 --cap-add=IPCLOCK -e 'VAULTDEVROOTTOKENID=aroot_token' vault

I console-applikationsexemplet ligger uppläsningen i uppsättningen av serviceprovidern, dvs i IoC-konfigurationen:

public class ServiceProviderConfiguration  
{
    public static ServiceProvider CreateProvider(string vaultServiceUri, string token)
    {
        var services = new ServiceCollection()
            .AddSingleton(service => 
                Configuration.Create(
                    VaultSecrets.Read(
                        new VaultClientSettings(vaultServiceUri, new TokenAuthMethodInfo(token)))))
            .AddSingleton<HttpClient>()
            .AddTransient<ThingSpeak>();

        return services.BuildServiceProvider();
    }
}

public class VaultSecrets  
{
    public static IDictionary<string, object> Read(VaultClientSettings clientSettings)
    {
        var vaultClient = new VaultClient(clientSettings);
        var secrets = vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(
                            "thingspeak", 
                            mountPoint: "blog-demo").Result;

        return secrets.Data.Data;
    }
}

VaultClient, VaultClientSettings etc kommer från nuget-paketet VaultSharp https://www.nuget.org/packages/VaultSharp/.

Configuration-instansen registreras som en singleton och injiceras i ThingSpeak-klassens konstruktor

public class ThingSpeak  
{
    private readonly IConfiguration _configuration;
    private readonly HttpClient _httpClient;

    public ThingSpeak(IConfiguration configuration, HttpClient httpClient)
    {
        _configuration = configuration;
        _httpClient = httpClient;
    }

    public async Task<ThingSpeakFeed> ReadFeed()
    {
        _httpClient.DefaultRequestHeaders.Clear();
        _httpClient.DefaultRequestHeaders.Add("THINGSPEAKAPIKEY", _configuration.Token);

        var httpResponseMessage = await _httpClient.GetAsync(_configuration.FieldUrl);
        var jsonContent = await httpResponseMessage.Content.ReadAsStringAsync();

        return Newtonsoft.Json.JsonConvert.DeserializeObject<ThingSpeakFeed>(jsonContent);
    }
}

I webbapplikationsexemplet lägger man istället till värdena från Vault-uppläsningen i en InMemoryCollection vid uppstart. Värdena i den är kollektionen är tillgängliga på alla ställen där en instans av en IConfiguration injiceras.

public class Program  
{
    // args[0] = "http://vps.freddes.se:81", args[1]="myroot"
    // in app args in project properties, in Debug - Profile: HelloVaultWWW
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost
            .CreateDefaultBuilder(args)
            .ConfigureAppConfiguration(config => 
                config.AddInMemoryCollection(
                    VaultSecrets.Read(
                        new VaultClientSettings(args[0], new TokenAuthMethodInfo(args[1])))))
            .UseStartup<Startup>();
}

I exemplet ovan ser vi att adress och token till Vault kommer in till applikationen via uppstartsparametrar. Detta skulle man såklart vilja få bort, vilket också är möjligt genom att tilldela applikationen en identitet och använda AD/LDAP-inloggning istället.

Summering

Dom här två posterna i ämnet konfiguration torde vara en bra start för att säkra konfigurationerna för sina system. Att få kontroll över dessa rörliga delar gör applikationerna och systemen robustare och mindre felbenägna.

Att ta kontrollen över konfigurationen är ett måste om man vill gå mot att containerisera applikationer och tjänster.