5.10. 令牌身份验证


5.10.1. 云供应商上的 Operator 的令牌身份验证

许多云提供商可以通过使用提供短期、有限权限安全凭据的帐户令牌来启用身份验证。

OpenShift Container Platform 包含 Cloud Credential Operator (CCO),用于将云供应商凭证作为自定义资源定义(CRD)进行管理。CredentialsRequest 自定义资源(CR)的 CCO 同步,允许 OpenShift Container Platform 组件使用所需的特定权限请求云供应商凭证。

在以前的版本中,在 CCO 处于 手动模式 的集群中,由 Operator Lifecycle Manager (OLM)管理的 Operator 通常会在 OperatorHub 中提供详细说明,供用户手动置备任何所需的云凭证。

从 OpenShift Container Platform 4.14 开始,CCO 可以在启用的集群中检测它,以便在某些云供应商中使用短期凭证。然后,它可以半自动置备某些凭证,只要 Operator 作者启用了其 Operator 来支持更新的 CCO。

5.10.2. 使用 AWS STS 的 OLM 管理的 Operator 基于 CCO 的工作流

当在 AWS 上运行的 OpenShift Container Platform 集群处于 Security Token Service (STS) 模式时,这意味着集群会利用 AWS 和 OpenShift Container Platform 的功能在应用程序级别使用 IAM 角色。STS 使应用程序能够提供可假定 IAM 角色的 JSON Web Token (JWT)。

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

Cloud Credential Operator (CCO) 是在云供应商上运行的 OpenShift Container Platform 集群中默认安装的集群 Operator。对于 STS,CCO 提供以下功能:

  • 检测它是否在启用了 STS 的集群中运行的
  • 检查 CredentialsRequest 对象中是否存在字段,该对象提供授予 Operator 对 AWS 资源访问权限所需的信息

即使处于手动模式,CCO 也会执行此检测。正确配置后,CCO 将带有所需访问信息的 Secret 对象项目到 Operator 命名空间中。

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

注意

不建议使用具有自动更新批准的订阅,因为更新前可能会有权限更改。使用手动批准的订阅可确保管理员有机会验证更新版本的权限,并在更新前采取必要的操作。

作为 Operator 作者准备一个 Operator 以用于 OpenShift Container Platform 4.14 或更高版本中更新的 CCO,您应该指示用户并添加代码来处理之前 CCO 版本的比较,除了处理 STS 令牌身份验证外(如果您的 Operator 还没有启用 STS)。推荐的方法是为 CredentialsRequest 对象提供正确填充的 STS 相关字段,并让 CCO 为您的 Secret 创建 Secret

重要

如果您计划支持早于版本 4.14 的 OpenShift Container Platform 集群,请考虑为用户提供有关如何使用 CCO 实用程序(ccoctl)手动创建带有 STS 额外信息的 secret 的说明。早期 CCO 版本不知道集群中的 STS 模式,且无法为您创建 secret。

您的代码应检查永远不会出现的 secret,并警告用户以遵循您提供的回退指令。如需更多信息,请参阅"Alternative method"子部分。

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

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

使用此方法时,Operator 负责创建 CredentialsRequest 对象,这意味着 Operator 需要 RBAC 权限来创建这些对象。然后,Operator 必须能够读取生成的 Secret 对象。

注意

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

先决条件

  • OpenShift Container Platform 4.14 或更高版本
  • 处于 STS 模式的集群
  • 基于 OLM 的 Operator 项目

流程

  1. 更新 Operator 项目的 ClusterServiceVersion (CSV)对象:

    1. 确保 Operator 有 RBAC 权限来创建 CredentialsRequests 对象:

      例 5.15. 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. 从 pod 上由 Subscription 对象设置的环境变量获取角色 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 对象已准备好修补并应用。例如:

      例 5.16. 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 项目代码的一部分),您可以以不同的方式处理它:

      例 5.17. 以 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 初始化过程中应用它:

      例 5.18. 在 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 可以等待 Secret 对象从 CCO 显示,如下例所示,以及您在 Operator 中协调的其他项目:

      例 5.19. 等待 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 对象的速度。您可能会考虑降低时间或为集群管理员创建自定义反馈,这可能会导致 Operator 尚未访问云资源的原因。
    5. 通过从凭证请求中读取 CCO 创建的 secret 并设置 AWS 配置,并创建包含该 secret 数据的 AWS 配置文件:

      例 5.20. 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
      }
      重要

      secret 被假定为存在,但在使用此 secret 时,您的 Operator 代码应等待和重试,以提供 CCO 创建 secret 的时间。

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

    6. 配置 AWS SDK 会话,例如:

      例 5.21. AWS SDK 会话配置示例

      sharedCredentialsFile, err := SharedCredentialsFileFromSecret(secret)
      if err != nil {
         // handle error
      }
      options := session.Options{
         SharedConfigState: session.SharedConfigEnable,
         SharedConfigFiles: []string{sharedCredentialsFile},
      }

5.10.2.2. 角色规格

Operator 描述应包含在安装前创建的角色的具体信息,最好是管理员可以运行的脚本的形式。例如:

例 5.22. 角色创建脚本示例

#!/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"

5.10.2.3. 故障排除

5.10.2.3.1. 身份验证失败

如果身份验证不成功,请确保您可以使用提供给 Operator 的令牌假设具有 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
5.10.2.3.2. Secret 无法正确挂载

以非 root 用户身份运行的 Pod 无法写入默认存在 AWS 共享凭证文件的 /root 目录。如果 secret 没有正确挂载到 AWS 凭证文件路径,请考虑将 secret 挂载到不同的位置,并在 AWS SDK 中启用共享凭证文件选项。

5.10.2.4. 其它方法

作为 Operator 作者的替代方法,您可以指定用户在安装 Operator 前负责为 Cloud Credential Operator (CCO) 创建 CredentialsRequest 对象。

Operator 指令必须向用户指示以下内容:

  • 通过在说明中内联提供 YAML 或将用户指向下载位置,提供 CredentialsRequest 对象的 YAML 版本
  • 指示用户创建 CredentialsRequest 对象

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

对于早期版本的 OpenShift Container Platform,Operator 指令还必须向用户指示以下内容:

  • 使用 CCO 实用程序(ccoctl)从 CredentialsRequest 对象生成 Secret YAML 对象
  • Secret 对象应用到适当的命名空间中的集群

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

  • 在 CSV 中的 Deployment 对象中定义显式挂载
  • 从 API 服务器以编程方式读取 Secret 对象,如推荐的"启用 Operator 以支持使用 AWS STS 的基于 CCO 的工作流"方法所示

5.10.3. 使用 Microsoft Entra Workload ID 为 OLM 管理的 Operator 基于 CCO 的工作流

当在 Azure 上运行的 OpenShift Container Platform 集群处于 Workload Identity / Federated Identity 模式时,这意味着集群会利用 Azure 和 OpenShift Container Platform 的功能在应用程序级别应用用户分配的管理的身份 或 Microsoft Entra Workload ID 中的 app 注册

Cloud Credential Operator (CCO) 是在云供应商上运行的 OpenShift Container Platform 集群中默认安装的集群 Operator。从 OpenShift Container Platform 4.14.8 开始,CCO 支持使用 Workload ID 的 OLM 管理的 Operator 工作流。

对于 Workload ID,CCO 提供以下功能:

  • 检测在启用了 Workload ID 的集群中运行的时间
  • 检查 CredentialsRequest 对象中是否存在字段,该对象提供授予 Operator 对 Azure 资源访问权限所需的信息

CCO 可以通过扩展 CredentialsRequest 对象来自动处理这个过程,该对象可以请求创建包含 Workload ID 工作流所需的信息的 Secret

注意

不建议使用具有自动更新批准的订阅,因为更新前可能会有权限更改。使用手动批准的订阅可确保管理员有机会验证更新版本的权限,并在更新前采取必要的操作。

作为 Operator 作者准备一个 Operator 以用于 OpenShift Container Platform 4.14 及之后的版本中的更新的 CCO,您应该指示用户并添加代码来处理早期 CCO 版本的划分,除了处理 Workload ID token 身份验证(如果您的 Operator 还没有启用)。推荐的方法是使用正确的 Workload ID 相关字段提供 CredentialsRequest 对象,并让 CCO 为您创建 Secret 对象。

重要

如果您计划支持早于版本 4.14 的 OpenShift Container Platform 集群,请考虑为用户提供有关如何使用 CCO 实用程序(ccoctl)手动创建带有 Workload ID 启用信息的 secret 的说明。早期 CCO 版本不知道集群中的 Workload ID 模式,且无法为您创建 secret。

您的代码应检查永远不会出现的 secret,并警告用户以遵循您提供的回退指令。

使用 Workload ID 进行身份验证需要以下信息:

  • azure_client_id
  • azure_tenant_id
  • azure_region
  • azure_subscription_id
  • azure_federated_token_file

Web 控制台中的 Install Operator 页面允许集群管理员在安装时提供此信息。然后,此信息会作为 Operator pod 上的环境变量传播到 Subscription 对象中。

5.10.3.1. 启用 Operator 以支持使用 Microsoft Entra Workload ID 的基于 CCO 的工作流

作为 Operator 作者设计在 Operator Lifecycle Manager (OLM) 上运行的项目,您可以通过自定义项目来支持 Cloud Credential Operator (CCO),使 Operator 能够对启用了 Microsoft Entra Workload ID 的 OpenShift Container Platform 集群进行身份验证。

使用此方法时,Operator 负责创建 CredentialsRequest 对象,这意味着 Operator 需要 RBAC 权限来创建这些对象。然后,Operator 必须能够读取生成的 Secret 对象。

注意

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

先决条件

  • OpenShift Container Platform 4.14 或更高版本
  • 处于 Workload ID 模式的集群
  • 基于 OLM 的 Operator 项目

流程

  1. 更新 Operator 项目的 ClusterServiceVersion (CSV)对象:

    1. 确保 Operator 有 RBAC 权限来创建 CredentialsRequests 对象:

      例 5.23. clusterPermissions 列表示例

      # ...
      install:
        spec:
          clusterPermissions:
          - rules:
            - apiGroups:
              - "cloudcredential.openshift.io"
              resources:
              - credentialsrequests
              verbs:
              - create
              - delete
              - get
              - list
              - patch
              - update
              - watch
    2. 添加以下注解来声明对使用 Workload ID 的基于 CCO 工作流的方法的支持:

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

    1. 从由 Subscription 对象设置的环境变量中获取客户端 ID、租户 ID 和订阅 ID。例如:

      // Get ENV var
      clientID := os.Getenv("CLIENTID")
      tenantID := os.Getenv("TENANTID")
      subscriptionID := os.Getenv("SUBSCRIPTIONID")
      azureFederatedTokenFile := "/var/run/secrets/openshift/serviceaccount/token"
    2. 确保具有 CredentialsRequest 对象已准备好修补并应用。

      注意

      目前不支持在 Operator 捆绑包中添加 CredentialsRequest 对象。

    3. 在凭证请求中添加 Azure 凭证信息和 Web 身份令牌路径,并在 Operator 初始化过程中应用它:

      例 5.24. 在 Operator 初始化过程中应用 CredentialsRequest 对象示例

      // apply credentialsRequest on install
      credReqTemplate.Spec.AzureProviderSpec.AzureClientID = clientID
      credReqTemplate.Spec.AzureProviderSpec.AzureTenantID = tenantID
      credReqTemplate.Spec.AzureProviderSpec.AzureRegion = "centralus"
      credReqTemplate.Spec.AzureProviderSpec.AzureSubscriptionID = subscriptionID
      credReqTemplate.CloudTokenPath = azureFederatedTokenFile
      
      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 可以等待 Secret 对象从 CCO 显示,如下例所示,以及您在 Operator 中协调的其他项目:

      例 5.25. 等待 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 对象的速度。您可能会考虑降低时间或为集群管理员创建自定义反馈,这可能会导致 Operator 尚未访问云资源的原因。
    5. CredentialsRequest 对象读取 CCO 创建的 secret,以与 Azure 进行身份验证并接收必要的凭证。
Red Hat logoGithubRedditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

通过我们的产品和服务,以及可以信赖的内容,帮助红帽用户创新并实现他们的目标。

让开源更具包容性

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

關於紅帽

我们提供强化的解决方案,使企业能够更轻松地跨平台和环境(从核心数据中心到网络边缘)工作。

© 2024 Red Hat, Inc.