通过将 GitHub 颁发的 OIDC 令牌交换为短期 OpenAI 访问令牌,将 GitHub Actions 用作工作负载身份提供商。这使得工作流无需在 GitHub 密钥中存储长期有效的 API 密钥即可向 OpenAI API 进行身份验证。
对于拥有 id-token: write 权限并请求身份令牌的工作流作业,GitHub 可以生成已签名的 OIDC JWT。OpenAI 在颁发 OpenAI 访问令牌之前,会验证令牌颁发者、受众、签名和映射属性。
授予工作流或作业请求 GitHub OIDC 令牌的权限:
1
2
3
permissions:
id-token: write
contents: read
The id-token: write 权限允许作业请求 OIDC JWT。它不授予对存储库内容的写入权限。 contents: read 权限是以下操作所需的 actions/checkout.
使用在 OpenAI 工作负载身份提供商中配置的完全一致的受众来请求令牌。自定义 JavaScript 操作可以调用 core.getIDToken("your-wif-audience");Shell 步骤可以直接调用 GitHub 的 OIDC 请求 URL。包含保留 URL 字符(例如 https://api.openai.com/v1)的受众值在附加到请求 URL 之前应进行 URL 编码:
1
2
3
4
5
AUDIENCE="https://api.openai.com/v1"
ENCODED_AUDIENCE=$(jq -rn --arg audience "$AUDIENCE" '$audience | @uri')
TOKEN=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${ENCODED_AUDIENCE}" | jq -r .value)
重要的 GitHub OIDC 声明包括:
iss:令牌颁发者。对于 GitHub Actions,它是 https://token.actions.githubusercontent.com.
aud:工作流请求的受众值。请将 OpenAI 配置为要求您所请求的精确值,例如 your-wif-audience or https://api.openai.com/v1.
sub:主主体字符串。GitHub 根据工作流元数据(例如存储库、分支、标签、拉取请求或环境)构建此字符串。
repository:运行工作流的存储库,例如 my-org/my-repo.
repository_owner:拥有存储库的组织或用户,例如 my-org.
ref:触发工作流的 Git 引用,例如 refs/heads/main or refs/tags/v1.0.0.
workflow:工作流声明。请使用 GitHub 发出的实际声明值,例如 deploy 如果那就是你工作中的工作流诉求。
workflow_ref:工作流文件路径和引用,例如 my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main.
environment:GitHub 环境名称,例如 production,当作业使用环境时。
run_id, run_number, run_attempt,且 job_workflow_ref:运行和作业标识符,可用于审计或更高级的信任规则。
有关完整的声明列表和主体格式,请参阅 GitHub 的 OpenID Connect 参考.
在配置工作负载身份联合之前,请解码工作流运行器中的示例 GitHub OIDC 令牌并检查其声明。在工作流步骤中请求令牌后:
1
2
3
4
5
6
7
8
9
TOKEN="$TOKEN" python3 - <<'PY'
import base64
import json
import os
payload = os.environ["TOKEN"].split(".")[1]
payload += "=" * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
此命令解码 JWT 负载而不验证令牌签名。对于生产环境令牌,请使用本地解码器,并避免将生产环境令牌粘贴到第三方工具中。切勿记录原始的 GitHub OIDC 令牌或交换后得到的 OpenAI 访问令牌。
解码后的 GitHub Actions OIDC token 示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"iss": "https://token.actions.githubusercontent.com",
"aud": "https://api.openai.com/v1",
"sub": "repo:my-org/my-repo:environment:production",
"repository": "my-org/my-repo",
"repository_owner": "my-org",
"ref": "refs/heads/main",
"workflow": "deploy",
"workflow_ref": "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main",
"environment": "production",
"run_id": "1234567890",
"run_attempt": "1"
}
声明中显现。 iss, aud, repository, ref,且 workflow_ref 设置工作负载身份联合
在 OpenAI 中为 GitHub Actions 创建工作负载身份提供者,然后添加与您信任的 GitHub 工作流声明相匹配的服务账号映射。
设置工作负载身份提供商
-
为一个唯一的值,例如 设置 名称 设置颁发者和受众。 github-actions-prod值。使用 描述,例如 Production GitHub Actions workflows,以帮助管理员识别该提供商。
-
OIDC 颁发者 URL 设置 设置为启用出站身份联合时返回的 AWS 特定于账户的颁发者 URL。此值必须与令牌的 to https://token.actions.githubusercontent.com。将 为传递给 设置为您的工作流请求的确切受众,例如 your-wif-audience or https://api.openai.com/v1.
-
使用 GitHub OIDC 发现。 保持禁用 使用上传的 JWKS 进行令牌验证 已禁用。OpenAI 使用 GitHub 的 OIDC 发现元数据和 JWKS 来验证 GitHub 签名的令牌。
-
仅在需要派生映射属性时才添加属性转换。 原始 GitHub 声明,例如 repository, ref,且 workflow 可以直接在映射断言中使用。如果您创建了派生属性,控制台会自动应用 openai. 前缀;例如,输入 github_repository with expression assertion.repository to create openai.github_repository。对于已经以以下内容开头的原始令牌声明 openai. 映射键,已以 openai. 开头的原始令牌声明将被忽略,除非配置了匹配的转换。
-
创建服务账户映射。 设置 名称 为 Workload Identity Provider 中的唯一值,例如 github-actions-main-deploy值。使用 描述,例如 Production deploy workflow on main,用于说明哪个工作流可以使用该映射。
-
添加精确的声明断言。 为每个必须匹配的 GitHub 声明添加一行 键 and 值 。OpenAI 要求每个已配置的行都匹配后,才会颁发访问令牌。对于生产部署工作流,请使用类似以下的断言:
iss == "https://token.actions.githubusercontent.com"
aud == "https://api.openai.com/v1"
repository == "my-org/my-repo"
ref == "refs/heads/main"
workflow_ref == "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main"
在适合工作流的地方,优先选择 workflow_ref 优于 workflow 用于特权映射,因为管理员通常打算信任特定的工作流文件路径和引用。工作流名称可以被重命名,并且多个工作流文件可以共享相同的名称。
在映射界面中,将它们作为键/值行输入,例如 键 repository with 值 my-org/my-repo, 键 ref with 值 refs/heads/main,且 键 workflow_ref with 值 my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main。如果作业使用了 GitHub 环境,还需添加 键 environment with 值 production.
Caution: 避免使用过于宽泛的映射,例如仅信任 repository_owner == "my-org",除非该所有者命名空间下的每个存储库都应该能够生成 OpenAI 访问令牌。
-
选择 OpenAI 目标。 设置 项目 指向拥有目标服务账户的 OpenAI 项目。将 服务账户 到 GitHub 工作流可以使用的 OpenAI 服务账号,例如 github-actions-prod-deploy.
-
根据需要缩小 API 权限范围。 选择适当的 权限 例如 api.model.request and api.vector_store.read 以进一步限制从此映射生成的访问令牌。将权限留空可避免添加特定于 WIF 的范围限制;该令牌仍会以映射的服务账户身份进行授权。
配置您的 OpenAI SDK 客户端以请求 GitHub OIDC 令牌,并将其交换为 OpenAI 颁发的访问令牌。
The workflow must grant id-token: write 权限,并将工作负载身份联合设置传递给 SDK 代码。SDK 将从以下位置请求 GitHub OIDC 令牌: ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN GitHub 向作业公开的环境变量,然后使用交换得到的 OpenAI 访问令牌对 API 请求进行身份验证。
例如,像这样从工作流中运行你的应用程序代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
name: deploy
on:
push:
branches:
- main
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Run OpenAI SDK code
env:
OPENAI_WIF_AUDIENCE: ${{ vars.OPENAI_WIF_AUDIENCE }}
OPENAI_IDENTITY_PROVIDER_ID: ${{ vars.OPENAI_IDENTITY_PROVIDER_ID }}
OPENAI_SERVICE_ACCOUNT_ID: ${{ vars.OPENAI_SERVICE_ACCOUNT_ID }}
run: node ./scripts/call-openai.js
将 OPENAI_WIF_AUDIENCE, OPENAI_IDENTITY_PROVIDER_ID,且 OPENAI_SERVICE_ACCOUNT_ID 存储为 GitHub Actions 变量。它们用于标识提供商和服务账户,但并非持有者凭据。
以下示例使用自定义主题令牌提供程序初始化 OpenAI 客户端。该提供程序会为配置的受众请求 GitHub OIDC 令牌,并将其用作工作负载身份联合的主题令牌。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import OpenAI from "openai";
import type { SubjectTokenProvider } from "openai/auth";
const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;
const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;
const audience = process.env.OPENAI_WIF_AUDIENCE;
const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (
!identityProviderId ||
!serviceAccountId ||
!audience ||
!requestURL ||
!requestToken
) {
throw new Error(
"Set OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID, OPENAI_WIF_AUDIENCE, and run inside GitHub Actions with id-token: write"
);
}
function githubActionsOIDCTokenProvider(
requestURL: string,
requestToken: string,
audience: string
): SubjectTokenProvider {
return {
tokenType: "jwt",
getToken: async () => {
const url = new URL(requestURL);
url.searchParams.set("audience", audience);
const response = await fetch(url, {
headers: { Authorization: `bearer ${requestToken}` },
});
if (!response.ok) {
throw new Error(
`Failed to request GitHub OIDC token: ${response.status} ${response.statusText}`
);
}
const body = (await response.json()) as { value?: string };
if (!body.value) {
throw new Error("GitHub OIDC token response did not include a value.");
}
return body.value;
},
};
}
const client = new OpenAI({
workloadIdentity: {
identityProviderId,
serviceAccountId,
provider: githubActionsOIDCTokenProvider(requestURL, requestToken, audience),
},
});
const response = await client.responses.create({
model: "gpt-4.1-mini",
input: "Say hello from GitHub Actions workload identity federation.",
});
console.log(response.output_text);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import json
import os
import urllib.parse
import urllib.request
from openai import OpenAI
from openai.auth import SubjectTokenProvider
def github_actions_oidc_token_provider(audience: str) -> SubjectTokenProvider:
request_url = os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"]
request_token = os.environ["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
def get_token() -> str:
parsed_url = urllib.parse.urlparse(request_url)
query = dict(urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True))
query["audience"] = audience
url = urllib.parse.urlunparse(
parsed_url._replace(query=urllib.parse.urlencode(query))
)
request = urllib.request.Request(
url,
headers={"Authorization": f"bearer {request_token}"},
)
with urllib.request.urlopen(request) as response:
payload = json.loads(response.read().decode("utf-8"))
token = payload.get("value")
if not token:
raise RuntimeError("GitHub OIDC token response did not include a value.")
return token
return {"token_type": "jwt", "get_token": get_token}
client = OpenAI(
workload_identity={
"identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
"service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
"provider": github_actions_oidc_token_provider(
os.environ["OPENAI_WIF_AUDIENCE"]
),
},
)
response = client.responses.create(
model="gpt-4.1-mini",
input="Say hello from GitHub Actions workload identity federation.",
)
print(response.output_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/auth"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
)
type githubActionsOIDCTokenProvider struct {
requestURL string
requestToken string
audience string
}
func (p githubActionsOIDCTokenProvider) TokenType() auth.SubjectTokenType {
return auth.SubjectTokenTypeJWT
}
func (p githubActionsOIDCTokenProvider) GetToken(ctx context.Context, httpClient auth.HTTPDoer) (string, error) {
if httpClient == nil {
httpClient = http.DefaultClient
}
oidcURL, err := url.Parse(p.requestURL)
if err != nil {
return "", &auth.SubjectTokenProviderError{
Provider: "github-actions",
Message: "failed to parse GitHub OIDC request URL",
Cause: err,
}
}
query := oidcURL.Query()
query.Set("audience", p.audience)
oidcURL.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, oidcURL.String(), nil)
if err != nil {
return "", &auth.SubjectTokenProviderError{
Provider: "github-actions",
Message: "failed to create GitHub OIDC token request",
Cause: err,
}
}
req.Header.Set("Authorization", "bearer "+p.requestToken)
resp, err := httpClient.Do(req)
if err != nil {
return "", &auth.SubjectTokenProviderError{
Provider: "github-actions",
Message: "failed to request GitHub OIDC token",
Cause: err,
}
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", &auth.SubjectTokenProviderError{
Provider: "github-actions",
Message: fmt.Sprintf("GitHub OIDC token request failed with status %s", resp.Status),
}
}
var body struct {
Value string `json:"value"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return "", &auth.SubjectTokenProviderError{
Provider: "github-actions",
Message: "failed to decode GitHub OIDC token response",
Cause: err,
}
}
if body.Value == "" {
return "", &auth.SubjectTokenProviderError{
Provider: "github-actions",
Message: "GitHub OIDC token response did not include a value",
}
}
return body.Value, nil
}
func main() {
client := openai.NewClient(
option.WithWorkloadIdentity(auth.WorkloadIdentity{
IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
Provider: githubActionsOIDCTokenProvider{
requestURL: os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL"),
requestToken: os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),
audience: os.Getenv("OPENAI_WIF_AUDIENCE"),
},
}),
)
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: openai.ChatModelGPT4_1Mini,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("Say hello from GitHub Actions workload identity federation."),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.OutputText())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.auth.SubjectTokenProvider;
import com.openai.errors.SubjectTokenProviderException;
import com.openai.auth.SubjectTokenType;
import com.openai.auth.WorkloadIdentity;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.ChatModel;
import com.openai.models.responses.ResponseCreateParams;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
public final class GitHubActionsWorkloadIdentityExample {
private GitHubActionsWorkloadIdentityExample() {}
static final class GitHubActionsOidcTokenProvider implements SubjectTokenProvider {
private final String requestUrl;
private final String requestToken;
private final String audience;
GitHubActionsOidcTokenProvider(String requestUrl, String requestToken, String audience) {
this.requestUrl = requestUrl;
this.requestToken = requestToken;
this.audience = audience;
}
@Override
public SubjectTokenType tokenType() {
return SubjectTokenType.JWT;
}
@Override
public String getToken(
com.openai.core.http.HttpClient httpClient, JsonMapper jsonMapper) {
try {
String separator = requestUrl.contains("?") ? "&" : "?";
URI uri = URI.create(
requestUrl
+ separator
+ "audience="
+ URLEncoder.encode(audience, StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder(uri)
.header("Authorization", "bearer " + requestToken)
.GET()
.build();
HttpResponse<String> response = java.net.http.HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new SubjectTokenProviderException(
"github-actions",
"GitHub OIDC token request failed with status "
+ response.statusCode(),
null);
}
JsonNode payload = jsonMapper.readTree(response.body());
String token = payload.path("value").asText("");
if (token.isEmpty()) {
throw new SubjectTokenProviderException(
"github-actions",
"GitHub OIDC token response did not include a value",
null);
}
return token;
} catch (SubjectTokenProviderException e) {
throw e;
} catch (Exception e) {
throw new SubjectTokenProviderException(
"github-actions",
"failed to request GitHub OIDC token",
e);
}
}
@Override
public CompletableFuture<String> getTokenAsync(
com.openai.core.http.HttpClient httpClient, JsonMapper jsonMapper) {
return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));
}
}
public static void main(String[] args) {
WorkloadIdentity workloadIdentity = WorkloadIdentity.builder()
.identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))
.serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))
.provider(new GitHubActionsOidcTokenProvider(
System.getenv("ACTIONS_ID_TOKEN_REQUEST_URL"),
System.getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),
System.getenv("OPENAI_WIF_AUDIENCE")))
.build();
OpenAIClient client = OpenAIOkHttpClient.builder()
.workloadIdentity(workloadIdentity)
.build();
ResponseCreateParams params = ResponseCreateParams.builder()
.model(ChatModel.GPT_4_1_MINI)
.input("Say hello from GitHub Actions workload identity federation.")
.build();
client.responses().create(params).output().stream()
.flatMap(item -> item.message().stream())
.flatMap(message -> message.content().stream())
.flatMap(content -> content.outputText().stream())
.forEach(outputText -> System.out.println(outputText.text()));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
require "json"
require "net/http"
require "openai"
require "uri"
class GitHubActionsOIDCTokenProvider
include OpenAI::Auth::SubjectTokenProvider
def initialize(request_url:, request_token:, audience:)
@request_url = request_url
@request_token = request_token
@audience = audience
end
def token_type
OpenAI::Auth::TokenType::JWT
end
def get_token
uri = URI(@request_url)
params = URI.decode_www_form(uri.query || "")
params.reject! { |key, _| key == "audience" }
params << ["audience", @audience]
uri.query = URI.encode_www_form(params)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "bearer #{@request_token}"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "GitHub OIDC token request failed with status #{response.code}",
provider: "github-actions"
)
end
token = JSON.parse(response.body).fetch("value", "").to_s
if token.empty?
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "GitHub OIDC token response did not include a value",
provider: "github-actions"
)
end
token
rescue JSON::ParserError, SystemCallError => e
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Failed to request GitHub OIDC token: #{e.message}",
provider: "github-actions",
cause: e
)
end
end
provider = GitHubActionsOIDCTokenProvider.new(
request_url: ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_URL"),
request_token: ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),
audience: ENV.fetch("OPENAI_WIF_AUDIENCE")
)
workload_identity = OpenAI::Auth::WorkloadIdentity.new(
identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
provider: provider
)
client = OpenAI::Client.new(workload_identity: workload_identity)
response = client.responses.create(
model: "gpt-4.1-mini",
input: "Say hello from GitHub Actions workload identity federation."
)
puts(response.output_text)
- 对生产部署使用环境保护。在工作流访问生产 OpenAI 资源之前,要求通过审批或分支限制。
- 按仓库限制映射。尽可能匹配特定于仓库的声明,而不是允许组织内所有仓库进行访问。
- 按分支或工作流限制映射。考虑匹配诸如
repository, ref, environment, or workflow_ref to limit token issuance.
- 为 CI/CD 和生产工作负载使用单独的 OpenAI 服务账户。构建流水线通常与已部署的应用程序需要不同的权限。
- 避免向不受信任的 fork 中的拉取请求授予访问权限。来自 fork 的拉取请求可能会执行受攻击者控制的代码,因此不应向其提供生产凭据。
- 使用短期交换。GitHub OIDC 令牌用于临时身份验证,应仅在需要时进行交换。
- 审计仓库所有权变更。仓库的转移、重命名和权限更改可能会影响现有映射背后的安全假设。
- 优先使用精确声明匹配。匹配诸如
repository, ref,且 environment 而不是依赖于组织范围的信任关系。