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 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:
- C# na versão 1.3.0. Veja um exemplo de C#.
- Python na versão 1.5.0. Veja um exemplo de Python.
- Java na versão 1.2.0.
Exemplo em C# de inicialização de credenciais e uso das 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 - eles não devem ser registrados ou disseminados 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 | Description |
---|---|
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 Gerenciada; 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 do 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 | Description |
---|---|
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 | Description |
---|---|
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. |
Erro de exemplo:
{"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 | Description |
---|---|---|
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 feitas para o subsistema de identidade gerenciado - especificamente as dependências 'upstream' (o serviço do Azure de Identidade Gerenciada 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
- Implantar um aplicativo do Service Fabric com Identidade Gerenciada em um cluster gerenciado
- Implantar um aplicativo do Service Fabric com uma Identidade Gerenciada atribuída pelo sistema em um cluster clássico
- Implantar um aplicativo do Service Fabric com uma Identidade Gerenciada atribuída pelo usuário em um cluster clássico
- Concedendo acesso à Identidade Gerenciada de um aplicativo do Service Fabric aos recursos do Azure
- Explore um aplicativo de exemplo usando o Service Fabric Managed Identity