一時的なファイルアクセスを承認する

CloudflareBeginner
オンラインで実践に進む

はじめに

サポート担当者が、プライベートな合成エクスポートデータを短時間だけ共有する必要があります。ただし、バケットをパブリックにしたり、ストレージ認証情報を渡したりしてはいけません。この実験では、署名付きダウンロード URL を発行し、有効期限を確認します。また、その署名用認証情報では、別の実験用バケットにアクセスできないことを検証します。

まず、「ドキュメントバケットを整理する」と「Worker を介してドキュメントをストリーミングする」を完了してください。この独立した VM には、Node.js 22.22.0、Wrangler 4.131.1、AWS SDK 3.888.0 が固定されています。アカウントで R2 が有効になっており、スコープを限定したユーザーオブジェクトトークンを作成する権限が必要です。使用するのは、小さな合成ファイル 2 つだけです。R2 の料金署名付きアクセスを確認してください。購入済みのドメインは必要ありません。生成したリンクや認証情報は共有しないでください。終了する前に、2 つのバケットを削除し、この実験で作成したトークンだけを失効させてください。

分離したアクセステスト用バケットを作成する

このステップでは、この VM を認証し、使い捨てのバケットを 1 つ作成します。デバイス認証によって、学習用アカウントを確認します。R2 バケットの管理には、そのアカウントだけに制限した別の API トークンを使用します。

以下で使用するコマンド構文のために Bash を起動し、準備済みのプロジェクトへ移動してツールを確認します。リソース名の変数を引き続き利用できるように、同じターミナルを開いたままにしてください。

bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version

表示されたデバイスコードを、自分のブラウザーで認証してください。承認する前に、学習用アカウントと、要求された account および user の読み取りスコープを確認します。

npx wrangler login --device --browser=false --scopes account:read user:read
npx wrangler whoami --json

loggedIn: true であることを確認します。アカウントが 1 つだけ表示される場合でも、アカウント名を読み取ってください。以下の YOUR_ACCOUNT_ID を、そのアカウントの実際の 32 文字の ID に置き換えます。openssl rand -hex 6 は、ランダムな 16 進数 12 文字を生成します。これにより、以前の実行結果とこの実験のリソース名が重複しません。ヒアドキュメントによって標準の設定ファイルが作成され、シェルが変数をその中に展開します。

ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r04-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON

バケットを管理するため、Cloudflare プロフィールの API Tokens ページを開き、この実験の名前を付けたカスタムトークンを作成します。Account → Workers R2 Storage → Edit を許可し、Account Resources を、保存した ID の学習用アカウントだけに制限します。短い有効期限を設定してください。他のアカウントや関係のない権限は含めないでください。このアカウントレベルの権限でバケットの作成と削除を行います。次のステップで作成するオブジェクト専用トークンには、この操作を実行できません。

トークンを 1 回だけ、この非表示の VM プロンプトに入力します。umask 077 によりファイルを自分のユーザーだけが読み取れるようにし、read -s により入力内容を非表示にします。このファイルは Wrangler 標準のトークン変数を使用し、Git の対象外です。

umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN

R2 の管理コマンドに限って --env-file=.env.management を使用します。通常の whoami は、引き続き VM のデバイス認証を確認します。

--env-file は各 Wrangler コマンドの末尾に置きます。これにより、ファイル引数のリストにコマンド名が取り込まれるのを防げます。各バケットの作成後、Wrangler が設定へのバインディング追加を尋ねたら、n を入力して Enter を押してください。必要なバインディングはすでに設定されています。

npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management

バケット一覧を表示し、生成された正確な名前を探します。他のバケットは別の作業に属しているため、そのままにしてください。

npx wrangler r2 bucket list --env-file=.env.management

Dashboard で Storage & databases → R2 → Overview を開き、この正確な名前のバケットを選択して、オブジェクト一覧が空であることを確認します。設定では、パブリック開発用 URL とカスタムドメインを無効のままにしてください。Dashboard に表示されたバケット名で対象を確認し、後のダウンロード確認で保存されたバイト列を検証します。

この実験が所有する 2 つ目のバケットを作成します。これは無関係なストレージ境界を表します。別の演習のバケットを代わりに使用しないでください。管理用認証情報を使って、それぞれのバケットに合成ファイルを 1 つずつアップロードします。

CONTROL="$NAME-control"
npx wrangler r2 bucket create "$CONTROL" --env-file=.env.management
npx wrangler r2 object put "$BUCKET/documents/report.txt" --remote --file document.txt --content-type text/plain --env-file=.env.management
npx wrangler r2 object put "$CONTROL/retained/handbook.txt" --remote --file retained.txt --content-type text/plain --env-file=.env.management

両方のバケットでパブリックアクセスを無効のままにしてください。次の認証情報には、ドキュメントバケットだけの読み取りを許可します。

署名用認証情報を制限する

このステップでは、1 つのバケットだけに制限した読み取り専用の認証情報を作成します。署名付き URL が許可できるアクセスは、その URL の署名に使用した認証情報が持つ権限を超えません。管理用トークンでは、この実験の両方のバケットを管理できるため、このレッスンで署名用の ID として使用してはいけません。

S3 互換 API により、標準のストレージ SDK から R2 にアクセスできます。Wrangler のデバイストークンとは別に、アクセスキーのペアを使用します。R2 Overview で Account Details → API Tokens → Manage を開き、この実験で生成したリソース名を付けた User API token を作成します。Object Read only を選択し、この実験で新しく作成した正確なバケットだけに制限します。フォームに短い有効期限の選択肢がある場合は、それを選択してください。すべてのバケットや Admin アクセスは選択しないでください。1 回しか表示されないシークレットを保存するまで、このトークンページを開いたままにします。

VM で以下の Bash プロンプトを使用します。read -s は入力内容を非表示にし、umask 077 は認証情報ファイルを自分のユーザーだけが読み取れるようにします。これらは AWS SDK 標準の環境変数名です。Access Key ID と Secret Access Key を、それぞれ対応するプロンプトに貼り付けてから Enter キーを押します。一般的な API トークンの値は貼り付けないでください。

umask 077
read -r -s -p 'Access Key ID: ' AWS_ACCESS_KEY_ID; printf '\n'
read -r -s -p 'Secret Access Key: ' AWS_SECRET_ACCESS_KEY; printf '\n'
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n' "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" > .env.s3
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY

再利用可能な標準 SDK クライアントを作成します。SDK にはリージョン文字列が必要です。R2 では auto を使用します。既存の設定を読み込むことで、CLI と SDK の操作対象を同じアカウントとバケットにそろえます。

cat > storage.mjs <<'JS'
import { S3Client } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
export const Bucket = config.r2_buckets[0].bucket_name;
export const s3 = new S3Client({
  region: "auto",
  endpoint: `https://${config.account_id}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});
JS

-docs バケットだけを選択し、-control バケットはこのトークンのリソーススコープの対象外にします。既存の storage.mjs クライアントは、--env-file=.env.s3 が指定された場合にだけ .env.s3 を読み込みます。Admin 権限や全バケットのスコープは使用しないでください。

URL を共有する前に、トークンのフォームで選択したバケットを確認します。次のステップで control バケットに対して実際にリクエストを送り、この境界が機能することを個別に検証します。

文書バケットのみに限定した読み取り専用トークン

この例では Object Read only と生成された -docs バケットのみを選択し、-control バケットは除外しています。生成される名前は異なります。トークンの TTL である 24 時間は、各署名付き URL の 5 分間または 5 秒間の有効期限とは別です。フォームは選択した範囲を示し、実際の権限は成功したリクエストと拒否されたリモートリクエストで確認します。

一時的なダウンロードに署名して実行する

このステップでは、特定のオブジェクトと時間範囲に対する GET リクエストに署名します。署名処理は R2 に接続せずローカルで実行されます。URL を作成しただけでは、権限があることは証明できません。クライアントがその URL を使用したときに、R2 が署名を評価します。URL を知っている人は誰でも、有効期限が切れるまで、その URL に付与された操作を実行できます。そのため、これらのリンクをログ、メッセージ、スクリーンショットに含めないでください。

小さな署名プログラムを作成します。expiresIn の単位は秒です。このファイルは、5 分間有効な URL、5 秒間有効な URL、意図的にスコープ外にした URL を保存しますが、URL 自体は表示しません。

cat > sign.mjs <<'JS'
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { readFileSync, writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
const command = new GetObjectCommand({ Bucket, Key: "documents/report.txt" });
const allowed = await getSignedUrl(s3, command, { expiresIn: 300 });
const short = await getSignedUrl(s3, command, { expiresIn: 5 });
const outside = await getSignedUrl(s3, new GetObjectCommand({
  Bucket: config.name + "-control", Key: "retained/handbook.txt"
}), { expiresIn: 300 });
const unsigned = new URL(allowed); unsigned.search = "";
writeFileSync("signed.json", JSON.stringify({ allowed, short, outside, unsigned: unsigned.href }), { mode: 0o600 });
console.log("Stored temporary request URLs in private signed.json");
JS
node --env-file=.env.s3 sign.mjs

許可された URL はすぐに使用します。この curl リクエストには Authorization ヘッダーがありません。署名は URL に含まれているためです。node -p は保存済みの URL をそのまま curl に渡し、ターミナル出力には表示しません。

curl -fsS "$(node -p "require('./signed.json').allowed")" -o signed-download.txt
cmp document.txt signed-download.txt

バイト列が完全に一致することを確認します。次に 10 秒待ち、5 秒間有効な URL の期限を切らしてから、拒否レスポンスを確認します。これらのリクエストでは、HTTP エラーを表示するため意図的に --fail を指定しません。

sleep 10
curl -sS -o expired.xml -w "%{http_code}\n" "$(node -p "require('./signed.json').short")"
curl -sS -o unsigned.xml -w "%{http_code}\n" "$(node -p "require('./signed.json').unsigned")"
curl -sS -o outside.xml -w "%{http_code}\n" "$(node -p "require('./signed.json').outside")"

期限切れのリクエストと権限範囲外のリクエストが 403 を返すことを確認します。この R2 S3 エンドポイントへの署名なしのリクエストは、InvalidArgument とメッセージ Authorization を伴う 400 を返します。必要な署名情報が欠けているためです。これは、有効な署名付き認証情報による別のバケットへのアクセスが拒否される場合とは異なります。リクエストの詳細を含む可能性があるため、レスポンス全体を表示せずにエラー名を確認します。

python3 - <<'PYXML'
from xml.etree import ElementTree
for path in ["expired.xml", "unsigned.xml", "outside.xml"]:
    root = ElementTree.parse(path).getroot()
    print(path, root.findtext("Code"))
PYXML

期限切れのレスポンスには、有効期限切れを示す内容が表示されます。一方、control バケットからは AccessDenied が返ります。ネットワーク障害は認可の結果ではありません。control バケットをテストする前に 5 分を超えた場合は、URL を再生成し、許可された URL とスコープ外 URL のリクエストをもう一度実行してから、短い URL を再び期限切れにしてください。

URL の権限とパブリックアクセスを区別する

このステップでは、確認したリクエストをアクセスモデルと結び付けます。成功したリンクが認可したのは、1 つのキーに対する署名付き GET 1 回だけです。バケットのパブリック開発用 URL を有効にしたわけでも、PUT を認可したわけでも、アプリケーションにユーザーをログインさせたわけでもありません。

Dashboard で、この実験が所有する両方のバケットの設定を開きます。パブリック開発用 URL は無効のままにし、カスタムドメインは設定しないでください。オブジェクトトークンのページで、読み取り専用トークンの対象が -docs バケットだけであることを確認します。1 回しか表示されないシークレットをスクリーンショットに写さないでください。

ここでプラットフォームチェックを実行します。このチェックは独自の署名付きリクエストを生成し、許可されたバイト列が正確であることを確認するとともに、署名なし、期限切れ、スコープ外のリクエストが拒否されることを検証します。これは実際のリモート API の証拠です。保存された signed.json は完了マーカーではありません。

本番アプリケーションでは、署名付きリンクを発行する前に、ユーザーがドキュメントへアクセスしてよいか判断します。発行後は、ベアラリンクをコピーできます。後続のアプリケーション認可の実験では、この判断を直接扱います。署名付き S3 URL の対象はアカウントの R2 S3 エンドポイントであり、パブリックな r2.dev アドレスやカスタムドメインではありません。

実験用バケットを両方とも削除する

このステップでは、明示的に作成した 2 つのオブジェクトキーを削除してから、バケットを削除します。プラットフォームによって両方のバケット名が存在しないことを確認するまで、管理用認証情報を有効なままにしてください。

BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
CONTROL=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).name + '-control'")
npx wrangler r2 object delete "$BUCKET/documents/report.txt" --remote --env-file=.env.management
npx wrangler r2 object delete "$CONTROL/retained/handbook.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
npx wrangler r2 bucket delete "$CONTROL" --env-file=.env.management
npx wrangler r2 bucket list --env-file=.env.management

プロンプトが表示されたら、それぞれの正確な名前を確認します。バケット一覧に両方の名前が含まれていなければ、削除は成功です。グローバルな一覧を空にするために、別のバケットを削除してはいけません。

実験用認証情報を失効させてログアウトする

このステップでは、この演習によって残されたアクセスを閉じます。R2 API Tokens ページで、この実験用に作成したオブジェクトトークンだけを失効させます。プロフィールの API Tokens ページで、この実験用に作成した別の R2 管理トークンを失効させます。バケットを削除してもトークンは失効せず、Wrangler からログアウトしても S3 認証情報は失効しません。

失効後、ローカルの認証情報ファイルを削除し、この VM からログアウトします。

rm .env.s3 .env.management signed.json
npx wrangler logout

構造化された ID 情報を確認します。ログアウト後に終了ステータスが 0 以外になるのは想定どおりです。

npx wrangler whoami --json || true

loggedIn: false であることを確認し、通常の Dashboard ログインは維持してください。プラットフォームは、ローカル認証情報の削除と Wrangler からのログアウトを確認します。この候補版では、2 つのトークンの失効は Dashboard で手動確認するチェックポイントです。ファイル削除から自動的に判断されるものではありません。

まとめ

スコープを限定したプライベートファイルアクセスを作成し、署名付き URL の成功と拒否の境界を確認し、パブリックアクセスと認可を区別して、実験用の認証情報を失効させます。