← Ascendy EN

infra

공개로 서빙되지만 비밀인 값 — 소유증명 키를 secret으로 다시 분류한 이야기

· Ascendy Engineering


TL;DR

소스 노트. 이 글은 상위 인프라 팀의 보안위생 회고(docs/intake/from-infra/2026-06-03-ownership-key-semi-secret.md)를 정제한 것이다. 도메인·Worker 이름·키 경로·PR 번호는 일반화했고, 실제 키 값은 본문 어디에도 없다. 이 분류 실수는 게시 전 리뷰에서 잡혀 rotate + 재설계로 끝났다 — 그 자체가 글의 한 교훈이다.

”이 값, 진짜 공개여도 돼?”

검색엔진 크롤을 앞당기려고 색인 자동 통보 Worker를 만들었다. 이 프로토콜은 도메인 소유를 증명하려고 https://<호스트>/<키>.txt에 키 파일을 두고, 통보를 보낼 때마다 그 키를 함께 보낸다.

우리는 이 키를 **“소유 증명용 공개값”**으로 분류했다. 논리는 단순했다 — 어차피 검색엔진이 URL로 가져가야 하는 값이니, 누구나 볼 수 있는 공개값 아닌가? 그래서 거리낌 없이 설정 파일에 평문으로 커밋하고, 정적 사이트가 /<키>.txt로 게시하게 설계했다.

리뷰에서 막혔다. 리뷰어가 공식 문서를 들고 왔다.

“Only you and the search engines should know the key and your file key location.”

즉 이 키는 공개값이 아니라 **준비밀(semi-secret)**이다. 키를 아는 사람은 우리 도메인 전체에 대해 “이 URL이 바뀌었다”는 색인 통보를 위조 제출할 수 있다. 읽기나 삭제는 못 하고 severity는 낮지만, 0은 아니다. “공개여도 된다”는 우리 전제 자체가 틀렸던 것이다.

함정 — “가져갈 수 있다”와 “박아도 된다”는 다르다

키를 git에 커밋한 시점에 우리가 한 판단은 “어차피 URL로 누구나 가져갈 수 있으니 공개값”이었다. 함정은 정확히 거기 있었다.

엔드포인트에서 가져갈 수 있다 ≠ 아무 데나 적어도 된다.

검색엔진이 /<키>.txt를 fetch하는 것과, 그 키가 git 히스토리·PR·핸드오프에 평문으로 박히는 것은 다른 노출이다. 전자는 의도된 공개(소유 검증)이고, 후자는 키를 영구적으로, 검색 가능하게, 되돌릴 수 없이 흩뿌리는 것이다. 문서가 “키 위치도 너와 검색엔진만 알아야 한다”고 한 이유가 이것이다 — obscurity가 약하게나마 방어선의 일부다.

교훈 1 — 값을 커밋하기 전에 “이건 secret인가?”를 명시적으로 분류하라. “공개여도 될 것 같다”는 직감이 아니라, 그 값을 아는 사람이 무엇을 할 수 있는지로 판단한다. 위조 제출이 가능하면, low-severity라도 그건 준비밀이고 git 밖에 있어야 한다.

패턴 — 공개로 서빙하되 git엔 두지 않기

준비밀로 다시 분류하니 까다로운 제약이 생겼다. 키 파일은 /<키>.txt공개로 서빙돼야 한다(검색엔진이 인증 없이 fetch하니까). 그런데 키 값과 키가 든 경로는 git에 없어야 한다. 정적 파일 방식은 이 둘을 동시에 만족할 수 없다 — 파일이 곧 repo에 들어가니까.

해법은 Worker가 키 파일을 직접 서빙하는 것이었다.

// 경로가 정확히 /<key>.txt면 secret을 반환. 인증 없음 —
// 검색엔진이 소유 검증용으로 가져가야 하므로 (이게 의도된 공개).
if (env.OWNERSHIP_KEY) {
  const keyPath = `/${env.OWNERSHIP_KEY}.txt`;
  if (path === keyPath) {
    return new Response(env.OWNERSHIP_KEY, {
      headers: { "Content-Type": "text/plain; charset=utf-8" },
    });
  }
}
// 그 외 경로는 인증 게이트(운영자 전용).
# 키는 운영자가 직접 생성해 secret으로만 주입 — 값을 공유/커밋하지 않는다.
openssl rand -hex 16 | tr -d '\n' | wrangler secret put OWNERSHIP_KEY
# 검증도 키를 출력하지 말 것(준비밀):
test "$(curl -fsS "https://<host>/$KEY.txt")" = "$KEY" && echo ok

결과: 정적 사이트는 키 파일을 보유하지 않고, 키 값도 키-경로도 git에 없으며, 검색엔진은 평소대로 /<키>.txt를 가져간다. 그리고 이미 노출됐던 옛 키는 rotate했다 — 한 번도 라이브로 게시된 적이 없어 영영 유효해지지 않으니, 폐기로 충분했다.

교훈 2 — “공개로 서빙돼야 하지만 git엔 없어야 하는 값”은, 정적 파일 대신 엣지에서 secret으로 서빙하면 둘 다 만족한다. 파일명·경로가 곧 비밀이면, 그 경로를 담는 설정(route)도 커밋에서 빼고 운영자 주입으로 돌린다.

그리고 — 리뷰가 잡은 게 정상이다

이 이야기에서 빠지면 안 되는 게 하나 있다. 잘못된 분류는 머지 전 리뷰에서 잡혔다. 값을 커밋하기 전에 누가 “이거 secret 아냐?” 한 줄을 물어줬다면 더 빨랐겠지만, 적어도 라이브로 게시되기 전에 잡혀 rotate + 재설계로 깔끔히 끝났다.

교훈 3 — 리뷰가 분류 실수를 잡는 건 사고가 아니라 정상 동작이다. 보안위생에서 “리뷰가 일했다”는 좋은 신호다. 한 번도 게시된 적 없는 키라면, 노출의 비용은 재설계의 시간뿐이다.

가져갈 것


저작·인용: 이 글은 Ascendy Engineering이 작성했으며 출처 표기 시 재인용 가능합니다. 잘못된 정보를 발견하면 GitHub 이슈로 알려주세요.


Tags: cloudflare-workers, secrets-hygiene, secret-classification, code-review, indexnow