Back-End

1 abr, 2019

Protegendo seu app ASP.NET Core com o Azure AD e a identidade do serviço gerenciado

Publicidade

Identidades gerenciadas para o App Service (MSI) são um recurso relativamente novo no Azure que tiram grande parte da dificuldade de lidar com as credenciais usadas para acessar os recursos do Azure a partir do seu código. Acabaram os segredos de clientes e senhas em strings de conexão.

Mas quando eu quis começar a usar esse recurso, não encontrei nenhum guia abrangente sobre como usar isso em um aplicativo ASP.NET Core. Este artigo é a minha chance de compilar as informações de várias fontes em um só lugar.

Foto de Jason Blackeye no Unsplash

Ativando o MSI

Há várias maneiras de fazer isso, inclusive usando a CLI do Azure. No entanto, para meus aplicativos de produção, prefiro usar templates ARM para provisionar a infraestrutura para os aplicativos, portanto, neste artigo, usaremos isso.

Utilizaremos as chamadas identidades atribuídas pelo sistema, portanto, este é um passo bastante fácil:

{
  "type": "Microsoft.Web/sites",
  "apiVersion": "2018-02-01",
  "name": "[variables('webSiteName')]",
  "location": "[resourceGroup().location]",
  "identity": {
    "type": "SystemAssigned"
  }
}

Portanto, a parte mágica aqui é adicionar a propriedade de identity e definir seu tipo como SystemAssigned. Tome cuidado para usar a apiVersion correta, pois o MSI é um recurso relativamente novo.

Seria uma boa ideia, neste momento, gerar a entidade de serviço que é criada automaticamente no Azure AD para nosso aplicativo. Precisaremos nos referir a ela depois. Então, vamos adicioná-lo à seção outputs do template ARM:

"outputs": {
  "appServicePrincipalId": {
    "type": "string",
    "value": "[reference(concat(resourceId('Microsoft.Web/sites', variables('webSiteName')), '/providers/Microsoft.ManagedIdentity/Identities/default'), '2015-08-31-PREVIEW').principalId]"
  }
}

Com isso, vamos fazer uma primeira implementação do template ARM, por exemplo, usando o PowerShell:

Login-AzureRmAccount
New-AzureRmResourceGroupDeployment `
  -Name initial_deploy `
  -ResourceGroupName msisample-rg `
  -TemplateFile '.\azuredeploy.json'

Podemos então ver o resultado no Portal do Azure:

Legenda: Um web app com uma identidade designada pelo sistema ativada. A ID do objeto corresponde à ID principal do serviço criado automaticamente, mencionado no template ARM.

Acessando um key vault do Azure

Agora que nossa identidade de serviço está criada, é hora de colocá-la em uso. A primeira coisa para a qual vamos usá-la é acessar um Azure Key Vault.

Um key vault é um módulo de segurança baseado em hardware que é um bom local para colocar certificados e outros segredos, em vez de tê-los em seu serviço de aplicativo.

Provisionamento de um key vault

A primeira coisa que precisamos fazer é provisionar um key vault. Não é de surpreender que, talvez, façamos isso no template ARM. Vamos adicionar o seguinte:

{
  "type": "Microsoft.KeyVault/vaults",
  "apiVersion": "2018-02-14",
  "name": "[variables('keyVaultName')]",
  "location": "[resourceGroup().location]",
  "properties": {
    "enabledForDeployment": true,
    "enabledForTemplateDeployment": true,
    "enabledForDiskEncryption": true,
    "tenantId": "[subscription().tenantId]",
    "accessPolicies": [],        
    "resources": []
  }
}

O key vault está vazio. Para nossos propósitos de demonstração, vamos adicionar um pequeno segredo para que possamos recuperar em nosso aplicativo mais tarde.

Coloque-o no array de recursos dentro do recurso do key vault:

"resources": [
  {
    "type": "secrets",
    "name": "chamber--secrets",
    "apiVersion": "2018-02-14",
    "properties": {
      "value": "basilisk"
    },
    "dependsOn": [
      "[concat('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]"
    ]
  }
]

Crie uma política de acesso ao key vault

A próxima coisa que precisamos fazer é conceder ao nosso aplicativo acesso ao key vault. Para isso, criamos uma política de acesso e precisamos encontrar o principal de serviço gerado.

Em nossa primeira versão do template ARM, adicionamos isso à saída do template, para que possamos reutilizar a função de template para ele:

[reference (concat (resourceId ('Microsoft.Web/ sites', variables ('webSiteName') ), '/ providers / Microsoft.ManagedIdentity / Identities / default'), '2015-08-31-PREVIEW'). principalId]

Adicionaremos a política ao array accessPolicies na definição do key vault:

"accessPolicies": [
  {
    "tenantId": "[subscription().tenantId]",
    "objectId": "[reference(concat(resourceId('Microsoft.Web/sites', variables('webSiteName')), '/providers/Microsoft.ManagedIdentity/Identities/default'), '2015-08-31-PREVIEW').principalId]",
    "permissions": {
      "keys": [
        "Get",
        "Decrypt",
        "Encrypt",
        "Verify",
        "Sign"
      ],
      "secrets": [
        "Get",
        "List"
      ],
      "certificates": [
        "Get",
        "GetIssuers",
        "ListIssuers"
      ]
    }
  }
]

Essa é uma política bastante padrão, concedendo ao aplicativo acesso a três tipos de recursos no key vault: chaves, segredos e certificados.

Não vamos nos deter nos detalhes: apenas enfatizamos que o aplicativo deve ter acesso apenas ao que precisa, não mais. Leia os detalhes sobre como proteger seu key vault aqui.

Acessando o key vault a partir de um app ASP.NET core

Temos nossa infraestrutura pronta, então a única coisa que resta é permitir que o aplicativo ASP.NET core (2.1+) se conecte ao key vault e recupere seu segredo.

Existem várias maneiras de codificar seu cliente de key vault. Minha favorita é torná-lo o mais transparente possível para o código do seu aplicativo, carregando chaves, segredos e certificados como parte de sua configuração normal.

Primeiro, vamos adicionar as dependências necessárias:

Install-Package Microsoft.Extensions.Configuration.AzureKeyVault
Install-Package Microsoft.Azure.Services.AppAuthentication -Prerelease

Em seguida, vamos configurar o carregamento a partir do key vault durante a criação do web host em Program.cs:

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

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureAppConfiguration((ctx, builder) =>
        {
            var config = builder.Build();
            var keyVaultBaseUrl = config["KeyVault:BaseUrl"];
            if (string.IsNullOrWhiteSpace(keyVaultBaseUrl)) return;
            
            var tokenProvider = new AzureServiceTokenProvider();
            var kvClient = new KeyVaultClient((authority, resource, scope)
                => tokenProvider.KeyVaultTokenCallback(authority, resource, scope));
            builder.AddAzureKeyVault(keyVaultBaseUrl, kvClient,
                new DefaultKeyVaultSecretManager());
        });

Observe que a única parte da configuração que precisamos é a URL do key vault. Em seguida, instanciamos um AzureServiceTokenProvider que cuidará de tudo para nós usando o serviço gerenciado para recuperar um token de acesso para o key vault.

Note também que, se KeyVault: BaseUrl não tiver nenhum valor, retornamos a partir do código de configuração do aplicativo sem nenhuma configuração.

Isso é útil porque o AzureServiceTokenProvider não funcionará ao executar o aplicativo na estação de trabalho do desenvolvedor.

Portanto, para sua configuração de desenvolvimento local, forneça qualquer valor para que seu código possa ser executado localmente.

Vamos voltar ao nosso template ARM para adicionar a configuração do aplicativo KeyVault:BaseUrl, confira a seguir.

Definição de uma configuração de aplicativo no web app do Azure que aponta para o Azure Key Vault (algumas propriedades são ignoradas por brevidade):

{
  "type": "Microsoft.Web/sites",
  "name": "[variables('webSiteName')]",
  "identity": {
    "type": "SystemAssigned"
  },
  "properties": {
    "siteConfig": {
      "appSettings": [
        {
          "name": "KeyVault:BaseUrl",
          "value": "[concat('https://', variables('keyVaultName'), '.vault.azure.net/')]"
        }
      ]
    }
  }
}

Usando informações do key vault no código do aplicativo

Finalmente, podemos acessar as informações do key vault em nosso aplicativo da mesma maneira que nas propriedades de configuração comuns. Veja, por exemplo, este simples controller ASP.NET:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly string _secret;
    public ValuesController(IConfiguration config)
    {
        _secret = config["chamber:secrets"];
    }

    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new[] { _secret };
    }
}

Observe que no código, nos referimos à definição de configuração como chamber:secrets, enquanto que na configuração do key vault, o nome é chamber--secrets.

Em outras palavras, a configuração do key vault usa dois hifens como um delimitador para estrutura hierárquica, enquanto o .NET core (e a configuração do web app do Azure) usa dois pontos.

Acessar o controller através de um navegador revela o segredo do key vault:

Acessando um servidor SQL

Portanto, o próximo recurso ao qual nos conectaremos é um servidor Azure SQL. Primeiro, vamos provisionar um adicionando-o como um recurso no template ARM:

{
  "type": "Microsoft.Sql/servers",
  "apiVersion": "2018-06-01-preview",
  "name": "[variables('sqlServerName')]",
  "location": "[resourceGroup().location]",
  "properties": {
    "administratorLogin": "[parameters('SQL Administrator Login')]",
    "administratorLoginPassword": "[parameters('SQL Administrator Password')]"
  },
  "resources": []
}

Concedendo acesso do aplicativo ao Azure SQL

Observe que temos propriedades para uma conta de administrador do sistema local no servidor.

O que faremos a seguir é conceder direitos de acesso de administrador do aplicativo ao servidor SQL por meio do Azure AD.

Para isso, vamos adicionar o seguinte ao array resources do nosso servidor Azure SQL:

{
  "type": "administrators",
  "name": "activeDirectory",
  "apiVersion": "2014-04-01-preview",
  "properties": {
    "administratorType": "ActiveDirectory",
    "login": "[variables('webSiteName')]",
    "sid": "[reference(concat(resourceId('Microsoft.Web/sites', variables('webSiteName')), '/providers/Microsoft.ManagedIdentity/Identities/default'), '2015-08-31-PREVIEW').principalId]",
    "tenantId": "[subscription().tenantId]"
  },
  "dependsOn": [
    "[concat('Microsoft.Sql/servers/', variables('sqlServerName'))]"
  ]
}

Note que usamos o nome do site como login e, para sid, usamos o mesmo principalId que utilizamos em nossa política do Azure Key Vault.

Isso significa que nosso administrador agora é um administrador do sistema para o servidor. Essa é uma abordagem simples, mas talvez você não queira conceder ao aplicativo tantos direitos de acesso.

Voltaremos a isso mais tarde.

Provisionamento de um banco de dados SQL

Para continuar com a configuração da nossa demonstração, vamos provisionar um banco de dados no servidor ao qual nosso aplicativo se conectará.

Vamos adicioná-lo ao template ARM:

{
  "type": "Microsoft.Sql/servers/databases",
  "apiVersion": "2017-10-01-preview",
  "name": "[concat(variables('sqlServerName'), '/', variables('sqlDbName'))]",
  "location": "[resourceGroup().location]",
  "dependsOn": [
    "[concat('Microsoft.Sql/servers/', variables('sqlServerName'))]"
  ],
  "properties": {
    "collation": "SQL_Latin1_General_CP1_CI_AS",
    "edition": "Basic",
    "maxSizeBytes": "1073741824",
    "requestedServiceObjectiveName": "Basic"
  }
}

Preparando o aplicativo

Agora que o aplicativo acessará o servidor Azure SQL usando sua identidade de serviço, podemos remover o nome de usuário e a senha explícitos da string de conexão. Vamos adicionar a string de conexão ao nosso template ARM, no array connectionStrings do nosso recurso de web app, confira logo abaixo.

Definição de uma string de conexão sem nome de usuário e senha explícitos para o servidor de banco de dados (algumas propriedades omitidas por brevidade):

{
  "type": "Microsoft.Web/sites",
  "name": "[variables('webSiteName')]",
  "identity": {
    "type": "SystemAssigned"
  },
  "properties": {
    "siteConfig": {
      "connectionStrings" : [
        {
          "name":"ChamberConnection",
          "connectionString":"[concat('Server=tcp:', variables('sqlServerName'), '.database.windows.net,1433;Database=',variables('sqlDbName'),';')]",
          "type":  "SQLAzure"
        }
      ]
    }
  }
}

A próxima coisa que precisamos é fazer com que o ADO.Net lide com a autenticação do banco de dados usando a identidade do serviço gerenciado. Se você ignorou a seção do Azure Key vault acima, adicione o pacote Nuget necessário:

Install-Package Microsoft.Azure.Services.AppAuthentication -Prerelease

Em seguida, no constructor do nosso SampleContext, vamos colocar o AzureServiceTokenProvider para funcionar:

public class SampleContext : DbContext
{
    public SampleContext(DbContextOptions<SampleContext> options, IHostingEnvironment host)
        : base(options)
    {
        if (host.IsDevelopment()) return;
        var conn = (SqlConnection)Database.GetDbConnection();
        conn.AccessToken = (new AzureServiceTokenProvider())
            .GetAccessTokenAsync("https://database.windows.net/").Result;
    }
}

Isso é tudo para a parte de configuração!

Código de aplicação

Agora, vamos criar um código de aplicativo bem simples na forma de um controller que busca informações a partir do banco de dados:

[Route("api/[controller]")]
[ApiController]
public class DbValuesController : ControllerBase
{
    private readonly SampleContext _ctx;
    public DbValuesController(SampleContext ctx) => _ctx = ctx;

    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
        => new[] { _ctx.Chamber.FirstOrDefault()?.Name };
}

E eis que podemos realmente ler a partir do banco de dados:

Restringindo o acesso ao servidor de banco de dados

Nossa configuração para acessar o servidor de banco de dados é bastante simples e é 100% tratada no template ARM.

No entanto, damos os direitos de administrador do sistema de aplicativos no servidor, o que não agradará qualquer pessoa preocupada com a segurança.

Vamos tentar restringir o acesso do aplicativo apenas ao banco de dados ao qual ele precisa se conectar.

Vamos torná-lo db_owner, embora alguns possam dizer que mesmo isso é muito inseguro.

Não podemos conseguir isso usando o template ARM sozinho, pois não podemos provisionar os usuários do banco de dados. Então, vamos utilizar o Azure CLI para nos ajudar. Mas, primeiro, certifique-se de remover o acesso de administrador do sistema ao servidor.

Criando um grupo do Azure AD

A melhor maneira de atribuir direitos de acesso ao banco de dados para nosso aplicativo é por meio da associação a um grupo do Azure AD. Primeiro, vamos criar o grupo e adicionar a entidade de serviço do aplicativo como membro desse grupo. Veja como isso é feito no PowerShell usando o Azure CLI:

$groupid=$(az ad group create --display-name msisample-admins `
  --mail-nickname msisample-admins --query objectId --output tsv)
$msiobjectid='<output from ARM template above>'
az ad group member add --group $groupid --member-id $msiobjectid
az ad group member list -g $groupid

Concedendo acesso ao banco de dados via grupo

A etapa final é usar a ferramenta sqlcmd.exe para criar um usuário do banco de dados para o grupo do Azure AD e atribuir a função db_owner a ele:

sqlcmd.exe -S msisample-sql.database.windows.net -d chamber -U '<AAD-username>' -P '<AAD-password>' -G -l 30
1> CREATE USER [msisample-admins] FROM EXTERNAL PROVIDER;
2> ALTER ROLE db_owner ADD MEMBER [msisample-admins];
3> GO

Observe que, acima, nós assumimos o seguinte:

  • Existe um administrador do Azure configurado para o banco de dados
  • O ADD-username acima é o administrador do Azure para Azure SQL
  • O switch -G fornecido ao sqlcmd denota que a autenticação do Azure AD deve ser usada durante a conexão com o SQL do Azure

Resumo

Vimos como podemos usar a MSI (Managed Service Identity) em um web app do Azure para se conectar ao Azure Key Vault e ao Azure SQL sem lidar explicitamente com IDs de clientes, segredos de clientes, usuários de bancos de dados e senhas de bancos de dados no aplicativo.

Também vimos como podemos fazer isso funcionar de ponta a ponta, desde templates ARM, configuração de aplicativos até lógica de aplicativos.

O código-fonte deste artigo pode ser encontrado aqui.

***

Vidar Kongsli faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://blog.bredvid.no/protecting-your-asp-net-core-app-with-azure-ad-and-managed-service-identity-78007d7a0774