임시 파일 액세스 권한 부여

CloudflareBeginner
지금 연습하기

소개

지원 담당자가 버킷을 공개로 전환하거나 스토리지 자격 증명을 전달하지 않고, 비공개 합성 내보내기 파일을 잠시 공유해야 합니다. 이 실습에서는 사전 서명된 다운로드 URL 을 발급하고, 만료를 확인하며, 해당 서명 자격 증명이 다른 실습 소유 버킷에 액세스할 수 없음을 검증합니다.

먼저 Organize a Document Bucket 및 Stream Documents Through a Worker 를 완료합니다. 이 독립 VM 에는 Node.js 22.22.0, Wrangler 4.131.1, AWS SDK 3.888.0 이 고정되어 있습니다. 계정에서 R2 가 활성화되어 있고, 범위가 제한된 사용자 객체 토큰을 만들 수 있는 권한이 있어야 합니다. 작은 합성 파일 두 개만 사용합니다. R2 pricingpresigned access를 확인합니다. 구매한 도메인은 필요하지 않습니다. 생성된 링크나 자격 증명을 공유하지 마세요. 실습을 종료하기 전에 두 버킷을 정리하고, 이 실습에서 만든 토큰만 폐기합니다.

격리된 액세스 테스트 버킷 만들기

이 단계에서는 이 VM 을 인증하고 임시 버킷 하나를 만듭니다. 디바이스 인증은 학습 계정을 확인합니다. 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인지 확인합니다. 계정이 하나만 표시되더라도 계정 이름을 확인합니다. 아래의 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 의 학습 계정으로 제한합니다. 만료 기간은 짧게 설정합니다. 다른 계정이나 관련 없는 권한은 포함하지 않습니다. 이 계정 수준 권한으로 버킷을 만들고 삭제할 수 있습니다. 다음 단계의 객체 전용 토큰으로는 이 작업을 수행할 수 없습니다.

토큰을 한 번만 복사하여 이 숨겨진 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 에 표시되는 버킷 이름은 대상을 확인하는 데 사용합니다. 저장된 바이트는 이후 다운로드 검사를 통해 확인합니다.

이 실습이 소유한 두 번째 버킷을 만듭니다. 이 버킷은 관련 없는 스토리지 경계를 나타내므로, 다른 실습의 버킷으로 대체하지 않습니다. 관리 자격 증명을 사용하여 두 버킷에 각각 합성 파일 하나를 업로드합니다.

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

두 버킷 모두 공개 액세스를 비활성화한 상태로 둡니다. 다음 자격 증명은 문서 버킷만 읽을 수 있도록 설정합니다.

서명 자격 증명 제한하기

이 단계에서는 한 버킷으로 제한된 읽기 전용 자격 증명을 만듭니다. 사전 서명 URL 은 해당 URL 에 서명하는 자격 증명보다 더 많은 액세스 권한을 부여할 수 없습니다. 관리 토큰은 두 실습 버킷을 모두 관리할 수 있으므로, 이 과정에서 서명 주체로 사용하면 안 됩니다.

S3 호환 API 를 사용하면 표준 스토리지 SDK 로 R2 에 액세스할 수 있습니다. 이 API 는 Wrangler 디바이스 토큰이 아니라 별도의 액세스 키 쌍을 사용합니다. R2 Overview 에서 Account Details → API Tokens → Manage를 선택한 다음, 이 실습에서 생성한 리소스 이름을 포함한 User API token을 만듭니다. Object Read only를 선택하고, 정확히 새로 만든 이 버킷으로 범위를 제한합니다. 양식에 옵션이 있다면 짧은 만료 기간을 선택합니다. 모든 버킷 또는 Admin 액세스는 선택하지 않습니다. 일회성 보안 비밀을 저장할 때까지 이 토큰 페이지를 열어 둡니다.

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 버킷은 제외합니다. 생성된 이름은 달라집니다. 토큰의 24 시간 TTL 은 각 서명 URL 의 5 분 또는 5 초 만료 시간과 별개입니다. 양식은 선택한 범위를 보여 주며, 성공하거나 거부된 원격 요청이 실제 권한을 입증합니다.

임시 다운로드에 서명하고 실행하기

이 단계에서는 특정 객체와 시간 범위에 대한 GET 요청에 서명합니다. 서명은 R2 에 연결하지 않고 로컬에서 실행되므로, URL 을 생성했다고 해서 권한이 증명되는 것은 아닙니다. 클라이언트가 URL 을 사용할 때 R2 가 서명을 평가합니다. URL 을 가진 누구나 만료 전까지 URL 에 부여된 작업을 수행할 수 있으므로, 이 링크를 로그, 메시지, 스크린샷에 남기지 않습니다.

간단한 서명 프로그램을 작성합니다. expiresIn의 단위는 초입니다. 다음 파일은 5 분 URL, 5 초 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

두 파일의 바이트가 동일해야 합니다. 이제 5 초 URL 이 만료되도록 10 초를 기다린 다음, 거부 응답을 확인합니다. 다음 요청에서는 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 권한과 공개 액세스 구분하기

이 단계에서는 지금까지 확인한 요청을 액세스 모델과 연결합니다. 성공한 링크는 하나의 키에 대해 서명된 GET 요청 하나만 허용했습니다. 버킷의 공개 개발 URL 을 활성화하거나 PUT 을 허용하거나, 사용자를 애플리케이션에 로그인시키지는 않았습니다.

Dashboard 에서 실습이 소유한 두 버킷의 설정을 엽니다. 공개 개발 URL 은 비활성화된 상태로 유지하고, 사용자 지정 도메인은 설정하지 않습니다. 객체 토큰 페이지에서 읽기 전용 토큰의 범위가 -docs 버킷 하나뿐인지 확인합니다. 일회성 보안 비밀이 스크린샷에 표시되지 않도록 합니다.

지금 플랫폼 검사를 실행합니다. 이 검사는 자체적으로 서명된 요청을 생성하고, 허용된 바이트가 정확히 일치하는지 확인하며, 서명되지 않은 요청, 만료된 요청, 범위를 벗어난 요청이 거부되는지 검증합니다. 이는 실제 원격 API 의 증거입니다. 저장된 signed.json은 완료를 나타내는 표시가 아닙니다.

프로덕션 애플리케이션에서는 서명 링크를 발급하기 전에 사용자의 문서 액세스 가능 여부를 결정합니다. 링크를 발급한 뒤에는 전달자 링크를 복사할 수 있습니다. 이후 애플리케이션 권한 부여 실습에서 이 결정을 직접 다룹니다. 서명된 S3 URL 은 계정의 R2 S3 엔드포인트를 대상으로 하며, 공개 r2.dev 주소나 사용자 지정 도메인을 대상으로 하지 않습니다.

실습이 소유한 두 버킷 정리하기

이 단계에서는 명시적으로 만든 두 fixture 키를 삭제한 다음, 해당 버킷을 삭제합니다. 플랫폼에서 두 버킷 이름이 모두 사라졌다고 확인할 때까지 관리 자격 증명을 활성 상태로 유지합니다.

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 로그아웃을 검사합니다. 이 실습에서는 두 토큰의 폐기가 수동 Dashboard 확인 항목입니다. 파일 삭제만으로 토큰 폐기를 추론하지 않습니다.

요약

범위가 제한된 비공개 파일 액세스를 만들고, 사전 서명 URL 의 성공 및 거부 경계를 확인하며, 공개 액세스와 권한 부여를 구분하고, 실습 자격 증명을 폐기합니다.