×

当在 AWS 上运行的 OpenShift Container Platform 集群处于安全令牌服务 (STS) 模式时,这意味着集群正在利用 AWS 和 OpenShift Container Platform 的功能在应用程序级别使用 IAM 角色。STS 使应用程序能够提供可以承担 IAM 角色的 JSON Web 令牌 (JWT)。

JWT 包含用于 `sts:AssumeRoleWithWebIdentity` IAM 操作的 Amazon 资源名称 (ARN),以允许为服务帐户授予临时权限。JWT 包含 AWS IAM 可以验证的 `ProjectedServiceAccountToken` 的签名密钥。服务帐户令牌本身(已签名)用作承担 AWS 角色所需的 JWT。

云凭证操作符 (CCO) 是在云提供商上运行的 OpenShift Container Platform 集群中默认安装的集群操作符。对于 STS,CCO 提供以下功能:

  • 检测它是否在启用 STS 的集群上运行

  • 检查 `CredentialsRequest` 对象中是否存在提供授予 Operator 访问 AWS 资源所需信息的字段

即使在手动模式下,CCO 也会执行此检测。正确配置后,CCO 会将包含所需访问信息的 `Secret` 对象投影到 Operator 命名空间。

从 OpenShift Container Platform 4.14 开始,CCO 可以通过扩展使用 `CredentialsRequest` 对象来半自动化此任务,该对象可以请求创建包含 STS 工作流所需信息的 `Secrets`。用户可以在从 Web 控制台或 CLI 安装 Operator 时提供角色 ARN。

不建议使用自动批准更新的订阅,因为在更新之前可能需要进行权限更改。使用手动批准更新的订阅可确保管理员有机会验证更高版本的权限,采取必要的步骤,然后进行更新。

作为准备在 OpenShift Container Platform 4.14 或更高版本中与更新的 CCO 一起使用的 Operator 的 Operator 作者,除了处理 STS 令牌身份验证(如果您的 Operator 尚未启用 STS)之外,您还应指导用户并添加代码来处理与早期 CCO 版本的差异。推荐的方法是提供一个带有正确填充的 STS 字段的 `CredentialsRequest` 对象,并让 CCO 为您创建 `Secret`。

如果您计划支持早于 4.14 版本的 OpenShift Container Platform 集群,请考虑向用户提供有关如何使用 CCO 实用程序 ( `ccoctl` ) 手动创建包含启用 STS 的信息的密钥的说明。早期版本的 CCO 无法识别集群上的 STS 模式,无法为您创建密钥。

您的代码应检查从未出现的密钥,并警告用户遵循您提供的回退说明。有关更多信息,请参见“替代方法”小节。

启用 Operator 以支持使用 AWS STS 的基于 CCO 的工作流

作为将您的项目设计为在 Operator Lifecycle Manager (OLM) 上运行的 Operator 作者,您可以通过自定义您的项目以支持云凭证操作符 (CCO) 来使您的 Operator 能够在启用 STS 的 OpenShift Container Platform 集群上对 AWS 进行身份验证。

使用此方法,Operator 负责并需要 RBAC 权限才能创建 `CredentialsRequest` 对象和读取生成的 `Secret` 对象。

默认情况下,与 Operator 部署相关的 Pod 会挂载 `serviceAccountToken` 卷,以便可以在生成的 `Secret` 对象中引用服务帐户令牌。

先决条件
  • OpenShift Container Platform 4.14 或更高版本

  • 处于 STS 模式的集群

  • 基于 OLM 的 Operator 项目

步骤
  1. 更新您的 Operator 项目的 `ClusterServiceVersion` (CSV) 对象

    1. 确保您的 Operator 具有创建 `CredentialsRequests` 对象的 RBAC 权限

      `clusterPermissions` 列表示例
      # ...
      install:
        spec:
          clusterPermissions:
          - rules:
            - apiGroups:
              - "cloudcredential.openshift.io"
              resources:
              - credentialsrequests
              verbs:
              - create
              - delete
              - get
              - list
              - patch
              - update
              - watch
    2. 添加以下注释以声明支持此使用 AWS STS 的基于 CCO 的工作流的方法

      # ...
      metadata:
       annotations:
         features.operators.openshift.io/token-auth-aws: "true"
  2. 更新您的 Operator 项目代码

    1. 从 `Subscription` 对象在 Pod 上设置的环境变量获取角色 ARN。例如:

      // Get ENV var
      roleARN := os.Getenv("ROLEARN")
      setupLog.Info("getting role ARN", "role ARN = ", roleARN)
      webIdentityTokenPath := "/var/run/secrets/openshift/serviceaccount/token"
    2. 确保您已准备好要修补和应用的 `CredentialsRequest` 对象。例如:

      `CredentialsRequest` 对象创建示例
      import (
         minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
         corev1 "k8s.io/api/core/v1"
         metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
      )
      
      var in = minterv1.AWSProviderSpec{
         StatementEntries: []minterv1.StatementEntry{
            {
               Action: []string{
                  "s3:*",
               },
               Effect:   "Allow",
               Resource: "arn:aws:s3:*:*:*",
            },
         },
      	STSIAMRoleARN: "<role_arn>",
      }
      
      var codec = minterv1.Codec
      var ProviderSpec, _ = codec.EncodeProviderSpec(in.DeepCopyObject())
      
      const (
         name      = "<credential_request_name>"
         namespace = "<namespace_name>"
      )
      
      var CredentialsRequestTemplate = &minterv1.CredentialsRequest{
         ObjectMeta: metav1.ObjectMeta{
             Name:      name,
             Namespace: "openshift-cloud-credential-operator",
         },
         Spec: minterv1.CredentialsRequestSpec{
            ProviderSpec: ProviderSpec,
            SecretRef: corev1.ObjectReference{
               Name:      "<secret_name>",
               Namespace: namespace,
            },
            ServiceAccountNames: []string{
               "<service_account_name>",
            },
            CloudTokenPath:   "",
         },
      }

      或者,如果您从 YAML 格式的 `CredentialsRequest` 对象(例如,作为您 Operator 项目代码的一部分)开始,您可以以不同的方式处理它

      YAML 格式的 `CredentialsRequest` 对象创建示例
      // CredentialsRequest is a struct that represents a request for credentials
      type CredentialsRequest struct {
        APIVersion string `yaml:"apiVersion"`
        Kind       string `yaml:"kind"`
        Metadata   struct {
           Name      string `yaml:"name"`
           Namespace string `yaml:"namespace"`
        } `yaml:"metadata"`
        Spec struct {
           SecretRef struct {
              Name      string `yaml:"name"`
              Namespace string `yaml:"namespace"`
           } `yaml:"secretRef"`
           ProviderSpec struct {
              APIVersion     string `yaml:"apiVersion"`
              Kind           string `yaml:"kind"`
              StatementEntries []struct {
                 Effect   string   `yaml:"effect"`
                 Action   []string `yaml:"action"`
                 Resource string   `yaml:"resource"`
              } `yaml:"statementEntries"`
              STSIAMRoleARN   string `yaml:"stsIAMRoleARN"`
           } `yaml:"providerSpec"`
      
           // added new field
            CloudTokenPath   string `yaml:"cloudTokenPath"`
        } `yaml:"spec"`
      }
      
      // ConsumeCredsRequestAddingTokenInfo is a function that takes a YAML filename and two strings as arguments
      // It unmarshals the YAML file to a CredentialsRequest object and adds the token information.
      func ConsumeCredsRequestAddingTokenInfo(fileName, tokenString, tokenPath string) (*CredentialsRequest, error) {
        // open a file containing YAML form of a CredentialsRequest
        file, err := os.Open(fileName)
        if err != nil {
           return nil, err
        }
        defer file.Close()
      
        // create a new CredentialsRequest object
        cr := &CredentialsRequest{}
      
        // decode the yaml file to the object
        decoder := yaml.NewDecoder(file)
        err = decoder.Decode(cr)
        if err != nil {
           return nil, err
        }
      
        // assign the string to the existing field in the object
        cr.Spec.CloudTokenPath = tokenPath
      
        // return the modified object
        return cr, nil
      }

      目前不支持向 Operator 包中添加 `CredentialsRequest` 对象。

    3. 将角色 ARN 和 Web 身份令牌路径添加到凭据请求,并在 Operator 初始化期间应用它

      在 Operator 初始化期间应用 `CredentialsRequest` 对象的示例
      // apply CredentialsRequest on install
      credReq := credreq.CredentialsRequestTemplate
      credReq.Spec.CloudTokenPath = webIdentityTokenPath
      
      c := mgr.GetClient()
      if err := c.Create(context.TODO(), credReq); err != nil {
         if !errors.IsAlreadyExists(err) {
            setupLog.Error(err, "unable to create CredRequest")
            os.Exit(1)
         }
      }
    4. 确保您的 Operator 可以等待 CCO 显示 `Secret` 对象,如下面的示例所示,该示例与您在 Operator 中协调的其他项目一起调用

      `Secret` 对象等待示例
      // WaitForSecret is a function that takes a Kubernetes client, a namespace, and a v1 "k8s.io/api/core/v1" name as arguments
      // It waits until the secret object with the given name exists in the given namespace
      // It returns the secret object or an error if the timeout is exceeded
      func WaitForSecret(client kubernetes.Interface, namespace, name string) (*v1.Secret, error) {
        // set a timeout of 10 minutes
        timeout := time.After(10 * time.Minute) (1)
      
        // set a polling interval of 10 seconds
        ticker := time.NewTicker(10 * time.Second)
      
        // loop until the timeout or the secret is found
        for {
           select {
           case <-timeout:
              // timeout is exceeded, return an error
              return nil, fmt.Errorf("timed out waiting for secret %s in namespace %s", name, namespace)
                 // add to this error with a pointer to instructions for following a manual path to a Secret that will work on STS
           case <-ticker.C:
              // polling interval is reached, try to get the secret
              secret, err := client.CoreV1().Secrets(namespace).Get(context.Background(), name, metav1.GetOptions{})
              if err != nil {
                 if errors.IsNotFound(err) {
                    // secret does not exist yet, continue waiting
                    continue
                 } else {
                    // some other error occurred, return it
                    return nil, err
                 }
              } else {
                 // secret is found, return it
                 return secret, nil
              }
           }
        }
      }
      1 timeout 值基于对 CCO 检测新增 CredentialsRequest 对象并生成 Secret 对象速度的估计。您可能需要考虑缩短时间,或为集群管理员创建自定义反馈,以解释操作符为何尚未访问云资源。
    5. 通过读取 CCO 从凭据请求创建的密钥并创建包含该密钥数据的 AWS 配置文件来设置 AWS 配置。

      AWS 配置创建示例
      func SharedCredentialsFileFromSecret(secret *corev1.Secret) (string, error) {
         var data []byte
         switch {
         case len(secret.Data["credentials"]) > 0:
             data = secret.Data["credentials"]
         default:
             return "", errors.New("invalid secret for aws credentials")
         }
      
      
         f, err := ioutil.TempFile("", "aws-shared-credentials")
         if err != nil {
             return "", errors.Wrap(err, "failed to create file for shared credentials")
         }
         defer f.Close()
         if _, err := f.Write(data); err != nil {
             return "", errors.Wrapf(err, "failed to write credentials to %s", f.Name())
         }
         return f.Name(), nil
      }

      假设密钥已存在,但在使用此密钥时,您的操作符代码应等待并重试,以便为 CCO 创建密钥留出时间。

      此外,等待时间最终应该超时并警告用户 OpenShift Container Platform 集群版本(以及 CCO)可能是一个较早的版本,不支持使用 STS 检测的 CredentialsRequest 对象工作流。在这种情况下,请指示用户必须使用其他方法添加密钥。

    6. 配置 AWS SDK 会话,例如

      AWS SDK 会话配置示例
      sharedCredentialsFile, err := SharedCredentialsFileFromSecret(secret)
      if err != nil {
         // handle error
      }
      options := session.Options{
         SharedConfigState: session.SharedConfigEnable,
         SharedConfigFiles: []string{sharedCredentialsFile},
      }

角色规范

操作符描述应包含安装前需要创建的角色的详细信息,理想情况下应以管理员可以运行的脚本形式呈现。例如

角色创建脚本示例
#!/bin/bash
set -x

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
OIDC_PROVIDER=$(oc get authentication cluster -ojson | jq -r .spec.serviceAccountIssuer | sed -e "s/^https:\/\///")
NAMESPACE=my-namespace
SERVICE_ACCOUNT_NAME="my-service-account"
POLICY_ARN_STRINGS="arn:aws:iam::aws:policy/AmazonS3FullAccess"


read -r -d '' TRUST_RELATIONSHIP <<EOF
{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Effect": "Allow",
     "Principal": {
       "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}"
     },
     "Action": "sts:AssumeRoleWithWebIdentity",
     "Condition": {
       "StringEquals": {
         "${OIDC_PROVIDER}:sub": "system:serviceaccount:${NAMESPACE}:${SERVICE_ACCOUNT_NAME}"
       }
     }
   }
 ]
}
EOF

echo "${TRUST_RELATIONSHIP}" > trust.json

aws iam create-role --role-name "$SERVICE_ACCOUNT_NAME" --assume-role-policy-document file://trust.json --description "role for demo"

while IFS= read -r POLICY_ARN; do
   echo -n "Attaching $POLICY_ARN ... "
   aws iam attach-role-policy \
       --role-name "$SERVICE_ACCOUNT_NAME" \
       --policy-arn "${POLICY_ARN}"
   echo "ok."
done <<< "$POLICY_ARN_STRINGS"

故障排除

身份验证失败

如果身份验证不成功,请确保您可以使用提供给操作符的令牌通过 Web 身份假定角色。

步骤
  1. 从 Pod 中提取令牌

    $ oc exec operator-pod -n <namespace_name> \
        -- cat /var/run/secrets/openshift/serviceaccount/token
  2. 从 Pod 中提取角色 ARN

    $ oc exec operator-pod -n <namespace_name> \
        -- cat /<path>/<to>/<secret_name> (1)
    1 路径不要使用 root。
  3. 尝试使用 Web 身份令牌假定角色

    $ aws sts assume-role-with-web-identity \
        --role-arn $ROLEARN \
        --role-session-name <session_name> \
        --web-identity-token $TOKEN

密钥挂载不正确

以非 root 用户身份运行的 Pod 无法写入默认情况下 AWS 共享凭据文件所在的 /root 目录。如果密钥未正确挂载到 AWS 凭据文件路径,请考虑将密钥挂载到其他位置,并在 AWS SDK 中启用共享凭据文件选项。

替代方法

作为操作符作者的替代方法,您可以指示用户负责在安装操作符之前为云凭据操作符 (CCO) 创建 CredentialsRequest 对象。

操作符说明必须向用户指示以下内容:

  • 提供 CredentialsRequest 对象的 YAML 版本,可以通过在说明中内联提供 YAML 或引导用户到下载位置来实现。

  • 指示用户创建 CredentialsRequest 对象。

在 OpenShift Container Platform 4.14 及更高版本中,在添加了相应的 STS 信息后,CredentialsRequest 对象出现在集群上,操作符随后可以读取 CCO 生成的 Secret 或挂载它,并在集群服务版本 (CSV) 中定义了挂载点。

对于早期版本的 OpenShift Container Platform,操作符说明还必须向用户指示以下内容:

  • 使用 CCO 实用程序 (ccoctl) 从 CredentialsRequest 对象生成 Secret YAML 对象。

  • Secret 对象应用于适当命名空间中的集群。

操作符仍然必须能够使用生成的密钥与云 API 通信。因为在这种情况下,密钥是在安装操作符之前由用户创建的,所以操作符可以执行以下任一操作:

  • 在 CSV 中的 Deployment 对象中定义显式挂载。

  • 以编程方式从 API 服务器读取 Secret 对象,如推荐的“使操作符能够支持基于 CCO 的 AWS STS 工作流”方法中所示。