# クラウドに Latchkey のジョブを信頼させる

> Latchkey CI のジョブが、保存したクラウドキーではなく短命の ID トークンで AWS にサインインする方法。発行者、信頼ポリシーが照合するサブジェクト、そしてトークンを求めるステップ。

Source: https://latchkey.dev/ja/documentation/cloud-identity

## Summary

- ステップはランナーに 1 つのオーディエンス向けの ID トークンを求め、Latchkey はそれに `https://oidc.latchkey.dev` として署名します。そのため、クラウドキーをどこにも保存せずにジョブがデプロイできます。
- 信頼ポリシーはトークンをその `sub` で照合します。`sub` にはワークスペース、リポジトリ、そして ref か、オーナーまたは管理者が実行を承認した環境、または承認を不要に設定した環境が入ります。
- トライアル実行のジョブや、トライアル中のパイプラインのジョブに発行されたトークンは `sub` の前に `advisory:` が付くため、ライブ実行向けに書いたロールがそれを信頼することはありません。
- AWS では、オーディエンス `sts.amazonaws.com` で `https://oidc.latchkey.dev` の OpenID Connect プロバイダを作成し、`latchkey/aws-login@1.0.0` でサインインします。

## 仕組み

Latchkey CI の実行のジョブは ID トークンを求めることができます。ID トークンは Latchkey が署名した短命のトークンで、ジョブがどのワークスペース、リポジトリ、そして ref または環境に属するかを示します。クラウドは Latchkey が公開する公開鍵で署名を確かめ、トークンのクレームをあなたが書いた信頼と比べ、一致すればジョブ専用の短命の認証情報を渡します。クラウドキーは Latchkey にも、シークレットにも、ワークフローファイルにも保存されません。

ステップは 1 つのオーディエンス向けのトークンを求めます。オーディエンスとは、クラウドがトークンの `aud` クレームに期待する名前で、AWS なら `sts.amazonaws.com` です。ほかのクレームはすべて、ジョブ、その実行、そのリポジトリについての Latchkey 自身の記録から取られ、リクエストから取られることはありません。

## 発行者

| 項目 | 値 |
| --- | --- |
| 発行者 | `https://oidc.latchkey.dev` |
| ディスカバリードキュメント | `https://oidc.latchkey.dev/.well-known/openid-configuration` |
| 公開鍵 | `https://oidc.latchkey.dev/.well-known/jwks.json` |
| 署名 | RS256。`kid` が示す鍵で署名します |
| 有効期間 | 15 分。ジョブのタイムアウトが近いときはそれより短くなりますが、5 分を下回ることはありません |

発行者は上のとおり正確に、末尾のスラッシュなしで書いてください。どちらのドキュメントも 1 時間キャッシュされることがあります。Latchkey が署名鍵をローテーションするときは、新しい鍵で署名する前に古い鍵の隣に新しい鍵を公開するため、発行者から公開鍵を読み取るクラウドでは、あなたの側で何も変える必要はありません。

## トークンを受け取れるジョブ

ジョブがトークンを求められるのは実行中で、かつリポジトリ自身のコミットに対するワークフローファイルの Latchkey CI の実行のジョブであるときです。次のジョブはトークンを受け取りません。

- フォークからのプルリクエストのジョブ。そのステップには求める手段が渡されません。
- `latchkey run` で開始したジョブ。
- セルフヒールが提案した修正を確かめるために Latchkey が開始した実行のジョブ。
- 環境を指定したジョブ。承認が不要な環境でない限り、オーナーまたは管理者がその実行を承認するまでは受け取らず、その環境が受け付けないブランチ上の実行では決して受け取りません。

トークンは、ジョブが終わった後も `exp` まで、最大 15 分有効です。ジョブが終わった後に発行されるトークンはありません。

## サブジェクト

信頼ポリシーが照合するのは `sub` クレームです。次の 4 つの形のいずれかになります。

| ジョブ | `sub` |
| --- | --- |
| 環境を指定していないジョブ | `org:<workspace id>:repo:<owner>/<name>:ref:<ref>` |
| 環境を指定し、承認されたか承認が不要なジョブ | `org:<workspace id>:repo:<owner>/<name>:environment:<environment>` |
| 1 つ目の形で、トライアル実行、トライアル中のパイプライン、またはアドバイザリーモードのとき | `advisory:org:<workspace id>:repo:<owner>/<name>:ref:<ref>` |
| 2 つ目の形で、トライアル実行、トライアル中のパイプライン、またはアドバイザリーモードのとき | `advisory:org:<workspace id>:repo:<owner>/<name>:environment:<environment>` |

`<workspace id>` はワークスペースの ID で、ワークスペースのジョブが受け取るどのトークンでも同じです。Settings, Account の **Workspace ID** からコピーしてください。Latchkey のすべてのワークスペースのトークンは同じ 1 つの発行者から発行されるため、信頼ポリシーは照合する `sub` に必ずワークスペースの ID を含めます。`<owner>/<name>` は Latchkey がコードホストから最後に読み取ったリポジトリ名です。そのためリポジトリの名前を変えるとサブジェクトも変わります。名前を変えたときはそのリポジトリを指定しているロールを更新し、削除したときはそのロールを削除してください。後からその名前を使うワークスペースのリポジトリは、同じサブジェクトを受け取るからです。`<ref>` はジョブが属する実行の ref、`<environment>` はジョブが結び付けられた環境の、ワークフローファイルに書かれたとおりの名前で、大文字と小文字も保たれます。マトリクスの値ならそのレグの環境、ブランチのマップなら選ばれた環境です。どの部分にも `:` は入らないため、あるサブジェクトが別のサブジェクトとして読まれることはありません。

ジョブが受け取る正確なサブジェクトを確かめるには、そのジョブでこのステップを一度実行してください。デコードした `sub` とワークスペースの ID を出力し、トークン自体は出力しません。トークンはいずれにせよログで隠されます。

yaml:
```yaml
- name: Show this job's identity
  run: |
    answer=$(curl -sS --noproxy '*' -H "Authorization: Bearer $LATCHKEY_ID_TOKEN_REQUEST_TOKEN" \
      "$LATCHKEY_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
    if ! token=$(jq -er .value <<<"$answer" 2>/dev/null); then
      echo "No identity token: ${answer:-no answer}" >&2
      exit 1
    fi
    jq -rR 'split(".")[1] | gsub("-"; "+") | gsub("_"; "/") | @base64d | fromjson | .sub, .organization_id' <<<"$token"
```

アドバイザリーモードのリポジトリは、今の CI の横でジョブをトライアルとして実行します。Latchkey CI に移ったリポジトリも、置き換える GitHub Actions ファイルがまだコミットにある間は、トライアル中のパイプラインを同じように実行します。トライアル実行のジョブやトライアル中のパイプラインのジョブに発行したトークンには `advisory:` の接頭辞が付きます。リポジトリがアドバイザリーモードの間に発行したトークンにもすべて付き、リポジトリがアドバイザリーモードに切り替えられる前に始まった実行のものであっても同じです。`advisory:` の付かないサブジェクトはすべて `org:` で始まるため、ライブ実行向けに書いた信頼ポリシーが `advisory:` の接頭辞の付いたトークンに一致することはありません。これは設計どおりです。トライアル実行もトライアル中のパイプラインもデプロイしてはならないからです。テストが読み取りに使う読み取り専用のロールのように、それらにも使うロールでは、`org:` で始まる形の隣に `advisory:` の形も並べます。

環境を指定したジョブが環境の形を受け取るのは、オーナーまたは管理者がその環境に対して実行を承認してからです。オーナーまたは管理者がその環境を承認不要に設定している場合は、承認なしで受け取ります。さらに、オーナーまたは管理者が Settings, Environments でその環境のブランチを設定している場合は、そのいずれかのブランチ上の実行に限られます。`ref` クレームは引き続き実行の ref を示します。したがって `environment:production` を信頼するロールが受け入れるのは、オーナーまたは管理者が production に対して承認したジョブか、production が承認なしで開始したジョブで、production が受け付ける実行のものだけです。環境を承認不要にできるのはブランチが設定されている間だけで、そのときはそのブランチだけが守りになります。オーナーまたは管理者がブランチを設定するまでは、production はどのブランチ、タグ、プルリクエストの ref の実行も受け付けるため、デプロイロールが環境を信頼する前にブランチを設定してください。

承認は、`jobs:approve` スコープを持つ API キーを通じて、そのキーを作成したオーナーまたは管理者に代わって行われることもあります。このようなキーを持ち、リポジトリにプッシュもできるエージェントは、自分の変更による実行を、他の誰の判断も経ずに承認できます。

## トークンが示す ref

どの ID トークンも、ジョブが属する実行の ref を 2 つのクレームで示します。

- `ref`。たとえば `refs/heads/main`、`refs/tags/v1.4.0`、`refs/pull/42/head` です。
- `sub`。ジョブが環境を指定していない限り、`:ref:<ref>` で終わります。

push とプルリクエストが同じコミットを指すときは、両者は 1 つの実行にまとまり、その実行のすべてのジョブがその実行の ref を持ちます。

### 同じリポジトリのブランチからのプルリクエスト

ほとんどの場合、プルリクエストのジョブはブランチの ref、`refs/heads/<branch>` を持ちます。ブランチを push すると、その push がコミットの実行を開始し、プルリクエストはその実行に加わります。すでに実行されたコミットに対してプルリクエストを開いても、新しい実行は始まりません。そのチェックは push のものです。

トークンが `refs/pull/<number>/head` を示すのは、プルリクエスト自身が実行を開始するときです。

- ワークフローファイルが `push` ではなく `pull_request` で実行されるため、push は何にも一致せず、プルリクエストが実行を開始する場合。または、
- Latchkey がコミットのジョブを push から計画する前に、プルリクエストの更新が Latchkey に届いた場合。

そのため、プルリクエストのジョブはワークフローのトリガーと、どのイベントが先に届くかによって、どちらの形も持ちえます。`event_class` クレームは実行がどのイベントに対して計画されたかを示し、たとえば push なら `branch_push`、プルリクエストなら `pr_update` です。プルリクエストの実行では、`base_ref` クレームがプルリクエストの対象ブランチを示します。

### スケジュール実行

スケジュール実行はデフォルトブランチの先頭に対する独立した実行で、そのジョブは `refs/heads/main` のようにデフォルトブランチの ref を持ちます。

### フォークからのプルリクエスト

フォークからのプルリクエストのジョブは ID トークンを受け取りません。トークンを求められるのは、あなたのリポジトリに push されたコードだけです。

## 信頼ポリシーが照合すべきもの

- **デプロイ:** デプロイする正確なサブジェクトに一致させます。環境を指定したジョブ向けのデプロイロールは `org:<workspace id>:repo:<owner>/<name>:environment:production` に、ブランチ向けのロールは `org:<workspace id>:repo:<owner>/<name>:ref:refs/heads/main` かリリースタグに一致させます。トークンが `refs/heads/main` を示すのは `main` にあるコミットの実行だけなので、`main` に結び付けたロールが実行するのは `main` にあるコードだけです。デプロイロールに `advisory:` の形を並べることはありません。
- **重要なものにはプルリクエストの ref を一致させないでください。** `refs/pull/<number>/head` はプルリクエストの番号ごとに変わり、すべてのプルリクエストのジョブに現れるわけでもありません。
- **リポジトリのどのジョブでも使えるロール** (ステージングのバケットを読むテスト、プレビュービルド): 接頭辞 `org:<workspace id>:repo:<owner>/<name>:ref:` でリポジトリのすべての ref に一致させます。この接頭辞は環境の形にも `advisory:` の形にも一致しないため、環境を指定したジョブ、トライアル実行、トライアル中のパイプラインも受け入れるロールでは、それらの接頭辞も並べます。
- **必ず、ワークスペースの ID を含めて `sub` に一致させてください。** Latchkey のすべてのワークスペースのトークンは同じ発行者から発行されるため、オーディエンスだけ、またはリポジトリ名だけを確かめる信頼では、別のワークスペースのジョブも受け入れてしまいます。

## クレーム

| クレーム | 内容 |
| --- | --- |
| `iss` | `https://oidc.latchkey.dev` |
| `aud` | ステップが求めた 1 つのオーディエンス |
| `sub` | 上の 4 つの形のいずれかのサブジェクト |
| `iat`、`nbf`、`exp` | トークンが発行された時刻、有効になる時刻、期限が切れる時刻 (秒) |
| `jti` | トークンごとに固有の ID |
| `organization_id` | ワークスペースの ID |
| `repository` | Latchkey がコードホストから最後に読み取ったリポジトリ名 (`owner/name` の形) |
| `repository_id` | Latchkey におけるリポジトリの ID |
| `job_id` | ジョブ |
| `run_id`、`attempt` | 実行と、1 から数えたその試行 |
| `ref` | 実行の ref |
| `sha` | 実行が固定されているコミット |
| `pipeline_id`、`step_key` | パイプラインとステップ。どの ref とコミットでも同じ ID を保ちます |
| `base_ref` | プルリクエストまたはマージグループのベースブランチ (`refs/heads/<branch>` の形)。push では、コードホストが送ったときの push 自身の `base_ref` |
| `actor` | 実行を開始した人 |
| `event_class` | 実行が計画されたイベント。たとえば `branch_push` や `pr_update` |

値のないクレームは空のまま送られるのではなく省かれるため、空の値に一致するルールが誤って一致することはありません。

## ステップでトークンを求める

トークンを受け取れるジョブでは、すべてのステップに 2 つの変数があります。ランナー自身のアドレスである `LATCHKEY_ID_TOKEN_REQUEST_URL` と、それに問い合わせるための認証情報である `LATCHKEY_ID_TOKEN_REQUEST_TOKEN` です。フォークからのプルリクエストのジョブにはどちらもありません。トークンを受け取れないほかのジョブは、下の語のいずれかで拒否されます。アドレスに `&audience=` とオーディエンスを付け、認証情報をベアラートークンとして送ると、ランナーは `{"value": "<token>", "job_id": "<job id>"}` を返します。

yaml:
```yaml
- name: Get an identity token for AWS
  run: |
    answer=$(curl -sS --noproxy '*' -H "Authorization: Bearer $LATCHKEY_ID_TOKEN_REQUEST_TOKEN" \
      "$LATCHKEY_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
    if ! token=$(jq -er .value <<<"$answer" 2>/dev/null); then
      echo "No identity token: ${answer:-no answer}" >&2
      exit 1
    fi
```

ジョブに CLI 0.9.0 以降をインストールしてあれば (`npm install -g @latchkeydev/cli`)、`token=$(latchkey oidc request-token --audience sts.amazonaws.com)` で同じリクエストを送れます。CLI はオーディエンスを自分でエンコードし、リクエストをランナー上にとどめ、トークンがないときは理由を示して exit 1 で終了します。`--out <file>` を付けると、トークンを表示する代わりに、所有者だけが読めるファイルに書き込みます。

オーディエンスは、1 から 255 文字の表示可能な ASCII 文字からなる 1 つの文字列で、スペースは含められません。`&`、`+`、`#`、`%` を含むオーディエンスはパーセントエンコードしてください。ランナーは、ステップがトークンを受け取る前に、ジョブのログとセルフヒールが読み取る内容の中でトークンを隠します。同じオーディエンスをもう一度求めると、ランナーは保持しているトークンの有効期間が 3 分の 1 以上残っている間はそれを返し、その後は新しいトークンを返します。`--noproxy '*'` は、ジョブがプロキシを設定している場合でもリクエストをランナー上にとどめます。

認証情報のないリクエストには本文のない 401 が、オーディエンス以外のものを送るリクエストには本文のない 400 が返ります。Latchkey がトークンを発行しない場合、ランナーはエラーのステータスと `{"refusal": "<word>"}` を返します。再試行が役立つ場合は、最大 25 秒まで再試行した後に返します。

| 語 | 意味 |
| --- | --- |
| `job_class_ineligible` | ジョブが、ワークフローファイルの実行でリポジトリ自身のコミットを実行していません。たとえば `latchkey run` で開始したジョブや、セルフヒールが提案した修正の実行です |
| `job_not_running` | ジョブが実行中ではありません |
| `configuration_incomplete` | Latchkey はここでトークンを発行する準備がまだできていません |
| `claims_unavailable`, `signing_failed`, `ledger_unavailable`, `unexpected`, `mint_unavailable` | Latchkey は今はトークンを発行できませんでした。ステップをもう一度実行してください |
| `mint_refused` | Latchkey がリクエストを拒否しました。オーディエンスが受け付けられないものか、ジョブが終わっている場合などにジョブ自身の認証情報が拒否されたかのいずれかです |
| `masker_refused` | トークンをログで隠せなかったため、ステップに渡しませんでした |

## AWS

AWS アカウントごとに一度、オーディエンス `sts.amazonaws.com` で発行者の OpenID Connect プロバイダを作成します。サムプリントは指定しないでください。AWS は発行者の証明書を、自身が信頼する認証局で確かめます。

terminal:
```console
$ aws iam create-open-id-connect-provider \
    --url https://oidc.latchkey.dev \
    --client-id-list sts.amazonaws.com
```

次に、ロールからプロバイダを信頼します。Latchkey のトークンが持つクレームのうち、AWS が照合できるのは `sub` と `aud` だけなので、ロールが依存するものはすべて照合するサブジェクトに含める必要があります。このデプロイロールが受け入れるのは、環境 `production` を指定した `acme/api` のジョブで、オーナーまたは管理者が承認した実行か、production が承認不要の場合はそのブランチ上の実行のものだけです。

`deploy-trust-policy.json`
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.latchkey.dev"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.latchkey.dev:aud": "sts.amazonaws.com",
          "oidc.latchkey.dev:sub": "org:<workspace id>:repo:acme/api:environment:production"
        }
      }
    }
  ]
}
```

テストが読み取りに使う読み取り専用のロールは、環境を指定していないリポジトリのどのジョブでも、トライアル実行とトライアル中のパイプラインも含めて受け入れてかまいません。`StringLike` は `*` を任意の文字の並びとして読み、そのリストはいずれか 1 つの値が一致すれば一致します。

`read-only-trust-policy.json`
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.latchkey.dev"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.latchkey.dev:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "oidc.latchkey.dev:sub": [
            "org:<workspace id>:repo:acme/api:ref:*",
            "advisory:org:<workspace id>:repo:acme/api:ref:*"
          ]
        }
      }
    }
  ]
}
```

ジョブでは [latchkey/aws-login](https://latchkey.dev/ja/documentation/steps) でサインインします。このステップはトークンを求め、ロールにサインインし、以降のすべてのステップのために認証情報を新しく保ちます。

`.latchkey/workflows/deploy.yml`
```yaml
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: small
    environment: production
    steps:
      - uses: latchkey/aws-login@1.0.0
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy
          aws-region: us-east-1
      - run: aws sts get-caller-identity
```

> **Google Cloud と Microsoft Entra**
>
> どちらも同じトークンを信頼できます。Latchkey がそれぞれを最初から最後まで確かめた時点で、その手順をこのページに追加します。

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
