Como aproveitar a identidade gerida da aplicação Service Fabric para aceder aos Serviços do Azure

Os aplicativos do Service Fabric podem aproveitar identidades gerenciadas para acessar outros recursos do Azure que oferecem suporte à autenticação baseada em ID do Microsoft Entra. Um aplicativo pode obter um token de acesso que representa sua identidade, que pode ser atribuído pelo sistema ou pelo usuário, e usá-lo como um token de "portador" para autenticar-se em outro serviço - também conhecido como servidor de recursos protegidos. O token representa a identidade atribuída ao aplicativo Service Fabric e só será emitido para recursos do Azure (incluindo aplicativos SF) que compartilham essa identidade. Consulte a documentação de visão geral da identidade gerenciada para obter uma descrição detalhada das identidades gerenciadas, bem como a distinção entre identidades atribuídas pelo sistema e atribuídas pelo usuário. Referir-nos-emos a um aplicativo do Service Fabric habilitado para identidade gerenciada como o aplicativo cliente ao longo deste artigo.

Veja um aplicativo de exemplo complementar que demonstra o uso de identidades gerenciadas de aplicativos do Service Fabric atribuídos pelo sistema e pelo usuário com serviços confiáveis e contêineres.

Importante

Uma identidade gerenciada representa a associação entre um recurso do Azure e uma entidade de serviço no locatário correspondente do Microsoft Entra associado à assinatura que contém o recurso. Como tal, no contexto do Service Fabric, as identidades geridas só têm suporte para aplicações implementadas como recursos do Azure.

Importante

Antes de usar a identidade gerenciada de um aplicativo do Service Fabric, o aplicativo cliente deve ter acesso ao recurso protegido. Consulte a lista de serviços do Azure que suportam a autenticação do Microsoft Entra para verificar se há suporte e, em seguida, a documentação do respetivo serviço para obter etapas específicas para conceder acesso de identidade a recursos de interesse.

Aproveite uma identidade gerenciada usando Azure.Identity

O SDK do Azure Identity agora dá suporte ao Service Fabric. Usar Azure.Identity facilita a escrita de código para usar identidades gerenciadas do aplicativo Service Fabric porque ele lida com a busca de tokens, tokens de cache e autenticação de servidor. Ao acessar a maioria dos recursos do Azure, o conceito de um token fica oculto.

O suporte do Service Fabric está disponível nas seguintes versões para estes idiomas:

Exemplo em C# de inicializar credenciais e usar as credenciais para buscar um segredo do Cofre de Chaves do Azure:

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

namespace MyMIService
{
    internal sealed class MyMIService : StatelessService
    {
        protected override async Task RunAsync(CancellationToken cancellationToken)
        {
            try
            {
                // Load the service fabric application managed identity assigned to the service
                ManagedIdentityCredential creds = new ManagedIdentityCredential();

                // Create a client to keyvault using that identity
                SecretClient client = new SecretClient(new Uri("https://mykv.vault.azure.net/"), creds);

                // Fetch a secret
                KeyVaultSecret secret = (await client.GetSecretAsync("mysecret", cancellationToken: cancellationToken)).Value;
            }
            catch (CredentialUnavailableException e)
            {
                // Handle errors with loading the Managed Identity
            }
            catch (RequestFailedException)
            {
                // Handle errors with fetching the secret
            }
            catch (Exception e)
            {
                // Handle generic errors
            }
        }
    }
}

Adquirindo um token de acesso usando a API REST

Em clusters habilitados para identidade gerenciada, o tempo de execução do Service Fabric expõe um ponto de extremidade localhost que os aplicativos podem usar para obter tokens de acesso. O ponto de extremidade está disponível em cada nó do cluster e é acessível a todas as entidades nesse nó. Os chamadores autorizados podem obter tokens de acesso chamando este ponto de extremidade e apresentando um código de autenticação; o código é gerado pelo tempo de execução do Service Fabric para cada ativação distinta do pacote de código de serviço e está vinculado ao tempo de vida do processo que hospeda esse pacote de código de serviço.

Especificamente, o ambiente de um serviço do Service Fabric habilitado para identidade gerenciada será semeado com as seguintes variáveis:

  • 'IDENTITY_ENDPOINT': o ponto de extremidade localhost correspondente à identidade gerenciada do serviço
  • 'IDENTITY_HEADER': um código de autenticação exclusivo que representa o serviço no nó atual
  • 'IDENTITY_SERVER_THUMBPRINT': impressão digital do servidor de identidade gerenciado do service fabric

Importante

O código do aplicativo deve considerar o valor da variável de ambiente 'IDENTITY_HEADER' como dados confidenciais - não deve ser registrado ou disseminado de outra forma. O código de autenticação não tem valor fora do nó local ou após o processo que hospeda o serviço ter terminado, mas representa a identidade do serviço Service Fabric e, portanto, deve ser tratado com as mesmas precauções que o próprio token de acesso.

Para obter um token, o cliente executa as seguintes etapas:

  • forma um URI concatenando o ponto de extremidade de identidade gerenciado (valor de IDENTITY_ENDPOINT) com a versão da API e o recurso (audiência) necessário para o token
  • cria uma solicitação GET http(s) para o URI especificado
  • Adiciona a lógica de validação de certificado de servidor apropriada
  • Adiciona o código de autenticação (valor IDENTITY_HEADER) como um cabeçalho à solicitação
  • submete o pedido

Uma resposta bem-sucedida conterá uma carga JSON representando o token de acesso resultante, bem como metadados que o descrevem. Uma resposta com falha também incluirá uma explicação da falha. Veja abaixo detalhes adicionais sobre o tratamento de erros.

Os tokens de acesso serão armazenados em cache pelo Service Fabric em vários níveis (nó, cluster, serviço do provedor de recursos), portanto, uma resposta bem-sucedida não implica necessariamente que o token foi emitido diretamente em resposta à solicitação do aplicativo do usuário. Os tokens serão armazenados em cache por menos do que sua vida útil e, portanto, um aplicativo tem a garantia de receber um token válido. Recomenda-se que o código do aplicativo armazene em cache todos os tokens de acesso adquiridos; A chave de cache deve incluir (uma derivação de) a audiência.

Sample request:

GET 'https://localhost:2377/metadata/identity/oauth2/token?api-version=2019-07-01-preview&resource=https://vault.azure.net/' HTTP/1.1 Secret: 912e4af7-77ba-4fa5-a737-56c8e3ace132

onde:

Elemento Descrição
GET O verbo HTTP, indicando que você deseja recuperar dados do ponto de extremidade. Nesse caso, um token de acesso OAuth.
https://localhost:2377/metadata/identity/oauth2/token O ponto de extremidade de identidade gerenciado para aplicativos do Service Fabric, fornecido por meio da variável de ambiente IDENTITY_ENDPOINT.
api-version Um parâmetro de cadeia de caracteres de consulta, especificando a versão da API do Serviço de Token de Identidade Gerenciado; Atualmente, o único valor aceito é 2019-07-01-preview, e está sujeito a alterações.
resource Um parâmetro de cadeia de caracteres de consulta, indicando o URI da ID do Aplicativo do recurso de destino. Isso será refletido como a aud reivindicação (do público) do token emitido. Este exemplo solicita um token para acessar o Azure Key Vault, cujo URI de ID de Aplicativo é https://vault.azure.net/.
Secret Um campo de cabeçalho de solicitação HTTP, exigido pelo Serviço de Token de Identidade Gerenciado do Service Fabric para serviços do Service Fabric para autenticar o chamador. Esse valor é fornecido pelo tempo de execução SF por meio de IDENTITY_HEADER variável de ambiente.

Exemplo de resposta:

HTTP/1.1 200 OK
Content-Type: application/json
{
    "token_type":  "Bearer",
    "access_token":  "eyJ0eXAiO...",
    "expires_on":  1565244611,
    "resource":  "https://vault.azure.net/"
}

onde:

Elemento Descrição
token_type O tipo de token; neste caso, um token de acesso "Portador", o que significa que o apresentador ('portador') deste token é o assunto pretendido do token.
access_token O token de acesso solicitado. Ao chamar uma API REST segura, o token é incorporado no campo de cabeçalho da Authorization solicitação como um token "portador", permitindo que a API autentique o chamador.
expires_on O carimbo de data/hora da expiração do token de acesso; representado como o número de segundos de "1970-01-01T0:0:0Z UTC" e corresponde à reivindicação do exp token. Neste caso, o token expira em 2019-08-08T06:10:11+00:00 (na RFC 3339)
resource O recurso para o qual o token de acesso foi emitido, especificado através do resource parâmetro de cadeia de caracteres de consulta da solicitação, corresponde à declaração 'aud' do token.

Adquirindo um token de acesso usando C#

O acima se torna, em C#:

namespace Azure.ServiceFabric.ManagedIdentity.Samples
{
    using System;
    using System.Net.Http;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;
    using Newtonsoft.Json;

    /// <summary>
    /// Type representing the response of the SF Managed Identity endpoint for token acquisition requests.
    /// </summary>
    [JsonObject]
    public sealed class ManagedIdentityTokenResponse
    {
        [JsonProperty(Required = Required.Always, PropertyName = "token_type")]
        public string TokenType { get; set; }

        [JsonProperty(Required = Required.Always, PropertyName = "access_token")]
        public string AccessToken { get; set; }

        [JsonProperty(PropertyName = "expires_on")]
        public string ExpiresOn { get; set; }

        [JsonProperty(PropertyName = "resource")]
        public string Resource { get; set; }
    }

    /// <summary>
    /// Sample class demonstrating access token acquisition using Managed Identity.
    /// </summary>
    public sealed class AccessTokenAcquirer
    {
        /// <summary>
        /// Acquire an access token.
        /// </summary>
        /// <returns>Access token</returns>
        public static async Task<string> AcquireAccessTokenAsync()
        {
            var managedIdentityEndpoint = Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT");
            var managedIdentityAuthenticationCode = Environment.GetEnvironmentVariable("IDENTITY_HEADER");
            var managedIdentityServerThumbprint = Environment.GetEnvironmentVariable("IDENTITY_SERVER_THUMBPRINT");
            // Latest api version, 2019-07-01-preview is still supported.
            var managedIdentityApiVersion = Environment.GetEnvironmentVariable("IDENTITY_API_VERSION");
            var managedIdentityAuthenticationHeader = "secret";
            var resource = "https://management.azure.com/";

            var requestUri = $"{managedIdentityEndpoint}?api-version={managedIdentityApiVersion}&resource={HttpUtility.UrlEncode(resource)}";

            var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
            requestMessage.Headers.Add(managedIdentityAuthenticationHeader, managedIdentityAuthenticationCode);
            
            var handler = new HttpClientHandler();
            handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, certChain, policyErrors) =>
            {
                // Do any additional validation here
                if (policyErrors == SslPolicyErrors.None)
                {
                    return true;
                }
                return 0 == string.Compare(cert.GetCertHashString(), managedIdentityServerThumbprint, StringComparison.OrdinalIgnoreCase);
            };

            try
            {
                var response = await new HttpClient(handler).SendAsync(requestMessage)
                    .ConfigureAwait(false);

                response.EnsureSuccessStatusCode();

                var tokenResponseString = await response.Content.ReadAsStringAsync()
                    .ConfigureAwait(false);

                var tokenResponseObject = JsonConvert.DeserializeObject<ManagedIdentityTokenResponse>(tokenResponseString);

                return tokenResponseObject.AccessToken;
            }
            catch (Exception ex)
            {
                string errorText = String.Format("{0} \n\n{1}", ex.Message, ex.InnerException != null ? ex.InnerException.Message : "Acquire token failed");

                Console.WriteLine(errorText);
            }

            return String.Empty;
        }
    } // class AccessTokenAcquirer
} // namespace Azure.ServiceFabric.ManagedIdentity.Samples

Acessando o Cofre da Chave a partir de um aplicativo do Service Fabric usando a Identidade Gerenciada

Este exemplo se baseia no acima para demonstrar o acesso a um segredo armazenado em um Cofre de Chaves usando a identidade gerenciada.

        /// <summary>
        /// Probe the specified secret, displaying metadata on success.  
        /// </summary>
        /// <param name="vault">vault name</param>
        /// <param name="secret">secret name</param>
        /// <param name="version">secret version id</param>
        /// <returns></returns>
        public async Task<string> ProbeSecretAsync(string vault, string secret, string version)
        {
            // initialize a KeyVault client with a managed identity-based authentication callback
            var kvClient = new Microsoft.Azure.KeyVault.KeyVaultClient(new Microsoft.Azure.KeyVault.KeyVaultClient.AuthenticationCallback((a, r, s) => { return AuthenticationCallbackAsync(a, r, s); }));

            Log(LogLevel.Info, $"\nRunning with configuration: \n\tobserved vault: {config.VaultName}\n\tobserved secret: {config.SecretName}\n\tMI endpoint: {config.ManagedIdentityEndpoint}\n\tMI auth code: {config.ManagedIdentityAuthenticationCode}\n\tMI auth header: {config.ManagedIdentityAuthenticationHeader}");
            string response = String.Empty;

            Log(LogLevel.Info, "\n== {DateTime.UtcNow.ToString()}: Probing secret...");
            try
            {
                var secretResponse = await kvClient.GetSecretWithHttpMessagesAsync(vault, secret, version)
                    .ConfigureAwait(false);

                if (secretResponse.Response.IsSuccessStatusCode)
                {
                    // use the secret: secretValue.Body.Value;
                    response = String.Format($"Successfully probed secret '{secret}' in vault '{vault}': {PrintSecretBundleMetadata(secretResponse.Body)}");
                }
                else
                {
                    response = String.Format($"Non-critical error encountered retrieving secret '{secret}' in vault '{vault}': {secretResponse.Response.ReasonPhrase} ({secretResponse.Response.StatusCode})");
                }
            }
            catch (Microsoft.Rest.ValidationException ve)
            {
                response = String.Format($"encountered REST validation exception 0x{ve.HResult.ToString("X")} trying to access '{secret}' in vault '{vault}' from {ve.Source}: {ve.Message}");
            }
            catch (KeyVaultErrorException kvee)
            {
                response = String.Format($"encountered KeyVault exception 0x{kvee.HResult.ToString("X")} trying to access '{secret}' in vault '{vault}': {kvee.Response.ReasonPhrase} ({kvee.Response.StatusCode})");
            }
            catch (Exception ex)
            {
                // handle generic errors here
                response = String.Format($"encountered exception 0x{ex.HResult.ToString("X")} trying to access '{secret}' in vault '{vault}': {ex.Message}");
            }

            Log(LogLevel.Info, response);

            return response;
        }

        /// <summary>
        /// KV authentication callback, using the application's managed identity.
        /// </summary>
        /// <param name="authority">The expected issuer of the access token, from the KV authorization challenge.</param>
        /// <param name="resource">The expected audience of the access token, from the KV authorization challenge.</param>
        /// <param name="scope">The expected scope of the access token; not currently used.</param>
        /// <returns>Access token</returns>
        public async Task<string> AuthenticationCallbackAsync(string authority, string resource, string scope)
        {
            Log(LogLevel.Verbose, $"authentication callback invoked with: auth: {authority}, resource: {resource}, scope: {scope}");
            var encodedResource = HttpUtility.UrlEncode(resource);

            // This sample does not illustrate the caching of the access token, which the user application is expected to do.
            // For a given service, the caching key should be the (encoded) resource uri. The token should be cached for a period
            // of time at most equal to its remaining validity. The 'expires_on' field of the token response object represents
            // the number of seconds from Unix time when the token will expire. You may cache the token if it will be valid for at
            // least another short interval (1-10s). If its expiration will occur shortly, don't cache but still return it to the 
            // caller. The MI endpoint will not return an expired token.
            // Sample caching code:
            //
            // ManagedIdentityTokenResponse tokenResponse;
            // if (responseCache.TryGetCachedItem(encodedResource, out tokenResponse))
            // {
            //     Log(LogLevel.Verbose, $"cache hit for key '{encodedResource}'");
            //
            //     return tokenResponse.AccessToken;
            // }
            //
            // Log(LogLevel.Verbose, $"cache miss for key '{encodedResource}'");
            //
            // where the response cache is left as an exercise for the reader. MemoryCache is a good option, albeit not yet available on .net core.

            var requestUri = $"{config.ManagedIdentityEndpoint}?api-version={config.ManagedIdentityApiVersion}&resource={encodedResource}";
            Log(LogLevel.Verbose, $"request uri: {requestUri}");

            var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
            requestMessage.Headers.Add(config.ManagedIdentityAuthenticationHeader, config.ManagedIdentityAuthenticationCode);
            Log(LogLevel.Verbose, $"added header '{config.ManagedIdentityAuthenticationHeader}': '{config.ManagedIdentityAuthenticationCode}'");

            var response = await httpClient.SendAsync(requestMessage)
                .ConfigureAwait(false);
            Log(LogLevel.Verbose, $"response status: success: {response.IsSuccessStatusCode}, status: {response.StatusCode}");

            response.EnsureSuccessStatusCode();

            var tokenResponseString = await response.Content.ReadAsStringAsync()
                .ConfigureAwait(false);

            var tokenResponse = JsonConvert.DeserializeObject<ManagedIdentityTokenResponse>(tokenResponseString);
            Log(LogLevel.Verbose, "deserialized token response; returning access code..");

            // Sample caching code (continuation):
            // var expiration = DateTimeOffset.FromUnixTimeSeconds(Int32.Parse(tokenResponse.ExpiresOn));
            // if (expiration > DateTimeOffset.UtcNow.AddSeconds(5.0))
            //    responseCache.AddOrUpdate(encodedResource, tokenResponse, expiration);

            return tokenResponse.AccessToken;
        }

        private string PrintSecretBundleMetadata(SecretBundle bundle)
        {
            StringBuilder strBuilder = new StringBuilder();

            strBuilder.AppendFormat($"\n\tid: {bundle.Id}\n");
            strBuilder.AppendFormat($"\tcontent type: {bundle.ContentType}\n");
            strBuilder.AppendFormat($"\tmanaged: {bundle.Managed}\n");
            strBuilder.AppendFormat($"\tattributes:\n");
            strBuilder.AppendFormat($"\t\tenabled: {bundle.Attributes.Enabled}\n");
            strBuilder.AppendFormat($"\t\tnbf: {bundle.Attributes.NotBefore}\n");
            strBuilder.AppendFormat($"\t\texp: {bundle.Attributes.Expires}\n");
            strBuilder.AppendFormat($"\t\tcreated: {bundle.Attributes.Created}\n");
            strBuilder.AppendFormat($"\t\tupdated: {bundle.Attributes.Updated}\n");
            strBuilder.AppendFormat($"\t\trecoveryLevel: {bundle.Attributes.RecoveryLevel}\n");

            return strBuilder.ToString();
        }

        private enum LogLevel
        {
            Info,
            Verbose
        };

        private void Log(LogLevel level, string message)
        {
            if (level != LogLevel.Verbose
                || config.DoVerboseLogging)
            {
                Console.WriteLine(message);
            }
        }

Processamento de erros

O campo 'código de status' do cabeçalho da resposta HTTP indica o status de sucesso da solicitação; um status '200 OK' indica sucesso, e a resposta incluirá o token de acesso conforme descrito acima. Segue-se uma breve enumeração de possíveis respostas de erro.

Código de Estado Motivo do erro Como lidar
404 Não encontrado. Código de autenticação desconhecido ou o aplicativo não recebeu uma identidade gerenciada. Retifique a configuração do aplicativo ou o código de aquisição de token.
429 Demasiados pedidos. Limite de aceleração atingido, imposto pelo Microsoft Entra ID ou SF. Tente novamente com Backoff Exponencial. Consulte as orientações abaixo.
4xx Erro na solicitação. Um ou mais parâmetros de solicitação estavam incorretos. Não tente novamente. Examine os detalhes do erro para obter mais informações. Os erros 4xx são erros de tempo de design.
5xx Erro do serviço. O subsistema de identidade gerenciado ou ID do Microsoft Entra retornou um erro transitório. É seguro tentar novamente depois de um curto período de tempo. Você pode atingir uma condição de limitação (429) ao tentar novamente.

Se ocorrer um erro, o corpo da resposta HTTP correspondente conterá um objeto JSON com os detalhes do erro:

Elemento Descrição
code Código de erro.
correlationId Um ID de correlação que pode ser usado para depuração.
mensagem Descrição detalhada do erro. As descrições de erros podem ser alteradas a qualquer momento. Não dependa da mensagem de erro em si.

Exemplo de erro:

{"error":{"correlationId":"7f30f4d3-0f3a-41e0-a417-527f21b3848f","code":"SecretHeaderNotFound","message":"Secret is not found in the request headers."}}

A seguir está uma lista de erros típicos do Service Fabric específicos para identidades gerenciadas:

Código Mensagem Descrição
SecretHeaderNotFound Segredo não é encontrado nos cabeçalhos de solicitação. O código de autenticação não foi fornecido com a solicitação.
ManagedIdentityNotFound Identidade gerenciada não encontrada para o host de aplicativo especificado. O aplicativo não tem identidade ou o código de autenticação é desconhecido.
ArgumentNullOrEmpty O parâmetro 'resource' não deve ser null ou vazio string. O recurso (audiência) não foi fornecido no pedido.
InvalidApiVersion A versão api '' não é suportada. A versão suportada é '2019-07-01-preview'. Versão da API ausente ou sem suporte especificada no URI da solicitação.
InternalServerError Ocorreu um erro. Foi encontrado um erro no subsistema de identidade gerenciada, possivelmente fora da pilha do Service Fabric. A causa mais provável é um valor incorreto especificado para o recurso (verifique se há '/'?)

Orientação de repetição

Normalmente, o único código de erro reprovável é 429 (muitas solicitações); Erros internos do servidor/5xx códigos de erro podem ser repetidos, embora a causa possa ser permanente.

Os limites de limitação aplicam-se ao número de chamadas efetuadas para o subsistema de identidade gerido - especificamente as dependências 'upstream' (o serviço Azure de Identidade Gerida ou o serviço de token seguro). O Service Fabric armazena tokens em cache em vários níveis no pipeline, mas, dada a natureza distribuída dos componentes envolvidos, o chamador pode enfrentar respostas de limitação inconsistentes (ou seja, ser limitado em um nó/instância de um aplicativo, mas não em um nó diferente ao solicitar um token para a mesma identidade). Quando a condição de limitação é definida, as solicitações subsequentes do mesmo aplicativo podem falhar com o código de status HTTP 429 (Muitas solicitações) até que a condição seja limpa.

Recomenda-se que as solicitações com falha devido à limitação sejam repetidas com um backoff exponencial, da seguinte forma:

Índice de chamadas Ação sobre o recebimento 429
1 Aguarde 1 segundo e tente novamente
2 Aguarde 2 segundos e tente novamente
3 Aguarde 4 segundos e tente novamente
4 Aguarde 8 segundos e tente novamente
4 Aguarde 8 segundos e tente novamente
5 Aguarde 16 segundos e tente novamente

IDs de recursos para serviços do Azure

Consulte Serviços do Azure que dão suporte à autenticação do Microsoft Entra para obter uma lista de recursos que dão suporte à ID do Microsoft Entra e suas respetivas IDs de recurso.

Próximos passos