.NET

12 ago, 2019

ASP.NET Core: protegendo segredos de uma aplicação com o Azure Key Vault

Publicidade

O armazenamento de configurações em que estão presentes informações sensíveis e utilizando arquivos como appsettings.json constitui uma prática bastante difundida. Haverá ocasiões, entretanto, nas quais mecanismos externos precisarão ser empregados a fim de dificultar o acesso a tais definições por parte de um usuário qualquer (que as obteria rapidamente pela simples leitura de um arquivo).

Considerando o desenvolvimento de projetos Web com ASP.NET Core, como proteger segredos de nossas aplicações de maneira simples e eficiente?

Uma boa resposta para esse questionamento está no Azure Key Vault, uma solução de armazenamento de configurações sensíveis oferecida pela plataforma de cloud computing da Microsoft. Possuindo integração com o Azure Active Directory, a manipulação (leitura, escrita) de segredos definidos no Key Vault acontece mediante a concessão prévia de acesso a uma aplicação.

Neste novo artigo demonstrarei o uso do Azure Key Vault em uma API REST criada com o ASP.NET Core 2.2. O exemplo em questão foi disponibilizado também no GitHub:

https://github.com/renatogroffe/ASPNETCore2.2_API-REST_AzureKeyVault

Registrando uma aplicação para utilização do Azure Key Vault

Neste primeiro momento acessaremos o Portal do Microsoft Azure:

Azure Active Directory destacado em vermelho

Na sequência acionar na barra lateral à esquerda o item Azure Active Directory:

Clicar agora na opção App registrations:

Aparecerão então eventuais aplicações já registradas no painel. Clicar na opção New registration, a qual permitirá que registremos a API REST empregada nos testes detalhados neste artigo:

Em Register an Application preencher:

  • O nome da aplicação (campo Name);
  • Manter selecionada a opção Accounts in this organizational directory only no campo Supported account types;
  • Em Redirect URI informar o valor https://localhost:5001 (neste artigo será feito um teste local apontando para o Azure Key Vault).

Confirmar este procedimento acionando o botão Register.

O registro da aplicação será exibido neste momento. O valor da configuração Application (client) ID deverá ser utilizado posteriormente, ao se configurar a comunicação da aplicação com o recurso do Key Vault:

Acionar agora a opção Certificates & secrets; o botão New client secretpermitirá a geração de uma chave, a ser empregada pela aplicação para validar seu acesso ao Azure Key Vault:

Em Add a client secret preencher o campo Description, selecionando ainda em Expires o valor Never. Confirmar as ações clicando no botão Add:

O valor indicado no campo VALUE do segredo deverá ser COPIADO IMEDIATAMENTE (um aviso no alto do painel alerta que tal configuração não estará mais disponível quando o usuário deixar esta funcionalidade no Portal do Azure):

E aproveito este espaço e o grande interesse por Docker também para um convite.

Tem interesse em conhecer mais sobre Docker? Que tal então fazer um curso completo, cobrindo desde fundamentos a diferentes possibilidades de uso de containers com tecnologias em alta no mercado? Adquira conhecimentos profundos sobre Docker, evolua e se diferencie no mercado, seja você um profissional DevOps, um Desenvolvedor ou um Arquiteto de Software!

Acompanhe o portal Docker Definitivo para ficar por dentro de novidades a serem anunciadas em breve!

Site: https://dockerdefinitivo.com/

Criando um recurso do Azure Key Vault

Ao solicitar a criação de um novo recurso no Portal do Azure pesquisar por Azure Key Vault, selecionado a opção destacada a seguir:

Clicar agora em Create:

Em Create key vault informar o nome do recurso (campo Name), um grupo de recursos (campo Resource Group) e o data center em que o mesmo será criado (Location). Acionar em seguida a opção Access policies:

Clicar em Add new, a fim de vincular ao recurso do Key Vault a aplicação registrada no Azure Active Directory:

No painel Add access policy selecionar em Configure from template (optional) a opção Secret Management, clicando na sequência em Select principal:

Em Principal localizar e selecionar a opção API REST Cotações, confirmando isso através do botão Select:

Já em Secret permissions habilitar apenas as opções Get e List, de forma que a API REST vinculada a este recurso do Azure Key Vault possua somente acesso de leitura às configurações cadastradas:

Concluir este procedimento clicando no botão OK:

Aparecerá agora em Access policies o item API REST Cotações registrado como APPLICATION; confirmar este ajuste por meio do botão OK:

Retornando ao painel Create key vault concluir o processo de criação clicando no botão Create. Após alguns segundos o recurso groffeartigoestará disponível; o endereço indicado em DNS NAME também será também utilizado para configurar o acesso na API REST de testes:


Adicionando configurações sensíveis no Azure Key Vault

Na seção Secrets do recurso do Key Vault serão cadastradas configurações que não deverão constar no arquivo appsettings.json. Acionar para isto a opção Generate/Import (destacada em vermelho):

Em Create a secret:

  • Manter o campo Upload options com o valor Manual selecionado;
  • Em Name informar o nome da configuração de forma similar à que a mesma estaria no appsettings.json. No caso de uma string de conexão, é normal que um item correspondente ao nome (BaseCotacoes) seja precedido pelo elemento ConnectionStrings. O uso de “–” (2 hífens)permite separar os níveis que existiriam em um arquivo JSON;
  • Preencher o campo Value com a Connection String;
  • Em Content type informar o valor Connection String.

Concluir esta ação pressionando o botão Create. O segredo ConnectionStrings–BaseCotacoes estará então disponível para uso:

Acessando o item ConnectionStrings–BaseCotacoes será exibida uma tela similar àquela apresentada na imagem a seguir:

Um clique na definição que está em CURRENT VERSION trará a próxima tela:

Com um simples clique em Show Secret Value podemos consultar o valor da Connection String armazenada no recurso groffeartigo:


Configurando o uso do Azure Key Vault em uma aplicação ASP.NET Core

O package Microsoft.Extensions.Configuration.AzureKeyVault deverá ser adicionado ao projeto que terá seus segredos armazenados no Key Vault:

Incluir agora a seção AzureKeyVault no arquivo appsettings.json, preenchendo as seguintes configurações:

  • Em DNS informar o conteúdo associado ao campo DNS NAME do recurso do Azure Key Vault criado na seção anterior;
  • Em ClientId preencher o valor do campo Application (client) IDcorrespondente à aplicação registrada anteriormente no Azure Active Directory;
  • O campo ClientSecret receberá a chave gerada durante a criação do segredo (em Certificates & secrets) para a aplicação API REST Cotação no Azure Active Directory.

Conforme é possível observar, não há qualquer menção a uma string de conexão neste arquivo de configurações:

{
  "AzureKeyVault": {
    "DNS": "https://groffeartigo.vault.azure.net/",
    "ClientId": "02d882bb-e1c9-4daa-a972-c56dbf6657ae",
    "ClientSecret": "aR[b-Gf/ksA*cJqFXVxrjuzo0nF3Ex47"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

A única alteração a ser realizada neste projeto acontecerá na classe Program:

  • Logo após a chamada ao método CreateDefaultBuilder da classe WebHost em CreateWebHostBuilder acrescentar uma chamada ao método ConfigureAppConfiguration (o acionamento do método UseStartup ficará logo na sequência);
  • Instanciar um novo builder de configurações em ConfigureAppConfiguration, invocando no objeto resultante o método AddAzureKeyVault e informando como parâmetros as chaves definidos para o elemento AzureKeyVault no arquivo appsettings.json (DNSClientId e ClientSecret).
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace APICotacoes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((context, config) =>
                {
                    var builtConfig = config.Build();

                    config.AddAzureKeyVault(
                        builtConfig["AzureKeyVault:DNS"],
                        builtConfig["AzureKeyVault:ClientId"],
                        builtConfig["AzureKeyVault:ClientSecret"]);
                })
                .UseStartup<Startup>();
    }
}

Testes

A seguir temos a implementação da classe Startup, com o uso normal do objeto Configuration. Isto inclui um acesso à chave ConnectionStrings:BaseCotacoes (linha 35), a qual não existe em appsettings.json e que numa situação normal implicaria na existência de dois níveis de propriedades no arquivo de configuração:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Mime;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Newtonsoft.Json;

namespace APICotacoes
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            // Adicionando a verificação de disponibilidade
            // do banco de dados
            services.AddHealthChecks()
                .AddSqlServer(Configuration["ConnectionStrings:BaseCotacoes"], name: "sql1");

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            // Ativando o middlweare de Health Check
            app.UseHealthChecks("/status",
               new HealthCheckOptions()
               {
                   ResponseWriter = async (context, report) =>
                   {
                       var result = JsonConvert.SerializeObject(
                           new
                           {
                               statusApplication = report.Status.ToString(),
                               healthChecks = report.Entries.Select(e => new
                               {
                                   check = e.Key,
                                   status = Enum.GetName(typeof(HealthStatus), e.Value.Status)
                               })
                           });
                       context.Response.ContentType = MediaTypeNames.Application.Json;
                       await context.Response.WriteAsync(result);
                   }
               });

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

Efetuando o debugging da aplicação é possível constatar que o ajuste na classe Program vinculou a string de conexão que está no recurso do Azure Key Vault (o segredo ConnectionStrings–BaseCotacoes) ao objeto Configuration. Tal fato permite a diferentes partes da aplicação acessar configurações sem grandes complicações, esteja uma determinada definição na nuvem ou no arquivo appsettings.json:

Um segundo teste envolverá a classe CotacoesController. Uma chamada a config.GetConnectionString(“BaseCotacoes”) – linha 18 – permitirá acessar o segredo que está no Azure Key Vault, como se a definição correspondente fosse um item chamado BaseCotacoes e estivesse vinculada ao elemento ConnectionStrings em appsettings.json:

using System.Data.SqlClient;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Dapper;

namespace APICotacoes.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CotacoesController : ControllerBase
    {
        [HttpGet]
        public ContentResult Get(
            [FromServices]IConfiguration config)
        {
            string valorJSON;
            using (SqlConnection conexao = new SqlConnection(
                config.GetConnectionString("BaseCotacoes")))
            {
                valorJSON = conexao.QueryFirst<string>(
                    "SELECT Sigla " +
                          ",NomeMoeda " +
                          ",UltimaCotacao " +
                          ",ValorComercial AS 'Cotacoes.Comercial' " +
                          ",ValorTurismo AS 'Cotacoes.Turismo' " +
                    "FROM dbo.Cotacoes " +
                    "ORDER BY NomeMoeda " +
                    "FOR JSON PATH, ROOT('Moedas')");
            }

            return Content(valorJSON, "application/json");
        }
    }
}

É o que demonstra o exemplo de debugging na próxima imagem: