---
title: GitHub Actions에서 WIF 사용하기
url: https://platform.claude.com/docs/ko/manage-claude/wif-providers/github-actions
description: 수명이 긴 API 키 대신 수명이 짧은 ID 토큰을 사용하여 GitHub Actions 워크플로를 Claude API에 인증합니다.
---

모든 GitHub Actions 워크플로 실행은 `https://token.actions.githubusercontent.com`에 있는 GitHub의 호스팅 발급자로부터 서명된 ID 토큰을 요청할 수 있습니다. "Workload Identity Federation"(워크로드 ID 페더레이션), 즉 WIF를 사용하면 워크플로가 해당 토큰을 수명이 짧은 Anthropic 액세스 토큰으로 교환하므로, 리포지토리에 `ANTHROPIC_API_KEY` 시크릿을 저장하지 않고도 CI 작업에서 Claude API를 호출할 수 있습니다.

토큰의 `sub` 클레임은 리포지토리와 트리거 컨텍스트를 인코딩합니다. 브랜치에 대한 푸시의 경우 `repo:<owner>/<repo>:ref:refs/heads/<branch>` 형식을 가집니다. 풀 리퀘스트 실행은 `repo:<owner>/<repo>:pull_request`를 사용하고, 환경으로 게이트된 배포는 `repo:<owner>/<repo>:environment:<name>`을 사용합니다. 페더레이션 규칙은 이 클레임(및 `repository_owner`, `ref`와 같은 다른 클레임)과 대조하여 어떤 워크플로 실행이 인증할 수 있는지 결정합니다.

## 사전 요구 사항

* [WIF 개념](https://platform.claude.com/docs/ko/manage-claude/workload-identity-federation#concepts)에 대한 이해: 서비스 계정, 페더레이션 발급자, 페더레이션 규칙.
* 워크플로 파일을 편집하고 `id-token: write` 권한을 부여할 수 있는 GitHub 리포지토리.
* Anthropic 조직의 Claude Console에서 서비스 계정, 페더레이션 발급자, 페더레이션 규칙을 생성할 수 있는 권한.
* Anthropic 조직 ID. Claude Console의 [Organization](https://platform.claude.com/settings/organization) 페이지(`platform.claude.com/settings/organization`)에서 확인할 수 있습니다.

## 워크플로 구성

GitHub는 명시적으로 요청하는 작업에만 ID 토큰을 발급합니다. 워크플로 또는 작업 수준에서 `id-token: write` 권한을 추가하세요:

```yaml
permissions:
  id-token: write
  contents: read
```

작업 내부에서 러너는 `ACTIONS_ID_TOKEN_REQUEST_URL`과 `ACTIONS_ID_TOKEN_REQUEST_TOKEN`이라는 두 개의 환경 변수를 노출합니다. 요청 토큰을 bearer 자격 증명으로, 선택한 audience를 쿼리 매개변수로 사용하여 요청 URL을 호출한 다음, 반환된 "JSON Web Token"(JSON 웹 토큰), 즉 JWT를 파일에 기록하세요:

```yaml
- name: Fetch GitHub OIDC token
  run: |
    curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
      | jq -r .value > /tmp/gha-jwt
```

JavaScript를 선호한다면, `actions/github-script`가 `core.getIDToken(audience)`를 통해 동일한 기능을 제공합니다:

```yaml
- name: Fetch GitHub OIDC token
  uses: actions/github-script@v8
  with:
    script: |
      const fs = require('fs');
      const token = await core.getIDToken('https://api.anthropic.com');
      fs.writeFileSync('/tmp/gha-jwt', token);
```

디코딩된 토큰에는 워크플로 실행을 설명하는 클레임이 포함되어 있습니다. 페더레이션 규칙은 이러한 클레임과 대조합니다:

```json
{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:your-org/your-repo:ref:refs/heads/main",
  "aud": "https://api.anthropic.com",
  "repository": "your-org/your-repo",
  "repository_owner": "your-org",
  "ref": "refs/heads/main",
  "sha": "abc123...",
  "workflow": "CI",
  "actor": "octocat",
  "event_name": "push"
}
```

`sub` 형식의 전체 목록은 [GitHub의 OIDC subject 클레임 참조](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims)를 참조하세요.

## Anthropic 구성

Claude Console에서 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 페이지(`platform.claude.com/settings/workload-identity-federation`)로 이동하여 **Connect workload**를 클릭하고 **GitHub Actions** 타일을 선택하세요. 마법사가 발급자 등록, 서비스 계정 생성, 페더레이션 규칙 생성 과정을 안내합니다.

마법사가 이러한 리소스를 대신 생성해 줍니다. 마법사에 입력하든 [Admin API](https://platform.claude.com/docs/ko/manage-claude/wif-admin-api)로 전송하든 다음 값을 사용하세요:

**페더레이션 발급자:** GitHub는 OIDC 디스커버리 문서와 JWKS를 공개적으로 게시하므로 디스커버리 모드를 사용하세요. GitHub가 키를 교체하면 Anthropic이 자동으로 키를 새로 고칩니다.

```json
{
  "name": "github-actions",
  "issuer_url": "https://token.actions.githubusercontent.com",
  "jwks": { "type": "discovery" }
}
```

**페더레이션 규칙:** 신뢰하려는 워크플로 실행만 일치시키세요. 이러한 클레임의 범위를 안전하게 지정하는 방법은 [인증할 수 있는 워크플로 제한](https://platform.claude.com/docs/ko/manage-claude/wif-providers/github-actions#restrict-which-workflows-can-authenticate)을 참조하세요.

```json
{
  "name": "gha-main",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "repo:your-org/your-repo:ref:refs/heads/main",
    "audience": "https://api.anthropic.com",
    "claims": {
      "repository_owner": "your-org"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

워크로드가 허용하는 한 최대한 구체적으로 지정하세요. `sub`의 마지막 세그먼트는 `ref:...`, `environment:...`, `pull_request` 이벤트 간에 달라지므로, 규칙이 동일한 리포지토리의 여러 이벤트 유형과 일치해야 하는 경우에만 `subject_prefix`를 `repo:your-org/your-repo:*`로 완화하세요(`claims.ref` 제약 조건과 함께 사용).

## 토큰 획득 및 사용

작업에 페더레이션 환경 변수를 설정하고 평소처럼 SDK를 호출하세요. `Anthropic()` (typescript: `new Anthropic()`; csharp: `new AnthropicClient()`; go: `anthropic.NewClient()`; java: `AnthropicOkHttpClient.fromEnv()`; php: `new Client()`; ruby: `Anthropic::Client.new`)는 `ANTHROPIC_IDENTITY_TOKEN_FILE`을 읽고, 첫 번째 요청 시 JWT를 교환하며, 액세스 토큰이 만료되기 전에 자동으로 갱신합니다.

<CodeGroup>
  ```yaml Workflow
  name: Call Claude
  on: push

  permissions:
    id-token: write
    contents: read

  jobs:
    call-claude:
      runs-on: ubuntu-latest
      env:
        ANTHROPIC_FEDERATION_RULE_ID: fdrl_...
        ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000
        ANTHROPIC_SERVICE_ACCOUNT_ID: svac_...
        ANTHROPIC_WORKSPACE_ID: wrkspc_...  # required when the rule covers multiple workspaces
        ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt
      steps:
        - uses: actions/checkout@v5
        - name: Fetch GitHub OIDC token
          run: |
            curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
              "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
              | jq -r .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
        - name: Run your script
          run: |
            pip install anthropic
            python your_script.py
  ```

  ```bash cURL
  JWT=$(cat /tmp/gha-jwt)

  RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
    -H "content-type: application/json" \
    --data @- <<JSON
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$JWT",
    "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
    "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
    "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
    "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
  }
  JSON
  )

  ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

  curl https://api.anthropic.com/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello, Claude"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
  ```

  ```python Python
  import anthropic

  # 작업 환경에서 ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE 값을
  # 읽습니다.
  client = anthropic.Anthropic()

  message = client.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello, Claude"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";

  // 작업 환경에서 ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  // ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE 값을
  // 읽습니다.
  const client = new Anthropic();

  const message = await client.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello, Claude" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
  ```

  ```go Go
  // ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  // ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE 값을
  // 작업 환경에서 읽습니다.
  client := anthropic.NewClient()

  message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
  	Model:     anthropic.ModelClaudeOpus5_5,
  	MaxTokens: 1024,
  	Messages: []anthropic.MessageParam{
  		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
  	},
  })
  if err != nil {
  	panic(err)
  }
  for _, block := range message.Content {
  	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
  		fmt.Println(textBlock.Text)
  		break
  	}
  }
  ```

  ```java Java
  AnthropicClient client = AnthropicOkHttpClient.fromEnv();

  var message = client.messages().create(MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(1024)
          .addUserMessage("Hello, Claude")
          .build());

  IO.println(message.content());
  ```

  ```csharp C#
  // ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  // ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE 값을
  // 작업 환경에서 읽어 옵니다.
  using var client = new AnthropicClient();

  var message = await client.Messages.Create(new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello, Claude" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }
  ```

  ```bash CLI
  # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE을
  # 작업 환경에서 읽습니다.
  ant messages create \
    --model claude-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello, Claude"}'
  ```

  ```php PHP
  use Anthropic\Client;

  // ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  // ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE 값을
  // 작업 환경에서 읽어옵니다.
  $client = new Client();

  $message = $client->messages->create(
      model: 'claude-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello, Claude']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
  ```

  ```ruby Ruby
  require "anthropic"

  # 작업 환경에서 ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE을
  # 읽습니다.
  client = Anthropic::Client.new

  message = client.messages.create(
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello, Claude"}]
  )
  puts message.content.find { it.type == :text }.text
  ```
</CodeGroup>

GitHub가 발급한 각 ID 토큰은 발급 후 약 5분이 지나면 만료됩니다. 토큰 요청 엔드포인트(`ACTIONS_ID_TOKEN_REQUEST_URL`)는 작업 전체 기간 동안 유효하므로 언제든지 새 토큰을 가져올 수 있습니다. SDK는 처음 사용할 때 토큰을 교환하고 그 결과로 얻은 Anthropic 액세스 토큰을 캐시합니다. Anthropic 토큰의 수명보다 오래 실행되는 작업의 경우, SDK는 새로 고칠 때마다 `ANTHROPIC_IDENTITY_TOKEN_FILE`을 다시 읽으므로, 파일을 최신 상태로 유지하려면 가져오기 단계를 주기적으로 다시 실행하세요(또는 백그라운드 루프로 감싸세요). 또는 Java를 제외하고는 파일 경로를 사용하는 대신 `ACTIONS_ID_TOKEN_REQUEST_URL`을 직접 호출하는 토큰 제공자를 SDK에 전달할 수도 있습니다.

## 설정 확인

교환에 성공하면 `sk-ant-oat01-`로 시작하는 `access_token`과 초 단위의 `expires_in` 값이 반환됩니다. 교환이 거부되면 어떤 검사가 실패했는지와 관계없이 고정 메시지 `Authentication failed`와 함께 불투명한 `401` `authentication_error`가 반환됩니다. 대부분의 경우 거부 사유는 Claude Console의 [인증 기록 페이지](https://platform.claude.com/settings/workload-identity-federation?tab=history)에 있는 해당 시도 항목에 기록되며, [실패한 교환 문제 해결](https://platform.claude.com/docs/ko/manage-claude/wif-reference#troubleshoot-a-failed-exchange)에서 검사 항목을 순서대로 안내합니다. GitHub Actions 측에서 가장 흔한 원인은 `sub` 클레임 형식이 일치하지 않는 것이며(마지막 세그먼트는 `ref:...`, `environment:...`, `pull_request` 이벤트에 따라 달라짐), 이 경우 기록 항목에 사유 `match_subject_prefix`가 표시됩니다.

## 인증할 수 있는 워크플로 제한

<Warning>
  `subject_prefix`를 `repo:your-org/*`만으로 지정하면 조직의 모든 리포지토리와 일치하며, `ref` 제약 조건이 없으면 포크에서 트리거된 `pull_request` 실행과도 일치합니다. 일치하는 리포지토리에 풀 리퀘스트를 열 수 있는 사람이라면 누구나 페더레이션된 Anthropic 토큰을 얻을 수 있습니다.
</Warning>

규칙의 `match` 블록을 사용 사례에 맞는 가장 좁은 범위로 고정하세요:

* **단일 리포지토리로 고정:** 조직의 다른 리포지토리가 일치하지 않도록 `subject_prefix: "repo:your-org/your-repo:*"`를 사용하세요.
* **보호된 브랜치로 고정:** 풀 리퀘스트 실행과 기능 브랜치가 일치하지 않도록 `claims` 아래에 `"ref": "refs/heads/main"`(또는 릴리스 브랜치)을 추가하세요.
* **소유자를 명시적으로 고정:** `sub` 파싱의 예외적인 경우에 대비한 심층 방어 검사로 `claims` 아래에 `"repository_owner": "your-org"`를 추가하세요.
* **배포 환경으로 고정:** 배포 작업의 경우 `subject_prefix: "repo:your-org/your-repo:environment:production"`과 일치시키고, GitHub에서 필수 검토자로 해당 환경을 게이트하세요.

## 다음 단계

* [Workload Identity Federation](https://platform.claude.com/docs/ko/manage-claude/workload-identity-federation): 전체 설정 안내, 환경 변수, 자격 증명 우선순위.
* [인증](https://platform.claude.com/docs/ko/manage-claude/authentication): 페더레이션과 API 키의 비교.
