IaaS SaaS その他

一時利用に使えそうなS3バックエンドのクラウドストレージを作ってみた

こんにちは、ひろかずです。

普段使いのクラウドストレージが急に使えなくなったらどーするの?という声が聞こえてきたので、一筆書きます。

もう少し丁寧なバックグラウンド

顧客とのデータ共有で利用している普段使いのクラウドストレージが、障害で長期間(とは言っても、数日〜2週間程度)利用できなくなったらどうするでしょうか?

メールで凌ぐのでしょうか?
添付ファイルをクラウドストレージの共有リンクに置き換えるような仕組みを使っていても、肝心のクラウドストレージが利用できないのであれば、直接メールに添付する事になります。

メール添付の場合、誤送信が心配なので、PPAPの時代に戻るのでしょうか?
パスワードを同じメールで送ってしまっては、意味がありませんよね?

重要なデータをやりとりする上で、アクセス権管理やアクセスログを取得できる事がビジネス上の要件の場合もあります。
慌てて別のクラウドストレージを契約しようとしても、時間とお金がかかります。

共有相手のクラウドストレージを利用させてもらうというやり方もありますね。
でも、データの場所がバラバラになってしまって、データを探しづらいのと、クラウドストレージ復旧後のデータの戻しも少し手間に思えます。
緊急時だから仕方がないと割り切るのでしょうか?

そんなわけで、一時的に利用できそうなクラウドストレージをクイックに立ち上るのはどうか?というお話をしようと思います。

忙しい人向けの要約

  • 一時的なクラウドストレージとして、 AWS Transfer Family Web App を試してみたという話です。
  • ずっと維持するにはお金がかかりますが、ある程度準備しておくことで、使いたい時にクイックに組み上げることはできそう。
  • ユーザー体験は、リッチではないものの、わかりやすさとシンプルさがあり、セキュリティ面も及第点と言えるので、一時的な退避先としては十分そう。

本件を考えるにあたっての要件

本件を考えるにあたって、以下の要件を設定します。

機能要件

  • 構築やデータ移行を実施するPCのOSの縛りがない。
  • 最低限、二要素認証での認証とグループメンバーシップでのフォルダアクセス制御、ロギングができること。
  • ブラウザがあれば、接続元のOSを問わず利用できる。

非機能要件

  • 普段使いのクラウドストレージのデータを別の場所にバックアップしている。
  • 普段使いのクラウドストレージは、数日以内に復旧する。
  • 退避先のクラウドストレージには、直近必要なデータのみを部分的に保存する。
  • 退避先のクラウドストレージには、社外ユーザーも招待する想定とする。
  • 環境を用意するにあたっての維持費はできるだけかからないようにする。
  • 求めるユーザー体験は、「アクセスのしやすさ」と「シンプルでわかりやすい」であり、リッチなユーザー体験は求めない。

今回試したソリューション

今回の要件には、AWS Transfer Family Web Appsが適合すると考えました。

  • AWS Transfer Family Web Appsは、re:Invent 2024で発表されたサービスで、S3バックエンドのブラウザで動作するシンプルなWeb UIのファイル転送アプリケーションです。

ざっくり構成

インプット

事前準備

事前準備として、以下の作業を実施します

  • データソースを配置するs3バケットを用意し、データソースを配置する
  • データソースを配置するs3バケットのオブジェクトロギングを設定する
  • IAM Identity Centerのアカウントインスタンスを作成する
  • IAM Identity Centerにユーザーとグループを作成する

データソースを配置するs3バケットを用意し、データソースを配置する

  • WS Transfer Family Web Appsを配置するリージョンを決定し、同じリージョンにデータソースを配置するs3バケットを作成し、s3バケットのarnを控えておきます。
  • s3バケットにフォルダやファイルを配置しておく。

データソースを配置するs3バケットのオブジェクトロギングを設定する

以下の手順を参考に、データソースを配置するs3バケットのオブジェクトロギングを設定します。

  • 高度なイベントセレクターを使用して、Amazon S3 バケットのすべての Amazon S3 イベントをログに記録します。

s3のログには、以下の2つの方式があります。
CloudTrailにs3バケットのオブジェクトログを出力する方式
s3のサーバーログをs3バケットに出力する方式
今回はユースケースを鑑みて、アクセスしたオブジェクトのファイル名の日本語表示が容易な「CloudTrailにs3バケットのオブジェクトログを出力する方式」を採用します。

AWSコンソールのCloudtrail管理画面を開き、CloudTrail証跡(なければ作成)のデータイベント欄の[編集]を選択します。

データイベントを選択し、データイベントタイプでは[s3]を選択します。

ログセレクターテンプレートを[カスタム]に設定し、高度のイベントセレクターに以下のように設定します。

  • フィールド:resource.ARN
  • オペレーター:次で始まる
  • Value:データソースを配置するs3バケットのarn

設定後のJSONビューは以下のようになります。

[
  {
    "Name": "xxxx-Transferfamily-Objectlog",
    "FieldSelectors": [
      {
        "Field": "eventCategory",
        "Equals": [
          "Data"
        ]
      },
      {
        "Field": "resources.type",
        "Equals": [
          "AWS::S3::Object"
        ]
      },
      {
        "Field": "resources.ARN",
        "StartsWith": [
          "arn:aws:s3:::YOURBACKETNAME"
        ]
      }
    ]
  }
]

AWS IAM Identity Centerアカウントインスタンスを作成する

以下URLを参考にして、AWS IAM Identity Centerアカウントインスタンスarnを控えておく。

既にOrganization内でAWS IAM Identity Centerを有効にしている場合

AWS Organization 管理アカウントのインスタンスが既に存在している場合、これを利用するかどうかを検討します。

今回のユースケースでは、「外部ユーザーを招待する」という要件があるため、組織で利用しているAWS IAM Identity Centerとは別のメンバーアカウント内のアカウントインスタンスを利用することにしました。

AWS IAM Identity Center組織インスタンスが2023年11月15日以前に有効にしている場合、メンバーアカウントでアカウントインスタンスを有効にする前に、メンバーアカウントでアカウントインスタンスを有効にしておく必要があります。

IAM Identity Centerにユーザーとグループを作成する

今回は、IAM Identity Centerディレクトリをアイデンティティソースとします。
以下URLを参考にユーザーとグループを作成します。

ユーザー作成イメージ

ここまでの状態

事前準備が終わると、こんな状態になっています。

環境構築1. Transfer Family Web Appsの作成

AWS管理コンソールのTransfer Family管理画面を開いて、左側ペインの[ウェブアプリ]を選択し、[ウェブアプリを作成]を選択します。

「Identity Centerのアカウントインスタンス」に接続を選択し、IAMアイデンティティセンターのarnが一致していることを確認する。

  • 今回は、組織インスタンスの他にTransfer Family Web Apps用のIdentity Centerのアカウントインスタンスを用意する前提です。

作成するTransfer Family Web Appsに付与するIAMロールを作成して、設定します。

  • ここでは、以下のIAMロールを作成しますが、 web-app-identity-bearer の方を指定します。
    • web-app-identity-bearer
    • access-grants-location

ユニット数を設定します。

  • 今回のユースケースであれば、5分間で250セッション(つまりユニット数は1)あれば十分でしょう。
    • ユニット数 x アプリケーションの稼働時間で課金が発生します。

[次へ]を選択します。
ウェブアプリのデザイン画面では、表示名とロゴを設定して、[次へ]を選択します。

[Webアプリを作成]ボタンを選択すると、Webアプリが作成されます。

  • アクセスエンドポイントの値を控えておきます。

ここまでの状態

現時点では、このような状態になっています。

環境構築2. s3バケットにCROSを設定する

s3バケット側に作成したTransfer Family Web Appsからのアクセスを受け付ける設定をします。

AWSマネジメントコンソールで事前準備で用意したs3バケットにアクセスし、[アクセス許可設定]タブを選択します。

CROSの項目を編集し、以下のように設定します。

  • 複数のアプリケーションからアクセスさせる場合は、AllowedOriginsに値を増やしていく形になります。
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "DELETE",
            "HEAD"
        ],
        "AllowedOrigins": [
            "https://webapp-xxxxxxxxxxxxxxxxx.transfer-webapp.ap-northeast-1.on.aws"
        ],
        "ExposeHeaders": [
            "last-modified",
            "content-length",
            "etag",
            "x-amz-version-id",
            "content-type",
            "x-amz-request-id",
            "x-amz-id-2",
            "date",
            "x-amz-cf-id",
            "x-amz-storage-class",
            "access-control-expose-headers"
        ],
        "MaxAgeSeconds": 3000
    }
]

動作確認

ターミナルから以下コマンドを実行します。

curl -v -X OPTIONS \
 -H "Origin: https://webapp-xxxxxxxxxxxxxxxxx.transfer-webapp.ap-northeast-1.on.aws" \
 -H "Access-Control-Request-Method: PUT" \
 -H "Access-Control-Request-Headers: Authorization" \
 -H "Access-Control-Expose-Headers: x-amz-meta-custom-header"\
    "https://buckename.s3.ap-northeast-1.amazonaws.com"   

200 OKというレスポンスが出ることを確認します。

前略
* Request completely sent off
< HTTP/1.1 200 OK
後略

ここまでの状態

現時点では、このような状態になっています。

環境構築3. AWS Transfer Family Web Appsにユーザーのアクセス権限 ( 認証 ) を付与する

以下URLを参考に、作成したグループをAWS Transfer Family Web Appsに登録します。

AWS Transfer Family管理コンソールの[ウェブアプリ]を選択して、ウェブアプリ名を選択します。

[ユーザーとグループの割り当て]を選択します。

今回は、事前に作成しておいたグループを割り当てるので、[既存を割り当てユーザーとグループ]を選択します。

検索窓にグループ名のプレフィックスを入力して、選択します。
今回は、以下の3グループを登録しました。

ここまでの状況

現時点では、このような状態になっています。

環境構築4. AWS Transfer Family Web Appsにユーザーのアクセス権限 ( 認可 ) を付与する

以下URLを参考に、AWS Transfer Family Web Appsを経由したs3へのアクセス許可を設定します。

AWSマネジメントコンソールのs3管理画面を開き、データ保存先s3バケットが配置されているリージョンに切り替えます。
左側ペインの[Access Grants]を選択し、[s3 Access Grants インスタンスを作成]を選択します。

<リージョン名>にIAMアイデンティティセンターインスタンスを追加のチェックボックスをONにして、冒頭で控えておいたIAM アイデンティティセンターインスタンス ARNを入力し、[次へ]を選択します。

[s3の参照]を選択し、データ保存先s3のバケットを選択します。

  • 範囲の欄にロケーションが自動入力されます。

[既存のIAMロールから選択]を選択して、事前に作っておいたIAMロール(access-grants-location)を指定して、[次へ]を選択します。

ここでは、社内グループのユーザーの認可範囲を設定します。

  • 社内グループのユーザーは、データ保存先s3バケットをすべて見られる想定ですので、権限範囲は、 s3://BACKETNAME/* とします。
  • IAMアイデンティティセンターグループIdentityに社内グループのIDを設定します。

[次へ]を選択します。

確認画面で、[別の権限を作成]を選択します。

同じ要領で、顧客1グループと顧客2グループ向けの権限を作成します。

  • 顧客1グループは、データ保存先s3バケットの顧客1用フォルダのみを見られる想定ですので、権限範囲を s3://BACKETNAME/ext1/* とします。
  • 顧客2グループは、データ保存先s3バケットの顧客1用フォルダのみを見られる想定ですので、権限範囲を s3://BACKETNAME/ext2/* とします。
  • それぞれ、IAMアイデンティティセンターグループIdentityに社内グループのIDを設定します。

設定が完了すると、こんな感じになります

ここまでの状況

最終的にこのようになっています。

動作確認 ( 社内ユーザー )

社内ユーザーでログインしてみます。

  • AWS IAM Identity CenterポータルのURLは、各ユーザーを作成した時に登録したメールアドレスに送付されます。

アプリケーションが表示されていますね。

バケット名が表示されているので、選択します。

バケット内のすべてのコンテンツが表示されています。

その下の階層にもアクセスできますね。

動作確認 ( 顧客1ユーザー )

顧客1ユーザーでログインしてみると、ext1フォルダだけが見られます。

  • 顧客2ユーザーの結果は割愛します。

動作確認(ロギング)

(作成していない場合)CloudTrailからAthenaテーブルを作成します。

  • Athenaのストレージの場所を指定して、[Create table]を選択して、テーブル名を設定します。

サインインログの確認

IAM Identity Centerにサインインしたログを確認してみます。

select 
  eventTime, 
  eventName, 
  eventSource, 
  sourceIpAddress, 
  useridentity.username as username,
  json_extract_scalar(serviceeventdetails, '$.UserAuthentication') as authorization_results,
  userAgent
from <TABLENAME>
where 
    eventName = 'UserAuthentication'
    AND eventTime BETWEEN '2025-05-01T23:00:00Z' and '2025-05-02T01:00:00Z'

クエリーは、IAM Identity Center サインインシナリオのイベント例を参考にするといいでしょう

https://docs.aws.amazon.com/ja_jp/singlesignon/latest/userguide/sign-in-events-examples.html

ユーザー名で、ログインユーザーが識別できていますね。

削除ログの確認

オブジェクトを削除したログを検索してみます。

SELECT
  eventTime, 
  eventName, 
  eventSource, 
  sourceIpAddress, 
  userAgent, 
  json_extract_scalar(requestParameters, '$.bucketName') as bucketName, 
  json_extract_scalar(requestParameters, '$.key') as object,
  userIdentity.arn as userArn
FROM <TABLENAME>
WHERE
  eventName = 'DeleteObject'
  AND eventTime BETWEEN '2025-05-02T00:00:00Z' and '2025-05-02T01:00:00Z'

クエリーは、Amazon S3 オブジェクトアクセスリクエストを識別するための Athena クエリの例を参考にすると良いでしょう

https://docs.aws.amazon.com/AmazonS3/latest/userguide/cloudtrail-request-identification.html#cloudtrail-identification-object-access

削除したファイルが、userArnとともに出力されました。

  • ただ、このuserArnはユーザーそのものを示していません。
  • ユーザーがassume roleした時に引き受けたセッション名だと考えてください。

操作ユーザーの特定

削除ログで取得したuserArnを基に、操作ユーザーを特定します。
userArnから以下の部分(セッション名)を控えておきます。

  • access-grants-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
arn:aws:sts::123456789012:assumed-role/xxxx-access-grants-location/access-grants-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

以下のクエリーで検索してみます。

SELECT 
  eventTime, 
  eventName, 
  eventSource, 
  sourceIpAddress, 
  userAgent, 
  json_extract_scalar(requestParameters, '$.roleSessionName') as SessionName, 
  additionaleventdata,
  userIdentity.arn as userArn
FROM
  <TABLENAME>
WHERE
  eventTime BETWEEN '2025-05-01T23:00:00Z' and '2025-05-02T01:00:00Z'
  AND eventName = 'AssumeRole'
  AND sourceIpAddress = 'access-grants.s3.amazonaws.com'
  AND requestParameters like '%xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx%'

以下のようにHitしました。

additionaleventdataの内容は以下のような形です。

  • sessionContext内の”identitystore:UserId”の値「17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx」を控えておきます。
  • “identitystore:UserId”の値が、IAM Identity Centerに登録したユーザーのUserIdです。
{"sessionContext":{"identitycenter:CredentialId":"省略","identitystore:UserId":"17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx","identitystore:IdentityStoreArn":"arn:aws:identitystore::123456789012:identitystore/d-xxxxxxxxxx"}}

AWS CLI Toolsで以下コマンドを実行すると、当該”identitystore:UserId”の値を持つユーザー名とメールアドレスを特定できます。

  • xxx-intというユーザー名のようですね。
  • ユーザー名から特定できない場合のために、利用者のメールアドレスも表示させます。
aws identitystore list-users --identity-store-id "d-xxxxxxxxxx" --region "AWS_IDENTITY_CENTERs_REGION" --query 'Users[?UserId == `17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx`].[{UserId: UserId, UserName: UserName, Email: Emails[].[Value][]}][]'
[
    {
        "UserId": "17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "UserName": "xxx-int",
        "Email": [
            "xxx+int@example.com"
        ]
    }
]

特定ユーザーの操作を追跡する場合

逆パターンとして、特定のユーザーが行った操作を追跡する場合は、以下の手順になります。
AWS CLI Toolsで以下コマンドを実行して、UserIdを特定します。

UserNameで特定する場合

aws identitystore list-users --identity-store-id "d-xxxxxxxxxx" --region "AWS_IDENTITY_CENTERs_REGION" --query 'Users[?UserName == `xxx-int`].[{UserId: UserId, UserName: UserName, Email: Emails[].[Value][]}][]'
[
    {
        "UserId": "17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "UserName": "xxx-int",
        "Email": [
            "xxx+int@example.com"
        ]
    }
]

メールアドレスで特定する場合

aws identitystore list-users --identity-store-id "d-xxxxxxxxxx" --region "AWS_IDENTITY_CENTERs_REGION" --query 'Users[?contains(Emails[].Value,`xxx+int@example.com`)].[{UserId: UserId, UserName: UserName, Email: Emails[].[Value][]}][]'
[
    {
        "UserId": "17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "UserName": "xxx-int",
        "Email": [
            "xxx+int@example.com"
        ]
    }
]

特定したUser ID「17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx」をキーにして、Athenaでセッション名を集計したテーブル「sessionname」を作成します。

CREATE TABLE sessionname
AS
    SELECT 
      json_extract_scalar(requestParameters, '$.roleSessionName') as SessionName
    FROM
      <TABLENAME>
    WHERE
      eventTime BETWEEN '2025-05-01T23:00:00Z' and '2025-05-02T01:00:00Z'
      AND eventName = 'AssumeRole'
      AND sourceIpAddress = 'access-grants.s3.amazonaws.com'
      AND additionaleventdata like '%17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx%' 

作成したセッション名を集計したテーブル「sessionname」の中身はこんな感じです。

  • 特定したUser ID「17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx」が作成したセッション名が集計できました。
SELECT * FROM sessionname

削除ログの確認で取得したuserArnを基に、userArnを再形成する文字列を特定します。
userArnから以下の部分を控えておきます。

  • arn:aws:sts::123456789012:assumed-role/xxxx-access-grants-location/
arn:aws:sts::123456789012:assumed-role/xxxx-access-grants-location/access-grants-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

作成したセッション名を集計したテーブル「sessionname」と結合してuserArnを再形成するとこんな感じになります。

SELECT concat('arn:aws:sts::123456789012:assumed-role/xxxx-access-grants-location/' , "sessionname") AS SelecteduserArn FROM sessionname

特定したUser ID「17f4aa38-xxxx-xxxx-xxxx-xxxxxxxxxxxx」が作成したセッション名からuserArnができたところで、これを条件にTransfer Family Web Appsに対する操作ログを取得します。

SELECT 
  eventTime, 
  eventName, 
  eventSource, 
  userAgent, 
  json_extract_scalar(requestParameters, '$.bucketName') as bucketName, 
  json_extract_scalar(requestParameters, '$.key') as object,
  userIdentity.arn as userArn 
FROM <TABLENAME>
WHERE
  eventName != 'ListObjects'
  AND userIdentity.arn = ANY (
    SELECT
        concat('arn:aws:sts::123456789012:assumed-role/xxxx-access-grants-location/' , "sessionname") AS SelecteduserArn 
    FROM sessionname
  )
  AND eventTime BETWEEN '2025-05-02T00:00:00Z' and '2025-05-02T01:00:00Z';

特定したユーザーIDが操作した一覧が取得できました。

おわりに

事前にユーザーやコンテンツを用意しておけば、簡単なUI操作でs3バックエンドのクラウドストレージが作成できました。
今回の作業で、機能要件と非機能要件は満たされているものと言えます。

ログの追跡性は、機能としては十分ですが、使い勝手については改善の余地はありそうです。

今日はここまでです。
お疲れ様でした。

ひろかず

実装可能・運用可能なセキュリティをライフワークとして、セキュリティエンジニアやってます。
主戦場のAWSに拘らず、広くチャレンジを続けています。
関連キーワード:PCIDSS, CSIRT, EDR, SASE, CASB, SIEM, OSINT, MSSP, CySec, CISSP
個人ブログもご贔屓に
https://medium.com/@fnifni21
https://www.fnifni.net