본문으로 건너뛰기
wooncloud

서명 URL로 보내는 302를 캐시하면 만료된 403이 계속 재생된다

S3 presigned URL 로 리다이렉트하는 302 를 프록시가 캐시하면 몇 분 뒤에도 만료된 Location 이 재생됩니다. Date 헤더로 캐시 층을 가려내고 no-store 로 푸는 과정을 정리했습니다.

·4 min read·— views··

이미지 주소 /img/<key>.png를 서버가 받으면 S3 presigned URL(유효 60초)을 새로 만들어 302로 보내는 구조였습니다. 그런데 어떤 이미지는 처음부터 썸네일이 깨져 있었습니다.

같은 주소를 몇 분 뒤 다시 받아 보니 302의 Date 헤더와 Location의 X-Amz-Date가 처음 요청한 시각에 고정돼 있었습니다. 그 Location을 열면 S3가 403 Request has expired를 돌려줬고요.

누가 302를 저장해 둘까

서버 코드는 요청마다 새로 서명합니다. 그런데도 첫 302가 그대로 재생된다면 누군가 응답을 저장해 두고 내주고 있다는 뜻입니다.

단서는 Date 헤더입니다. 앱이 서명 URL만 캐시한다면 Date는 매번 새 값이어야 합니다. Date까지 고정이면 앱이 아니라 HTTP 캐시 층, 그러니까 앞단 프록시 캐시입니다.

캐시가 범인인지는 같은 요청을 오리진(인스턴스)에 직접 보내 Date가 바뀌는지로 가립니다.

콘솔에는 왜 만료가 안 보일까

브라우저에서는 S3 오류 본문(application/xml)이 <img> 응답으로 오니 ERR_BLOCKED_BY_ORB로만 보입니다. 원인이 만료라는 게 콘솔에 드러나지 않습니다. 그래서 더 헷갈렸습니다.

403이면 객체는 있는 걸까

아닙니다. 진단하다가 가장 빠지기 쉬운 오판이 이겁니다.

S3는 presigned 요청의 서명과 만료를 객체 조회보다 먼저 검사합니다. 그러니 만료된 URL은 키가 없어도 403입니다. 게다가 버킷에 ListBucket 권한이 없으면 키가 없을 때도 404 NoSuchKey 대신 403 AccessDenied가 옵니다.

그래서 "만료 403을 봤으니 객체는 있다"는 결론은 성립하지 않습니다. 객체 유무를 보려면 캐시 버스터 쿼리(?cb=<now>)로 새 서명을 받고, 서명 직후 만료 전에 S3 응답 코드를 봐야 합니다.

302에는 no-store

수명이 짧은 서명 URL로 가는 리다이렉트에는 Cache-Control: no-store를 붙입니다.

HTTP/1.1 302 Found
Location: https://bucket.s3.amazonaws.com/a.png?X-Amz-Expires=60&...
Cache-Control: no-store

캐시가 원본을 받아 가는 구조라면 다른 방법도 있습니다. 302 대신 바이트를 직접 내려서, 캐시가 리다이렉트가 아니라 이미지 자체를 저장하게 하는 겁니다.

서명 URL이 60초짜리면 그걸 가리키는 302도 60초짜리 응답이라는 걸 기억해 두기로 했습니다.

참고