我先给你介绍一下背景,然后给你看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集成的作者所说的。
在联合之前:
- 创建一个谷歌云服务账户并授予IAM权限
- 导出长期存在的JSON服务账户密钥
- 将JSON服务账户密钥上传到GitHub秘籍中
之后:
- 创建一个谷歌云服务账户并授予IAM权限
- 为GitHub创建并配置一个工作负载身份提供者
- 将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_URL
和ACTIONS_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
复制代码
接下来,我修改了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
,然后是list
,deploy
,以此类推,但这里我们使用的是--token
。OpenFaaS Pro支持各种OAuth2流程,你可以在这里阅读更多。OpenFaaS单点登录
令我惊讶的是,它成功了!这是我的第一反应。
只是还有一点工作要做,我需要写一个访问控制列表(ACL),并使用GitHub Actions的一个字段来只授权我的朋友。我使用了actors
字段,并使该字段中的任何人都能成为OpenFaaS REST API的管理员。
然后我需要一些测试人员的帮助。GitHub的DevRel总监Martin Woodward很乐意帮忙。
他首先得到了 "未授权",然后我把他加入到ACL中,就成功了。
后来,OpenFaaS的核心贡献者Lucas Roesler部署了一个他自己的功能,结果如愿以偿。Lucas对OAuth和OIDC非常了解,这两者都有很多移动的部分,所以当我做一些新的事情时,他是一个好人选。
OIDC规范也是非常可读的,而且有许多用于各种语言的客户端库。你应该尽可能地使用这些,而不是自己编写。
不要复制秘密或任何东西!!!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账户上找到。