TT Lab
はじめる
学ぶ 学習パス コース

オブジェクトストレージとS3

署名付きURLが解く問題

TT Labで続きを見る

一言でいうと

署名付きURLは、「この人がこのオブジェクトに、この時間だけ、この操作だけできる」を、署名された1つのURLで表したものです。バイトがアプリサーバーを通過する必要がなくなります。

なぜ必要なのか

ユーザーがアップロードしたファイルをダウンロードさせる、最も素朴な実装は、次のとおりです。アプリケーションが権限を確認し、ストレージからオブジェクトを読み、レスポンス本文として流します。

この方式の問題は、規模で現れます。500MBの動画100個が同時にダウンロードされると、アプリサーバーが50GBを中継します。ワーカーがその間ふさがり、帯域幅がアプリサーバーのスペックに閉じ込められ、メモリバッファリングを間違えるとOOMが起きます。アプリサーバーはロジックを処理するものであって、バイトを運ぶパイプになってはいけません。

署名付きURLは、役割を分けます。アプリは権限を判断し、URLに署名して返します。実際のバイト転送は、クライアントとストレージの間で直接行われます。アプリは、数バイトのURLを作るだけで済みます。

どう動くのか

URLには、署名とともに制約が含まれます。どの操作(GET/PUT)か、どのオブジェクトか、いつまでか(X-Amz-Expires)、どの資格情報でか(X-Amz-Credential)、どのヘッダーを署名に含めたか(X-Amz-SignedHeaders)。ストレージは、この署名を検証してリクエストを許可します。署名は資格情報で作られていますが、シークレットキー自体はURLにないという点が核心です。

アップロードにも使います。署名付きPUT URLを渡せば、クライアントがストレージに直接アップロードします。ここで注意すべき点は、クライアントが何をどれだけアップロードするかを制限する必要があることです。署名付きPOSTポリシーを使えば、Content-Lengthの範囲とContent-Typeを、署名に結びつけられます。

有効期限は、短いほどよいです。ダウンロード用は数分、アップロード用は数十分あれば、たいてい十分です。URLがログやリファラーヘッダーで漏れることがあるからです。逆に、短すぎると、遅い回線のユーザーがアップロードを終えられません。

ポリシーは、別の層です。署名付きURLが一時的な委任なら、ポリシーは常時の権限です。IAMスタイルのJSONで、どのプリンシパルが、どのリソースに、どの操作をできるかを書きます。ここでよくあるミスが、Resourceをバケット全体にすることです。プレフィックス単位に絞れば、サービス1つが侵害されても、被害がそのプレフィックスの中に閉じ込められます。

現場での姿

バケットを匿名読み取りで開けておくことは、ほとんどのデータ流出事故の出発点です。「画像しかないから大丈夫」と開けたら、その下にバックアップやユーザーのアップロードが混ざり込むことがよくあります。匿名公開が本当に必要なら、バケット全体ではなく、public/のような特定のプレフィックスだけを開け、そのプレフィックスには公開してよいものだけを入れるというルールを、コードで強制します。

そして、MinIOのCVE-2025-62506のように、セッションポリシーのバイパスで権限が昇格する脆弱性が、実際に出ています。ポリシーを狭く書くことは、実装のバグに対する防御でもあります。

ブラウザーから直接アップロードするときに引っかかること

署名付きURLを最初に付けると、たいてい同じ場所で止まります。順番に押さえておきます。

CORS。ブラウザーが別のオリジンであるストレージにPUTを送るには、まずOPTIONSでプリフライトリクエストを送り、ストレージが許可ヘッダーで答える必要があります。この設定はバケットに別に入れる必要があり、ないと、署名は問題ないのに、ブラウザーのコンソールにだけCORSエラーが出ます。サーバーからcurlでテストするとうまくいくので、原因を見当違いの場所に探しやすくなります。

署名したヘッダーは、必ずそのまま送る必要があります。Content-Typeを署名に含めたなら、クライアントがまったく同じ値を送る必要があり、1つでも違うとSignatureDoesNotMatchが起きます。ブラウザーやHTTPライブラリがヘッダーを勝手に追加したり、大文字・小文字を変えたりすることがあるので、署名に入れるヘッダーは、本当に必要なものだけに絞るほうが安全です。

時計。署名には発行時刻が入り、ストレージは自分の時計で検証します。2つの時計が数分以上ずれていると、作ったばかりのURLが「すでに期限切れ」として拒否されます。コンテナでNTPがないときや、ノートPCがスリープから復帰した直後に、実際に経験します。

アップロードが終わったあとの処理も、あらかじめ決めておく必要があります。クライアントがストレージに直接アップロードしたので、アプリケーションは、アップロードが終わったことを自分では知れません。クライアントが「全部アップロードした」と知らせる方式は、そのリクエストが来なければ、そのまま途切れます。そのため、実務では2つを併用します。ストレージのイベント通知でオブジェクトの作成を受け取って処理し、同時に、一定時間が経っても完結しない項目を定期的に走査して整理します。

大きなファイルは、一度にアップロードしません。マルチパートアップロードは、ファイルをパートに分けて並列でアップロードし、最後に結合する方式なので、途中で切れても、そのパートだけをもう一度アップロードすればよいのです。その代わり、結合(完了)を呼び出していないパートは、そのまま残って料金だけがかかります。目に見えるオブジェクトの一覧には現れないので、数か月後に、容量が合わないことで発見されることが多いです。未完了のアップロードを、数日後に自動で削除するライフサイクルルールを、バケットを作るときに一緒に入れておくのが定石です。

次のラボですること

匿名アクセスが拒否されることを確認したあと、署名付きGETとPUTを作って使い、有効期限を検証します。そのあと別のラボで、ユーザーを作り、読み取り専用ポリシーとプレフィックス限定ポリシーを書いてアタッチします。