AWS STSのSession Tagを使った一時トークンに付与する必要十分なS3権限の動的管理

こんにちは、こんにちは、SREグループの浜地です。 GO株式会社では、ユーザ向けのタクシーアプリ『GO』をはじめとして乗務員向け端末やタクシー後部座席タブレットなど、様々な種類の端末アプリを開発しております。 これらの端末アプリからファイルのアップロード・ダウンロードなどのためにAWS S3へアクセスが必要なユースケースがあるのですが、新しい機能提供のためにAWS STSのSession Tagを使った方法にて一時トークンを払い出し、必要十分なS3の権限を払い出す方法を今回採用しました。その内容についてご紹介します。

構成概要と要件

事情により詳細に記載できないためある程度簡略化しますが、構成としてはAPIサーバとそれにアクセスする端末アプリという構成で、提携会社ごとに複数台ある端末アプリからアクセスされるというものになります。

この端末アプリから何かしらの方法でS3にファイルをアップロードする機能を実装したいというのが今回のゴールになります。 なお、この機能を実装するにあたって以下の要件が定められています。

  • 端末アプリからファイルをアップロードし、最終的にS3で保管する。
  • 既にS3にあるファイルを端末アプリにダウンロードする。
  • アップロードは端末1台につき1分毎にリクエストされる。
  • アップロードするファイル数は固定ではなく、その都度変わる。ファイルサイズも増えるおそれがある。
  • 端末の数は将来的に数万台になる見込みがある。
  • ある端末アプリAが別の端末アプリBのS3オブジェクトを上書きしたり、ファイル名を参照したり、ダウンロードするのを禁止するような権限制御を入れる。
  • 端末に依存しないファイル群(マスターデータ)はダウンロードできるようにする。可能であれば一覧を取得できるようにする。

従来の手法と不採用理由

端末アプリからAWSAPIを叩く手段としていくつか考えられますが、今回はAWS STSのSession Tag機能を採用しました。 他の手法と不採用の理由について紹介します。

APIサーバを介してアクセス

APIサーバにアップロードおよびダウンロードするためのエンドポイントを用意し、S3のプロキシとして端末アプリとデータを送受信する方法です。 この方法はファイルのサイズやリクエスト頻度によってAPIサーバがボトルネックになる可能性が非常に高く、将来的にファイルサイズやリクエスト頻度が増える可能性が考えられることから不採用としました。

IAMユーザのアクセスキーによるアクセス

AWS IAMにてIAMユーザを発行し、そのユーザのアクセスキーを端末アプリに埋め込んで利用する方法です。考えられる最も簡単な方法ですが、以下の理由により不採用としました。

  • アクセスキー漏洩というセキュリティリスクを受け入れられなかった(アクセスキーを共通化する場合なおさら)
  • 端末全台分のIAMユーザの発行運用が現実的でない
  • 通化する場合、端末ごとの権限制御が非常に困難

S3 署名付きURL(Presigned URL)

バックエンドサーバ側でS3のAPIを呼び出し、アップロードやダウンロード用の署名付きURL(Presigned URL)を発行する方法です。URLに付与された認証情報は有効期限があるため、漏洩に対して一定のリスク回避が可能となりますが、この方法も同様に以下の理由により不採用としました。

  • 署名付きURLは単一オブジェクトに対するURLのため、アップロードするファイル数が一定ではない本要件との相性が悪い。アップロードファイル数分APIリクエストをするとオーバーヘッドが大きくなってしまい将来的にボトルネックになると懸念したため

また、ファイル一覧が署名付きURLで実現できないため、APIサーバがこの機能を提供する必要があります。

AWS IoT Coreを利用したアクセス

AWS IoT Coreを利用することで、証明書を利用した認証とアクセスが実現できますが、以下の理由により不採用としました。

  • クライアント側である端末アプリの実装工数の都合により見送り

AWS STS Session Tagの採用とポリシー策定

前述の通り、それぞれの不採用理由があったなかで今回はAWS STSのSession Tagを利用した一時トークンを発行する方法を採用しました。

AWS STS Session Tagとは

AWS STSのAssumeRoleアクションでは、必須であるIAMロールARNやSession Nameだけでなく、Tagを引数に取ることが可能です。 ここで指定したTagはIAMポリシー上で変数として参照することが可能で、これを利用することで動的にポリシーを制御することが可能です。

以下にSession Tagを利用したAssume Roleのコマンドを記載します。

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/role-to-assume \
  --role-session-name sample \
  --tags Key=company_id,Value=abcde \ # 今回のキモ
  --duration-seconds 900

このコマンドで company_id:abcde というタグを設定しており、S3のPath等で abcde という値を使うことができる、ということになります。 今回はこうしたSession Tagを利用して適切な権限を与える方法について紹介します。

なお本記事では、以下の設定値とS3の構成を一例として定めます。

設定値名 設定値
Session Tagその1 company_id
Session Tagその2 device_id
S3バケット device-data-upload-bucket
構成要素 S3パスパターン 備考
アップロード先 device_logs/${company_id}/${device_id}/ company_iddevice_id ごとにアップロード先を分ける。異なる company_id および device_id からアップロード不可能にする
ダウンロード元その1 master_data/ すべての company_id および device_id からファイルの取得を許可する。ファイルは日付別に分けられており、最新の日付のものを取得したい
ダウンロード元その2 device_metadata/${company_id}/${device_id}/ company_iddevice_id ごとにダウンロード元を分ける。異なる company_id および device_id からダウンロード不可能にする

ファイルアップロードの権限制御

ファイルのアップロード先は company_id および device_id をキーに含み、端末ごとに区別するようにします。 一時トークンのSession Tagにこれらの値を設定すると、AWS IAMポリシー上で aws:PrincipalTag/{Session Tag名} という変数が利用することができます。 これを利用して、S3のアップロード先ARNを設定します。これにより、一時トークンごとに動的にアップロード先のPathを制御することができます。

{
  "Effect": "Allow",
  "Action": ["s3:PutObject"],
  "Resource": [
    "arn:aws:s3:::device-data-upload-bucket/device_logs/${aws:PrincipalTag/company_id}/${aws:PrincipalTag/device_id}/*"
  ]
}

このように ${aws:PrincipalTag/company_id}${aws:PrincipalTag/device_id} といった変数をリソース名で指定することで、Assume Role時に指定したTagの値以外のS3 Pathへの権限をフィルタすることができます。 ちなみに、許可されたPath以外にアップロードしようとすると以下のように見慣れたS3の権限エラーが発生します。 今回は例として company_id = 1, device_id = 1 のタグでAssumeRoleしたとします。

# 成功例
aws s3 cp index.html s3://device-data-upload-bucket/device_logs/1/1/

upload: ./index.html to s3://device-data-upload-bucket/device_logs/1/1/index.html

---
# 失敗例
aws s3 cp index.html s3://device-data-upload-bucket/device_logs/2/2/

upload failed: ./index.html to s3://device-data-upload-bucket/device_logs/2/2/index.html An error occurred (AccessDenied) when calling the PutObject operation: User: arn:aws:sts::012345678901:assumed-role/iam-role-name/session-name is not authorized to perform: s3:PutObject on resource: "arn:aws:s3:::device-data-upload-bucket/device_logs/2/2/index.html" because no identity-based policy allows the s3:PutObject action

ファイル一覧とダウンロードの権限制御

ファイルのダウンロードは company_id および device_id で区別されるものとそうでないものが含まれます。 後者である区別されないものについては、日付ベースでファイルがアップロードされており、常に最新のものを利用するものとします。以下のようなファイル構成であると想定してください。

master_data/20251101.json
master_data/20251127.json

ここで利用するのが s3:ListBucket アクションですが、このアクションはResource句にS3のPath(S3オブジェクト)を設定できないため、Condition句を使うことで対応します。 S3では s3:prefix という条件キーが用意されているため、これを利用しつつSession TagでPath(S3オブジェクト)の動的制御を行います。 以下がそのポリシーとなります。

{
  "Effect": "Allow",
  "Action": ["s3:ListBucket"],
  "Resource": [
    "arn:aws:s3:::device-data-upload-bucket"
  ],
  "Condition": {
    "ForAnyValue:StringLike": {
      "s3:prefix": [
        "master_data/*",
        "device_metadata/${aws:PrincipalTag/company_id}/${aws:PrincipalTag/device_id}/*"
      ]
    }
  }
}

上述のポリシーを利用することで、s3:ListBucket の制御が実現します。 例えば company_id = 1, device_id = 1としたとき、aws s3 ls s3://device-data-upload-bucket/5/5/ は403エラーとなります。

なお、ダウンロードに利用する s3:GetObject はダウンロードと同様の設定になるため、説明は省略します。

{
  "Effect": "Allow",
  "Action": ["s3:GetObject"],
  "Resource": [
    "arn:aws:s3:::device-data-upload-bucket/master_data/*",
    "arn:aws:s3:::device-data-upload-bucket/device_metadata/${aws:PrincipalTag/company_id}/${aws:PrincipalTag/device_id}/*"
  ]
}

最終的な構成

これら元に、最終的に以下のような構成で実装しました。

APIサーバは一時トークンの発行リクエストだけ処理すればよく、かつアップロードやダウンロードはAPIサーバは関与せず端末アプリがS3と直接やりとりすることから、APIサーバがボトルネックになることを避けることができました。 今回は特に「アップロードするファイルの数が未知数」「ファイルの一覧を取得したい」という要件から署名付きURLを採用できなかったのですが、AWS STSのSession Tagという機能があったことで少ない労力で実現できたことにとても嬉しく思います。 S3だけでなく、その他のAWSリソースでもこの方法は採用できるため、なにかしらの参考になれば幸いです。