如何利用 Service Fabric 應用程式的受控身分識別來存取 Azure 服務
Service Fabric 應用程式可以利用受控識別來存取支援 Microsoft Entra ID 型驗證的其他 Azure 資源。 應用程式可以取得代表其身分識別 (可能是由系統指派或使用者指派) 的存取權杖,並使用其作為「持有人」權杖,向其他服務 (也就是受保護的資源伺服器) 驗證其本身。 權杖代表指派給 Service Fabric 應用程式的身分識別,而且只會向共用該身分識別的 Azure 資源 (包括 SF 應用程式) 發出。 如需受控識別的詳細描述,以及系統指派和使用者指派身分識別之間的差異,請參閱受控識別槪觀文件。 在本文中,我們會將已啟用受控識別的 Service Fabric 應用程式稱為用戶端應用程式。
請參閱隨附的範例應用程式,其示範如何使用系統指派及使用者指派的 Service Fabric 應用程式受控識別搭配 Reliable Services 和容器。
重要
受控識別代表 Azure 資源與對應的 Microsoft Entra 租用戶 (與包含該資源的訂用帳戶相關聯) 中服務主體之間的關聯。 因此,在 Service Fabric 的內容中,只有部署為 Azure 資源的應用程式才支援受控識別。
重要
在使用 Service Fabric 應用程式的受控識別之前,必須先將受保護資源的存取權授與用戶端應用程式。 請參閱支援 Microsoft Entra 驗證的 Azure 服務清單以了解支援情況,然後查閱個別服務的文件,以了解將感興趣資源的存取權授與身分識別的特定步驟。
使用 Azure.Identity 來利用受控識別
Azure 身分識別 SDK 現在支援 Service Fabric。 使用 Azure.Identity 可讓撰寫程式碼以使用 Service Fabric 應用程式受控識別的動作變得更簡單,因為其可處理擷取權杖、快取權杖和伺服器驗證。 存取大部分的 Azure 資源時,權杖的概念都會隱藏。
Service Fabric 支援在下列版本中為這些語言提供:
- 版本 1.3.0 的 C#。 請參閱 C# 範例。
- 版本 1.5.0 的 Python。 請參閱 Python 範例。
- 版本 1.2.0 的 Java。
將認證初始化和使用認證從 Azure Key Vault 擷取秘密的 C# 範例:
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
}
}
}
}
使用 REST API 取得存取權杖
在啟用受控識別的叢集中,Service Fabric 執行階段會公開可供應用程式用來取得存取權杖的 localhost 端點。 此端點可在叢集的每個節點上使用,並且可供該節點上的所有實體存取。 獲授權的呼叫端可以藉由呼叫此端點並出示驗證碼來取得存取權杖;該驗證碼是 Service Fabric 執行階段為每個不同服務程式碼封裝啟動所產生,並且會繫結至裝載該服務程式碼封裝的程序存留期。
具體而言,已啟用受控識別的 Service Fabric 服務環境將會使用下列變數植入:
- 'IDENTITY_ENDPOINT':對應至服務受控識別的 localhost 端點
- 'IDENTITY_HEADER':代表目前節點上的服務的唯一驗證碼
- 'IDENTITY_SERVER_THUMBPRINT':Service Fabric 受控識別伺服器的指紋
重要
應用程式程式碼應該將 'IDENTITY_HEADER' 環境變數的值視為敏感資料,不應將其記錄或散播。 驗證碼在本機節點以外不會有值,或在裝載服務的程序終止之後也不會有值,但其確實代表 Service Fabric 服務的身分識別,因此應該與對待存取權杖本身一樣謹慎。
若要取得權杖,用戶端會執行下列步驟:
- 藉由串連受控識別端點 (IDENTITY_ENDPOINT 值) 與權杖所需的 API 版本和資源 (對象) 來形成 URI
- 為指定的 URI 建立 GET http(s) 要求
- 新增適當的伺服器憑證驗證邏輯
- 新增驗證碼 (IDENTITY_HEADER 值) 作為要求的標頭
- 提交要求
成功的回應會包含代表所產生存取權杖的 JSON 承載,以及描述該權杖的中繼資料。 失敗的回應也會包含失敗的說明。 如需有關錯誤處理的其他詳細資料,請參閱下文。
存取權杖將會由 Service Fabric 在各個層級 (節點、叢集、資源提供者服務) 上進行快取,因此成功的回應不一定表示權杖是為回應使用者應用程式的要求而直接發出。 權杖的快取時間不會超過其存留期,因此應用程式保證會收到有效的權杖。 建議讓應用程式程式碼快取其取得的任何存取權杖;快取金鑰應該包含對象 (衍生對象)。
範例要求:
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
其中:
元素 | 描述 |
---|---|
GET |
HTTP 指令動詞,指出您想要擷取端點中的資料。 在此案例中是 OAuth 存取權杖。 |
https://localhost:2377/metadata/identity/oauth2/token |
Service Fabric 應用程式的受控識別端點,透過 IDENTITY_ENDPOINT 環境變數提供。 |
api-version |
查詢字串參數,指定受控識別權杖服務的 API 版本;目前唯一接受的值是 2019-07-01-preview ,而且可能會變更。 |
resource |
查詢字串參數,指出目標資源的應用程式識別碼 URI。 這會反映為已核發權杖的 aud (受眾) 宣告。 此範例會要求權杖以存取 Azure Key Vault,其應用程式識別碼 URI 為 https://vault.azure.net/. |
Secret |
HTTP 要求標頭欄位,Service Fabric 受控識別權杖服務需要此項目來讓 Service Fabric 服務驗證呼叫端。 此值是由 SF 執行階段透過 IDENTITY_HEADER 環境變數提供。 |
範例回應:
HTTP/1.1 200 OK
Content-Type: application/json
{
"token_type": "Bearer",
"access_token": "eyJ0eXAiO...",
"expires_on": 1565244611,
"resource": "https://vault.azure.net/"
}
其中:
元素 | 描述 |
---|---|
token_type |
權杖的類型;在此案例中是「持有人」存取權杖,表示此權杖的出示者 (「持有人」) 是權杖的預期主體。 |
access_token |
要求的存取權杖。 呼叫受保護的 REST API 時,權杖會內嵌在 Authorization 要求標頭欄位中成為「持有人」權杖,以允許 API 驗證呼叫端。 |
expires_on |
存取權杖到期的時間戳記;以從 "1970-01-01T0:0:0Z UTC" 開始的秒數表示,並對應至權杖的 exp 宣告。 在此案例中,權杖會在 2019-08-08T06:10:11+00:00 到期 (遵循 RFC 3339) |
resource |
已發出其存取權杖的資源 (透過要求的 resource 查詢字串參數指定);對應至權杖的 'aud' 宣告。 |
使用 C# 取得存取權杖
上述情況在 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
使用受控識別從 Service Fabric 應用程式存取 Key Vault
此範例以上述內容為基礎,示範如何使用受控識別存取儲存在 Key Vault 中的秘密。
/// <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);
}
}
錯誤處理
HTTP 回應標頭的 [狀態碼] 欄位會指出要求的成功狀態;'200 OK' 狀態表示成功,而回應將會包含存取權杖,如上所述。 以下是可能錯誤回應的簡短列舉。
狀態碼 | 錯誤原因 | 處理方式 |
---|---|---|
404 找不到。 | 未知的驗證碼,或未將受控識別指派給應用程式。 | 修正應用程式設定或權杖取得代碼。 |
429 要求太多。 | 已達到 Microsoft Entra 或 SF 實施的節流限制。 | 使用指數輪詢重試。 請參閱下面的指引。 |
要求中的 4xx 錯誤。 | 一個或多個要求參數不正確。 | 請勿重試。 檢查錯誤詳細資料以取得更多資訊。 4xx 錯誤是設計階段錯誤。 |
來自服務的 5xx 錯誤。 | 受控識別子系統或 Microsoft Entra ID 傳回了暫時性錯誤。 | 您可以放心地在短時間後重試。 重試時,您可能會遇到節流狀況 (429)。 |
如果發生錯誤,對應的 HTTP 回應主體會包含 JSON 物件及錯誤詳細資料:
元素 | 描述 |
---|---|
code | 錯誤碼。 |
correlationId | 可以用來進行偵錯的相互關聯識別碼。 |
message | 錯誤的詳細資訊描述。 錯誤描述可以隨時變更。 請勿依賴錯誤訊息本身。 |
範例錯誤:
{"error":{"correlationId":"7f30f4d3-0f3a-41e0-a417-527f21b3848f","code":"SecretHeaderNotFound","message":"Secret is not found in the request headers."}}
以下是受控識別特有的一般 Service Fabric 錯誤清單:
代碼 | 訊息 | 描述 |
---|---|---|
SecretHeaderNotFound | 在要求標頭中找不到祕密。 | 未在要求中提供驗證碼。 |
ManagedIdentityNotFound | 找不到指定應用程式主機的受控識別。 | 應用程式沒有身分識別,或驗證碼不明。 |
ArgumentNullOrEmpty | 'resource' 參數不可以是 null 或空字串。 | 要求中未提供資源 (受眾)。 |
InvalidApiVersion | 不支援 'api-version'。 支援的版本為 '2019-07-01-preview'。 | 要求 URI 中指定的 API 版本遺失或不受支援。 |
InternalServerError | 發生錯誤。 | 受控識別子系統中發生錯誤,可能是在 Service Fabric 堆疊之外。 最可能的原因是為資源指定了不正確的值 (檢查尾端的 '/'?) |
重試指引
一般來說,唯一可重試的錯誤碼為 429 (太多要求);內部伺服器錯誤/5xx 錯誤碼可能可重試,但原因可能是永久性的。
節流限制適用於對受控識別子系統發出的呼叫次數,特別是「上游」相依性 (受控識別 Azure 服務或安全權杖服務)。 Service Fabric 會在管線中的各個層級上快取權杖,但由於相關元件的分散本質,呼叫端可能會遇到不一致的節流回應 (亦即,對應用程式的一個節點/執行個體進行節流,但在不同節點上要求相同身分識別的權杖時未進行節流。)當設定節流設定條件時,來自相同應用程式的後續要求可能會失敗,並出現 HTTP 狀態碼 429 (太多要求),直到清除該條件為止。
建議因節流造成失敗的要求,以指數輪詢來重試,如下所示:
呼叫索引 | 收到 429 時的動作 |
---|---|
1 | 等候 1 秒並重試 |
2 | 等候 2 秒並重試 |
3 | 等候 4 秒並重試 |
4 | 等候 8 秒並重試 |
4 | 等候 8 秒並重試 |
5 | 等候 16 秒並重試 |
Azure 服務的資源識別碼
關於支援 Microsoft Entra ID 的資源清單及其各自的資源識別碼,請參閱支援 Microsoft Entra 驗證的 Azure 服務。