Condividi tramite


Come sfruttare l'identità gestita di un'applicazione di Service Fabric per accedere ai servizi di Azure

Le applicazioni di Service Fabric possono sfruttare le identità gestite per accedere ad altre risorse di Azure che supportano l'autenticazione basata su ID Di Microsoft Entra. Un'applicazione può ottenere un token di accesso che rappresenta la propria identità, che può essere assegnata dal sistema o assegnata dall'utente e usarla come token di connessione per autenticarsi in un altro servizio, noto anche come server di risorse protetto. Il token rappresenta l'identità assegnata all'applicazione di Service Fabric e verrà rilasciata solo alle risorse di Azure (incluse le applicazioni SF) che condividono tale identità. Per una descrizione dettagliata delle identità gestite, vedere la documentazione sulla panoramica delle identità gestite e la distinzione tra identità assegnate dal sistema e identità assegnate dall'utente. In questo articolo si farà riferimento a un'applicazione di Service Fabric abilitata per l'identità gestita come applicazione client.

Vedere un'applicazione di esempio complementare che illustra l'uso di identità gestite dell'applicazione di Service Fabric assegnate dal sistema e assegnate dall'utente con Reliable Services e contenitori.

Importante

Un'identità gestita rappresenta l'associazione tra una risorsa di Azure e un'entità servizio nel tenant Microsoft Entra corrispondente associato alla sottoscrizione contenente la risorsa. Di conseguenza, nel contesto di Service Fabric, le identità gestite sono supportate solo per le applicazioni distribuite come risorse di Azure.

Importante

Prima di usare l'identità gestita di un'applicazione di Service Fabric, all'applicazione client deve essere concesso l'accesso alla risorsa protetta. Fare riferimento all'elenco dei servizi di Azure che supportano l'autenticazione di Microsoft Entra per verificare la disponibilità del supporto e quindi alla documentazione del rispettivo servizio per i passaggi specifici per concedere a un'identità l'accesso alle risorse di interesse.

Sfruttare un'identità gestita con Azure.Identity

Azure Identity SDK supporta ora Service Fabric. L'uso di Azure.Identity semplifica la scrittura di codice per l'uso delle identità gestite dell'app di Service Fabric perché gestisce il recupero di token, la memorizzazione nella cache dei token e l'autenticazione server. Durante l'accesso alla maggior parte delle risorse di Azure, il concetto di token è nascosto.

Il supporto di Service Fabric è disponibile nelle versioni seguenti per queste lingue:

Esempio C# di inizializzazione delle credenziali e uso delle credenziali per recuperare un segreto da Azure Key Vault:

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
            }
        }
    }
}

Acquisizione di un token di accesso tramite l'API REST

Nei cluster abilitati per l'identità gestita, il runtime di Service Fabric espone un endpoint localhost che le applicazioni possono usare per ottenere i token di accesso. L'endpoint è disponibile in ogni nodo del cluster ed è accessibile a tutte le entità in tale nodo. I chiamanti autorizzati possono ottenere i token di accesso chiamando questo endpoint e presentando un codice di autenticazione; il codice viene generato dal runtime di Service Fabric per ogni attivazione distinta del pacchetto del codice del servizio ed è associato alla durata del processo che ospita il pacchetto di codice del servizio.

In particolare, l'ambiente di un servizio service Fabric abilitato per l'identità gestita verrà sottoposto a seeding con le variabili seguenti:

  • 'IDENTITY_ENDPOINT': l'endpoint localhost corrispondente all'identità gestita del servizio
  • 'IDENTITY_HEADER': codice di autenticazione univoco che rappresenta il servizio nel nodo corrente
  • 'IDENTITY_edizione StandardRVER_THUMBPRINT': identificazione personale del server di identità gestito di Service Fabric

Importante

Il codice dell'applicazione deve considerare il valore della variabile di ambiente "IDENTITY_HEADER" come dati sensibili. Non deve essere registrato o distribuito in altro modo. Il codice di autenticazione non ha alcun valore all'esterno del nodo locale o dopo che il processo che ospita il servizio è terminato, ma rappresenta l'identità del servizio Service Fabric e pertanto deve essere trattato con le stesse precauzioni del token di accesso stesso.

Per ottenere un token, il client esegue la procedura seguente:

  • forma un URI concatenando l'endpoint dell'identità gestita (valore IDENTITY_ENDPOINT) con la versione dell'API e la risorsa (gruppo di destinatari) necessaria per il token
  • crea una richiesta HTTP(s) GET per l'URI specificato
  • aggiunge la logica di convalida del certificato server appropriata
  • aggiunge il codice di autenticazione (valore IDENTITY_HEADER) come intestazione alla richiesta
  • invia la richiesta

Una risposta con esito positivo conterrà un payload JSON che rappresenta il token di accesso risultante, nonché i metadati che la descrivono. Una risposta non riuscita includerà anche una spiegazione dell'errore. Per altri dettagli sulla gestione degli errori, vedere di seguito.

I token di accesso verranno memorizzati nella cache da Service Fabric a vari livelli (nodo, cluster, servizio provider di risorse), quindi una risposta con esito positivo non implica necessariamente che il token sia stato rilasciato direttamente in risposta alla richiesta dell'applicazione utente. I token verranno memorizzati nella cache per un periodo inferiore alla durata e pertanto un'applicazione viene garantita la ricezione di un token valido. È consigliabile che il codice dell'applicazione memorizza nella cache tutti i token di accesso acquisiti; La chiave di memorizzazione nella cache deve includere (derivazione di) il gruppo di destinatari.

Richiesta di esempio:

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

dove:

Elemento Descrizione
GET Verbo HTTP che indica la volontà di recuperare dati dall'endpoint. In questo caso, un token di accesso OAuth.
https://localhost:2377/metadata/identity/oauth2/token Endpoint dell'identità gestita per le applicazioni di Service Fabric, fornito tramite la variabile di ambiente IDENTITY_ENDPOINT.
api-version Parametro della stringa di query che specifica la versione API del servizio token di identità gestita; attualmente l'unico valore accettato è 2019-07-01-previewe è soggetto a modifiche.
resource Parametro della stringa di query che indica l'URI dell'ID app della risorsa di destinazione. Questa operazione verrà riflessa come aud attestazione (gruppo di destinatari) del token rilasciato. Questo esempio richiede un token per accedere ad Azure Key Vault, il cui URI dell'ID app è https://vault.azure.net/.
Secret Campo di intestazione della richiesta HTTP, richiesto dal servizio token di identità gestito di Service Fabric per i servizi di Service Fabric per autenticare il chiamante. Questo valore viene fornito dal runtime sf tramite IDENTITY_HEADER variabile di ambiente.

Risposta di esempio:

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

dove:

Elemento Descrizione
token_type Tipo di token; in questo caso, un token di accesso "Bearer", ovvero il relatore ('bearer') di questo token è l'oggetto previsto del token.
access_token Token di accesso richiesto. Quando si chiama un'API REST protetta, il token è incorporato nel campo di intestazione della richiesta Authorization come token di connessione, in modo da consentire all'API di autenticare il chiamante.
expires_on Timestamp della scadenza del token di accesso; rappresentato come numero di secondi da "1970-01-01T0:0:0Z UTC" e corrisponde all'attestazione del exp token. In questo caso, il token scade il 2019-08-08T06:10:11+00:00 (in RFC 3339)
resource La risorsa per cui è stato rilasciato il token di accesso, specificata tramite il resource parametro della stringa di query della richiesta, corrisponde all'attestazione 'aud' del token.

Acquisizione di un token di accesso con C#

Il codice precedente diventa, in 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

Accesso all'insieme di credenziali delle chiavi da un'applicazione di Service Fabric tramite l'identità gestita

Questo esempio si basa su quanto sopra per illustrare l'accesso a un segreto archiviato in un insieme di credenziali delle chiavi usando l'identità gestita.

        /// <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);
            }
        }

Gestione degli errori

Il campo "codice di stato" dell'intestazione della risposta HTTP indica lo stato di esito positivo della richiesta; lo stato "200 OK" indica l'esito positivo e la risposta includerà il token di accesso come descritto in precedenza. Di seguito è riportata un'enumerazione breve delle possibili risposte di errore.

Codice di stato Motivo dell'errore Come gestirlo
404 - Non trovato. Codice di autenticazione sconosciuto oppure all'applicazione non è stata assegnata un'identità gestita. Rettificare il codice di installazione o acquisizione del token dell'applicazione.
429 - Numero eccessivo di richieste. Limite di limitazione raggiunto, imposto da Microsoft Entra ID o SF. Riprovare con il backoff esponenziale. Seguire le indicazioni riportate di seguito.
Errore 4xx nella richiesta. Uno o più dei parametri della richiesta non sono corretti. Non riprovare. Esaminare i dettagli dell'errore per maggiori informazioni. Errori 4xx in fase di progettazione.
Errore 5xx dal servizio. Il sottosistema identità gestita o l'ID Microsoft Entra ha restituito un errore temporaneo. È sicuro riprovare dopo un breve periodo di tempo. È possibile che venga raggiunta una condizione di limitazione (429) al tentativo.

Se si verifica un errore, il corpo della risposta HTTP corrispondente contiene un oggetto JSON con i dettagli dell'errore:

Elemento Descrizione
codice Codice di errore.
correlationId ID di correlazione che può essere usato per il debug.
messaggio Descrizione dettagliata dell'errore. Le descrizioni degli errori possono cambiare in qualsiasi momento: Non dipendere dal messaggio di errore stesso.

Errore di esempio:

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

Di seguito è riportato un elenco di errori tipici di Service Fabric specifici per le identità gestite:

Codice Message Descrizione
SecretHeaderNotFound Il segreto non viene trovato nelle intestazioni della richiesta. Il codice di autenticazione non è stato fornito con la richiesta.
ManagedIdentityNotFound Identità gestita non trovata per l'host applicazione specificato. L'applicazione non ha identità o il codice di autenticazione è sconosciuto.
ArgumentNullOrEmpty Il parametro 'resource' non deve essere null o una stringa vuota. La risorsa (gruppo di destinatari) non è stata fornita nella richiesta.
InvalidApiVersion La versione api '' non è supportata. La versione supportata è '2019-07-01-preview'. Versione dell'API mancante o non supportata specificata nell'URI della richiesta.
InternalServerError Si è verificato un errore. Si è verificato un errore nel sottosistema dell'identità gestita, possibilmente all'esterno dello stack di Service Fabric. La causa più probabile è un valore non corretto specificato per la risorsa (verificare la presenza di '/'?)

Indicazioni per nuovi tentativi

In genere l'unico codice di errore riprovabile è 429 (troppe richieste); i codici di errore interni del server/5xx possono essere riprovabili, anche se la causa può essere permanente.

I limiti di limitazione si applicano al numero di chiamate effettuate al sottosistema di identità gestita, in particolare le dipendenze "upstream" (il servizio Azure identità gestita o il servizio token sicuro). Service Fabric memorizza nella cache i token a vari livelli della pipeline, ma, data la natura distribuita dei componenti coinvolti, il chiamante può riscontrare risposte di limitazione incoerenti (ad esempio, vengono limitate in un nodo o in un'istanza di un'applicazione, ma non in un nodo diverso durante la richiesta di un token per la stessa identità). Quando viene impostata la condizione di limitazione, le richieste successive dalla stessa applicazione potrebbero non riuscire con il codice di stato HTTP 429 (Troppe richieste) fino a quando la condizione non viene cancellata.

È consigliabile che le richieste non riuscite a causa della limitazione vengano ritentate con un backoff esponenziale, come indicato di seguito:

Indice di chiamata Azione sulla ricezione 429
1 Attendere 1 secondo e riprovare
2 Attendere 2 secondi e riprovare
3 Attendere 4 secondi e riprovare
4 Attendere 8 secondi e riprovare
4 Attendere 8 secondi e riprovare
5 Attendere 16 secondi e riprovare

ID di risorsa per servizi di Azure

Vedere Servizi di Azure che supportano l'autenticazione di Microsoft Entra per un elenco di risorse che supportano Microsoft Entra ID e i rispettivi ID risorsa.

Passaggi successivi