Sdílet prostřednictvím


Jak využít spravovanou identitu aplikace Service Fabric pro přístup ke službám Azure

Aplikace Service Fabric můžou využívat spravované identity pro přístup k dalším prostředkům Azure, které podporují ověřování založené na ID microsoftu Entra. Aplikace může získat přístupový token představující svou identitu, která může být přiřazena systémem nebo přiřazena uživatelem, a použít ji jako nosný token k ověření v jiné službě – označované také jako chráněný server prostředků. Token představuje identitu přiřazenou k aplikaci Service Fabric a bude vydána pouze prostředkům Azure (včetně aplikací SF), které tuto identitu sdílejí. Podrobný popis spravovaných identit a rozdíl mezi identitami přiřazenými systémem a identitami přiřazenými uživatelem najdete v dokumentaci k přehledu spravované identity. Jako klientskou aplikaci v tomto článku se budeme odkazovat na aplikaci Service Fabric s podporou spravované identity.

Podívejte se na doprovodnou ukázkovou aplikaci, která ukazuje použití identit spravovaných aplikací Service Fabric přiřazených systémem a uživatelem s využitím Reliable Services a kontejnerů.

Důležité

Spravovaná identita představuje přidružení mezi prostředkem Azure a instančním objektem v odpovídajícím tenantovi Microsoft Entra přidruženém k předplatnému obsahujícímu prostředek. V kontextu Service Fabric jsou spravované identity podporované jenom pro aplikace nasazené jako prostředky Azure.

Důležité

Před použitím spravované identity aplikace Service Fabric musí být klientské aplikaci udělen přístup k chráněnému prostředku. Projděte si seznam služeb Azure, které podporují ověřování Microsoft Entra, a vyhledejte podporu a v dokumentaci příslušné služby si projděte konkrétní kroky pro udělení přístupu identit k prostředkům, které zajímají.

Využití spravované identity pomocí Azure.Identity

Sada Azure Identity SDK teď podporuje Service Fabric. Použití Azure.Identity usnadňuje psaní kódu pro použití identit spravovaných aplikací Service Fabric, protože zpracovává načítání tokenů, ukládání tokenů do mezipaměti a ověřování serveru. Při přístupu k většině prostředků Azure je koncept tokenu skrytý.

Podpora Service Fabric je k dispozici v následujících verzích pro tyto jazyky:

Ukázka inicializace přihlašovacích údajů v jazyce C# a použití přihlašovacích údajů k načtení tajného kódu ze služby 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
            }
        }
    }
}

Získání přístupového tokenu pomocí rozhraní REST API

V clusterech s povolenou spravovanou identitou modul runtime Service Fabric zveřejňuje koncový bod localhost, který můžou aplikace použít k získání přístupových tokenů. Koncový bod je k dispozici na každém uzlu clusteru a je přístupný pro všechny entity v daném uzlu. Autorizovaní volající mohou získat přístupové tokeny voláním tohoto koncového bodu a předložením ověřovacího kódu; Kód je generován modulem runtime Service Fabric pro každou aktivaci balíčku kódu služby a je vázán na dobu životnosti procesu hostujícího balíček kódu služby.

Konkrétně se prostředí služby Service Fabric s podporou spravované identity zasadí s následujícími proměnnými:

  • 'IDENTITY_ENDPOINT': koncový bod localhost odpovídající spravované identitě služby
  • 'IDENTITY_HEADER': jedinečný ověřovací kód představující službu v aktuálním uzlu
  • 'IDENTITY_SERVER_THUMBPRINT': Kryptografický otisk serveru spravované identity Service Fabric

Důležité

Kód aplikace by měl vzít v úvahu hodnotu proměnné prostředí "IDENTITY_HEADER" jako citlivá data – neměla by být protokolována ani jinak šířena. Ověřovací kód nemá žádnou hodnotu mimo místní uzel nebo po ukončení procesu hostujícího službu, ale představuje identitu služby Service Fabric, a proto by se mělo zacházet se stejnými preventivními opatřeními jako samotný přístupový token.

K získání tokenu klient provede následující kroky:

  • vytvoří identifikátor URI zřetězením koncového bodu spravované identity (hodnota IDENTITY_ENDPOINT) s verzí rozhraní API a prostředkem (cílovou skupinou) požadovaným pro token.
  • vytvoří požadavek GET http(s) pro zadaný identifikátor URI.
  • přidá odpovídající logiku ověření certifikátu serveru.
  • přidá ověřovací kód (IDENTITY_HEADER hodnotu) do požadavku jako hlavičku.
  • odešle žádost.

Úspěšná odpověď bude obsahovat datovou část JSON představující výsledný přístupový token a také metadata popisující ji. Neúspěšná odpověď bude obsahovat také vysvětlení selhání. Další podrobnosti o zpracování chyb najdete níže.

Přístupové tokeny bude Service Fabric ukládat do mezipaměti na různých úrovních (uzel, cluster, služba poskytovatele prostředků), takže úspěšná odpověď nemusí nutně znamenat, že token byl vydán přímo v reakci na žádost aplikace uživatele. Tokeny budou uloženy do mezipaměti po dobu kratší než jejich životnost, a proto je zaručeno, že aplikace obdrží platný token. Doporučuje se, aby kód aplikace ukápil všechny přístupové tokeny, které získá; klíč pro ukládání do mezipaměti by měl obsahovat (odvození) cílové skupiny.

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

kde:

Element (Prvek) Popis
GET Příkaz HTTP označující, že chcete načíst data z koncového bodu. V tomto případě přístupový token OAuth.
https://localhost:2377/metadata/identity/oauth2/token Koncový bod spravované identity pro aplikace Service Fabric poskytovaný prostřednictvím proměnné prostředí IDENTITY_ENDPOINT.
api-version Parametr řetězce dotazu určující verzi rozhraní API služby tokenů spravované identity; v současné době platí, že jediná akceptovaná hodnota je 2019-07-01-previewa může se změnit.
resource Parametr řetězce dotazu označující identifikátor URI ID aplikace cílového prostředku. To se projeví jako aud deklarace identity vydaného tokenu (cílová skupina). Tento příklad požaduje token pro přístup ke službě Azure Key Vault, jehož identifikátor URI ID aplikace je https://vault.azure.net/.
Secret Pole hlavičky požadavku HTTP vyžadované službou tokenů spravované identity Service Fabric pro služby Service Fabric k ověření volajícího. Tuto hodnotu poskytuje modul runtime SF prostřednictvím proměnné prostředí IDENTITY_HEADER.

Ukázková odpověď:

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

kde:

Element (Prvek) Popis
token_type Typ tokenu; v tomto případě je přístupový token "Bearer", což znamená, že prezentující ("nosný") tohoto tokenu je zamýšleným předmětem tokenu.
access_token Požadovaný přístupový token. Při volání zabezpečeného rozhraní REST API se token vloží do Authorization pole hlavičky požadavku jako nosný token, který rozhraní API umožňuje ověřit volajícího.
expires_on Časové razítko vypršení platnosti přístupového tokenu; představuje počet sekund od 1970-01-01T0:0:0Z UTC a odpovídá deklaraci tokenu exp . V tomto případě platnost tokenu vyprší 2019-08-08T06:10:11+00:00 (v RFC 3339)
resource Prostředek, pro který byl přístupový token vystavený, zadaný prostřednictvím resource parametru řetězce dotazu požadavku, odpovídá deklaraci identity tokenu aud.

Získání přístupového tokenu pomocí jazyka C#

Výše uvedený kód se stane v jazyce 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

Přístup ke službě Key Vault z aplikace Service Fabric pomocí spravované identity

Tato ukázka vychází z výše uvedených ukázek, která demonstruje přístup k tajnému kódu uloženému ve službě Key Vault pomocí spravované identity.

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

Zpracování chyb

Pole Stavový kód hlavičky odpovědi HTTP označuje stav úspěchu požadavku; Stav 200 OK značí úspěch a odpověď bude obsahovat přístupový token, jak je popsáno výše. Následuje krátký výčet možných chybových odpovědí.

Kód stavu Důvod chyby Zpracování
404 Nenalezena. Neznámý ověřovací kód nebo aplikace nebyla přiřazena spravovaná identita. Opravte kód nastavení aplikace nebo získání tokenu.
429 Příliš mnoho požadavků. Dosáhli jsme limitu omezení stanoveného id Microsoft Entra NEBO SF. Zkuste to znovu s exponenciálním zpochybněním. Prohlédnou si následující doprovodné materiály.
Chyba 4xx v požadavku. Jeden nebo více parametrů požadavku bylo nesprávné. Opakujte akci. Další informace najdete v podrobnostech o chybě. Chyby 4xx jsou chyby v době návrhu.
Chyba 5xx ze služby Subsystém spravované identity nebo ID Microsoft Entra vrátilo přechodnou chybu. Po krátké době je bezpečné to opakovat. Při opakování může dojít k omezení podmínky (429).

Pokud dojde k chybě, odpovídající text odpovědi HTTP obsahuje objekt JSON s podrobnostmi o chybě:

Element (Prvek) Popis
code Kód chyby
correlationId ID korelace, které lze použít k ladění.
zpráva Podrobný popis chyby Popisy chyb se můžou kdykoli změnit. Nespoléhejte na samotné chybové zprávě.

Ukázková chyba:

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

Následuje seznam typických chyb Service Fabric specifických pro spravované identity:

Kód Message Popis
SecretHeaderNotFound V hlavičce požadavku nebyl nalezen tajný klíč. Ověřovací kód nebyl s požadavkem poskytnut.
ManagedIdentityNotFound Spravovaná identita nebyla nalezena pro zadaného hostitele aplikace. Aplikace nemá žádnou identitu nebo ověřovací kód není neznámý.
ArgumentNullOrEmpty Parametr "resource" nesmí být null ani prázdný řetězec. Prostředek (cílová skupina) nebyl v požadavku poskytnut.
InvalidApiVersion Verze api-version není podporována. Podporovaná verze je 2019-07-01-preview. Chybějící nebo nepodporovaná verze rozhraní API zadaná v identifikátoru URI požadavku
InternalServerError Došlo k chybě. V subsystému spravované identity, pravděpodobně mimo zásobník Service Fabric, došlo k chybě. Nejpravděpodobnější příčinou je nesprávná hodnota zadaná pro prostředek (kontrola koncové hodnoty /?)

Pokyny k opakování

Jediný kód chyby, která se dá opakovat, je obvykle 429 (Příliš mnoho požadavků); vnitřní chyby serveru / kódy chyb 5xx mohou být opakovatelné, i když příčinou může být trvalé.

Omezení omezení se vztahují na počet volání provedených v subsystému spravované identity – konkrétně upstreamové závislosti (služba Azure spravované identity nebo služba zabezpečeného tokenu). Service Fabric ukládá tokeny do mezipaměti na různých úrovních kanálu, ale vzhledem k distribuované povaze zahrnutých komponent může volajícímu docházet k nekonzistentním odpovědím na omezování (tj. dochází k omezování na jednom uzlu nebo instanci aplikace, ale ne na jiném uzlu při vyžádání tokenu pro stejnou identitu.) Pokud je nastavena podmínka omezování, následné požadavky ze stejné aplikace mohou selhat se stavovým kódem HTTP 429 (Příliš mnoho požadavků), dokud se podmínka nevymaže.

Doporučuje se, aby požadavky selhaly kvůli omezování, a to exponenciálním zpomalováním, a to následujícím způsobem:

Index volání Akce při přijetí chyby 429
0 Počkejte 1 sekundu a zkuste to znovu.
2 Počkejte 2 sekundy a zkuste to znovu.
3 Počkejte 4 sekundy a zkuste to znovu.
4 Počkejte 8 sekund a zkuste to znovu.
4 Počkejte 8 sekund a zkuste to znovu.
5 Počkejte 16 sekund a zkuste to znovu.

ID prostředků pro služby Azure

Podívejte se na služby Azure, které podporují ověřování Microsoft Entra, a seznam prostředků, které podporují ID Microsoft Entra a jejich id prostředků.

Další kroky