Cara memanfaatkan identitas terkelola aplikasi Service Fabric untuk mengakses layanan Azure

Aplikasi Service Fabric dapat memanfaatkan identitas terkelola untuk mengakses sumber daya Azure lainnya yang mendukung autentikasi berbasis ID Microsoft Entra. Aplikasi dapat memperoleh token akses yang merepresentasikan identitasnya yang mungkin ditetapkan sistem atau ditetapkan pengguna, dan menggunakannya sebagai token 'pembawa' untuk mengautentikasi dirinya ke layanan lain - juga dikenal sebagai server sumber daya yang dilindungi. Token merepresentasikan identitas yang ditetapkan untuk aplikasi Service Fabric, dan hanya akan dikeluarkan untuk sumber daya Azure (termasuk aplikasi SF) yang membagikan identitas tersebut. Lihat dokumentasi ringkasan identitas terkelola untuk deskripsi terperinci tentang identitas terkelola serta perbedaan antara identitas yang ditetapkan sistem dan yang ditetapkan pengguna. Di artikel ini, kita akan merujuk ke aplikasi Service Fabric yang mendukung identitas terkelola sebagai aplikasi klien.

Lihat aplikasi sampel pendamping yang menunjukkan penggunaan identitas terkelola aplikasi Service Fabric yang ditetapkan sistem dan ditetapkan pengguna dengan Reliable Service dan kontainer.

Penting

Identitas terkelola mewakili hubungan antara sumber daya Azure dan perwakilan layanan di penyewa Microsoft Entra terkait yang terkait dengan langganan yang berisi sumber daya. Dengan demikian, dalam konteks Service Fabric, identitas terkelola hanya didukung untuk aplikasi yang digunakan sebagai sumber daya Azure.

Penting

Sebelum menggunakan identitas terkelola dari aplikasi Service Fabric, aplikasi klien harus mendapatkan akses ke sumber daya yang dilindungi. Silakan merujuk ke daftar layanan Azure yang mendukung autentikasi Microsoft Entra untuk memeriksa dukungan, lalu ke dokumentasi layanan masing-masing untuk langkah-langkah tertentu guna memberikan akses identitas ke sumber daya yang menarik.

Memanfaatkan identitas terkelola menggunakan Azure.Identity

Azure Identity SDK sekarang mendukung Service Fabric. Menggunakan Azure.Identity membuat kode tulis menggunakan identitas terkelola aplikasi Service Fabric lebih mudah karena menangani pengambilan token, penyimpanan token, dan autentikasi server. Saat mengakses sebagian besar sumber daya Azure, konsep token disembunyikan.

Dukungan Service Fabric tersedia dalam versi berikut untuk bahasa berikut:

Sampel C# menginisialisasi kredensial dan menggunakan kredensial untuk mengambil rahasia dari 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
            }
        }
    }
}

Memperoleh token akses menggunakan REST API

Dalam kluster yang diaktifkan untuk identitas terkelola, runtime Service Fabric memaparkan titik akhir localhost yang dapat digunakan aplikasi untuk mendapatkan token akses. Titik akhir tersedia di setiap node kluster dan dapat diakses oleh semua entitas pada node tersebut. Pemanggil resmi dapat memperoleh token akses dengan memanggil titik akhir ini dan menyajikan kode autentikasi; kode dihasilkan oleh runtime Service Fabric untuk setiap aktivasi paket kode layanan yang berbeda dan terikat pada masa pakai proses yang meng-host paket kode layanan tersebut.

Secara khusus, lingkungan layanan Service Fabric yang mendukung identitas terkelola akan diisi dengan variabel berikut:

  • 'IDENTITY_ENDPOINT': titik akhir localhost yang sesuai dengan identitas terkelola layanan
  • 'IDENTITY_HEADER': kode autentikasi unik yang merepresentasikan layanan pada node saat ini
  • 'IDENTITY_SERVER_THUMBPRINT' : Thumbprint server identitas terkelola service fabric

Penting

Kode aplikasi harus mempertimbangkan nilai variabel lingkungan 'IDENTITY_HEADER' sebagai data sensitif - ini tidak boleh dicatat atau disebarluaskan. Kode autentikasi tidak memiliki nilai di luar node lokal, atau setelah proses hosting layanan telah dihentikan, tetapi ini merepresentasikan identitas layanan Service Fabric sehingga harus diperlakukan dengan tindakan pencegahan yang sama seperti token akses itu sendiri.

Untuk mendapatkan token, klien melakukan langkah-langkah berikut:

  • membentuk URI dengan menggabungkan titik akhir identitas terkelola (nilai IDENTITY_ENDPOINT) dengan versi API dan sumber daya (audiens) yang diperlukan untuk token
  • membuat permintaan GET http untuk URI yang ditentukan
  • menambahkan logika validasi sertifikat server yang sesuai
  • menambahkan kode autentikasi (IDENTITY_HEADER nilai) sebagai header ke permintaan
  • mengirimkan permintaan

Respons yang berhasil akan berisi payload JSON yang merepresentasikan token akses yang dihasilkan serta metadata yang menjelaskannya. Respons yang gagal juga akan mencakup penjelasan tentang kegagalan tersebut. Lihat di bawah ini untuk detail tambahan tentang penanganan kesalahan.

Token akses akan di-cache oleh Service Fabric di berbagai tingkatan (node, cluster, layanan penyedia sumber daya), sehingga respons yang berhasil tidak selalu menyiratkan bahwa token dikeluarkan langsung sebagai tanggapan atas permintaan aplikasi pengguna. Token akan di-cache kurang dari masa pakainya sehingga aplikasi dijamin akan menerima token yang valid. Sebaiknya kode aplikasi melakukan cache sendiri pada token akses apa pun yang diperolehnya; kunci caching harus mencakup (turunan dari) audiens.

Permintaan sampel:

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

di mana:

Elemen Deskripsi
GET Metode permintaan HTTP, menunjukkan Anda ingin mengambil data dari titik akhir. Dalam hal ini, token akses OAuth.
https://localhost:2377/metadata/identity/oauth2/token Titik akhir identitas terkelola untuk aplikasi Service Fabric, disediakan melalui variabel IDENTITY_ENDPOINT.
api-version Parameter string kueri, menentukan versi API dari Layanan Token Identitas Terkelola; saat ini satu-satunya nilai yang diterima adalah 2019-07-01-preview, dan dapat berubah.
resource Parameter untai (karakter) kueri, menunjukkan URI ID Aplikasi dari sumber daya target. Ini akan tercermin sebagai klaim (audiens) aud dari token yang dikeluarkan. Contoh ini meminta token untuk mengakses Azure Key Vault dengan URI ID Aplikasi https://vault.azure.net/.
Secret Bidang header permintaan HTTP yang diperlukan oleh Layanan Token Identitas Terkelola Service Fabric untuk layanan Service Fabric guna mengautentikasi pemanggil. Nilai ini disediakan oleh runtime SF melalui variabel lingkungan IDENTITY_HEADER.

Respons sampel:

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

di mana:

Elemen Deskripsi
token_type Jenis token; dalam hal ini, token akses "Pembawa", yang berarti penyaji ('pembawa') token ini adalah subjek token yang dimaksudkan.
access_token Token akses yang diminta. Saat memanggil REST API yang aman, token disematkan di Authorization bidang header permintaan sebagai token "pembawa", yang memungkinkan API untuk mengautentikasi pemanggil.
expires_on Tanda waktu kedaluwarsa token akses; dinyatakan sebagai jumlah detik dari "1970-01-01T0:0:0Z UTC" dan sesuai dengan klaim exp token. Dalam hal ini, token kedaluwarsa pada 2019-08-08T06:10:11+00:00 (dalam RFC 3339)
resource Sumber daya tempat token akses dikeluarkan, ditentukan melalui parameter string resource kueri permintaan; sesuai dengan klaim 'aud' token.

Memperoleh token akses menggunakan C#

Hal di atas dalam C# menjadi:

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

Mengakses Key Vault dari aplikasi Service Fabric menggunakan Identitas Terkelola

Contoh ini dibuat berdasarkan hal di atas untuk menunjukkan cara mengakses rahasia yang disimpan di Key Vault menggunakan identitas terkelola.

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

Penanganan kesalahan

Bidang 'kode status' dari header respons HTTP menunjukkan status keberhasilan permintaan; status '200 OK' menunjukkan keberhasilan, dan respons akan menyertakan token akses seperti yang dijelaskan di atas. Berikut ini adalah enumerasi singkat dari kemungkinan respons kesalahan.

Kode status Alasan Kesalahan Cara Menangani
404 Tidak Ditemukan. Kode autentikasi tidak diketahui, atau aplikasi tidak diberi identitas terkelola. Memperbaiki pengaturan aplikasi atau kode akuisisi token.
429 Terlalu banyak Permintaan. Batas pembatasan tercapai, diberlakukan oleh MICROSOFT Entra ID atau SF. Coba lagi dengan Backoff Eksponensial. Lihat pedoman di bawah ini.
4xx Kesalahan dalam permintaan. Satu atau beberapa parameter permintaan salah. Jangan coba lagi. Periksa detail kesalahan untuk informasi selengkapnya. 4xx kesalahan adalah kesalahan waktu desain.
5xx Kesalahan dari layanan. Subsistem identitas terkelola atau ID Microsoft Entra mengembalikan kesalahan sementara. Anda dapat mencoba kembali setelah beberapa saat. Anda dapat mencapai kondisi pembatasan (429) saat mencoba kembali.

Jika terjadi kesalahan, isi respons HTTP yang sesuai memuat objek JSON dengan detail kesalahan:

Elemen Deskripsi
kode Kode Kesalahan.
correlationId ID korelasi yang dapat digunakan untuk debugging.
pesan Deskripsi kesalahan Verbose. Deskripsi kesalahan dapat berubah sewaktu-waktu. Jangan tergantung pada pesan kesalahan itu sendiri.

Contoh kesalahan:

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

Berikut ini adalah daftar kesalahan Service Fabric umum khusus untuk identitas terkelola:

Kode Pesan Deskripsi
SecretHeaderNotFound Rahasia tidak ditemukan di header permintaan. Kode autentikasi tidak disediakan dengan permintaan.
ManagedIdentityNotFound Identitas terkelola tidak ditemukan untuk host aplikasi yang ditentukan. Aplikasi tidak memiliki identitas, atau kode autentikasi tidak diketahui.
ArgumennullOrempty Parameter 'sumber daya' tidak boleh null atau string kosong. Sumber daya (audiens) tidak disediakan dalam permintaan.
InvalidApiVersion Versi API '' tidak didukung. Versi yang didukung adalah '2019-07-01-preview'. Versi API yang hilang atau tidak didukung yang ditentukan dalam URI permintaan.
InternalServerError Terjadi kesalahan. Terjadi kesalahan di subsistem identitas terkelola, kemungkinan di luar tumpukan Service Fabric. Kemungkinan besar penyebabnya adalah nilai yang salah yang ditentukan untuk sumber daya (periksa trailing '/'?)

Panduan percobaan ulang

Biasanya, satu-satunya kode kesalahan yang dapat dicoba lagi adalah 429 (Terlalu Banyak Permintaan); kesalahan server internal/kode kesalahan 5xx mungkin bisa dicoba lagi meskipun penyebabnya mungkin permanen.

Batas pembatasan berlaku untuk jumlah panggilan yang dilakukan pada subsistem identitas terkelola - khususnya dependensi 'upstream' (layanan Azure Identitas Terkelola, atau layanan token aman). Token cache Service Fabric di berbagai tingkatan dalam alur, tetapi mengingat sifat terdistribusi dari komponen yang terlibat, pemanggil mungkin mengalami respons pembatasan yang tidak konsisten (yaitu mendapatkan dibatasi pada satu node/instans aplikasi, tetapi tidak pada node yang berbeda saat meminta token untuk identitas yang sama.) Ketika ketentuan pembatasan ditetapkan, permintaan berikutnya dari aplikasi yang sama mungkin gagal dengan kode status HTTP 429 (Terlalu Banyak Permintaan) sampai ketentuan dipenuhi.

Sebaiknya permintaan gagal karena pembatasan dicoba lagi dengan backoff eksponensial, sebagai berikut:

Indeks panggilan Tindakan menerima 429
1 Tunggu 1 detik dan coba lagi
2 Tunggu 2 detik dan coba lagi
3 Tunggu 4 detik dan coba lagi
4 Tunggu 8 detik dan coba lagi
4 Tunggu 8 detik dan coba lagi
5 Tunggu 16 detik dan coba lagi

ID sumber daya untuk layanan Azure

Lihat Layanan Azure yang mendukung autentikasi Microsoft Entra untuk daftar sumber daya yang mendukung ID Microsoft Entra, dan ID sumber daya masing-masing.

Langkah berikutnya