S3 の署名付き URL とは、AWS の認証情報を持たない相手にも、期限つきで1つのオブジェクトの取得やアップロードを許可できる URL です。 発行する人の認証情報で署名したクエリ文字列が URL に付き、受け取った人はその URL に curl やブラウザでアクセスするだけで操作できます。

この記事では、AWS CLI の aws s3 presign と、boto3 の generate_presigned_url(GET・PUT)、generate_presigned_post を並べて実行し、発行された URL の読み方と curl での実行結果を載せます。有効期限の決まり、PUT と POST の違い、エラーの原因別の整理もまとめました。

確認環境:Ubuntu 24.04、AWS CLI 1.46.1、Python 3.11.15、boto3 1.43.103、curl 8.5.0。リージョンは ap-northeast-1。AWS の API をローカルで再現するツール(moto 5.2.3)で実行しています。掲載するコマンドは接続先の指定を外してあり、実際の AWS でそのまま使える形です。

S3署名付きURLの早見表

やりたいこと 方法 受け取る側の操作
ダウンロード用 URL を CLI で発行 aws s3 presign s3://バケット/キー --expires-in 600 curl -o ファイル "URL"
ダウンロード用 URL を Python で発行 s3.generate_presigned_url("get_object", Params={…}, ExpiresIn=600) curl -o ファイル "URL"
アップロード用 URL(PUT)を発行 s3.generate_presigned_url("put_object", Params={…}, ExpiresIn=600) curl -X PUT -T ファイル "URL"
アップロード用フォーム(POST)を発行 s3.generate_presigned_post(バケット, キー, Fields=…, Conditions=…) curl -F 項目… -F file=@ファイル URL
項目 値
有効期限の既定 3600秒(1時間)。CLI・boto3 とも同じ
有効期限の上限 604800秒(7日)。署名バージョン4(SigV4)の決まり
一時認証情報で発行した場合 指定した期限と、認証情報の期限の早いほうで切れる
CLI で発行できる操作 ダウンロード(GET)のみ
URL を使える回数 期限までは何度でも使える
発行に必要なもの 有効な認証情報。発行者がその操作の権限を持っていること

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「S3署名付きURLの発行方法 CLI・boto3とPUTアップロード例」

aws s3 presign でダウンロード用URLを発行する

aws s3 presign は、S3 URI を渡すとダウンロード(GET)用の URL を1行で返します。

aws s3 presign s3://example-docs/reports/report.txt --expires-in 600

今回の環境(AWS CLI 1.46.1、ap-northeast-1)では、次の形の URL が返りました。

https://example-docs.s3.amazonaws.com/reports/report.txt?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Signature=3clL0RB84RU1qkrRzjZGRSZKhCU%3D&Expires=1791180942

クエリ文字列が AWSAccessKeyId・Signature・Expires の3つで、これは署名バージョン2の形です。同じコマンドに --region ap-northeast-3 を付けると、X-Amz- で始まる署名バージョン4の形になりました。同じコマンドでも、設定とリージョンによって URL の形が変わります。

署名バージョン4に固定する

期限の上限(7日)や一時認証情報の扱いなど、公式ドキュメントの説明は署名バージョン4を前提にしています。設定で固定しておくと、URL の形がそろいます。

aws configure set default.s3.signature_version s3v4

~/.aws/config には次のように書き込まれます。

[default]
region = ap-northeast-1
s3 =
    signature_version = s3v4

設定後に同じコマンドを実行した結果です。

https://example-docs.s3.amazonaws.com/reports/report.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261005%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20261005T060632Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=512b5b5632ec3756bd07a054fa0a5e0f4623e2e7b08f45729c67b7065b513fc9
オプション 意味
--expires-in 秒数 有効期限。省くと 3600秒
--region リージョン 署名に使うリージョン。バケットのあるリージョンに合わせる

aws s3 presign が発行するのは GET 用の URL です。アップロード用は、次に紹介する boto3 で発行します。

発行されたURLのクエリ文字列を読む

署名バージョン4(X-Amz- で始まる形)

上の URL を & で区切ると、6つのパラメータに分かれます。

パラメータ 今回の値 意味
X-Amz-Algorithm AWS4-HMAC-SHA256 署名の方式(署名バージョン4)
X-Amz-Credential AKIAIOSFODNN7EXAMPLE/20261005/ap-northeast-1/s3/aws4_request アクセスキー ID / 日付 / リージョン / サービス。%2F は /
X-Amz-Date 20261005T060632Z 署名した日時(UTC)
X-Amz-Expires 600 署名した日時からの有効秒数。1〜604800
X-Amz-SignedHeaders host 署名に含めたヘッダー名。リクエストでも同じ値を送る必要がある
X-Amz-Signature 512b5b56… 署名。URL のどこかを書き換えると、S3 側の計算と合わなくなる
X-Amz-Security-Token (一時認証情報のときだけ付く) セッショントークン

期限が切れる時刻は「X-Amz-Date + X-Amz-Expires 秒」 です。上の例なら 06:06:32(UTC)の600秒後、06:16:32(UTC)です。日本時間では9時間を足します。

シークレットアクセスキーは URL に含まれません。一方で、アクセスキー ID・バケット名・キーは URL から読み取れるので、URL は渡したい相手にだけ届けるようにします。

署名バージョン2(AWSAccessKeyId で始まる形)

パラメータ 意味
AWSAccessKeyId アクセスキー ID
Signature 署名
Expires 期限の時刻(UNIX 時間の秒)

こちらは期限が秒数ではなく時刻そのもので入っています。date コマンドで読める形にできます。

date -u -d @1791180941 +%FT%TZ
2026-10-05T06:15:41Z

boto3 の generate_presigned_url で発行する(GET・PUT)

boto3 では、generate_presigned_url の第1引数に操作名(get_object や put_object)を、Params にその操作の引数を渡します。Config(signature_version="s3v4") を付けて、署名バージョン4に固定しています。

import boto3
from botocore.config import Config

s3 = boto3.client("s3", config=Config(signature_version="s3v4"))

# ダウンロード用(GET)
get_url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": "example-docs", "Key": "reports/report.txt"},
    ExpiresIn=600,
)

# アップロード用(PUT)。ContentType を指定すると、同じ値のヘッダーが必要になる
put_url = s3.generate_presigned_url(
    "put_object",
    Params={"Bucket": "example-docs", "Key": "uploads/memo.txt", "ContentType": "text/plain"},
    ExpiresIn=600,
)

print(get_url)
print(put_url)

発行された PUT 用 URL のクエリ文字列を見ると、GET 用との違いがわかります。

https://example-docs.s3.amazonaws.com/uploads/photo.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261005%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20261005T060634Z&X-Amz-Expires=600&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=35406d51…(省略)…

(この出力は Key を uploads/photo.png、ContentType を image/png にして発行したものです。)

Params の指定 X-Amz-SignedHeaders アップロード時に必要なヘッダー
ContentType を指定 content-type;host(%3B は ;) 発行時と同じ値の Content-Type
ContentType を省く host 特になし

ContentType を指定して発行すると、Content-Type が署名の対象に入ります。 アップロードできるファイルの種類を発行側で決めておきたいときに使えます。

引数 意味
第1引数(ClientMethod) "get_object"、"put_object" など、client のメソッド名
Params そのメソッドに渡す引数(Bucket、Key、ContentType など)
ExpiresIn 有効秒数。省くと 3600(省いて発行した URL は X-Amz-Expires=3600 でした)
HttpMethod HTTP メソッドを明示したいときに指定

curl でダウンロードする(GET)

curl -s -w '\nHTTP %{http_code}\n' "$GET_URL"
hello presigned url

HTTP 200

ファイルに保存するときは -o を付けます。

curl -s -o downloaded.txt -w 'HTTP %{http_code}\n' "$GET_URL"
cat downloaded.txt
HTTP 200
hello presigned url

署名のクエリ文字列を付けずに同じオブジェクトへアクセスすると、403 が返りました。

curl -s -o /dev/null -w 'HTTP %{http_code}\n' "https://example-docs.s3.amazonaws.com/reports/report.txt"
HTTP 403

URL を変数に入れるときや curl に渡すときは、必ず引用符で囲みます。& がシェルに解釈されると、URL が途中で切れてしまいます。

curl でアップロードする(PUT)

ContentType="text/plain" で発行した URL に、同じ Content-Type を付けて PUT します。

echo "memo from curl" > memo.txt
curl -s -X PUT -T memo.txt -H "Content-Type: text/plain" -w 'HTTP %{http_code}\n' "$PUT_URL"
HTTP 200

アップロードされたオブジェクトを確かめます。

aws s3api head-object --bucket example-docs --key uploads/memo.txt --query '[ContentLength,ContentType]'
aws s3 cp s3://example-docs/uploads/memo.txt -
[
    15,
    "text/plain"
]
memo from curl

公式ドキュメントによると、同じキーのオブジェクトがすでにある場合は、アップロードしたもので置き換えられます。キーに利用者 ID や日時を含めておくと、意図しない置き換えを避けられます。

generate_presigned_post でフォーム用のアップロード情報を発行する

generate_presigned_post は、URL 1本ではなく 「送信先 URL」と「フォームの項目」の組を返します。ブラウザのフォームから直接 S3 にアップロードさせる用途に向いていて、ファイルサイズの範囲などの条件を付けられます。

import boto3
from botocore.config import Config

s3 = boto3.client("s3", config=Config(signature_version="s3v4"))

post = s3.generate_presigned_post(
    "example-docs",
    "uploads/form.txt",
    Fields={"Content-Type": "text/plain"},
    Conditions=[
        {"Content-Type": "text/plain"},
        ["content-length-range", 1, 1048576],
    ],
    ExpiresIn=600,
)

print("POST:", post["url"])
for name in post["fields"]:
    print("  field:", name)

# curl で送るシェルスクリプトを書き出す(フィールドを先に、file を最後に並べる)
lines = ["curl -s -w 'HTTP %{http_code}\\n' \\"]
for k, v in post["fields"].items():
    lines.append(f"  -F '{k}={v}' \\")
lines.append("  -F 'file=@memo.txt' \\")
lines.append(f"  '{post['url']}'")
with open("post_upload.sh", "w") as f:
    f.write("\n".join(lines) + "\n")
POST: https://example-docs.s3.amazonaws.com
  field: Content-Type
  field: key
  field: x-amz-algorithm
  field: x-amz-credential
  field: x-amz-date
  field: policy
  field: x-amz-signature

fields の中の policy は、条件を JSON にして Base64 にしたものです。デコードすると、指定した条件と、boto3 が自動で足した条件(bucket、key、署名関連)が入っていることがわかります。次は Conditions に key の前方一致も加えて発行したときの中身です。

{
  "expiration": "2026-10-05T06:16:34Z",
  "conditions": [
    { "Content-Type": "image/png" },
    ["starts-with", "$key", "uploads/"],
    ["content-length-range", 1, 1048576],
    { "bucket": "example-docs" },
    …(省略)…
  ]
}
条件の書き方 意味 例
{"項目": "値"} 完全一致 {"Content-Type": "image/png"}
["starts-with", "$項目", "前方"] 前方一致 ["starts-with", "$key", "uploads/"]
["content-length-range", 最小, 最大] ファイルサイズの範囲(バイト) ["content-length-range", 1, 1048576]

Fields に入れた項目は、Conditions にも対応する条件を書きます(上の例の Content-Type)。

curl でアップロードする(POST)

書き出した post_upload.sh の中身は次の形です。fields の全項目を -F で並べ、file を最後に置きます。

curl -s -w 'HTTP %{http_code}\n' \
  -F 'Content-Type=text/plain' \
  -F 'key=uploads/form.txt' \
  -F 'x-amz-algorithm=AWS4-HMAC-SHA256' \
  -F 'x-amz-credential=AKIA…/20261005/ap-northeast-1/s3/aws4_request' \
  -F 'x-amz-date=20261005T060722Z' \
  -F 'policy=eyJleHBpcmF0aW9uIjog…(省略)…' \
  -F 'x-amz-signature=b5c6ce58…(省略)…' \
  -F 'file=@memo.txt' \
  'https://example-docs.s3.amazonaws.com'
bash post_upload.sh
aws s3 ls s3://example-docs/uploads/
HTTP 204
2026-10-05 15:07:22         15 form.txt
2026-10-05 15:07:22         15 memo.txt

POST は成功すると 204(本文なし)を返しました。PUT でアップロードした memo.txt と、POST でアップロードした form.txt が並んでいます。

HTML のフォームにする場合は、fields の各項目を hidden の入力欄にし、最後にファイル選択欄を置きます(boto3 の公式ガイドに載っている形です)。

<form action="https://example-docs.s3.amazonaws.com" method="post" enctype="multipart/form-data">
  <input type="hidden" name="key" value="uploads/form.txt" />
  <input type="hidden" name="Content-Type" value="text/plain" />
  <!-- x-amz-algorithm、x-amz-credential、x-amz-date、policy、x-amz-signature も同じように並べる -->
  <input type="file" name="file" />
  <input type="submit" value="アップロード" />
</form>

PUT と POST の違い

比べる点 PUT(generate_presigned_url) POST(generate_presigned_post)
返ってくるもの URL 1本 送信先 URL と、フォーム項目の辞書
送り方 ファイルの中身をそのまま本文にして PUT multipart/form-data で POST
Content-Type の固定 Params に ContentType を指定すると署名対象になる Fields と Conditions に書く
ファイルサイズの制限 URL に範囲を指定する項目がない content-length-range で最小・最大を指定できる
キーの決め方 発行時に1つに決まる 完全一致のほか、starts-with で前方一致にもできる
成功時の応答 200 204(今回の実行結果)
向いている用途 スクリプトやアプリからの単純なアップロード ブラウザのフォーム、サイズや種類を条件にしたいアップロード

サイズの上限を発行側で決めたいなら POST、URL 1本で手軽に渡したいなら PUT が選びやすい基準です。

有効期限の決まり

条件 期限
--expires-in / ExpiresIn を省略 3600秒(1時間)
CLI・SDK で指定できる上限 604800秒(7日)
S3 コンソールで発行 1分〜12時間
IAM ユーザーのアクセスキーで発行(SigV4) 最長7日
STS の一時認証情報で発行 認証情報の期限まで(IAM ユーザーの get-session-token なら最長36時間)
EC2 のインスタンスプロファイルで発行 認証情報の期限まで(最長6時間)
発行に使った認証情報が無効化・削除された その時点で使えなくなる

公式ドキュメントでは、一時認証情報で発行した URL は、もっと後の期限を指定していても、認証情報の期限が来た時点で切れると説明されています。Lambda や EC2 のロールで発行した URL が、指定より早く使えなくなるのはこのためです。長い期限が必要なときは、発行に使う認証情報の種類から見直します。

期限の判定はリクエストの開始時に行われます。公式ドキュメントによると、期限の直前に始めた大きなファイルのダウンロードは、途中で期限を過ぎても続きます。接続が切れて期限後にやり直した場合は、新しい URL が必要です。

7日を超える値を指定したとき

ExpiresIn=604801 や --expires-in 604801 を指定しても、手元では URL が発行され、クエリ文字列は X-Amz-Expires=604801 になりました。発行時には止まらないので、上限は自分で守る必要があります。AWS CLI の GitHub Issue では、この URL にアクセスすると S3 が次のメッセージで拒否すると報告されています。

X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds

一時認証情報で発行した URL の見分け方

セッショントークンつきの認証情報で発行すると、クエリ文字列に X-Amz-Security-Token が加わります。

…&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=FQoGZXIvYXdzEXAMPLETOKEN&X-Amz-Signature=3c720164…

URL に X-Amz-Security-Token があれば、一時認証情報で発行された URL です。期限が想定より早く切れるときは、まずここを見ます。

エラーの原因別の整理

応答 原因 直し方
SignatureDoesNotMatch(403) 発行時と違う Content-Type で PUT した、または署名していないヘッダーを付けた 発行時の ContentType と同じ値を -H "Content-Type: …" で送る
SignatureDoesNotMatch(403) GET 用の URL に PUT した(HTTP メソッド違い) 操作ごとに URL を発行する
SignatureDoesNotMatch(403) バケット名やキーを書き換えた(大文字・小文字も区別される) 発行された URL をそのまま使う
SignatureDoesNotMatch(403) 署名のリージョンがバケットのリージョンと違う aws s3api get-bucket-location --bucket バケット名 で確かめ、--region や region_name を合わせる
AccessDenied / Request has expired(403) URL の期限が過ぎた 新しい URL を発行する
ExpiredToken / The provided token has expired.(400) 発行に使った一時認証情報の期限が過ぎた 新しい認証情報で発行し直す。長い期限が必要なら認証情報の種類を見直す
NoSuchKey(404) キーが存在しない(URL の発行はできる) キーの綴りを確かめる
AccessDenied(403) 発行者にその操作の権限がない 発行者に s3:GetObject や s3:PutObject の許可を付ける
EntityTooLarge / EntityTooSmall(400) POST のファイルサイズが範囲外 content-length-range を見直すか、ファイルサイズを合わせる
InvalidPolicyDocument(400) POST のフォーム内容が policy の条件に合わない fields をそのまま送り、項目を足したら Conditions にも書く

エラーコードと説明は、S3 の公式エラー一覧と AWS のナレッジセンターで確認した内容です。

Content-Type が合わないと SignatureDoesNotMatch になる理由

ContentType を指定して発行した URL は、X-Amz-SignedHeaders=content-type%3Bhost となり、Content-Type の値も含めて署名が計算されています。受け取った側が別の Content-Type で送ると、S3 が計算する署名と URL の署名が合わなくなります。公式のエラー一覧では、次のメッセージで説明されています。

The request signature we calculated does not match the signature you provided. Check your AWS secret access key and signing method.
発行時 送信時 結果
ContentType="text/plain" -H "Content-Type: text/plain" 署名が合う(今回 200 で成功)
ContentType="text/plain" -H "Content-Type: application/json" 署名が合わなくなる
ContentType なし ヘッダーの指定なし 署名対象は host だけ

curl の -d や --data-binary は、Content-Type を自動で application/x-www-form-urlencoded にします。ファイルを送るときは -T を使い、Content-Type は -H で明示すると、発行時の値とそろえやすくなります。ブラウザの fetch から送る場合も、headers に同じ Content-Type を書きます。

期限切れは Request has expired

AWS のナレッジセンターには、期限を過ぎた URL にアクセスしたときの応答として、次の XML が載っています。

<Code>AccessDenied</Code>
<Message>Request has expired</Message>
<Expires>2022-10-28T07:13:14Z</Expires>
<ServerTime>2022-10-28T20:03:02Z</ServerTime>

Expires(URL の期限)と ServerTime(S3 側の現在時刻)が並ぶので、どれだけ過ぎているかがわかります。一方、認証情報の期限が先に来た場合は ExpiredToken になります。同じ「使えなくなった」でも、コードを見ればどちらの期限かを切り分けられます。

存在しないキーでもURLは発行できる

署名付き URL の発行は、手元の認証情報で署名を計算するだけの処理です。S3 への問い合わせは行われないので、存在しないキーでも URL が返ります。

URL=$(aws s3 presign s3://example-docs/reports/none.txt --expires-in 600)
curl -s -w '\nHTTP %{http_code}\n' "$URL"
<?xml version="1.0" encoding="utf-8"?>
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>reports/none.txt</Key>…(省略)…</Error>
HTTP 404

アクセスした時点で、はじめて NoSuchKey(404)がわかります。公式の API リファレンスによると、発行者にバケットの s3:ListBucket の許可がない場合は、404 ではなく 403 が返ります。URL を渡す前に、aws s3api head-object などでオブジェクトがあることを確かめておくと安心です。

発行前のチェックリスト

確認 内容
期限は必要な長さか 用途に合う短めの秒数にする。上限は 604800秒
認証情報の種類 一時認証情報なら、その期限より長くは使えない
署名バージョン s3v4 に固定して URL の形をそろえる
リージョン バケットのリージョンで署名する
PUT の Content-Type 発行時に指定したら、送る側にも同じ値を伝える
キーの置き換え 同じキーへのアップロードは置き換えになる
URL の扱い 期限内は誰でも使えるので、渡す相手と経路を決めておく

よくある質問

Q. 署名付き URL は1回使うと使えなくなりますか? A. 期限までは何度でも使えます。回数を限りたい場合は、期限を短くします。

Q. 7日より長く使える URL は作れますか? A. 署名バージョン4の署名付き URL は、最長7日です。それより長く共有したい場合は、期限が来る前に新しい URL を発行し直す運用にします。

Q. aws s3 presign でアップロード用の URL は作れますか? A. aws s3 presign はダウンロード(GET)用です。アップロード用は boto3 などの SDK で put_object を指定して発行します。

Q. 発行した URL を途中で無効にできますか? A. 公式ドキュメントによると、発行に使った認証情報を無効化・削除すると、その URL は使えなくなります。ふだんは短い期限で発行しておくのが基本です。

まとめ

  • ダウンロード用は aws s3 presign、アップロード用は boto3 の generate_presigned_url("put_object") か generate_presigned_post で発行します。
  • 有効期限は 既定 3600秒、上限 604800秒(7日) です。一時認証情報で発行した URL は、認証情報の期限が先に来ます。
  • クエリ文字列の X-Amz-Date + X-Amz-Expires が期限、X-Amz-SignedHeaders が送るべきヘッダー、X-Amz-Security-Token が一時認証情報の目印です。
  • 署名バージョンは s3v4 に固定しておくと、環境による URL の形の違いがなくなります。
  • PUT で ContentType を指定して発行したら、同じ Content-Type を付けて送ります。合わないと SignatureDoesNotMatch です。
  • サイズの範囲を決めたいときは POST の content-length-range を使います。
  • 存在しないキーでも URL は発行できるので、渡す前にオブジェクトの有無を確かめます。

参考・出典

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「S3署名付きURLの発行方法 CLI・boto3とPUTアップロード例」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?