SourceOrgIDとPrincipalOrgIDでOrganiations環境下のアクセス制御を実現する

導入: リソースベースポリシーとプリンシパル

AWSにおいてアクセス元を識別するための仕組みとしてプリンシパルがある。 IAMポリシーにおいてリソースベースのポリシーを定義するとき、さまざまな種類のプリンシパルから指定することができる。

docs.aws.amazon.com

例えばS3バケットに付与するリソースベースポリシーである、S3バケットポリシーにはプリンシパル要素として誰がそのS3バケットにアクセス可能かを指定する。 CloudTrailがS3バケットにログファイルを書き込むことを許可するのであれば、CloudTrailサービスを指定するAWSサービスプリンシパルとして "Service": "cloudtrail.amazonaws.com" を指定する。 同じようにVPCフローログをS3に書き込むことを許可するのであれば、ログ配信サービスのAWSサービスプリンシパルとして "Service": "delivery.logs.amazonaws.com" を指定する。 それ以外にも、AWSアカウント全体に許可するのであればAWSアカウントプリンシパルとして "AWS": "arn:aws:iam::123456789012:root"を指定したり、特定のIAMロールに許可するのであればIAMロールプリンシパルとして "AWS": "arn:aws:iam::AWS-account-ID:role/role-name" を指定する。 すべてのプリンシパルに許可するのであればワイルドカードを用いて "AWS" : "*" を指定する。

Organizations環境下におけるクロスアカウントのアクセス

特にプリンシパルを考慮する必要があるケースとしてOrganizations環境下におけるクロスアカウントのアクセスがある。 同一Organizations下のアカウントであればアクセスを許可し、それ以外のアカウントからはアクセスを許可したくないケースがある。例えばCloudTrailログを特定のAWSアカウントのS3バケットに集約したいケースや、特定S3バケットを同一Organizations下のIAMロールにはアクセス許可したい場合など。

このようなAWSアカウントをまたいだアクセスをクロスアカウントと呼ぶ。 クロスアカウントでのアクセスを許可したい場合は特に誰にアクセスを許可するかが重要。広くアクセスを許可してしまうと、Organizations外からのアクセスも許可してしまう可能性がある。

アクセス許可するプリンシパルがIAMロールプリンシパルのようにAWSアカウントを含んでいれば該当AWSアカウントしかアクセス許可されない。一方でOrganizations下でアクセスを許可するとき、メンバアカウント追加するたびにプリシンシパルを更新することが大変だからとAWSサービスプリンシパルで許可すると、Organizations外にもアクセスを許可してしまうことになる。

グローバル条件コンテキストキーを利用する

このようなケースにおいてグローバル条件コンテキストキーを利用する。

特にプリンシパルのプロパティとしてaws:PrincipalOrgIDが便利。これを指定すればプリンシパルが対象Organizationであることを指定できる。 例えばKMSキーの利用をOrganizationsに制限するために利用できる。このKMSキーポリシーでは、プリンシパルワイルドカードを指定しているので広く許可しているが、グローバル条件コンテキストキーを利用することでOrganizationsに制限できている。

{
  "Sid": "Allow use of the KMS key for organization",
  "Effect": "Allow",
  "Principal": {
    "AWS": "*"
  },
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey",
    "kms:Encrypt",
    "kms:ReEncrypt*",
    "kms:GetKeyPolicy"
  ],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "aws:PrincipalOrgID": "o-xxxxxxxxxxx"
    }
  }
}

repost.aws

一方で、aws:PrincipalOrgIDは常に使えるわけではない。特にプリンシパルAWSサービスプリンシパルの場合は利用できない。 許可するプリンシパルがIAMロールプリンシパルのような組織のメンバーである場合にのみコンテキストに含まれる。これはグローバル条件コンテキストキーaws:PrincipalOrgIDの可用性の欄で説明されている。

このキーは、プリンシパルが組織のメンバーである場合にのみリクエストコンテキストに含まれます。匿名リクエストには、このキーは含まれません。

dev.classmethod.jp

このようなケースでは、同じくグローバル条件コンテキストキーのaws:SourceOrgIDが利用できる。SourceOrgIDは2023年11月にリリースされた機能で、AWSサービスプリンシパルの場合にコンテキストに含まれるようになる。

dev.classmethod.jp

以上からOrganizations環境下において、AWSサービスプリンシパルにはSourceOrgIDを、IAMロールなどの組織のメンバーにはPrincipalOrgIDを、それぞれ利用することで適切なアクセス制御が実現できる。

Principalsに指定したIAMユーザを削除するとS3バケットポリシーが変化する

まさにこれに遭遇した。

dev.classmethod.jp

S3バケットポリシーにIAMユーザのアクセス許可を付与していた。 IAMユーザを削除したが、S3バケットポリシーの修正もれが生じていた。 S3バケットポリシーはterraformで管理していたが、IAMユーザの指定ではIAMユーザリソースで構成せず文字列で "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/temporary-user" のように指定していた。 このため削除時には気付けず後日ドリフトとして検知された。

誰が更新したのか調べるためにCloudTrailログを調査したがバケットポリシーの更新ログが存在しなかった。 また、バケットポリシーは不適切な内容に思え、実際マネコンのバケットポリシー編集画面で確認・保存しようとしたらエラーになった。

バケットポリシー編集画面で保存しようとするとエラーになる

上記の警告メッセージにある通り、IAMユーザarnから置き換えられた文字列はPrincipal IDで、Principal IDをマネコン等で確認することはできないもののPrincipal IDを指定してS3バケットポリシーを構築することはできるみたい。だからバケットポリシーの編集画面ではエラーではなく警告扱いになっている。 実際には非推奨であり、かつ存在しないPrincipal IDを指定することはできないので保存時にエラーとして弾いてくれる。

docs.aws.amazon.com

注意点として、このように置き換えてくれるのはPrincipalとして指定した場合のみで、Condition句などで利用しても置き換えられない。 これはPrincipal要素にIAMユーザやIAMロールを指し示すarnが含まれている場合に変換されることがドキュメントに記載されている。このためPrincipal要素以外で指定しても置き換えられない。 また、Principal要素にクロスアカウントでIAMユーザarnを指定することはできず、クロスアカウントの場合はAWSアカウントIDを指定する必要がある。

docs.aws.amazon.com

GitHub Composite Actionsではboolean inputsがstringとして扱われる

Composite Actions概要

GitHub Actionsにて複数のworkflowから利用できるactionsとしてComposite Actions (日本語では複合アクション)を作成できる。 Composite Actionsを作成すれば、複数のworkflowから参照することもできるし、複数リポジトリで共有することもできる。

docs.github.com

zenn.dev

workflowにおけるinput type

本題に入る前にworkflowのinputについて確認する。 workflowは複数のジョブで構成されたCI/CDのパイプライン。各ジョブは複数のステップから構成され、ステップはcomposite actionsなどを用いて実現する。

docs.github.com

workflowでは動作をカスタマイズするために入力となるinputが指定できる。 また、inputには入力が必須かを指定するRequiredやデータ型を指定するtypeなどが定義できる。

workflowには再利用可能なworkflow(reusable workflow, workflow_call)と手動で実行するworkflow(manual workflow, workflow_dispatch)があり、それぞれで指定可能なinput typeが異なる点に注意が必要。 workflow_callではboolean, number, stringが型として指定できる。

docs.github.com

workflow_dispatchではこれに加えてchoiceenvironmentが指定できる。 docs.github.com

また、従来のworkflow_callとworkflow_dispatchのinputは提供されるコンテキストが異なっていた。workflow_callでは inputs コンテキストが与えられ、inputs.username のように入力を参照できたが、workflow_dispatchでは github.event.inputs コンテキストが与えられ、github.event.inputs.usernameのように入力を参照する必要があった。

この問題は2022年6月に解消され、どのworkflowでも inputsコンテキストが利用できるようになった。 また、この変更に伴い workflow_dispatchにおいても input typeが提供されるようになった。このため、互換性のために残されている github.event.inputs 経由でinputsを参照すると型情報が無いので常にstringとして扱われてしまう問題がある。inputsコンテキスト経由で参照すれば型情報が反映される。

github.blog

github.com

Composite Actionsにおけるinput type

workflowとは異なり、composite actionではinput typeはサポートされていない。

docs.github.com

inputs定義時にtypeを指定することはできるが実際には機能せず常にstringとして扱われる。このため、boolean型だと思って処理しようとすると上手く動作しないという不具合が発生する。

boolean型のように利用したければ、if: ${{ inputs.input-value }} ではなく if: ${{ inputs.input-value == 'true' }} のように利用する必要がある。

github.com

また、workflow用のシンタックスとcomposite actions用のシンタックが似ているけど互換性が無いことも影響している。 上記のようにworkflowではworkflow_callとworkflow_dispatchでinput typeなどが統一され互換性があるのに、composite actionsは互換性が無くtype指定できないことが直感的でない。

ちなみに、github actions用ファイルのlinterとしてよく利用されるactionlintもあくまでworkflow file用のlinterであり、composite actions用のlint機能は提供していない。

github.com

論理属性をVueでバインディングする

<button type="button" disabled>送信</button> のように、disabled属性をVueで制御したい。

これを実現するには以下のようにv-bindを利用する

<script setup lang="ts">
import {ref} from "vue";
const isButtonDisabled = ref(true);
</script>

<template>
  <button v-bind:disabled="isButtonDisabled">Button</button>
</template>

これにより、isButtonDisabled がtruthyの場合は disabled属性が設定され、 falthyの場合は disabled属性は取り除かれる。

ja.vuejs.org

HTMLにおいてdisabled属性論理属性の一種で、trueかfalseの値を取る。 また、disabled属性が存在すればtrue、存在しなければfalseと解釈される。そのためMDNに記載の通り、どのような値を指定してもtrueと解釈される。

<!-- 以下はすべてクリック不可能なボタンとしてレンダリングされる -->
<button type="button" disabled>送信</button>
<button type="button" disabled="">送信</button>
<button type="button" disabled="true">送信</button>
<button type="button" disabled="false">送信</button>
<button type="button" disabled="any value">送信</button>

<!-- 以下はクリック可能なボタンとしてレンダリングされる
<button type="button">送信</button>

実際、vueで isButtonDsiabled にfalseを指定した場合のみdisabled属性は取り除かれて選択可能なボタンとしてレンダリングされる。 trueや"any value"、"disabled"などの値をしてもすべて選択不可能なボタンとしてレンダリングされる。

developer.mozilla.org

試してみたところ、Chromeでは値を指定しない <button type="button" disabled> としてレンダリングされ、Firefoxでは値を空文字で指定した <button type="button" disabled="">としてレンダリングされた。Firefoxにて空文字指定でレンダリングされるのは、trueやその他の文字列を指定した場合でも同様で、指定した値自体は無視された。

Nuxtにおけるルーティング設定とCatch-all Route

Nuxtのpagesやserverではfile-system routingを採用しており、URLの構造のソースコードディレクトリ構造を一致させることができる。

nuxt.com

nuxt.com

serverのディレクトリなどにおいて、リクエストパスがいずれのルーティング設定とも一致しない場合のデフォルト値(フォールバック)として、Catch-all Routeを定義できる。 例えば example.com/api/foo/bar/baz のようなリクエストパスに対して、~/server/api/foo/[...].tsというファイルを配置しておくと、~/server/api/foo/bar/baz.ts ファイルが存在しなくても [...].ts が評価されるようになる。

nuxt.com

初めて Catch-all Routeの仕組みを知ったのだが、最初に[...].ts という記述をドキュメントで見たときは適当な文字列で置き換えてね、という意味だと勘違いして混乱した。 実際はCatch-all Routeという仕組みであり本当にブラケットを含むファイルを作成する必要がある。

また、この記法はNext.jsなどでも同じように使われているらしい。

nextjs.org

Nuxt/TypeScriptにおけるパスエイリアス

TypeScriptにおいて import { NuxtAuthHandler } from "#auth"; のようなインポート文を見かけることがある。この#authのような例をパスエイリアスと呼ぶ。 パスエイリアスを利用することで、相対パスコンポーネントを指定する場合に比べてディレクトリ構造の変化に強くなるしわかりやすくなる。 パスエイリアスには# だけでなく、@~など、自由に指定できる。 パスエイリアスは tsconfig.json 内の compilerOptions.paths にて定義する。

www.typescriptlang.org

Nuxt3においてはプロジェクトルートの tsconfig.json{ "extends": "./.nuxt/tsconfig.json" } のような内容となっており、ビルド時に自動的に適切な値で生成してくれる。

nuxt.com

実際に試したところ、./.nuxt/tsconfig.json の compilerOptions.paths は以下のような内容となっていた。

{
  "compilerOptions": {
...
    "paths": {
      "~": [
        ".."
      ],
      "~/*": [
        "../*"
      ],
      "@": [
        ".."
      ],
      "@/*": [
        "../*"
      ],
      "~~": [
        ".."
      ],
      "~~/*": [
        "../*"
      ],
      "@@": [
        ".."
      ],
      "@@/*": [
        "../*"
      ],
      "assets": [
        "../assets"
      ],
      "public": [
        "../public"
      ],
      "public/*": [
        "../public/*"
      ],
      "#app": [
        "../node_modules/.pnpm/nuxt@3.10.1_vite@5.1.1/node_modules/nuxt/dist/app"
      ],
      "#app/*": [
        "../node_modules/.pnpm/nuxt@3.10.1_vite@5.1.1/node_modules/nuxt/dist/app/*"
      ],
      "vue-demi": [
        "../node_modules/.pnpm/nuxt@3.10.1_vite@5.1.1/node_modules/nuxt/dist/app/compat/vue-demi"
      ],
      "#vue-router": [
        "./vue-router-stub"
      ],
      "#imports": [
        "./imports"
      ],
      "#build": [
        "."
      ],
      "#build/*": [
        "./*"
      ],
      "#components": [
        "./components"
      ]
    }
  },
...

GitHub Actionsでmatrixの実行結果(outputs)を取得する

GitHub ActionsにおいてMatrix Jobを実行した結果(outputs)を参照したい。 2024年1月の仕様では、最後に実行したジョブの結果で上書きされるので、ジョブによって結果が異なる場合にそれぞれの値を取得することができない。 これは以下のコミュニティディスカッションでも提案・議論されている。

github.com

現時点では上手く結果を取得する方法は無く、回避策としてartifactsとして結果をアップロード/ダウンロードする方法が提案されている。 似た方法が日本語でも解説されている。

zenn.dev

また、この方法がactionsとしても実装されている。

github.com

根本的にはGitHub側の対応待ちで、一応は解決に向けて実装は進んでいる様子。ただし、まだサーバサイドの実装が完了しておらずもう少し時間が掛かりそう。

github.com