정적 블로그 글 작성 시각이 새는 곳들 — Last-Modified, ETag, CT 로그
frontmatter 날짜만 가짜로 적어두면 끝일 줄 알았다. 사실은 HTTP 헤더 한 줄로 빌드 시각이 다 새고 있었다.
블로그 글에 적은 date: 2026-04-26가 진짜 그날 쓴 글일까? 외부에서 검증할 수 있을까?
아무도 안 까보겠지만, 까보면 꽤 까진다. 정리하면서 동시에 막을 수 있는 건 자동화로 다 막았다.
새는 곳 정리
글 frontmatter 날짜 ← 본인이 적는 값. 거짓말 가능.
HTTP 응답 헤더
├─ Last-Modified ← 빌드 시각. 즉시 노출.
├─ ETag ← mtime+size 인코딩. 즉시 노출.
└─ cf-cache-status / Age ← 캐시 진입 시각 단서.
외부 시스템
├─ Certificate Transparency 로그 ← TLS 발급일. 영구 공개.
├─ WHOIS ← 도메인 등록일. 영구 공개.
├─ Wayback Machine ← 첫 크롤링 시각. 영구 공개.
└─ Google 인덱싱 ← 첫 인덱싱 시각.
거짓말이 들통날 만한 진짜 위협은 첫 두 줄(Last-Modified, ETag) 이다. 한 줄 curl로 누구나 확인 가능.
$ curl -sI https://blog.hyeonbin.net/posts/2026-04-26-hello/ | grep -i 'last-modified\|etag'
last-modified: Sun, 26 Apr 2026 12:02:04 GMT
etag: "..."
서버에 파일이 언제 들어갔는지 그대로 박힌다. nginx의 ETag는 기본이 "mtime-size" 형태라 mtime이 그대로 인코딩된다.
Cloudflare Transform Rules로 한 방에
해결은 간단하다. Cloudflare 엣지에서 응답 헤더를 떼면 된다.
대시보드 클릭 작업이지만, API로 자동화하는 게 깔끔하다. 토큰 하나만 발급받으면 끝.
1단계: API 토큰
https://dash.cloudflare.com/profile/api-tokens에서 Custom Token 발급. 권한:
- Zone / Transform Rules / Edit
- Zone / Zone / Read (zone ID 조회용)
- Zone Resources: 특정 영역 =
<도메인>
2단계: API 호출
엔드포인트는 http_response_headers_transform 단계의 ruleset entrypoint.
ZONE_ID=$(curl -s -H "Authorization: Bearer $CF_TOKEN" \
"https://api.cloudflare.com/client/v4/zones?name=hyeonbin.net" \
| jq -r '.result[0].id')
curl -X PUT \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_response_headers_transform/entrypoint" \
--data '{
"rules": [{
"expression": "(http.host eq \"blog.hyeonbin.net\")",
"description": "Strip timestamp headers (blog)",
"action": "rewrite",
"action_parameters": {
"headers": {
"Last-Modified": {"operation": "remove"},
"ETag": {"operation": "remove"}
}
}
}]
}'
PUT이 ruleset 통째로 덮어쓰는 방식이라 처음 만들 땐 이걸로 OK. 이미 룰이 있으면 GET으로 받아서 추가하고 다시 PUT 하거나, /rules 엔드포인트로 POST.
3단계: 검증
$ curl -sI https://blog.hyeonbin.net/posts/2026-04-26-hello/ | grep -iE 'last-modified|etag'
(아무것도 안 나옴)
엣지에서 떨어져 나가서 클라이언트한텐 도달 안 한다. origin 부하 0.
Trade-off
Last-Modified/ETag 떼면 HTTP 304 Not Modified 재검증이 안 된다. 만료 후엔 전체 본문 재다운로드.
블로그 시나리오에선 거의 영향 없다:
- HTML은 몇 KB. 재다운로드 비용 무시 가능.
- Astro의
/_astro/*자산은 해시 파일명 →Cache-Control: immutable로 영구 캐시 가능. 재검증 자체가 불필요. - Cloudflare CDN이 origin 앞에 캐시하므로 origin은 어차피 hit 안 함.
신경 쓰이면 Cache Rules로 /_astro/ 경로에 Edge TTL 1년 + Browser TTL 1년 명시.
못 막는 것들
위 작업으로 막힌 건 “글 단위 작성 시각” 추론이다. 사이트 단위 시각은 여전히 공개된다.
| 단서 | 보이는 정보 | 막을 방법 |
|---|---|---|
| Certificate Transparency 로그 | TLS 인증서 발급일 ≈ 도메인이 HTTPS로 처음 살아난 시점 | 없음 (CT는 브라우저가 요구하는 표준) |
| WHOIS | 도메인 등록일 | 없음 (등록 자체가 공개 기록) |
| Wayback Machine | 첫 크롤링 시점 | robots.txt로 차단 가능 |
| Google 인덱싱 | 첫 인덱싱 시점 | robots.txt / noindex (SEO 포기) |
| 본인이 링크 공유한 곳 | 트위터/디스코드 등 | 본인이 안 뿌리는 수밖에 |
CT 로그는 crt.sh에서 누구나 본다. WHOIS도. 둘 다 도메인 단위라 “이 글이 그날 쓰였다”는 못 알려주지만 “이 도메인은 N월 N일 이후에 살아 있다”는 알려준다.
받아들일 부분과 막을 부분을 구분하는 게 깔끔하다.
정리
| 위협 | 차단 가능? | 방법 |
|---|---|---|
| Last-Modified 헤더 | ✓ | Cloudflare Transform Rule |
| ETag 헤더 | ✓ | 같음 |
| Wayback 크롤 | ✓ | robots.txt + X-Robots-Tag: noarchive |
| 검색엔진 인덱싱 | ✓ | robots.txt (단 SEO 포기) |
| CT 로그 | ✗ | 구조적 불가 |
| WHOIS 등록일 | ✗ | 구조적 불가 |
블로그 글 단위 위장에 진짜 위협은 위 두 헤더와 Wayback. 셋 다 막는데 합쳐서 10분.
나머지는 신경 끈다.