← ~/notes · 3 min read

정적 블로그 글 작성 시각이 새는 곳들 — Last-Modified, ETag, CT 로그

frontmatter 날짜만 가짜로 적어두면 끝일 줄 알았다. 사실은 HTTP 헤더 한 줄로 빌드 시각이 다 새고 있었다.

목차
  1. 새는 곳 정리
  2. Cloudflare Transform Rules로 한 방에
  3. 1단계: API 토큰
  4. 2단계: API 호출
  5. 3단계: 검증
  6. Trade-off
  7. 못 막는 것들
  8. 정리

블로그 글에 적은 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분.

나머지는 신경 끈다.