<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>File Download on jyukki's Blog</title><link>https://jyukki.com/tags/file-download/</link><description>Recent content in File Download on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Sat, 18 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/file-download/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Presigned URL 접근 제어 운영 플레이북</title><link>https://jyukki.com/learning/deep-dive/deep-dive-presigned-url-access-control-playbook/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-presigned-url-access-control-playbook/</guid><description>Presigned URL을 단순 파일 링크가 아니라 만료, 권한 재검증, 객체 버전, 감사 로그, 취소 한계까지 가진 임시 접근 권한으로 운영하는 기준을 정리합니다.</description><content:encoded><![CDATA[<p>Presigned URL은 백엔드 파일 기능에서 자주 쓰입니다. 앱 서버가 대용량 파일을 직접 흘리지 않아도 되고, S3 같은 object storage가 업로드와 다운로드 트래픽을 처리해 줍니다. 구현도 처음에는 간단합니다. 서버가 사용자의 권한을 확인한 뒤 <code>GetObject</code>나 <code>PutObject</code>에 서명한 URL을 내려주고, 클라이언트가 그 URL로 직접 접근합니다.</p>
<p>하지만 운영에 들어가면 질문이 달라집니다. 사용자가 권한을 잃은 직후에도 이전에 받은 URL로 파일을 열 수 있는가? URL이 채팅방에 공유되면 누가 접근했는지 알 수 있는가? CDN이 비공개 파일을 cache하면 어떻게 되는가? 객체가 새 버전으로 바뀌었는데 예전 URL은 무엇을 가리키는가? 이 질문에 답하지 못하면 Presigned URL은 서버 부하를 줄인 만큼 권한 경계를 흐리게 만듭니다.</p>
<p>이 글은 <a href="/learning/deep-dive/deep-dive-object-storage-s3/">Object Storage와 파일 관리</a>, <a href="/learning/deep-dive/deep-dive-system-design-file-serving/">파일 업로드와 서빙 시스템 설계</a>, <a href="/learning/deep-dive/deep-dive-object-upload-quarantine-scanning-playbook/">Object Upload Quarantine</a>, <a href="/learning/deep-dive/deep-dive-authorization-models-rbac-abac-rebac/">Authorization 모델</a>과 이어집니다. 핵심은 Presigned URL을 &ldquo;파일 링크&quot;가 아니라 <strong>짧게 위임한 임시 접근 권한</strong>으로 보는 것입니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>Presigned URL을 발급할 때 서버가 어떤 권한, 상태, 객체 버전, TTL을 확인해야 하는지 이해합니다.</li>
<li>이미 발급된 URL은 즉시 취소하기 어렵다는 한계를 전제로 설계할 수 있습니다.</li>
<li>파일 민감도별 TTL, CDN cache, audit log, rate limit 기준을 숫자로 잡을 수 있습니다.</li>
<li>다운로드 URL 발급 API를 object key가 아니라 file metadata와 권한 계약 중심으로 설계하는 방법을 가져갑니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-presigned-url은-인증을-생략하는-링크가-아니다">1) Presigned URL은 인증을 생략하는 링크가 아니다</h3>
<p>Presigned URL은 object storage에 &ldquo;이 요청은 특정 시간 동안 허용해도 된다&quot;고 서명한 요청입니다. 사용자는 URL 자체만 있으면 object storage에 접근할 수 있습니다. 그래서 발급 전 서버 권한 검사가 전부입니다. 발급 API가 느슨하면 object storage는 그 실수를 바로잡아 주지 않습니다.</p>
<p>나쁜 패턴은 사용자가 <code>bucket</code>과 <code>key</code>를 넘기면 서버가 그대로 서명해 주는 방식입니다. 이러면 object key를 추측하거나 다른 tenant의 key를 넣는 공격에 약해집니다. 좋은 패턴은 사용자가 <code>file_id</code>만 넘기고, 서버가 DB에서 파일 메타데이터를 조회한 뒤 권한을 다시 판단하는 방식입니다.</p>
<p>발급 전 확인해야 할 최소 조건은 아래입니다.</p>
<table>
  <thead>
      <tr>
          <th>확인 항목</th>
          <th>이유</th>
          <th>실패 시 처리</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>tenant_id와 owner/group 권한</td>
          <td>다른 조직 파일 접근 차단</td>
          <td>403</td>
      </tr>
      <tr>
          <td>file 상태</td>
          <td>스캔 전, 삭제, 만료 파일 차단</td>
          <td>404 또는 409</td>
      </tr>
      <tr>
          <td>object key와 version</td>
          <td>잘못된 객체 또는 예전 객체 접근 방지</td>
          <td>409</td>
      </tr>
      <tr>
          <td>파일 민감도</td>
          <td>TTL과 audit 강도 결정</td>
          <td>정책 적용</td>
      </tr>
      <tr>
          <td>요청 빈도</td>
          <td>URL 대량 발급과 scraping 방지</td>
          <td>429</td>
      </tr>
  </tbody>
</table>
<p>이 검사는 <a href="/learning/deep-dive/deep-dive-authorization-models-rbac-abac-rebac/">Authorization 모델</a>의 RBAC/ABAC와 연결됩니다. 단순히 &ldquo;로그인했는가&quot;가 아니라 &ldquo;이 사용자, 이 tenant, 이 파일 상태, 이 시점에 다운로드 목적이 허용되는가&quot;를 봐야 합니다.</p>
<h3 id="2-url-만료는-권한-취소와-다르다">2) URL 만료는 권한 취소와 다르다</h3>
<p>Presigned URL의 TTL은 안전장치지만 완전한 취소 장치는 아닙니다. 이미 발급된 URL은 만료 전까지 동작할 수 있습니다. 사용자의 프로젝트 권한이 삭제되어도, 30분 TTL URL을 이미 받았다면 남은 시간 동안 접근 가능할 수 있습니다. 따라서 TTL은 &ldquo;권한 변경 후 최대 잔여 노출 시간&quot;입니다.</p>
<p>초기 기준은 이렇게 둘 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>파일 유형</th>
          <th style="text-align: right">권장 TTL</th>
          <th>추가 조건</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>개인정보/계약서/정산 파일</td>
          <td style="text-align: right">1~5분</td>
          <td>발급 audit 필수, CDN cache 금지</td>
      </tr>
      <tr>
          <td>로그인 사용자 첨부파일</td>
          <td style="text-align: right">5~15분</td>
          <td>file_id 기반 재검증, rate limit</td>
      </tr>
      <tr>
          <td>임시 미리보기 이미지</td>
          <td style="text-align: right">1~10분</td>
          <td>thumbnail도 원본 권한 상속</td>
      </tr>
      <tr>
          <td>대용량 다운로드 artifact</td>
          <td style="text-align: right">10~30분</td>
          <td>1회성 작업 token, 만료 후 재발급</td>
      </tr>
      <tr>
          <td>공개 정적 파일</td>
          <td style="text-align: right">signed URL 불필요 가능</td>
          <td>public 경로로 분리</td>
      </tr>
  </tbody>
</table>
<p>TTL을 너무 짧게 잡으면 사용자가 큰 파일을 내려받다가 실패할 수 있습니다. 반대로 너무 길게 잡으면 권한 변경과 URL 공유에 취약합니다. 대용량 파일은 URL TTL만 늘리는 대신 range request, resumable download, 재발급 API를 설계하는 편이 낫습니다.</p>
<h3 id="3-file-metadata가-source-of-truth여야-한다">3) File metadata가 source of truth여야 한다</h3>
<p>object storage에 객체가 있다고 해서 사용자에게 보여줘도 된다는 뜻은 아닙니다. 파일의 진짜 상태는 DB의 metadata row가 가져야 합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> file_asset (
</span></span><span style="display:flex;"><span>  file_id           <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">64</span>) <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>  tenant_id         <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">64</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  owner_id          <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">64</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  object_key        <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">512</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  object_version    <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">128</span>),
</span></span><span style="display:flex;"><span>  status            <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">32</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  sensitivity       <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">32</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  content_type      <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">120</span>),
</span></span><span style="display:flex;"><span>  size_bytes        <span style="color:#8be9fd;font-style:italic">bigint</span> <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  checksum_sha256   <span style="color:#8be9fd;font-style:italic">varchar</span>(<span style="color:#bd93f9">80</span>),
</span></span><span style="display:flex;"><span>  created_at        timestamptz <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  deleted_at        timestamptz,
</span></span><span style="display:flex;"><span>  retention_until   timestamptz
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>다운로드 API는 이 row를 기준으로 판단합니다. <code>status=PUBLISHED</code> 또는 <code>CLEAN</code>인지, <code>deleted_at</code>이 비어 있는지, <code>retention_until</code> 정책에 걸리지 않는지, object version이 현재 버전인지 확인합니다. 이 관점은 <a href="/learning/deep-dive/deep-dive-object-upload-quarantine-scanning-playbook/">Object Upload Quarantine</a>과 같습니다. 업로드 완료, 스캔 완료, 공개 가능, 삭제 예정은 서로 다른 상태입니다.</p>
<h3 id="4-object-version을-고정하지-않으면-url-의미가-흔들린다">4) object version을 고정하지 않으면 URL 의미가 흔들린다</h3>
<p>같은 object key에 덮어쓰기가 가능하면 URL이 어떤 파일을 가리키는지 헷갈릴 수 있습니다. 사용자가 <code>reports/monthly.csv</code>에 대한 URL을 받았는데, 그 사이 새 파일이 같은 key로 올라오면 이전 URL이 새 객체를 가리킬 수 있습니다. 감사와 재현성 측면에서 좋지 않습니다.</p>
<p>민감 파일이나 리포트 artifact는 object key를 불변으로 두는 편이 안전합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tenant/{tenantId}/files/{fileId}/versions/{versionId}/original
</span></span><span style="display:flex;"><span>tenant/{tenantId}/exports/{jobId}/artifact/{artifactId}.csv
</span></span></code></pre></div><p>S3 versioning을 쓰는 경우에도 발급 시점의 version id를 metadata에 저장하고, URL도 해당 version을 대상으로 발급합니다. 덮어쓰기가 필요한 public asset과 감사가 필요한 private file을 같은 key 정책으로 다루면 운영 사고가 납니다.</p>
<h3 id="5-cdn과-cache는-비공개-파일에서-별도-경계다">5) CDN과 cache는 비공개 파일에서 별도 경계다</h3>
<p>Presigned URL을 CDN 뒤에 붙이면 다운로드 성능은 좋아질 수 있습니다. 하지만 CDN cache가 비공개 파일을 보관하면 권한 변경 후에도 잔여 노출이 커집니다. 특히 <code>Cache-Control</code>을 잘못 두거나 signed URL query string을 cache key에서 제외하면 다른 사용자의 응답이 재사용될 수 있습니다.</p>
<p>비공개 파일 기준은 보수적으로 잡습니다.</p>
<ul>
<li>private origin과 public origin을 분리한다.</li>
<li>민감 파일은 <code>Cache-Control: private, no-store</code> 또는 매우 짧은 TTL을 둔다.</li>
<li>CDN cache key에 서명 관련 query/header 정책이 반영되는지 확인한다.</li>
<li>권한 변경이나 삭제 이벤트 후 purge가 필요한 파일 유형을 분리한다.</li>
<li>썸네일, 변환본, preview PDF도 원본 권한을 상속한다.</li>
</ul>
<p><a href="/learning/deep-dive/deep-dive-system-design-file-serving/">파일 업로드와 서빙 시스템 설계</a>에서 말하는 파일 서빙 경로는 단순히 빠른 전송이 아니라 권한과 cache 경계를 함께 설계하는 일입니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-다운로드-url-발급-api는-object-key를-숨긴다">1) 다운로드 URL 발급 API는 object key를 숨긴다</h3>
<p>권장 API는 아래처럼 시작합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>POST /files/{fileId}/download-url
</span></span><span style="display:flex;"><span>  request: { &#34;purpose&#34;: &#34;preview&#34; | &#34;download&#34; | &#34;export&#34; }
</span></span><span style="display:flex;"><span>  response: { &#34;url&#34;: &#34;...&#34;, &#34;expires_at&#34;: &#34;...&#34;, &#34;file_version&#34;: &#34;v17&#34; }
</span></span></code></pre></div><p>서버 내부 흐름은 이렇습니다.</p>
<ol>
<li><code>file_id</code>로 metadata를 조회한다.</li>
<li>tenant, owner, group, role, ABAC 조건을 확인한다.</li>
<li>파일 상태가 <code>CLEAN/PUBLISHED</code>인지 확인한다.</li>
<li>sensitivity와 purpose에 따라 TTL을 결정한다.</li>
<li>object key와 version을 고정해 presigned URL을 만든다.</li>
<li>발급 이벤트를 audit log에 남긴다.</li>
<li>같은 사용자와 파일에 대한 발급 rate limit을 적용한다.</li>
</ol>
<p>사용자에게 object key를 직접 받지 않으면 key 추측 공격과 tenant 경계 우회를 크게 줄일 수 있습니다.</p>
<h3 id="2-민감도별-정책을-코드가-아니라-테이블로-둔다">2) 민감도별 정책을 코드가 아니라 테이블로 둔다</h3>
<p>파일 정책은 코드 곳곳의 <code>if</code>로 흩어지기 쉽습니다. 최소한 아래 정도의 정책표를 둡니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">download_url_policy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">public_asset</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">signed</span>: <span style="color:#ff79c6">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">cdn_cache_ttl</span>: <span style="color:#bd93f9">86400</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">private_attachment</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">signed</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">url_ttl_seconds</span>: <span style="color:#bd93f9">600</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">audit</span>: basic
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">cdn_cache_ttl</span>: <span style="color:#bd93f9">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">sensitive_report</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">signed</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">url_ttl_seconds</span>: <span style="color:#bd93f9">120</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">audit</span>: strict
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">cdn_cache_ttl</span>: <span style="color:#bd93f9">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">require_object_version</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">settlement_export</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">signed</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">url_ttl_seconds</span>: <span style="color:#bd93f9">300</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">audit</span>: strict
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">require_job_owner</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">max_issue_per_hour</span>: <span style="color:#bd93f9">10</span>
</span></span></code></pre></div><p>의사결정 우선순위는 <strong>권한 재검증 &gt; 짧은 TTL &gt; cache 차단 &gt; 감사 로그 &gt; 사용자 편의</strong>입니다. 편의 때문에 TTL을 늘릴 수는 있지만, 민감 파일에서는 그 결정이 잔여 노출 시간을 늘린다는 점을 명시해야 합니다.</p>
<h3 id="3-감사-로그는-파일-접근이-아니라-url-발급부터-남긴다">3) 감사 로그는 파일 접근이 아니라 URL 발급부터 남긴다</h3>
<p>object storage access log만으로는 부족합니다. 사용자가 실제 다운로드했는지 보는 것도 중요하지만, 서버가 언제 어떤 판단으로 URL을 발급했는지가 먼저입니다.</p>
<p>권장 audit 필드:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>event_type=FILE_DOWNLOAD_URL_ISSUED
</span></span><span style="display:flex;"><span>file_id, tenant_id, requester_id, subject_owner_id
</span></span><span style="display:flex;"><span>object_version, sensitivity, purpose
</span></span><span style="display:flex;"><span>expires_at, ttl_seconds
</span></span><span style="display:flex;"><span>authorization_result, policy_version
</span></span><span style="display:flex;"><span>request_ip_hash, user_agent_class
</span></span></code></pre></div><p>민감 파일은 실제 object storage access log와 발급 로그를 주기적으로 맞춰 봅니다. 발급 로그는 있는데 access가 없을 수 있고, access는 있는데 발급 로그가 없다면 우회 경로나 오래된 URL 공유 가능성을 의심해야 합니다.</p>
<h3 id="4-권한-변경-이벤트와-잔여-노출-시간을-연결한다">4) 권한 변경 이벤트와 잔여 노출 시간을 연결한다</h3>
<p>사용자가 프로젝트에서 제거되거나 파일이 삭제되면 이미 발급된 URL이 남아 있을 수 있습니다. 이때 목표는 &ldquo;0초 취소&quot;가 아니라 정책적으로 허용한 잔여 노출 시간 안에 닫히는지 확인하는 것입니다.</p>
<p>운영 기준 예시:</p>
<ul>
<li>민감 파일 권한 변경 후 잔여 URL 노출: 최대 5분</li>
<li>일반 첨부파일 권한 변경 후 잔여 URL 노출: 최대 15분</li>
<li>삭제/비공개 이벤트 후 CDN purge 완료: p95 2분, p99 10분</li>
<li>URL 발급 실패율: 정상 상태 0.5% 이하</li>
<li>파일별 URL 발급 rate limit: 사용자당 분당 20회, 민감 파일은 시간당 10회</li>
</ul>
<p>이 수치를 문서화하면 보안 리뷰에서 &ldquo;Presigned URL이라 안전합니다&quot;가 아니라 &ldquo;권한 변경 후 최대 5분까지 잔여 노출을 허용하고, 그 이상은 알람입니다&quot;라고 말할 수 있습니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, Presigned URL은 앱 서버 부하를 줄이지만 access control의 일부를 storage로 위임합니다. 발급 API가 틀리면 storage는 그대로 허용합니다. 그래서 인증/인가 테스트는 파일 API에 남아야 합니다.</p>
<p>둘째, URL TTL을 짧게 잡으면 보안은 좋아지지만 사용자 경험이 나빠질 수 있습니다. 특히 큰 파일이나 불안정한 네트워크에서는 만료 재발급이 필요합니다. 이 문제를 TTL 증가로만 풀지 말고 재발급 API, range request, 다운로드 세션을 검토합니다.</p>
<p>셋째, 이미 발급된 URL을 즉시 취소하기 어렵습니다. object 삭제, key 변경, bucket policy 변경, CDN purge 같은 조치는 비용이 크고 부작용이 있습니다. 따라서 민감 파일은 처음부터 짧은 TTL과 version 고정, cache 차단을 기본값으로 둬야 합니다.</p>
<p>넷째, public과 private을 같은 bucket/prefix/CDN 정책으로 섞으면 나중에 분리하기 어렵습니다. 완전 공개 파일과 권한 기반 파일은 저장소 경계, cache 정책, 메타데이터 상태를 다르게 가져가는 편이 안전합니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> URL 발급 API가 object key가 아니라 <code>file_id</code>를 입력으로 받는다.</li>
<li><input disabled="" type="checkbox"> 발급 전 tenant, owner, role, 파일 상태, object version을 재검증한다.</li>
<li><input disabled="" type="checkbox"> 파일 민감도별 TTL과 CDN cache 정책이 숫자로 정해져 있다.</li>
<li><input disabled="" type="checkbox"> 민감 파일은 URL 발급 audit log와 object access log를 함께 본다.</li>
<li><input disabled="" type="checkbox"> 이미 발급된 URL의 잔여 노출 시간을 보안 정책에 명시했다.</li>
<li><input disabled="" type="checkbox"> 삭제, 비공개, 권한 변경 이벤트가 CDN purge 또는 짧은 TTL 정책과 연결되어 있다.</li>
<li><input disabled="" type="checkbox"> 썸네일, 변환본, export artifact도 원본 권한을 상속한다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>현재 서비스의 파일 다운로드 API 하나를 골라 사용자가 object key를 직접 전달할 수 있는지 확인해 보세요. 가능하다면 <code>file_id</code> 기반 발급 구조로 바꿀 때 필요한 metadata 필드를 적어 봅니다.</li>
<li>개인정보가 포함된 CSV export를 가정하고 URL TTL, 발급 rate limit, audit 필드, 권한 변경 후 잔여 노출 시간을 숫자로 정해 보세요.</li>
<li>CDN을 쓰는 비공개 파일 경로가 있다면 cache key에 서명 query/header가 어떻게 반영되는지, 삭제 이벤트 후 purge가 얼마나 걸리는지 측정해 보세요.</li>
</ol>
]]></content:encoded></item></channel></rss>