ForgeVision Engineer Blog

フォージビジョン エンジニア ブログ

S3 署名付き URL を 12 時間以上利用したい (Elastic Beanstalk も使ってみた)

こんにちは、AWS チームのインフラエンジニア尾谷です。
先週、会社で以下の記事を教えてもらいました。

repost.aws

みなさんは、普段、Amazon S3 の署名付き URL を使っていらっしゃいますか?

長期間払い出す用途であれば Amazon Cognito などの認可サービスを利用した方が適切だと思いますが、上記の記事によると、どうやら Amazon EC2 インスタンスを使って AWS IAM ロールで生成した署名付き URL は最長で 6 時間で有効期限切れになるようです。

この情報が正しいのか?実際に AWS Cloud9 と AWS Elastic Beanstalk を利用して確認します。

概要

この記事で取り上げるのは、以下の 2 と 4 です。

  1. root ユーザーが発行した署名は最長 1 時間
  2. インスタンスプロファイルロールを使った場合は最長 6 時間
  3. IAM ユーザー (STS) が発行した署名は最長 36 時間
  4. IAM ユーザーが発行した署名は最長 7 日間

Cloud9 で素組みの PHP ページを作成

まずは Cloud9 で環境を作成します。
フレームワークを使わずに PHP アプリケーションを素組みします。

AWS SDK をインストール

Elastic Beanstalk のドキュメントに PHP アプリケーションの作り方が記載されていましたので参考にしながら Cloud9 のターミナルにコマンドを投入していきました。

docs.aws.amazon.com

sudo yum install -y php
sudo yum install -y php-mbstring
sudo yum install -y php-intl
curl -s https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
composer

Composer がインストールできました。

続いて、プロジェクトディレクトリを作成し、その中に AWS SDK for PHP v3 をインストールします。

mkdir otani-php
cd otani-php
composer require aws/aws-sdk-php

プロジェクトディレクトリで aws-sdk-php 3.275 が利用できるようになりました。
これでミドルウェアの部分は準備が完了しました。

index.php

続いて、コンテンツにあたる index.php を以下のような形でコーディングしました。
内容は非常にシンプルで、ページを開くと 1 分後に有効期限が切れる署名 URL リンクが表示されるようにしています。

<html>
    <head>
        <title>otani-test-php</title>
    </head>
    <body>
        <?php
            require 'vendor/autoload.php';
            use Aws\S3\S3Client;
            $s3Client = new Aws\S3\S3Client([
                'region' => 'ap-northeast-1',
                'version' => '2006-03-01',
                'credential' => 'default'
            ]);
            $cmd = $s3Client->getCommand('GetObject', [
                'Bucket' => 'バケット名,
                'Key' => 'オブジェクト名'
            ]);
            $request = $s3Client->createPresignedRequest($cmd, '+1 minutes');
            $s3PresignedUri = $request->getUri();
            echo "<p><a href=\"";
            echo $s3PresignedUri;
            echo "\" target=\"_blank\"/>1 分のリンク</a><br/>";
            echo $s3PresignedUri;
            echo "</p>";
        ?>
    </body>
</html>

プレビュー

マスクした部分が多く分かりづらいのですが、以下は Cloud9 にてページをプレビューした状態です。
画面向かって左側がコードで、右側がプレビューとなっております。

Credential と記載された部分 (AS で始まり、ZWA で終わっている箇所) を確認すると、どのアクセスキー、あるいは一時的な STS クレデンシャルで発行された URL か判断できます。

Cloud9 で利用される認証情報

ここから少し余談になりますが、Cloud9 の認証に関しても調査してみました。
Cloud9 のデフォルトの認証は、AWS managed temporary credentials です。

Cloud9 に割り当てられた IAM ロールは AWSCloud9SSMAccessRole です。

この AWSCloud9SSMAccessRole には、AWSCloud9SSMInstanceProfile というポリシーが割り当てられています。

検証のため、以下、s3 の対象のバケットからファイルを取得できなくするポリシーをアタッチしました。このロールを使って署名付き URL が発行されているのであれば、エラーになるはずです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DenyS3SpecificBucket",
            "Effect": "Deny",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::対象のバケット名/*"
        }
    ]
}

暫定的に拒否ポリシーをアタッチしましたが、署名付き URL は発行できてしまいました。
Cloud9 のインスタンスに割り当てられた AWSCloud9SSMAccessRole は通らずに、AWS managed temporary credentials を介して処理が行われていることが明確になりました。

次に、AWS managed temporary credentials を外すと、そもそも Cloud9 に備わっている built-in web server が起動せず検証ができませんでした。一時的に設定したインラインポリシーを削除して、AWS managed temporary credentials に戻して、この検証はここまでとしました。

Elastic Beanstalk で PHP アプリケーションをデプロイ

次は、EC2 インスタンスでの挙動を確認していきます。
PHP アプリケーションは、Elastic Beanstalk で環境をデプロイしました。

Elastic Beanstalk のコンソールにアクセスし、環境 から ウェブサーバー環境 を選択しました。
アプリケーション名は適当に設定しました。

環境名は自動で入力されました。

プラットフォームは PHP 8.1 としました。

ほとんどの項目をデフォルトの設定のまま次へ次へと進めていきましたが、ストレージは gp3 にしました。

モニタリングはベーシックだとエラーになったので、拡張を選択し、最低限のインスタンスヘルスチェックだけにしました。

デプロイできました!
発行されたエンドポイントにアクセスすると Welcome ページが表示されました。

PHP のソースを Elastic Beanstalk に入れる

Elastic Beanstalk でアプリケーションがデプロイできたので、コードをアップロードしたいと思います。
Cloud9 で作成した PHP コードを以下のように更新しました。

  • 1 分の署名付き URL
  • 2 時間の署名付き URL
  • 6 時間の署名付き URL
  • 7 時間の署名付き URL
  • 24 時間の署名付き URL
  • 48 時間の署名付き URL
  • 7 日の署名付き URL

Elastic Beanstalk コンソールからアップロードします。
ドキュメントに記載の方法で zip 圧縮をして、

zip ../otani-php.zip -r * .[^.]* -x "vendor/*"

zip ファイルを download しました。

Elastic Beanstalk の画面で、[アップロードとデプロイ] ボタンをクリックして、

zip ファイルを指定してアップロードしました。

デプロイ完了後にブラウザでアクセスすると、以下のような形でページが表示されました。
※ マスクばかりで見づらく申し訳ありません。

検証

前準備が長くなりましたが、ここから検証を進めていきます。

Elastic Beanstalk のインスタンスプロフィールロールのセッション時間は 1 時間です。

08:23 に Cloud9 と、Elastic Beanstalk の両環境にで署名付き URL を発行しました。

1 分経過

1 分後には以下のようにアクセスができなくなっていました。

最初にアクセスした際のキャッシュが残っており、URL にアクセスすると画像が表示されてしまいました。
リロードすると AccessDenied の画面に切り替わりました。これは検証時に注意すべき点だと感じました。

※ スクリーンショットを取り忘れたため、09:10 に再度アクセスしました。タイムスタンプがズレております。ご容赦ください。

1 時間経過

1 時間が経過しました。
Cloud9 側の署名付き URL リンクは全滅しました。
一方で、Elastic Beanstalk 側のリンクは生きており、IAM ロールの最大セッション時間は影響しないように見受けられました。

Cloud9 は前述のように AWS managed temporary credentials にて発行されているためか、その後も何度か URL を発行しましたが、10 分や、30 分で全ての画像が有効期限切れになるなど、挙動が不安定でした。

2 時間経過

10:30 にアクセスしてみました。 2 時間以上が経過しているため、Elastic Beanstalk 環境もアクセスできなくなっていました。

5 時間、6 時間経過

発行から 5 時間後の、13:30 にアクセスしてみましたが変わらずでしたが、6 時間後にアクセスしたところ、以下の通り、6 時間のリンクも 7 日のリンクもアクセスができなくなりました。


ドキュメント通り、EC2 インスタンスのロールで発行した署名付き URL の有効期限は 6 時間のようです。

アクセスキーを利用

最後にアクセスキーを使った署名付き URL 発行を試みました。

ini プロバイダーという手法を使って、暫定的に Credential 情報を ini ファイルに書き出して再度試してみました。

docs.aws.amazon.com

index.php で書き換えた部分だけ抜粋しておきます。

require 'vendor/autoload.php';
$profile = 'otani-ak';
$path = './otani.ini';
$provider = CredentialProvider::ini($profile, $path);
$provider = CredentialProvider::memoize($provider);

$s3Client = new Aws\S3\S3Client([
      'region' => 'ap-northeast-1',
      'version' => '2006-03-01',
      'credentials' => $provider
]);

見極めのため、EC2 インスタンスの IAM ロールに AWSDenyAll ポリシーを入れて実行したところ、署名付き URL がエラーになったのでアクセスキーで払い出せていると思います。
発行された URL にも IAM ユーザーのアクセスキーが記録されていました。

以下、状況の変化を確知した時間です。

時間 発行からの経過時間 状況
06:44 0 min URL 発行
06:50 6 min 1 min URL 無効を確認
09:50 3 hour 6 min 120 min URL 無効
12:50 6 hour 6 min 変化なし
13:50 7 hour 6 min 360 min URL 無効
翌 12:00 over 24 hour 48 hour, 7 days は有効のまま!!

アクセスキーを使うことで 6 時間の壁を突破することができました!

まとめ

アクセスキーを使うのはアンチパターンで、IAM ロールが推奨されます。
とはいえ、今回のような要件を満たすには必要な対応だと思います。

また上記では、アクセスキーを ini ファイルに埋め込みましたが、AWS Secret Manager にアクセスキーとシークレットキーを保存して、AWS SDK でコールして呼び出すようにすればセキュリティは担保できると考えました。

以上です。
最後までお読みくださりありがとうございました。