教程:将 SPIFFE/SPIRE 工作负载标识与 Microsoft Entra ID 联合

SPIFFE(适用于每个人的安全生产标识框架)是一组开源标准,为软件工作负载提供与平台无关的标识,称为 SPIFFE ID。 工作负载通过出示一种称为 SVID 的短期凭据来证明其身份。 SPIRE 是 SPIFFE 标准的参考实现。

在 Azure 外部运行的工作负载通常通过存储的机密信息或证书进行身份验证,这些凭据必须受到保护并定期轮换;如果在你完成替换之前过期,可能会导致服务中断。 工作负载身份联合无需再使用该已存储的凭据:工作负载无需持有密钥,而是提供其现有的 SPIFFE 身份,并用其换取 Microsoft Entra 令牌。

在本教程中,将在 Kubernetes 群集中设置 SPIRE,为示例工作负荷提供 SPIFFE ID,并将该标识与Microsoft Entra ID联合。 建立信任关系后,工作负载会交换其 SPIFFE JWT-SVID 以获取Microsoft Entra访问令牌,并调用Azure资源(例如Azure Blob 存储),而无需存储任何机密或证书。

本教程包含四个依次进行的部分,共同逐步构建成一个完整的场景。 每个部分都取决于前一部分的输出。

在本教程中,你将了解:

  • 在 Kubernetes 群集中使用 JWT-SVID 和 OIDC 发现支持部署 SPIRE。
  • 部署示例工作负荷并为其分配 SPIFFE ID。
  • 将Microsoft Entra应用程序配置为信任 SPIFFE ID。
  • 将 SPIFFE JWT-SVID 换取为 Microsoft Entra 访问令牌,并访问 Azure 资源。

下图显示了工作负载身份联合流程:工作负载从外部身份提供程序获取令牌,将其与 Microsoft 标识平台交换以获取访问令牌,并使用该访问令牌来访问 Azure 资源。 在本教程中,外部标识提供者是 SPIRE,令牌是 SPIFFE JWT-SVID。

工作负载标识联合流程示意图:工作负载使用外部令牌与 Microsoft 标识平台交换,以获取 Azure 访问权限。

先决条件

  • 一个 Microsoft Entra 租户和一个 Azure 订阅。 如果没有 Azure 订阅,可在开始前创建一个试用帐户
  • 可以托管 SPIRE 服务器、SPIRE 代理和 OIDC 发现提供程序的 Kubernetes 群集。 群集必须通过外部 IP 地址公开服务。
  • 一个由你控制、用于 OIDC 发现端点的已注册域名,以及管理其 DNS 记录的能力。 本教程使用 OIDC 发现域的占位符 oidc.contoso.com
  • 用于保存示例工作负荷映像的容器注册表。 本教程使用注册表名称的占位符 <your-registry>
  • 已安装并配置为可访问你的群集、注册表和租户的 kubectldocker 和 Azure CLI(az)命令行工具。
  • 将联合标识凭据添加到应用注册或托管标识的权限。 若要将联合凭据添加到应用注册,你的帐户必须是应用的所有者,或者拥有 应用程序管理员应用程序开发人员云应用程序管理员 角色之一,或者具有 microsoft.directory/applications/credentials/update 权限。

本教程使用 SPIFFE 信任域 example.orgexample.org 是整个官方 SPIRE 快速入门中使用的信任域示例;在实际部署中将其替换为自己的信任域。 以下步骤server-configmap-oidc.yamlagent-configmap.yaml(等等)中引用的 SPIRE 配置文件来自 SPIRE 部署清单。 请基于当前官方 SPIRE 文档 和 Kubernetes 快速入门来配置 SPIRE 服务器、代理和 OIDC 发现提供程序,以确保节点证明器和镜像版本保持最新。

第 1 部分:使用 JWT 和 OIDC 发现支持部署 SPIRE

在本部分中,你将部署 SPIRE 服务器、SPIRE 代理和 OIDC 发现提供程序。 OIDC 发现提供程序发布一个标准的 OpenID Connect 发现文档以及一个 JWKS 终结点,以便 Microsoft Entra ID 能够验证 SPIFFE JWT-SVID。

  1. 根据您的环境自定义 SPIRE 配置文件:

    • server-configmap-oidc.yaml:设置 OIDC 发现域 FQDN(例如 oidc.contoso.com)和群集名称。
    • agent-configmap.yaml:设置群集名称。
    • oidc-ingress.yaml:设置 OIDC 发现端点的 FQDN。
    • oidc-dp-configmap.yaml:设置 OIDC 发现 FQDN、联系邮箱和 set_key_use = true

    Important

    当你在"use": "sig"中设置oidc-dp-configmap.yaml时,SPIRE OIDC 发现提供程序可以将set_key_use = true参数添加到已发布的签名密钥中。 启用它,因为Microsoft Entra ID需要在 OIDC 发现文档的 JWKS 中的签名密钥上使用此参数。

    Important

    Microsoft Entra ID支持使用 RS256 算法对令牌进行签名的外部颁发者。 SPIRE 默认签发使用 EC(ES256)签名的 JWT-SVID,因此请将 SPIRE 服务器证书颁发机构配置为签发使用 RS256 签名的 JWT-SVID(例如,将服务器 CA jwt_key_type 设置为 RSA 密钥类型),以便 Microsoft 标识平台接受这些令牌。 有关详细信息,请参阅联合 标识凭据的重要注意事项和限制中支持的签名算法和颁发者指南。

  2. 部署 SPIRE 服务器:

    kubectl apply -f spire-namespace.yaml
    kubectl apply -f server-account.yaml -f spire-bundle-configmap.yaml -f server-cluster-role.yaml
    kubectl apply -f server-configmap-oidc.yaml -f server-statefulset.yaml -f server-service.yaml
    
  3. 部署 SPIRE 代理:

    kubectl apply -f agent-account.yaml -f agent-cluster-role.yaml
    kubectl apply -f agent-configmap.yaml -f agent-daemonset.yaml
    
  4. 验证 SPIRE Pod 是否正在运行。 每个节点应有一个 spire-server-0 Pod 和一个 spire-agent-* Pod,且都处于 Running 状态。

    kubectl get pods -n spire
    
  5. 注册代理节点标识,以便 SPIRE 服务器信任代理。 将 <your-cluster-name> 替换为您的集群名称。

    kubectl exec -n spire spire-server-0 -- /opt/spire/bin/spire-server entry create \
      -spiffeID spiffe://example.org/ns/spire/sa/spire-agent \
      -selector k8s_sat:cluster:<your-cluster-name> \
      -selector k8s_sat:agent_ns:spire \
      -selector k8s_sat:agent_sa:spire-agent -node
    
  6. 部署 SPIRE OIDC 发现服务提供程序。 根据官方 "use": "sig",请使用支持 选项的当前提供程序镜像,以便发布的密钥包含 set_key_use

    kubectl apply -f oidc-account.yaml -f oidc-dp-configmap.yaml
    kubectl apply -f oidc-ingress.yaml -f oidc-service.yaml
    kubectl apply -f oidc-deployment.yaml
    
  7. 注册 OIDC 提供方身份:

    kubectl exec -n spire spire-server-0 -- /opt/spire/bin/spire-server entry create \
      -spiffeID spiffe://example.org/oidc-discovery \
      -parentID spiffe://example.org/ns/spire/sa/spire-agent \
      -selector k8s:ns:spire -selector k8s:sa:spire-oidc
    
  8. OIDC 发现服务作为一个具有外部 IP 地址的 LoadBalancer 运行。 将 OIDC 发现域的 DNS A 记录指向该外部 IP 地址。

  9. 验证发现终结点是否已解析。 发现文档列出了 issuerjwks_uriid_token_signing_alg_values_supported 的值,而 JWKS 列出了签名密钥。

    • https://oidc.contoso.com/.well-known/openid-configuration
    • https://oidc.contoso.com/keys

    确认位于 /keys 的 JWKS 中的每个密钥都包含 "use": "sig"。 Microsoft Entra ID在签名密钥上需要此参数。

第 2 部分:部署示例工作负荷并为其分配 SPIFFE ID

在本部分中,你将生成和部署示例工作负荷,然后基于其 Kubernetes 命名空间和服务帐户为其分配 SPIFFE ID。

  1. 生成示例工作负荷容器映像并将其推送到注册表。 将 <your-registry> 替换为你的注册表名称。

    docker build -f deployment/docker/dockerfile -t spiffe-demo .
    docker tag spiffe-demo <your-registry>.azurecr.io/spiffe-demo:v1
    az acr login -n <your-registry>.azurecr.io
    docker push <your-registry>.azurecr.io/spiffe-demo:v1
    
  2. 编辑 deployment.yaml,使其引用你的映像(例如 <your-registry>.azurecr.io/spiffe-demo:v1),然后部署工作负载:

    kubectl apply -f demo-namespace.yaml
    kubectl apply -f serviceaccount.yaml
    kubectl apply -f deployment.yaml
    kubectl apply -f service.yaml
    
  3. 根据其命名空间和服务账户为工作负载分配 SPIFFE ID:

    kubectl exec -n spire spire-server-0 -- /opt/spire/bin/spire-server entry create \
      -spiffeID spiffe://example.org/ns/demo-spiffe/sa/demo-sa \
      -parentID spiffe://example.org/ns/spire/sa/spire-agent \
      -selector k8s:ns:demo-spiffe -selector k8s:sa:demo-sa
    

工作负荷现已具有 SPIFFE ID spiffe://example.org/ns/demo-spiffe/sa/demo-sa。 在第 3 部分配置 Microsoft Entra 应用程序时,请将此值用作联合身份凭据的主题。

第 3 部分:配置Microsoft Entra应用程序以信任 SPIFFE ID

在本部分中,你需要向 Microsoft Entra 应用注册添加联合标识凭据,以便 Microsoft Entra ID 信任为你的工作负载的 SPIFFE ID 颁发的令牌。 联合标识凭据需要三个输入:

  • 颁发者:OIDC 发现 URL,例如:https://oidc.contoso.com
  • 主题:工作负荷的 SPIFFE ID。 spiffe://example.org/ns/demo-spiffe/sa/demo-sa
  • 访问群体["api://AzureADTokenExchange"].
  1. 创建包含以下内容的名为 credential.json 的文件。 将签发者和值主题值替换为您自己的值。

    {
      "name": "AccessUsingSpiffe",
      "issuer": "https://oidc.contoso.com",
      "subject": "spiffe://example.org/ns/demo-spiffe/sa/demo-sa",
      "audiences": ["api://AzureADTokenExchange"],
      "description": "Federated credential for SPIFFE workload"
    }
    
  2. 将联合标识凭据添加到应用注册。 将 <your-app-id> 替换为你的应用的对象 ID。

    az ad app federated-credential create --id <your-app-id> --parameters credential.json
    

还可以通过在Microsoft Entra 管理中心中选择“其他颁发者”方案并提供 OIDC 发现 URL 作为颁发者和 SPIFFE ID 作为主题来添加联合凭据。 有关详细步骤,请参阅 配置应用以信任外部标识提供者。 若要在用户分配的托管标识而不是应用注册上配置凭据,请参阅 配置用户分配的托管标识以信任外部标识提供者

向你的应用或托管标识授予访问你的工作负载所调用的 Azure 资源的权限,例如对你的存储帐户进行角色分配。

第 4 部分:将 JWT-SVID 换取为 Microsoft Entra 访问令牌

在本部分中,您的工作负载会获取 SPIFFE JWT-SVID,并用其换取 Microsoft Entra 访问令牌,然后使用该令牌调用 Azure 资源。

SPIFFE 定义工作负荷 API,工作负荷使用该 API 从本地 SPIRE 代理中提取其 SVID。 为了向 Microsoft Entra ID 表明身份,工作负载会获取受众为 api://AzureADTokenExchange 的 JWT-SVID。 此受众必须与您在第 3 部分中为 Microsoft Entra 应用注册配置的联合标识凭据上的受众一致。

  1. 从 SPIFFE Workload API 获取 JWT-SVID。 以下示例代码片段为 api://AzureADTokenExchange 受众请求 JWT-SVID,并返回该 SVID 字符串。 该概念在具有 SPIFFE 工作负荷 API 客户端的任何语言中都是相同的。

    async function getSpiffeJwt() {
      const svid = await workloadApiClient.fetchJwtSvid({
        audience: ["api://AzureADTokenExchange"],
      });
      return svid.token;
    }
    
  2. 使用 Azure Identity SDK 中的 ClientAssertionCredential 将 JWT-SVID 交换为 Microsoft Entra 令牌。 ClientAssertionCredential 接受一个返回联合断言的回调函数——在本例中为 JWT-SVID。

    import { ClientAssertionCredential } from "@azure/identity";
    
    const credential = new ClientAssertionCredential(tenantId, clientId, getSpiffeJwt);
    
  3. 将此凭据用于任何 Azure SDK 客户端。 例如,若要调用Azure Blob 存储:

    const { BlobServiceClient } = require("@azure/storage-blob");
    
    const blobClient = new BlobServiceClient(blobUrl, credential);
    

当客户端需要令牌时,它会调用该回调以获取新的 JWT-SVID,使用该 JWT-SVID 向 Microsoft 标识平台 换取访问令牌,并缓存生成的访问令牌。 由于 ClientAssertionCredential 通过回调提供联合断言,因此工作负荷永远不会存储机密。

ClientAssertionCredential可在Azure标识 SDK 中使用,包括.NET、Java、JavaScript、Python和 Go。 如果需要对令牌交换进行较低级别的控制,MSAL 库还支持客户端断言。

您的 SPIFFE/SPIRE 工作负载现在可以无需存储任何机密信息即可访问受 Microsoft Entra 保护的资源。

清理资源

如果不再需要在本教程中创建的资源,请将其删除以避免持续收费:

  • 从群集中删除 SPIRE 和演示工作负荷:

    kubectl delete namespace demo-spiffe
    kubectl delete namespace spire
    
  • 删除为 OIDC 发现域创建的 DNS A 记录。

  • 从注册表中删除容器映像,如果仅为本教程创建了容器映像,请删除群集。

  • 从应用注册中删除联合标识凭据:

    az ad app federated-credential delete --id <your-app-id> --federated-credential-id AccessUsingSpiffe