通过 Okta 安全访问 AWS

avatar
HR @Tubi

Tubi 的基础设施团队旨在鼓励大家使用最佳实践,不过我们不是通过宣扬和说教,而是通过工具、策略和流程来实现这一目标。我们期望帮助开发同事更容易地作出正确决策,尤其是在安全方面。

决策之一是关于如何管理用于访问 AWS 的密钥。虽然使用静态 IAM 用户凭据是个好的开端,但它也为开发和运维同学埋下了隐患:这些密钥的的传输和存储都非常困难,并且它们本身没有过期时间,必须定期手动更新。

我们认为可以通过使用我们的 SSO 厂商 Okta 提供的认证功能来优化这一过程。这样不仅避免了入职/离职时需要自动化增减 IAM 用户, 同时也实现了具有时效性、基于角色的、可以绑定到 Okta 支持的多重认证(MFA)的用户访问控制。它避免了直接使用 IAM 用户登录,而是使用 AWS STS 生成基于角色的凭据,这样服务和用户都使用相同的方式来访问资源。(同时也有利于将来过渡到基于角色的跨账号关联方式 [1])。

Okta & AWS 集成配置指南

我们构建了一些自定义工具来集成 Okta 和 AWS。这些工具跟我们的内部系统紧密耦合。

下面我们会一步步来说明如何使用简单的 bash 脚本完成这个工作。完成之后就可以避免使用静态 AWS 密钥,转而让用户使用 Okta 来认证(支持 MFA),同时也不再需要通过 IAM 手动管理入职/离职用户。

图片

在本指南的最后,我们应该可以做到:

  • 通过 Okta 登录

  • 使 Okta 通过 SAML 登录到 IAM

  • 访问 AWS 安全令牌服务(STS)生成一系列会话凭据

  • 使用这些凭据访问 AWS 资源

本指南需要读者熟悉 Okta,AWS IAM,并且具备一些 bash 脚本知识。

集成配置

开始之前需要先登录你的 AWS 和 Okta 账号。你可以参考 Okta 的官方文档[2],我们在此总结了以下步骤以供参考。

准备工作

假定你具备 Admin 身份访问 Okta 和 AWS IAM 资源,并且有创建 AWS 资源及权限的相关知识,接下来可以参考以下步骤。

配置 Okta

首先,你需要为 Okta 账号创建一个 Amazon Web Service 应用。(这里的操作之后可能会发生变化,所以如果遇到问题请参考上面的官方文档链接。

  1. 在你的 Okta 管理员页面,访问 Applications → Applications → Add Application → Amazon Web Services → Add。

  2. 这里可以根据需要自行配置,默认的配置也可以正常工作。在 Sign-on 菜单下,选择 SAML 2.0。这可以让 Okta 生成一个 SAML XML 文档发给 AWS,用来将 Okta 会话转换为 AWS 会话。这里也可能有其他可用的登录方案,但是在本文写作的时候 SAML 似乎是 Okta 文档里提到的最好的方案。

  3. 页面上应该还有一个叫做 Identity Provider metadata 的链接,指向一个 XML 文档。现在下载该文档,并保留当前窗口,后面配置 AWS 的时候我们还需要从这个页面获取一些信息。

配置AWS

接下来,我们需要在 AWS 上创建一些资源,以使 AWS 信任 Okta 作为授权认证提供商生成会话 token。我们还需要为 Okta 授权以使其浏览 AWS IAM 环境中的预设角色。

  1. 在 AWS IAM 控制台导航至 Services → IAM → Identity Providers 创建一个Provider 并上传我们之前下载的的元文档命名为 Okta。获取该资源的 ARN 信息以供后面使用。

  2. 在 Services → IAM → Users 选项下,创建一个新用户并赋予这些权限。该用户将被用于为 Okta 获取可分配角色的元数据。我们需要通过编程访问,所以下载 Access key 和 Secret key 供后面使用。同时记录下改用户对象的 ARN 以供后面使用。

 {
   "Statement": \[
     {
       "Action": \[
         "iam:ListRoles",
         "iam:ListAccountAliases"
       \],
       "Effect": "Allow",
       "Resource": "\*"
     }
   \],
   "Version": "2012-10-17"
 }
  1. 在 IAM Roles 页面,使用此信任关系创建 一个新角色。一旦最终用户使用 Okta 用户凭据登录到 AWS 就会被分配该角色。请确保你为该角色分配了一些策略以便后面测试,例如 AWS 提供的 ReadOnly 策略。
{
  "Version": "2012-10-17",
  "Statement": \[
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<your-aws-account-id>:saml-provider/okta"
      },
      "Action": "sts:AssumeRoleWithSAML",
      "Condition": {
        "StringEquals": {
          "SAML:aud": "https://signin.aws.amazon.com/saml"
        }
      }
    }
  \]
}

继续设置 Okta

回到之前的 Okta 应用程序创建页面。现在我们已经创建了 AWS 资源(并记下他们的 ARNs ),可以继续配置了。

  1. 回到应用程序创建窗口,填写以下表单然后点击 Done
  • Identity Provider ARN:我们创建的授权认证提供商的 ARN。

  • Session Duration:默认为1小时,不过设置为一天比较实用。

  1. 访问 Provisioning → Configure API Integration → Enable API integration
  • 将之前创建的 IAM 用户的凭据信息填入 Access Key 和 Secret Key 输入框

  • 在 Provisioning → To App → Edit → Update User Attributes页面,勾选Enable 然后点击Save

  1. 访问 Assignments → Assign → Assign to People → Your account → Assign
  • 在 Role 下拉列表中选择之前所创建的角色,选中 SAML User Roles 选项,保存。

会看到类似 app/amazon_aws/instance/ 这样的 URL,记录下来,我们后面将会用到那个 Application ID。

认证

当 Okta 和 AWS 账号都设置好后,你就可以使用 Okta API 生成 AWS 凭据登录了。当前已经有 一些可用工具 [3],但是目前支持 MFA 认证的还不是很多,因为目前在 Okta Python SDK 中还没支持。必须要做的请求可以参考下面的脚本。

准备工作

需要先为后面的要写的脚本准备一些工具:

  • awscli:用于测试你的凭据并分配角色。如果要写程序实现,那可以替换为 boto 或 AWS SDK for Java 等 AWS SDK。

  • jq:用于命令行 JSON 解析。它自带一种查询语言可以从 JSON 响应中获取特定字段。

  • xq:用于 XML 的 jq(SAML文档中使用的是 XML 格式)

  • curl 、recode 和 sed:linux 自带的一些工具

使用 Okta 获取会话

先为登录做一些设置。

OKTA\_ORG=tubi
OKTA\_USERNAME=james
OKTA\_PASSWORD=correcthorsebatterystaple

首先,向 Okta 请求状态 token

RESPONSE=$(
  curl -X POST \\\\
    "https://${OKTA\_ORG}.okta.com/api/v1/authn" \\\\
    -H "Content-Type: application/json" \\\\
    -d "{\\\\"username\\\\": \\\\"${OKTA\_USERNAME}\\\\", \\\\"password\\\\": \\\\"${OKTA\_PASSWORD}\\\\"}"
)
echo ${RESPONSE} | jq -r .status # MFA\_REQUIRED

Tubi 有个组织级策略会强制启用 MFA[4],所以这里我从响应信息中获取了 MFA_REQUIRED 状态信息。如果你没有开启 MFA(为什么不呢?),你可以跳过下一个用来提交 MFA 认证的 curl 请求。

提交 MFA 认证

_embedded 字段列出的是我的 MFA 设备列表。在这里我使用的例子是我的 TOTP 双重认证。

{...
  "\_embedded":{...
    "factors":\[... 
      {...
        "factorType":"token:software:totp",
        "id":"<factor-id>",
        "provider":"GOOGLE"
      }
    \]
  }
}

为了提交 MFA 认证,我们需要先设置一些字段。首先,我们需要保留请求中的 stateToken 以保持登录会话。其次,我们需要认证类型的 id 字段。最后需要的是 MFA app 生成的验证码,在这个例子中,我从我的 Authenticator 应用中获取验证码。

STATE\_TOKEN=$(echo ${RESPONSE} | jq -r .stateToken)
FACTOR\_ID=$(echo ${RESPONSE} | jq -r '.\_embedded.factors\[\] | select(.provider == "GOOGLE") | .id')
MFA\_CODE=206847
RESPONSE=$(
  curl -X POST \\\\
    "https://${OKTA\_ORG}.okta.com/api/v1/authn/factors/${FACTOR\_ID}/verify" \\\\
    -H "Content-Type: application/json" \\\\
    -d "{\\\\"stateToken\\\\": \\\\"${STATE\_TOKEN}\\\\", \\\\"passCode\\\\": \\\\"${MFA\_CODE}\\\\"}"
)
echo ${RESPONSE} | jq -r .status # SUCCESS

注意: 状态 token 只在响应有效期内有效。如果你的 token 过期,请使用上面的 POST 命令再生成一个。

获取 SAML XML 文档

还记得我们当时决定使用 SAML 来集成 Okta 和 AWS 吗?这里我们需要一个中间资源来转换已有的 Okta 会话为 AWS 会话。我们先转换该会话为一个 session id,它允许我们生成一个可以通过 Okta 传递给 AWS 的 SAML XML 会话文档。

SESSION\_TOKEN=$(echo ${RESPONSE} | jq -r .sessionToken)
RESPONSE=$(
  curl -X POST \\\\
    "https://${OKTA\_ORG}.okta.com/api/v1/sessions" \\\\
    -H "Content-Type: application/json" \\\\
    -d "{\\\\"sessionToken\\\\": \\\\"${SESSION\_TOKEN}\\\\"}"
)
echo ${RESPONSE} | jq -r .id # session id

最终,我们可以使用这个 session ID 重新找回我们的 SAML 文档。在这里你需要 Okta app id,你可以在上面提到的 admin 页面的 url 中获取到。

SESSION\_ID=$(echo ${RESPONSE} | jq -r .id)
OKTA\_APP\_ID=T0tUQV9PUkc9dHViaQpP
RESPONSE=$(
  curl -L \\\\
    "https://${OKTA\_ORG}.okta.com/app/amazon\_aws/${OKTA\_APP\_ID}/login" \\\\
    -H "Cookie: sid=${SESSION\_ID}"
)
SAML\_B64=$(echo ${RESPONSE} | sed 's/^.\*name="SAMLResponse" type="hidden" value="\\\\(\[^"\]\*\\\\).\*$/\\\\1/g' | recode html)
echo ${SAML\_B64} | base64 --decode | xq -r .

响应是 HTML 编码,所以首先我们需要使用 recode 将它解码。然后通过 base-decode,配合 xq 来解析 XML 文档。

你可以在 SAML 文档分配一个角色,当然你需要有一个角色才行。可用角色列表在文档中有。

ROLES=$(echo ${SAML_B64} | base64 --decode | xq -r '.["saml2p:Response"]["saml2:Assertion"]["saml2:AttributeStatement"]["saml2:Attribute"][] | select(.["@Name"] == "https://aws.amazon.com/SAML/Attributes/Role") | .["saml2:AttributeValue"][] | .["#text"]')

使用 SAML 文档获取 AWS 关联角色

现在我们有了 SAML XML 文档,它作为 Okta 和 AWS 认证凭据的中间媒介,最终我们可以用它转换为一个真实的 AWS 会话。

当登录到 AWS 时,可能有一个或者多个角色,这里,我们只使用第一个角色。现在我们有了运行 assume-role 命令的所需的一切。

ROLE=$(echo ${ROLES} | awk '{print $1}')
PRINCIPAL\_ARN=$(echo ${ROLE} | jq -r -R 'split(",")\[0\]')
ROLE\_ARN=$(echo ${ROLE} | jq -r -R 'split(",")\[1\]')
CREDENTIALS=$(
  aws sts assume-role-with-saml \\\\
    --role-arn=${ROLE\_ARN} --principal-arn=${PRINCIPAL\_ARN} \\\\
    --saml-assertion=${SAML\_B64} --duration=3600
)
echo ${CREDENTIALS} | jq -r .

现在我们有了可以成功运行 awscli 的凭据。

export AWS\_ACCESS\_KEY\_ID=$(echo ${CREDENTIALS} | jq -r .Credentials.AccessKeyId)
export AWS\_SECRET\_ACCESS\_KEY=$(echo ${CREDENTIALS} | jq -r .Credentials.SecretAccessKey)
export AWS\_SESSION\_TOKEN=$(echo ${CREDENTIALS} | jq -r .Credentials.SessionToken)
aws sts get-caller-identity | jq -r .UserId # AROAQGUD4GZ4NDOKIDOKI:james

完成

从这里开始,我们可以用 awscli 或者其他 AWS SDK 执行一些其他请求。

因为我们使用的是有时间限制的 STS 凭据,所以需要设置那个 SessionToken 变量。

记住这些凭据将在你 duration 指定的期限内将持续有效,它本身跟由 AWS 定义角色中指定的 max_session_duration 变量绑定。这两个期限也将受限于我们在 Okta 指定的会话生命周期。

下一步?

你可以使用上面的列出的脚本来完成这个工作,但是在 Tubi 我们更倾向于围绕我们的工作流程构建额外的自动化工具。生成的凭据会被安装到 ~/.aws/credentials,当开发同学使用 awscli 时会用到他们。他们也可以通过环境变量的方式用于 Kubernetes pod 的本地调试。最后,我们也会通过 AWS 认证方法 [5]使用这些凭据认证我们的 Hashicorp Vault [6]部署,因此我们也可以使用 AWS 会话生成动态的、有时间期限的数据库凭据。AWS [7]和 Vault[8] 都提供了非常细粒度的访问控制策略,所以我们不仅可以非常明确的指定哪些资源可以被访问,还能将其与 Okta 和 AWS 会话关联起来。

以下是 Tubi 一个内部工具的架构图,使用我们多账号、基于角色的 Okta 集成配置以访问资源。尽管并非所有工具都需要如此高的复杂性,但是使用代码构建最佳实践的灵活性将保证满足组织在未来几年内在规模和复杂性上进行扩展。

图片

如果你也喜欢安全相关的工作挑战和构建好用的工具来提高开发伙伴的效率,Tubi 正在招聘!

图片

原文:James Wu,  Lead Production Infra Engineer

译者:Youqing Han, Senior SRE

点击 “阅读原文” 查看英文版

图片