使用GitHub Actions和OIDC进行无证书部署

205 阅读15分钟

我先给你介绍一下背景,然后给你看AWS和GCP的故事,接着是我如何将其与OpenFaaS整合,以便GitHub上的一组用户可以部署到我的OpenFaaS集群上,而无需给他们任何凭证。

联合的意义何在?

我记得在2018年KubeCon上看到一个主题演讲,Kelsey Hightower演示了两个服务之间的整合,其中一个运行在AWS上,另一个运行在GCP上。通常情况下,这意味着产生一些秘密,并在任何一方配置它们。他的演示是不同的,因为双方之间存在着一定程度的信任,如果你愿意的话,就是联邦化。

牛津字典将 "联邦 "描述为

将国家或组织组成一个具有集中控制权的单一集团的行为。

在单一集团内的集中控制听起来很有用,就像Kelsey向我们展示了GCP和AWS的和谐工作,稍后我将向你展示GitHub Actions部署到OpenFaaS而没有任何共享凭证。

这段视频绝对值得一看,即使你是在现场。他解释了无服务器平台如何与Kubernetes很好地发挥,与管理云服务整合。

Kelsey可能是在使用GCP的工作负载身份联盟技术。

AWS的IAM(身份访问管理)、IdP(身份提供者)和安全令牌服务(STS)也有类似技术。

为什么凭证共享会成为一种反模式?

对于一些系统来说,凭证是与人的身份相联系的。我们都可能在我们的时间里创建了一次或两次GitHub个人访问令牌PAT)。这些令牌相当于我们采取的行动,而且大多数时候都有非常粗略的权限,即 "读/写所有仓库"。

如果我在一个集成中使用PAT,然后我离开了公司,我的访问令牌将仍然在使用,我的身份将与此挂钩。另一方面,如果我离开并停用了我的账户,集成将停止工作。

即使API令牌和服务账户将身份与访问令牌脱钩,仍然需要分享、存储和旋转这些秘密,这带来了风险。我们做得越少,出错的风险就越低。

它是什么样子的?

下面是GCP集成的作者所说的。

在联合之前:

  1. 创建一个谷歌云服务账户并授予IAM权限
  2. 导出长期存在的JSON服务账户密钥
  3. 将JSON服务账户密钥上传到GitHub秘籍中

之后:

  1. 创建一个谷歌云服务账户并授予IAM权限
  2. 为GitHub创建并配置一个工作负载身份提供者
  3. 将GitHub行动的OIDC令牌换成短期的谷歌云访问令牌

简而言之,以这种方式配置后,GitHub Actions 所提供的令牌和身份足以部署到 GCP 或 AWS。这意味着使用SDK、CLI、Terraform和其他类似工具。它可能还可以与Kubernetes认证和授权一起使用。

GCP的例子

由于GitHub还没有记录下API,所以GCP的例子是我最了解其工作原理的地方。

这个GitHub动作使用Workload Identity Federation将GitHub Actions的OIDC令牌交换成Google Cloud的访问令牌。这样就不需要导出一个长期存在的谷歌云服务账户密钥,并在特定的GitHub Actions工作流调用和谷歌云的权限之间建立信任委托关系。

通过阅读源代码。

该Action提供了两个变量:ACTIONS_ID_TOKEN_REQUEST_URLACTIONS_ID_TOKEN_REQUEST_TOKEN

通过将ACTIONS_ID_TOKEN_REQUEST_TOKEN 发布到ACTIONS_ID_TOKEN_REQUEST_URL ,你会收到一个 JSON 响应,其中包含一个未记录的count 变量和一个value 属性

接下来,产生的JWT令牌被用于谷歌的STS服务,为谷歌云创建一个短命的令牌。

然后从我读到的内容来看,该令牌可以用于gcloud CLI等。

在谷歌方面,需要事先对服务账户进行一些配置。

请看Seth Vargo的例子。@google-github-actions/auth

AWS的例子

Aidan Steele似乎已经完成了这里的重任。

Aidan写道:

GitHub Actions有新的功能,可以向平台上运行的作业提供OpenID Connect凭证。这对AWS账户管理员来说是非常令人兴奋的,因为这意味着CI/CD作业不再需要在GitHub中存储任何长期秘密。但说得够多了,下面是它的工作原理。

他还提到:

在写这篇文章的时候,这个功能是存在的,但还没有公布或记录。不过,它还是能用的!

这一点很重要,因为就在我正在演示与OpenFaaS的整合时,该团队改变了令牌发放地点的URL。在宣布之前,如果它能进入大会,直接将其投入生产可能不是一个好主意。我希望它能做到。

Aidan分享了一个他从OIDC端点收到的JWT的例子,如果你想探究一下。

如果你是AWS的客户,可以在这里试试Aidan的例子。AWS联盟来到了GitHub行动中

写完这篇文章后,我联系了Aiden,他告诉我这是他在2020年初要求的一个功能,你可以在这里看到GitHub支持请求

让它与OpenFaaS一起工作

现在我们看到的两个例子都让你有能力访问谷歌云或AWS的API--这非常强大,但也非常广泛。

我想知道我是否可以遵循同样的原则,为OpenFaaS Pro制作一个认证插件,以同样的方式行事。

首先,我用Go写了一个小小的HTTP服务器,并把它部署在我的机器上,打印出webhooks:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "OK")
	fmt.Fprintf(os.Stdout, "Method: %s Path: %s \nHeaders:\n", r.Method, r.URL.Path)

	for k, v := range r.Header {
		fmt.Fprintf(os.Stdout, "%s=%s", k, v)
	}

	if r.Body != nil && r.ContentLength > 0 {
		defer r.Body.Close()
		fmt.Fprintf(os.Stdout, "\nBody (%d bytes):\n", r.ContentLength)

		io.Copy(os.Stdin, r.Body)
	}
	fmt.Fprintf(os.Stdout, "\n")

}

func main() {
	http.HandleFunc("/", handler)
	log.Println("Starting server on 8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

想学习写成语Go的模式和实践,包括微服务、CLI和通过GitHub Actions构建Go?请看我的新书。日常围棋

然后我运行了一个inlets隧道,它为我提供了一个安全、私密和自我托管的方式来接收webhooks。我不希望这个令牌通过Ngrok或Cloudflare隧道共享服务器。

inlets-pro http client --token=$TOKEN \
  --url=wss://minty-tunnel.exit.o6s.io \
  --upstream http://127.0.0.1:8080 \
  --license-file $HOME/.inlets/LICENSE \
  --auto-tls=false

获得第一个OIDC令牌

接下来,我写了一个Action来转出环境变量。我在想,这个令牌是否已经被铸造出来并可用:

name: federate

on:
  push:
    branches:
    - '*'

jobs:
  auth:

    # Add "id-token" with the intended permissions.
    permissions:
      contents: 'read'
      id-token: 'write'

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
        with:
          fetch-depth: 1
      - name: Dump env
        run: env

运行后,我注意到它不是,但有两件事引起了我的注意:

  • ACTIONS_ID_TOKEN_REQUEST_URL
  • ACTIONS_ID_TOKEN_REQUEST_TOKEN

所以我决定将该令牌发布到指定的URL上。然后通过一些搜索,我看到谷歌的Aaction也是这样做的。

点击". "的另一个好处是,你可以从VSCode中获得搜索,这比使用GitHub的UI更容易使用,也更快。

这在你的本地机器上运行,所以不需要支付或等待代码空间的启动。pic.twitter.com/Lbq8UnSs9E

- Alex Ellis (@alexellisuk)October 6, 2021

我没有在用户界面上搜索,而是点击了. ,这是一个新的GitHub用户界面快捷方式。它在一个只有客户端的体验中打开VSCode,具有更好的搜索能力。

于是我就运行了这个动作:

name: federate

on:
  push:
    branches:
    - '*'

jobs:
  auth:

    # Add "id-token" with the intended permissions.
    permissions:
      contents: 'read'
      id-token: 'write'

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
        with:
          fetch-depth: 1
      - name: Dump env
        run: env
      - name: Post the token
        run: |
          OIDC_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=o6s" -H "User-Agent: actions/oidc-client" -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN")

          curl -i -s --data-binary "$OIDC_TOKEN" \
            https://minty.exit.o6s.io/github-oidc

检查字段

令我惊讶的是,它成功了。

我看到一个像这样的响应:

{
  "count: 3118,
  "value": "VALID_JWT_TOKEN",
}

接下来,我提取了.value属性并扔掉了计数。我还是不知道那个整数是什么意思,比如说我还没有运行过那么多的token请求。

将令牌粘贴到JWT.io中,我得到的输出结果和Aidan的很像:

{
  "actor": "aidansteele",
  "aud": "https://github.com/aidansteele/aws-federation-github-actions",
  "base_ref": "",
  "event_name": "push",
  "exp": 1631672856,
  "head_ref": "",
  "iat": 1631672556,
  "iss": "https://token.actions.githubusercontent.com",
  "job_workflow_ref": "aidansteele/aws-federation-github-actions/.github/workflows/test.yml@refs/heads/main",
  "jti": "8ea8373e-0f9d-489d-a480-ac37deexample",
  "nbf": 1631671956,
  "ref": "refs/heads/main",
  "ref_type": "branch",
  "repository": "aidansteele/aws-federation-github-actions",
  "repository_owner": "aidansteele",
  "run_attempt": "1",
  "run_id": "1235992580",
  "run_number": "5",
  "sha": "bf96275471e83ff04ce5c8eb515c04a75d43f854",
  "sub": "repo:aidansteele/aws-federation-github-actions:ref:refs/heads/main",
  "workflow": "CI"
}

我不认为这些令牌一旦过期就会被滥用,但我决定不逐字逐句地与你分享我的一个。

我发现这些字段很有意思:

  • 行为者--谁触发了这个行动?我们希望他们访问OpenFaaS吗?
  • iss - 谁发出的这个令牌?我们可以使用这个URL来获取他们的公钥,然后验证JWT
  • repository_owner - 谁拥有这个 repo?它是我们公司组织的一部分吗?

代币交换

现在我开始研究谷歌是如何进行令牌交换的,并发现了一个不太为人所知的OAuth2授予类型--令牌交换

事实证明,各种IdPs,如Okta、Keycloak和Auth0认为它是实验性的,可能需要额外的配置来启用它。

你可以在其互联网工程任务组(IETF)的文件草案中阅读关于OAuth 2.0令牌交换的内容:rfc8693

grant_type 字段应该被填充为urn:ietf:params:oauth:grant-type:token-exchange ,你应该注意到许多其他以urn:ietf:params:oauth 为前缀的字段。它们在规范中并非都是必需的,但在我调查的一些IdP中,许多可选字段被列为必需。

你不喜欢标准化吗?

我所做的是

任何IdP都可以用于OpenFaaS,甚至那些不支持Token Exchange的IdP,所以我决定尝试直接从GitHub验证OIDC令牌。

它的工作方式是这样的:

  • iss 字段映射到https://token.actions.githubusercontent.com
  • 通过将OIDC配置端点添加到路径/.well-known/openid-configuration ,我们会得到一个包含各种URL的JSON包,然后我们可以用它来下载服务器的公钥
  • 该公钥可用于验证JWT令牌
  • 如果令牌是好的,并且没有过期,那么我们就知道它来自GitHub的行动

下面是OIDC规范中规定的OIDC发现URL网址的结果

curl -s https://token.actions.githubusercontent.com/.well-known/openid-configuration
{
  "issuer": "https://token.actions.githubusercontent.com",
  "jwks_uri": "https://token.actions.githubusercontent.com/.well-known/jwks",
  "subject_types_supported": [
    "public",
    "pairwise"
  ],
  "response_types_supported": [
    "id_token"
  ],
  "claims_supported": [
    "sub",
    "aud",
    "exp",
    "iat",
    "iss",
    "jti",
    "nbf",
    "ref",
    "repository",
    "repository_owner",
    "run_id",
    "run_number",
    "run_attempt",
    "actor",
    "workflow",
    "head_ref",
    "base_ref",
    "event_name",
    "ref_type",
    "environment",
    "job_workflow_ref"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "scopes_supported": [
    "openid"
  ]
}

我们关心的字段是jwks ,我们可以按照这个字段来下载用于签署JWT令牌的公钥:

{
  "keys": [
    {
      "n": "zW2j18tSka65aoPgmyk7aUkYE7MmO8z9tM_HoKVJ-w_alYIknkf7pgBeWWfqRgkRfmDuJa8hATL20-bD9cQZ8uVAG1reQfIMxqxwt3DA6q37Co41NdgZ0MUTTQpfC0JyDbDwM_ZIzis1cQ1teJcrPBTQJ3TjvyBHeqDmEs2ZCmGLuHZloep8Y_4hmMBfMOFkz_7mWH7NPuhOLWnPTIKxnMuHl4EVdNL6CvIYEnzF24m_pf3IEM84vszL2s6-X7AbFheZVig8WqhEwiVjbUVxXcY4PtbK0z3jhgxcpjc6WTH0JlRedpq2ABowWZg-pxOoWZUAETfj6qBlbIn_F9kpyQ",
      "kty": "RSA",
      "kid": "DA6DD449E0E809599CECDFB3BDB6A2D7D0C2503A",
      "alg": "RS256",
      "e": "AQAB",
      "use": "sig",
      "x5c": [ "MIIDrDCCApSgAwIBAgIQBJyUm+htTmG6lfzlIyswTjANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEyt2c3RzLXZzdHNnaHJ0LWdoLXZzby1vYXV0aC52aXN1YWxzdHVkaW8uY29tMB4XDTIxMDkwODE4MTEyN1oXDTIzMDkwODE4MjEyN1owNjE0MDIGA1UEAxMrdnN0cy12c3RzZ2hydC1naC12c28tb2F1dGgudmlzdWFsc3R1ZGlvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM1to9fLUpGuuWqD4JspO2lJGBOzJjvM/bTPx6ClSfsP2pWCJJ5H+6YAXlln6kYJEX5g7iWvIQEy9tPmw/XEGfLlQBta3kHyDMascLdwwOqt+wqONTXYGdDFE00KXwtCcg2w8DP2SM4rNXENbXiXKzwU0Cd0478gR3qg5hLNmQphi7h2ZaHqfGP+IZjAXzDhZM/+5lh+zT7oTi1pz0yCsZzLh5eBFXTS+gryGBJ8xduJv6X9yBDPOL7My9rOvl+wGxYXmVYoPFqoRMIlY21FcV3GOD7WytM944YMXKY3Olkx9CZUXnaatgAaMFmYPqcTqFmVABE34+qgZWyJ/xfZKckCAwEAAaOBtTCBsjAOBgNVHQ8BAf8EBAMCBaAwCQYDVR0TBAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwNgYDVR0RBC8wLYIrdnN0cy12c3RzZ2hydC1naC12c28tb2F1dGgudmlzdWFsc3R1ZGlvLmNvbTAfBgNVHSMEGDAWgBSWfvMfh/F4A7d7WzhYMmM2jZhYNDAdBgNVHQ4EFgQUln7zH4fxeAO3e1s4WDJjNo2YWDQwDQYJKoZIhvcNAQELBQADggEBAJxwcMczvuXRVZUAF+jYaWKLdaa7HeeU3vOVrgeuPehLh9BquEu+asKVswMdEDvLMsrVrMRhJjXYaOW+B1UnlKHiKZzIx030e3GypAci/KNBXSvFB3KCZ4yk1Yvs3+hWV+5DWGRjDf5x3pp+zNWcHG12I+1F1KdC4vvPZ0G624imeucDzZurRD66SrLE/PqlMaFos8YqRr3QaY7hGhEtnwuu5P2POD6iRGIU60EpIkmFauuTv7eXRKN1u/RaQf6Qc4LGNysHT46gqEp9AGts/0AeAYFpEnvAdBXcHPrXhzPD72eAEdVzIFcwtzbB++sf2lBEqQxYPIfjFmiwB24T+bM="
      ],
      "x5t": "2m3USeDoCVmc7N-zvbai19DCUDo"
    },
  }
}

现在,这个URL上的kid ,也在GitHub Actions的OIDC令牌中发送过来,不过是在Aidan在他的博客上分享的头而不是正文中。

在将令牌中的kid 与JWKS的有效载荷相匹配后,你可以找到正确的公钥,并验证发送给你的OIDC令牌。这就是OpenFaaS插件的作用。

但在这一点上,我们所做的只是允许任何使用GitHub Action的人部署到我们的集群。所以我们还没有完全完成,因为我想把它限制在只有我的朋友和同事。

我更新了inlets隧道,使其指向我在KinD上运行的OpenFaaS实例:

kubectl port-forward -n openfaas deploy/gateway 8080:8080

inlets client .. \
  --upstream http://127.0.0.1:8080

我也可以用舵手图隧道部署到KinD,但不需要将其永久化。

接下来,我修改了GitHub Action来安装OpenFaaS CLI,并使用我在环境变量中输入的token来运行faas-cli list:

name: federate

on:
  workflow_dispatch:
  push:
    branches:
    - '*'

jobs:
  auth:

    # Add "id-token" with the intended permissions.
    permissions:
      contents: 'read'
      id-token: 'write'

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
        with:
          fetch-depth: 1
      - name: Install faas-cli
        run: curl -sLS https://cli.openfaas.com | sudo sh
        
      - name: Get token and use the CLI
        run: |
          OIDC_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=o6s" -H "User-Agent: actions/oidc-client" -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN")
          JWT=$(echo $OIDC_TOKEN | jq -j '.value')
          export OPENFAAS_URL=https://minty.exit.o6s.io/

          faas-cli list --token "$JWT"

通常我们会运行faas-cli login ,然后是listdeploy ,以此类推,但这里我们使用的是--token 。OpenFaaS Pro支持各种OAuth2流程,你可以在这里阅读更多。OpenFaaS单点登录

令我惊讶的是,它成功了!这是我的第一反应。

只是还有一点工作要做,我需要写一个访问控制列表(ACL),并使用GitHub Actions的一个字段来只授权我的朋友。我使用了actors 字段,并使该字段中的任何人都能成为OpenFaaS REST API的管理员。

Deploy without credentials with GitHub Actions and OIDC

然后我需要一些测试人员的帮助。GitHub的DevRel总监Martin Woodward很乐意帮忙。

Deploy without credentials with GitHub Actions and OIDC

他首先得到了 "未授权",然后我把他加入到ACL中,就成功了。

后来,OpenFaaS的核心贡献者Lucas Roesler部署了一个他自己的功能,结果如愿以偿。Lucas对OAuth和OIDC非常了解,这两者都有很多移动的部分,所以当我做一些新的事情时,他是一个好人选。

OIDC规范也是非常可读的,而且有许多用于各种语言的客户端库。你应该尽可能地使用这些,而不是自己编写。

https://t.co/6csQKTbtd1

不要复制秘密或任何东西!!!t.co/3Ljji9QZ4t pic.twitter.com/RRUBaljKSM

- Lucas Roesler 🇺🇸🇩🇪 (@TheAxeR)10月6日, 2021

为什么我这么喜欢这个?

在过去,我建立了一个复杂的PaaS,叫做OpenFaaS Cloud,它与GitHub和GitLab紧密结合,但那是更早的时代,比GitHub Actions还要早。OpenFaaS Cloud让你在几秒钟内从提交代码到链接仓库,到拥有一个实时终端。

然而,这需要相当多的代码来维护,并且需要许多额外的组件。毕竟,他们确实说过,你可以有一个安全的系统,也可以有一个简单的系统。有一些部分我认为我们做得很好,比如多用户用户界面、秘密支持和用户体验。但用户希望定制一切,或与他们现有的CI系统一起构建。

如果你想了解更多关于我们当时的构建,高潮可能是2019年的这个会议演讲:KubeCon:OpenFaaS云+Linkerd。一个安全的、多租户的无服务器平台 - Charles Pretzer & Alex Ellis

通过OpenFaaS中新的OIDC配置、GitHub行动和多命名空间支持,你可以非常接近于一个多租户、高度集成和可移植的无服务器平台。

但这还不是全部。

更进一步

授权规则还可以进一步加强,这样Lucas就只能部署到一组映射的命名空间,我们就不会以这种方式互相碰撞了。也许我们会建立一个中央共享的OpenFaaS服务器,我们可以在那里部署任何我们需要的东西。

到目前为止,这都是在Kubernetes上完成的,但是faasd已经成为运行OpenFaaS的一种有希望的替代方式。它使用containerd,但没有集群支持,这意味着它可以很好地运行在一个5美元的虚拟机上,一个工作管理程序,甚至在一个边缘计算设备上,如Raspberry Pi。所有的OpenFaaS专业组件都可以在faasd上运行,因为它们不是专门针对Kubernetes的,但我没有一个客户要求这样做。

OpenFaaS的功能是容器镜像,GitHub Actions对用Docker构建它们有非常令人印象深刻的支持,包括缓存、buildx的多架构支持。第一次把这些东西放在一起可能很麻烦,所以在我的电子书《Serverless For Everyone Else》中,我给出了一个参考例子。

我希望这个插件的使用方法是:获取一个提交事件,构建一组多架构镜像,将其推送到GitHub容器注册中心,然后使用OIDC联盟触发部署。如果集群是公开的,只需要分享OpenFaaS网关的URL,这是非保密的数据。如果该功能是去边缘设备或私有集群,那么就需要一个inlets隧道来访问网关。

当与Hashicorp Vault结合使用时,GitLab还在CI工作中提供OIDC令牌。只要在新的openfaas插件中配置适当的发行者,那么任何有效的发行者都会以与GitHub Actions相同的方式工作。我非常喜欢的一件事是写一次代码,然后再次重复使用它。

我想把OpenFaaS的GitHub Actions联盟提供给社区试用并提供反馈。请在OpenFaaS Slack上与我联系,试用它:

如果你等不及了,OpenFaaS Pro已经支持与任何OIDC兼容的IdP如Azure LDAP、Auth0、Okta和Keycloak的SSO。客户还可以访问扩展到零以获得更好的效率、Kafka事件集成、带有指数退避的重试以及影响未来路线图的机会。

我的Go应用和我分享的GitHub Actions工作流程都可以在我的GitHub账户上找到。