블로그의 보안 원칙을 배포 증거로 바꾸기
운영자 블로그의 「API 비용 폭탄·DB 노출·AI 에이전트 사고를 막는 실전 보안 가이드」(2026년 9월 3일)는 AI의 실행 권한과 비밀정보 관리를 다뤘습니다. 이 글은 그 문제의식을 작은 웹사이트 배포에 적용한 별도 점검표입니다. 원문의 도구별 설정이나 요금제를 복제하지 않고, 무엇을 검사하고 어떤 증거가 남아야 하는지에 집중합니다.
아래 회원 메모 앱은 설명용 가상 사례입니다. 쭈니박스 본도메인에 로그인·메모 저장 기능이 있다는 뜻도, 별도 서비스의 보안 검사가 모두 끝났다는 뜻도 아닙니다.
1. 사이트의 역할에 따라 검사 범위를 나눕니다
| 유형 | 먼저 확인할 것 | 완료 증거 |
|---|---|---|
| HTML·CSS 소개 사이트 | 배포 폴더에 내부 자료가 섞이는지, 외부 스크립트가 무엇인지 | 공개 파일 목록과 네트워크 요청 목록 |
| 로그인·저장 앱 | 세션 검증과 사용자별 읽기·쓰기 권한 | 사용자 A/B/로그아웃 상태의 결과표 |
| 유료 API를 호출하는 앱 | 서버 측 호출 권한, 요청량·입력 크기·재시도 제한 | 정상 요청과 제한 초과 시나리오 |
정적 사이트에 쓰지도 않는 DB 검사를 억지로 붙일 필요는 없습니다. 반대로 로그인 버튼을 숨겼다는 이유로 서버의 권한 검사를 생략해서도 안 됩니다. 기능이 없는 항목은 ‘해당 없음’과 이유를 적고, 아직 검사하지 않은 항목은 ‘미확인’으로 남깁니다.
2. 저장소가 아니라 공개되는 폴더를 봅니다
정적 호스팅에서는 게시 디렉터리에 포함된 자료가 공개될 수 있습니다. 비공개 Git 저장소라는 사실만으로 게시 파일까지 비공개가 되지는 않습니다. 이 허브도 저장소 루트를 출력 위치로 쓰므로 작업 문서에 비밀정보를 넣지 않는 원칙을 유지합니다.
- Cloudflare 대시보드 → Workers & Pages → 해당 Pages 프로젝트 → Settings → Builds & deployments에서 출력 디렉터리를 확인합니다. UI 명칭이 바뀌면 빌드 설정의 출력 경로 항목을 찾습니다.
- 그 폴더의 파일 이름을 먼저 검토합니다. 환경 파일, 개인 메모, DB 덤프, 로그, 자격증명 파일은 공개 자료가 아닙니다. 파일 내용을 통째로 로그에 출력하지 않습니다.
- Chrome → 확인할 사이트 → F12 → Network에서 새로고침해 실제로 요청되는 스크립트의 출처를 봅니다. 토큰이나 요청 헤더가 포함된 화면은 그대로 공유하지 않습니다.
- 키 검사는 파일 이름만으로 끝나지 않습니다. 공개 산출물과 Git 변경분을 비밀정보 검사 도구로 검사하되 결과는 값을 가린 형태로 확인합니다.
Supabase의 publishable 키와 기존 anon 키는 브라우저 사용을 전제로 합니다. 그러나 공개 키가 있다는 사실이 데이터 접근을 허용해도 된다는 뜻은 아닙니다. Secret 키와 service_role 키는 RLS를 우회할 수 있어 브라우저에 두면 안 됩니다. Supabase 키 종류 문서에서 사용 위치를 구분해 확인할 수 있습니다.
비밀 키가 이미 공개됐다면 파일 삭제만으로 끝내지 않습니다. 해당 제공자의 키 관리 화면에서 폐기·교체하고 사용 이력을 확인해야 합니다. 공개된 커밋 이력의 정리는 그다음 문제입니다. GitHub의 민감정보 제거 안내도 키 폐기·회전을 먼저 다룹니다.
이 정적 사이트에서 실제로 바꾼 것
2026년 9월 10일 이 허브의 콘텐츠를 보강하며 공개 파일과 작업 문서를 함께 검토했습니다. 소개 페이지에만 민감정보가 없으면 끝이라고 생각하기 쉽지만, 저장소 루트를 게시하는 구조에서는 Markdown 문서도 같은 검토 대상입니다. 문서의 개인 PC 경로를 일반적인 작업 폴더 표기로 바꾸고, 문의 주소는 연락처 페이지를 참조하도록 정리했습니다.
| 공개에서 제외한 자료 | 대신 남긴 근거 |
|---|---|
| 업무 원본 Java 파일과 CI 전체 로그 | 새로 만든 작은 한글 예제, 종료 코드와 최소 오류 문구 |
| AI 계정 화면과 실제 사용 이력 | 가상 사용량 60%, 고정 시각의 계산 결과 |
| 개인 PC 사용자 경로 | 실험 폴더 기준 파일 이름과 실행 명령 |
| 배포 계정 화면과 전체 HTTP 헤더 | 환경 구분, 상태 코드, 제목 포함 여부 |
예를 들어 인코딩 실험은 원본 업무 프로젝트를 공개하지 않고도 실패와 성공을 비교할 수 있습니다. 계산기 실험도 계정 연결 없이 공개 함수에 넣은 예제 값만 사용합니다. 독자가 확인할 핵심과 공개할 필요 없는 정보를 분리한 것입니다.
이 편집은 문서의 비공개 전환이 아닙니다. 화면에 보이지 않게 접거나 CSS로 가려도 HTML 원문은 내려받을 수 있습니다. robots.txt·noindex 역시 접근 제어가 아닙니다. 비공개 자료는 처음부터 게시 디렉터리 밖에 두거나 인증을 요구하는 저장소에 보관해야 합니다. 이 허브의 작업 문서도 계속 공개 자료로 취급합니다.
현재 파일을 바꿔도 과거 배포본, 이전 미리보기, Git 이력과 캐시의 사본이 함께 바뀌지는 않습니다. 이미 자격증명이 유출된 경우에는 폐기·교체와 과거 공개 범위 확인을 따로 진행해야 합니다. 이번 문서 정리를 전체 보안 점검이나 Supabase 권한 시험의 완료로 표현하지 않습니다.
3. 성공하는 계정 말고, 거절돼야 할 계정으로 확인합니다
가상의 개인 메모 앱을 시험한다고 가정합니다. 운영 고객 계정 대신 격리된 시험 환경에 A와 B 계정을 만들고, A에게만 보이는 시험 메모를 하나 둡니다. 두 계정은 서로 다른 브라우저 프로필이나 별도 세션으로 열어 로그인 상태가 섞이지 않게 합니다.
| 시도 | 기대 결과 | 실패라면 |
|---|---|---|
| A가 자신의 메모 읽기 | 자신의 내용만 표시 | 소유자 값·정책·세션 확인 |
| B가 A의 메모 주소 또는 ID로 읽기 | A의 내용이 응답에 없음 | 배포 중단, 읽기 정책 확인 |
| B가 A의 메모 수정 요청 | A의 원본 내용이 바뀌지 않음 | 쓰기 정책과 서버 권한 확인 |
| 로그아웃 후 같은 자료 요청 | 비공개 내용이 응답에 없음 | 익명 접근과 공개 캐시 확인 |
접근 거절이 항상 HTTP 403으로 나타나는 것은 아닙니다. RLS로 행이 필터링되면 정상 응답에 빈 목록이 올 수도 있습니다. 상태 코드만 보지 말고 응답 내용과 실제 저장 결과를 같이 봐야 합니다. Supabase 대시보드 → 프로젝트 → Database → Tables에서 대상 테이블을 찾고 RLS/Policies를 확인합니다. 정책은 RLS 공식 문서와 실제 스키마를 기준으로 검토합니다.
서버가 관리자 권한 키를 사용하는 경우에는 RLS만으로 보호됐다고 생각하면 안 됩니다. 그 서버 경로에서 사용자 신원과 대상 데이터에 대한 권한을 검사해야 합니다. CORS의 허용 도메인 목록 역시 인증이나 데이터 권한 검사를 대신하지 않습니다.
4. 실패가 반복될 때 어디서 멈추는지 정합니다
외부 API 호출이 있는 앱은 정상 응답 한 번보다 실패 이후 동작이 중요합니다. 예를 들어 저장 버튼이 응답 지연 중 계속 눌리고, 브라우저와 서버가 각각 재시도하면 사용자가 생각한 한 번의 작업이 여러 호출이 될 수 있습니다.
- 같은 작업을 중복 실행하지 않도록 요청 식별과 서버 처리를 설계합니다. 버튼 비활성화만으로는 직접 요청을 막을 수 없습니다.
- 최대 시도 수, 전체 제한 시간, 재시도할 오류 종류를 정합니다. 권한 오류처럼 기다려도 해결되지 않는 실패를 무한 반복하지 않습니다.
- 비용 알림과 실제 요청 차단을 구분합니다. 제공자의 과금 집계 지연과 제한 적용 범위를 확인하고, 앱에서도 사용자별 요청량과 입력 크기를 제한합니다.
- 시험은 모의 응답이나 별도 시험 자원에서 합니다. 제한 확인을 위해 운영 API를 대량 호출하거나 실제 지출을 늘리지 않습니다.
프롬프트에 ‘조심해’라고 적는 것은 실행 환경의 권한 제한을 대체하지 않습니다. 코드 검토, 파일 수정, 운영 DB 변경, 배포를 한 번의 자동 승인 범위로 묶지 않고 복구 가능한 단계로 구분합니다.
실험: ‘세 번만 재시도’가 왜 아홉 번 호출이 될까요?
‘재시도 3회’는 처음 요청을 포함하는지부터 모호합니다. 아래에서는 최초 요청을 포함해 최대 3회로 고정합니다. 일시 실패를 나타내는 503 뒤에만 다음 시도를 하고, 401·429·그 밖의 응답에서는 멈추는 작은 모형입니다. 외부 서버를 호출하지 않으며 실제 과금도 발생시키지 않습니다.
VS Code → File → New Text File에 코드를 붙여넣고 retry-budget.mjs로 저장합니다. Node.js가 있는 환경의 Terminal → New Terminal에서 그 폴더로 이동해 node retry-budget.mjs를 실행합니다. HTTP 클라이언트가 아니라 중단 규칙을 시험하는 코드이므로 운영 요청 함수에 그대로 연결하지 않습니다.
import assert from 'node:assert/strict';
function readWithBudget(read) {
for (let attempts = 1; attempts <= 3; attempts++) {
const status = read();
if (status !== 503 || attempts === 3) {
return { status, attempts };
}
}
}
const cases = [
[[200], { status: 200, attempts: 1 }],
[[503, 503, 200], { status: 200, attempts: 3 }],
[[503, 503, 503], { status: 503, attempts: 3 }],
[[401, 200], { status: 401, attempts: 1 }],
[[429, 200], { status: 429, attempts: 1 }],
];
for (const [responses, expected] of cases) {
let calls = 0;
const result = readWithBudget(() => responses[calls++]);
assert.deepEqual(result, expected);
assert.equal(calls, expected.attempts);
}
let calls = 0;
readWithBudget(() =>
readWithBudget(() => { calls++; return 503; }).status);
assert.equal(calls, 9);
console.log('5 stop-rule cases passed; nested calls = 9');
실행 결과는 5 stop-rule cases passed; nested calls = 9입니다. 바깥 세 번 각각에서 안쪽 세 번이 새로 시작돼 3 × 3이 됩니다. 한 곳에서만 시도 횟수를 관리하면 같은 모형의 상한은 3입니다. 브라우저·서버·SDK가 각자 재시도한다면 각 층의 설정을 읽어야 하는 이유입니다.
| 상황 | 무턱대고 반복하면 | 다음 결정 |
|---|---|---|
| 401 권한 실패 | 같은 자격증명으로 같은 실패를 반복 | 인증 상태 확인으로 종료. 자동 무한 반복 금지 |
| 429 요청 제한 | 제한 시간에 요청을 더 쌓음 | 제공자 규약과 Retry-After, 전체 대기 예산 확인 |
| 저장 후 응답만 유실 | 첫 저장이 실패했다고 보고 중복 저장 | 작업 식별자·서버 중복 방지·결과 조회 방식 설계 |
| 계속되는 503 | 호출 수와 대기 시간이 함께 증가 | 횟수뿐 아니라 전체 제한 시간과 취소 경로 설정 |
GET과 POST는 같은 재시도 규칙으로 다룰 수 없습니다. 조회와 달리 일반적인 POST는 반복이 추가 부작용을 만들 수 있습니다. 위 모형에는 실제 네트워크 시간 초과, 예외, 대기·지수 백오프, 서버의 중복 방지 계약이 없습니다. 따라서 이 실험이 증명하는 것은 ‘이 모의 입력에서 지정한 횟수에 멈춘다’는 것뿐이지, 실제 서비스의 비용 상한이나 결제 안전성이 아닙니다.
5. ‘문제없음’ 대신 확인한 사실을 인계합니다
대상: 개인 메모 앱 / 시험 환경 / 대상 커밋
공개 파일: 검토한 출력 폴더와 제외한 자료
권한: A 읽기 성공, B 읽기 내용 없음, B 수정 후 원본 불변
비용: 모의 오류에서 정해진 횟수 후 종료
복구: 이전 코드 버전, 데이터 백업, 복원 시험 결과
미확인: 외부 로그인 제공자 연동
판정: 로그인 검증 전 운영 전환 보류
Git으로 이전 코드로 돌아갈 수 있어도 배포 이후 새로 저장된 데이터가 저절로 복구되는 것은 아닙니다. 코드와 데이터의 복구 범위를 따로 정합니다. 구체적인 전환 계획은 사이트 이전 체크리스트에서 이어집니다.
검사를 통과했다는 말은 적어 둔 범위에서 기대 결과를 확인했다는 뜻입니다. 보안 인증이나 모든 공격에 대한 안전 보장은 아닙니다. 기능이 바뀌면 권한과 공개 범위를 다시 검사합니다.