14장. 검증팀② — 보안검토자: 부탁이 아니라 문

이 장이 끝나면: 온티켓의 비밀값과 고객 개인정보를 지키는 보안 체계 두 겹 — 위험을 찾아내는 보안검토자(Subagent) 와, 위반을 원천 차단하는 보안 Hook — 이 서 있다. 그리고 공격자처럼 일부러 어겨 보는(레드팀) 방식으로 그 문이 정말 잠겨 있는지 확인한다.


14.1 개념 — 보안 사고의 대부분은 해킹이 아니다

영화와 현실

보안 사고라고 하면 후드 쓴 해커가 방화벽을 뚫는 장면을 떠올리지만, 현실의 사고 대부분은 훨씬 시시하게 시작된다.

공통점은 '뚫린' 것이 아니라 '열려 있던' 것이다. 그래서 보안의 첫 번째 일은 공격 방어가 아니라 문단속 — 열려 있는 곳을 찾아 잠그는 것이다.

이 네 가지 사례에는 공통된 심리가 하나 더 깔려 있다. 아무도 나쁜 뜻이 없었다는 점이다. 키를 코드에 박은 개발자는 "일단 돌아가게 하고 나중에 빼자"고 생각했고, 로그에 전화번호를 찍은 사람은 "디버깅에 편하니까"였고, 전체 허용으로 연 사람은 "테스트만 끝나면 닫자"였다. 악의가 아니라 편의와 망각이 사고의 원인이다. 그래서 이 장의 방어 전략은 "나쁜 사람을 막는다"가 아니라 "착한 사람의 실수를 막는다"에 초점이 맞춰져 있다. 후자가 훨씬 자주 일어나고, 다행히 훨씬 막기 쉽다.

세상의 보안 사고 통계도 이 직관을 뒷받침한다. 크게 알려진 유출 사고의 상당수가 정교한 해킹이 아니라 설정 실수에서 비롯됐다 — 공개로 열려 있던 저장소, 비밀번호 없이 인터넷에 노출된 데이터베이스, 공개 저장소에 딸려 올라간 키. 방화벽을 뚫을 필요가 없다. 문이 이미 열려 있으니 그냥 걸어 들어가면 된다. 공격자가 가장 먼저 하는 일도 '뚫기'가 아니라 '열린 문 찾기'다 — 인터넷을 훑으며 실수로 노출된 키와 서버를 자동으로 수집하는 봇이 지금 이 순간에도 돌고 있다.

[짚고 가기] 이 절의 목표는 보안에 대한 인식을 통째로 바꾸는 것이다. 개발이 처음이면 보안을 "나는 아직 실력이 없어서 나중에 배울 어려운 것"으로 미루기 쉽다. 하지만 사고의 대부분이 '실력'이 아니라 '부주의'에서 온다는 것을 알고 나면, 보안이 전문가의 영역이 아니라 오늘부터 지킬 수 있는 습관임이 납득된다. "방화벽이 뚫릴 걱정은 아직 안 해도 된다. 하지만 키를 실수로 올릴 걱정은 오늘 당장 해야 한다"는 한마디가 이 장 전체의 문을 연다.

온티켓이 지켜야 할 세 가지

자산 유출되면
비밀값(시크릿) — DB 연결 문자열, 결제 키 DB 통째로 열람·조작, 결제 도용
고객 개인정보 — 이름·전화·이메일·구매 내역 법적 책임(개인정보보호법), 신뢰 붕괴
판정의 무결성 — 금액·재고 계산 900원에 9만 9천 원 티켓 (이미 서버 계산으로 방어)

이 표를 조금 더 구체적으로 풀어 보자. 각 자산이 무너지면 온티켓에 실제로 무슨 일이 벌어지는지 그려 보면, 왜 지켜야 하는지가 피부에 와닿는다.

세 자산은 성격이 다르다. 비밀값은 한 번 새면 끝(재발급 전까지 계속 위험), 개인정보는 샌 만큼 피해(한 명 새면 한 명 피해), 무결성은 실시간 방어(들어오는 요청마다 판정)다. 그래서 방어 방식도 달라진다 — 비밀값은 '애초에 안 새게', 개인정보는 '필요한 만큼만 다루게', 무결성은 '매번 서버가 다시 계산하게'. 이 장은 앞의 두 가지, 즉 비밀값과 개인정보의 문단속에 집중한다.

두 겹의 체계 — 눈과 문

보안도 검증 직군의 문법을 따르되, 한 겹이 더 붙는다.

왜 눈만으로 부족한가. 검토는 특정 시점의 상태를 확인하는 일이다 — 검토 다음 날의 실수는 못 잡는다. 왜 문만으로 부족한가. 훅은 아는 패턴만 막는다 — 새로운 유형의 구멍은 눈이 찾아야 한다. 그래서 둘 다 필요하다.

이 '눈과 문'의 대비를 조금 더 깊이 들여다보자. 지금까지 우리가 만든 검증 역할들 — 디자인 리뷰어, 코드리뷰어, QA — 은 모두 이었다. 이들은 지적하고 등급을 매기지만, 그 지적을 지킬지 말지는 사람에게 달려 있다. 리뷰어가 "이 키는 위험합니다"라고 말해도, 사람이 무시하고 커밋하면 사고는 난다. 리뷰는 본질적으로 부탁이다 — "이렇게 해 주세요."

훅은 다르다. 훅은 부탁이 아니라 이다. .env를 커밋하려는 명령이 실행되기 직전에 훅이 가로막으면, 사람이 아무리 승인 키를 눌러도 그 명령은 실행되지 않는다. 지키기를 '바라는' 것이 아니라 어길 수 없게 '만드는' 것이다. 이것이 이 장 제목 "부탁이 아니라 문"의 뜻이다.

두 겹이 왜 둘 다 필요한지, 시간 축으로 보면 명확해진다.

보안검토자(눈) 보안 훅(문)
언제 작동 사람이 점검을 시킬 때 위험한 행동이 일어나려 할 때마다
잡는 것 넓다 — 아직 몰랐던 유형의 구멍까지 좁다 — 미리 정한 패턴만
놓치는 것 점검과 점검 사이의 실수 패턴에 없는 새로운 구멍
성격 발견(부탁) 차단(강제)

정리하면 이렇다. 문은 아는 위험을 항상 막고, 눈은 모르는 위험을 가끔 찾는다. 문이 24시간 서 있는 경비라면, 눈은 정기적으로 집을 순회하며 새로 생긴 취약점을 찾는 점검원이다. 경비만 있으면 새로운 침입 경로를 못 찾고, 점검원만 있으면 점검과 점검 사이가 비어 버린다. 그래서 두 겹이다.

[짚고 가기] "리뷰가 있는데 왜 또 훅이 필요하냐"는 의문이 자연스럽게 든다. 답의 핵심은 '강제성의 차이'다. 리뷰어의 지적은 사람이 무시할 수 있지만, 훅은 무시할 수 없다. 직접 해 보면 그 차이가 강렬하게 다가온다 — 보안검토자가 "이 파일 위험합니다"라고 리포트한 뒤에도 그냥 커밋할 수 있는 것을 확인하고, 그다음 훅을 설치한 뒤 같은 커밋을 시도하면 명령 자체가 튕겨 나가는 것을 나란히 놓고 보면, "말리는 것과 막는 것은 다르다"가 한눈에 들어온다.


14.2 용어 정리

이 장에서 처음 만나는(또는 다시 만나는) 보안 용어들이다. 개발이 처음이라면 낯설 수 있지만, 하나하나가 오늘 실습에서 직접 손에 잡히는 개념이라 겁먹을 필요는 없다. "이런 게 있구나" 정도로 눈에 익히고 넘어가면 된다.

시크릿 (secret)

노출되면 안 되는 모든 비밀값의 총칭 — API 키, DB 연결 문자열, 토큰, 비밀번호. 시크릿의 제1규칙은 이미 실천하고 있다: 코드에 쓰지 않고(하드코딩 금지) .env 파일에 둔다. .env는 절대 저장소에 올리지 않는다.

'시크릿'이라는 말을 조금 더 풀어 보자. 우리가 온티켓을 만들며 만난 비밀값들이 전부 시크릿이다.

시크릿의 예 무엇 새면
DB 연결 문자열 데이터베이스에 접속하는 주소+비밀번호가 한 줄에 담긴 것 DB 직접 접속
결제 키(토스) 결제를 요청할 때 우리 상점임을 증명하는 열쇠 결제·환불 도용
API 토큰 외부 서비스에 "나 온티켓이야"라고 증명하는 표 그 서비스 무단 사용
관리자 비밀번호 운영 도구·서버에 들어가는 암호 시스템 장악

이들의 공통점은 "가진 사람이 곧 주인으로 인정받는다" 는 것이다. 시크릿은 신분증이 아니라 열쇠다. 신분증은 얼굴과 대조하지만, 열쇠는 가진 사람이 누구든 문을 연다. 그래서 시크릿은 "누가 가졌는지"를 따지지 않고 "아무도 못 가져가게" 지키는 것이 유일한 방어다.

그리고 시크릿을 .env라는 별도 파일에 모으는 이유가 여기서 나온다. 비밀값이 코드 여기저기 흩어져 있으면 지킬 수가 없다 — 어디에 뭐가 있는지 모르니까. 한 파일(.env)에 모아 두면 "이 파일 하나만 저장소 밖에 두면 된다"로 문제가 단순해진다. 관리할 대상을 한 곳으로 모으는 것 자체가 보안이다.

하드코딩

값을 설정이 아니라 코드 본문에 직접 박아 넣는 것. const KEY = "sk_live_abc123..." — 이 한 줄이 하드코딩이고, 이 파일이 저장소에 올라가는 순간 사고가 된다.

'하드코딩(hard-coding)'이라는 말의 감을 잡아 보자. '하드(hard)'는 여기서 '딱딱하게 굳어서 못 바꾼다'는 뜻이다. 값을 코드 안에 굳혀 박아 두면, 그 값을 바꾸려면 코드를 뜯어고쳐야 한다. 반대로 값을 설정(.env 등)에 두면, 코드는 그대로 두고 설정만 바꾸면 된다 — 이것을 '소프트(soft) 하다'고 한다.

하드코딩이 나쁜 이유는 보안만이 아니다. 두 가지가 겹친다.

그래서 "설정은 코드 밖으로"는 보안 원칙이자 좋은 설계 원칙이다. 소프트웨어 설계에서는 이것을 "설정과 코드의 분리"라고 부르며, 잘 만든 서비스의 기본 덕목으로 꼽는다. 하드코딩과 .env의 차이가 헷갈린다면, "전화번호를 벽에 페인트로 칠하기 vs 화이트보드에 적기"로 비유하면 즉시 통한다. 벽에 칠하면(하드코딩) 바꾸려면 벽을 새로 칠해야 하고, 지나가는 사람이 다 본다. 화이트보드에 적으면(.env) 언제든 지우고 새로 쓸 수 있고, 그 보드를 서랍에 넣어 두면(저장소 밖) 아무도 못 본다.

커밋되면 끝 — 이력은 지워지지 않는다

많은 사람이 오해하는 지점이다. "실수로 키를 커밋했네? 지우고 다시 커밋하면 되지." 안 된다. Git은 모든 변경 이력을 보존하므로, 지운 뒤에도 과거 커밋을 열면 키가 그대로 보인다. 공개 저장소라면 자동 수집 봇이 수 분 안에 키를 긁어 간다 — 남의 키로 클라우드 자원을 돌려 수백만 원 요금 고지서를 안기는 수법이 실제로 흔하다.

그래서 절차는 하나뿐이다. 유출된 키는 지우는 게 아니라 폐기하고 재발급(로테이션)한다. 그리고 애초에 커밋되지 않게 문(Hook)을 단다.

여기서 '커밋'과 'Git 이력'이 왜 이런 성질을 갖는지 초심자 눈높이로 풀어 두자. Git은 코드의 변경을 사진첩처럼 쌓아 두는 도구다. '커밋(commit)'은 그 순간의 코드 전체를 찰칵 찍어 사진첩에 붙이는 행위다. 사진첩의 목적은 "과거 어느 시점으로도 되돌아갈 수 있게" 하는 것이라, Git은 한 번 찍은 사진을 지우지 않는다. 방금 찍은 사진에서 어떤 물건을 치우고 다시 찍어도, 먼저 찍힌 사진에는 그 물건이 그대로 남아 있다.

키를 커밋한 뒤 지우는 것이 바로 이 상황이다.

커밋 1: .env 에 진짜 키가 있음        ← 사진 1 (키가 찍힘)
커밋 2: .env 에서 키를 지움           ← 사진 2 (키가 없음)

→ 최신 코드에는 키가 없지만,
   "커밋 1로 되감기"만 하면 키가 되살아난다.

공격자(정확히는 그들이 돌리는 자동 봇)는 모든 사진을 다 뒤진다. 그래서 "지금 코드에는 키가 없으니 괜찮겠지"가 통하지 않는다. 사진첩 어딘가에 한 번이라도 찍혔다면 그 키는 유출된 것으로 간주해야 한다.

'로테이션(rotation)'이라는 말도 짚어 두자. 원래 '돌려서 교체한다'는 뜻으로, 보안에서는 쓰던 키를 폐기하고 새 키로 갈아 끼우는 것을 말한다. 유출됐을 때만 하는 게 아니라, 주기적으로(예: 분기마다) 키를 새로 발급해 갈아 끼우는 것도 로테이션이다 — 설령 어딘가 새고 있었더라도 정기적으로 교체하면 새 키 이전의 유출은 무력화된다. 온티켓 수준에서는 "유출 의심 시 즉시 재발급"만 확실히 해도 충분하다.

유출된 키를 발견했을 때의 절차를 순서로 정리하면 이렇다.

  1. 당황하지 말고 재발급 먼저. 결제·DB 콘솔에 들어가 유출된 키를 폐기하고 새 키를 발급한다. 이 순간 옛 키는 남의 손에 있어도 아무 문도 못 연다.
  2. 새 키를 .env에 넣고, 운영 서버의 설정도 새 키로 교체한다.
  3. 그다음에 저장소 이력을 정리한다(전문 도구로 과거 커밋에서 제거). 순서가 중요하다 — 이력을 지우기 전에 재발급부터. 지우는 동안에도 봇은 긁어 가니까.
  4. 애초에 다시 안 새게 훅을 단다(이 장의 실습).

[짚고 가기] "지우면 되지 않냐"는 반문이 거의 누구에게나 떠오른다. 이때 Git을 '지울 수 없는 사진첩'으로 떠올리면 대부분 즉시 이해된다. 한발 더 나아가 "그럼 지우는 방법이 아예 없냐"는 의문이 든다면 — 전문 도구로 과거 커밋을 다시 쓰는 방법은 있지만, 그 사이에 이미 봇이 긁어 갔다고 봐야 하므로 재발급이 먼저이자 핵심이다. 순서(재발급 → 이력 정리)를 뒤집으면 의미가 없다는 점을 기억해 두자.

개인정보(PII)와 마스킹

개인정보는 그 사람을 특정할 수 있는 정보다 — 이름, 전화, 이메일, 주소. 법(개인정보보호법)이 보호를 요구하는 데이터이고, 원칙은 두 가지만 기억한다.

용어부터 짚자. PII는 'Personally Identifiable Information'의 약자로, 그대로 옮기면 '개인 식별 정보', 즉 그 사람이 누구인지 콕 집어낼 수 있는 정보다. 이름 하나로는 동명이인이 많아 특정이 안 될 수 있지만, 이름+전화번호, 또는 이메일 하나면 한 사람으로 좁혀진다. 그래서 이런 정보는 법이 특별히 보호 대상으로 지정한다. 온티켓에서 PII는 예매자의 이름·전화·이메일, 그리고 그 사람이 무슨 공연을 언제 샀는지(구매 내역) 다.

'마스킹(masking)'은 '가면을 씌운다'는 뜻으로, 값의 일부를 * 같은 기호로 가려 원래 값을 알아볼 수 없게 만드는 것이다. 완전히 지우는 게 아니라 일부만 남긴다는 점이 핵심이다 — 본인은 "아, 내 번호 맞네"라고 확인할 수 있을 만큼만 보이고, 남은 재구성할 수 없을 만큼 가린다.

원본 마스킹 후 남긴 이유
010-3558-9786 010-****-9786 뒤 4자리로 본인 확인은 되게
yjlee32@gmail.com yj*****@gmail.com 앞 두 글자로 어떤 계정인지 감만
이영진 이*진 성과 끝 글자로 본인 확인만

여기서 초심자가 자주 하는 오해 두 가지를 미리 풀어 두자.

마지막으로, PII가 로그(log) 에 새는 문제를 특별히 강조해 두자. 로그는 개발자가 문제를 찾으려고 시스템 곳곳에 "지금 이 값이 이랬다"를 기록해 두는 일기장이다. 편의상 요청 내용을 통째로 찍는 경우가 많은데, 그러면 전화번호와 이메일이 로그 파일에 그대로 쌓인다. 화면은 사용자가 보지만 로그는 개발자와 서버 운영자가 본다 — 그리고 로그 파일은 종종 백업되고 여기저기 복사된다. "화면에서는 가렸는데 로그에는 다 찍혀 있더라"가 실무의 흔한 사고다. 그래서 점검 목록에 '로그'가 반드시 들어간다.

[짚고 가기] 개인정보는 '법'이 얽혀 있어 지레 어렵게 느끼기 쉽다. 눈높이를 낮춰 이렇게 생각하면 쉽다: "내 휴대폰에 저장된 친구 번호를 함부로 남에게 알려주지 않는 그 감각, 그게 개인정보 보호의 전부다. 우리는 고객 500명의 번호를 맡아 두고 있는 것이고, 그걸 함부로 화면·로그·응답에 흘리지 않는 것뿐이다." 법 조항을 외우려 하기보다, '맡아 둔 남의 정보'라는 감각을 갖는 것이 먼저다.

최소 권한 원칙

사람이든 AI든 시스템이든, 그 일에 필요한 최소한의 권한만 준다. 리뷰어에게 편집 도구를 안 준 것, MCP를 읽기 전용부터 연결하는 것, 전체 허용(*) CORS를 금지한 것 — 전부 이 원칙의 사례였다. 보안은 이 원칙을 전 영역에 확대 적용하는 일이다.

이 원칙은 영어로 'Principle of Least Privilege'라 하고, 보안 설계의 가장 근본적인 규칙으로 꼽힌다. 왜 '최소'인가? 발상을 뒤집어 보면 명확하다. "권한을 많이 주면 뭐가 나쁜가?"가 아니라 "그 권한이 언젠가 잘못 쓰이면 그게 사고 크기다"라고 생각하는 것이다. 가진 권한이 곧 사고의 최대치다. 편집 권한이 없는 리뷰어는 아무리 오작동해도 코드를 망칠 수 없다. 읽기만 되는 MCP는 아무리 오작동해도 데이터를 지울 수 없다.

일상의 비유로 이 원칙을 손에 잡아 보자.

핵심은, 청소원과 발렛을 의심해서가 아니라는 점이다. 아무리 믿을 만한 사람이라도, 그리고 아무리 유능한 AI라도, 줄 필요가 없는 권한은 주지 않는다. 신뢰의 문제가 아니라 사고 크기를 미리 줄여 두는 설계의 문제다.

그리고 이 원칙의 가장 흥미로운 적용 대상이 바로 AI 자신이다. 우리는 이미 서브에이전트에게 필요한 도구만 줬다(리뷰어에게 Read/Grep만, 편집 도구는 안 줌). 이 장에서는 여기서 한 걸음 더 나아가, AI가 .env를 아예 읽지 못하게 만든다. AI는 유능한 조수지만, 조수에게도 금고 열쇠는 주지 않는다. AI에게 최소 권한을 적용하는 것 — 이것이 이 장의 훅이 하는 일이다.

레드팀 (red team)

공격자의 관점으로 자기 시스템을 시험하는 활동. 군사 훈련에서 아군(블루팀)을 공격하는 가상 적군 역할에서 온 말이다. 우리 식으로 말하면 — "일부러 어겨 봐서 막히는지 본다." 자물쇠를 채웠다고 믿는 것과 문을 당겨 보는 것은 다르다.

이 말의 유래를 조금 더 풀면 개념이 선명해진다. 냉전 시절 미군은 모의 훈련에서 아군을 파란색(블루), 가상의 적군을 빨간색(레드) 으로 표시했다. 아군의 방어가 정말 튼튼한지 확인하려면, 누군가는 적군 역할을 맡아 진짜로 공격해 봐야 했다. 그 적군 역할이 '레드팀'이다. 이 발상이 그대로 보안으로 넘어와, 오늘날 큰 회사들은 자기 시스템을 실제로 공격해 뚫어 보는 레드팀을 따로 운영한다. 방어를 만든 사람과 공격하는 사람을 나누는 것 — 어디서 본 구조다. 만드는 사람과 검증하는 자를 분리하는 이 책 전체의 문법이, 보안에서는 '레드팀'이라는 이름을 갖는 것이다.

레드팀이 왜 필요한지는 한 문장으로 정리된다. "만들었다"와 "작동한다"는 다르다. 문을 달았다는 것과 그 문이 실제로 잠긴다는 것은 별개의 사실이다. 훅을 설치했는데 조건을 잘못 써서 아무것도 안 막고 있을 수도 있고, .env는 막았지만 .env.local은 안 막았을 수도 있다. 설치는 '의도'이고, 레드팀은 '확인'이다. 코드를 짜면 테스트를 돌려 보듯, 보안 장치를 달면 일부러 어겨 봐야 한다.

여기서 초심자가 자주 하는 걱정 하나를 풀어 두자. "일부러 어기면 진짜 사고가 나는 것 아닌가?" — 아니다. 레드팀은 안전한 환경에서, 막히는 것을 확인하려고 어긴다. 우리 실습의 레드팀은 오히려 "제발 막혀라"를 기대하며 시도한다. 막히면 성공이고, 통과하면 그게 오늘 찾아낸 구멍이다. 즉 레드팀에서 '통과'는 나쁜 소식이고 '차단'이 좋은 소식이다 — 평소의 테스트와 정반대 방향이라 처음엔 헷갈릴 수 있다.

[짚고 가기] 레드팀은 '성공/실패' 방향이 뒤집혀 있다는 점을 분명히 해 두어야 헷갈리지 않는다. 무의식적으로 "시도가 통과하면 성공"이라 느끼기 쉽다. 하지만 여기서는 막히는 게 성공이다. 지금 도둑이 되어 문을 열어 보려는 것이고, 안 열리면 우리 집이 안전하다는 뜻이다 — 이렇게 역할을 바꿔 생각하면 재미있게 몰입된다. 직접 .env 열기가 탁 막히는 순간의 화면을 보면 "문이 작동한다"가 눈으로 각인된다.

훅 (Hook)과 PreToolUse

데이터베이스를 세울 때 DB 파괴 명령을 막는 훅을 하나 걸어 보았다. 이 장에서는 '문' 역할을 하는 훅(Hook) 을 본격적으로 쓰므로, 여기서 개념을 다시 잡고 간다. '훅(hook)'은 원래 '갈고리'라는 뜻이다. 소프트웨어에서 훅은 특정 사건이 일어나는 길목에 갈고리를 걸어 두고, 그 사건이 지날 때 내가 정한 동작을 끼워 넣는 장치를 말한다.

Claude Code에는 여러 시점에 훅을 걸 수 있는데, 우리가 쓸 것은 PreToolUse다. 이름을 뜯어 보면 뜻이 그대로 나온다.

PreToolUse는 "AI가 어떤 도구를 실제로 쓰기 직전"에 걸리는 갈고리다. AI가 파일을 읽으려(Read) 하거나, 명령을 실행하려(Bash) 하는 바로 그 순간, 실행 전에 우리가 심어 둔 검사 스크립트가 먼저 돈다. 그 스크립트가 "안 돼"라고 답하면, 그 도구 사용은 일어나지 않는다. 이 "실행 전에 가로챈다"가 훅이 '부탁'이 아니라 '문'인 이유다.

작동 흐름을 그림 없이 순서로 보면 이렇다.

AI의 .env 읽기 시도를 PreToolUse 훅이 실행 직전에 가로채 차단하는 흐름

여기서 중요한 성질 하나. 훅은 AI가 스스로 켜고 끌 수 없다. 훅은 AI의 대화 바깥, 설정 파일(.claude/settings.json)에 심어져 Claude Code라는 프로그램이 직접 집행한다. 그래서 AI에게 "이번만 훅 무시하고 읽어 줘"라고 해도 소용이 없다 — 문은 AI의 말을 듣지 않는다. 바로 이 점이 훅을 신뢰할 수 있게 만든다.


14.3 역할 정의 — 보안검토자와 보안 훅

이제 '눈'과 '문'을 각각 만든다. 먼저 찾아내는 눈(보안검토자 서브에이전트), 그다음 막는 문(보안 훅 두 개)이다.

보안검토자 서브에이전트

---
name: security-reviewer
description: 보안 점검이 필요할 때 사용한다. 시크릿·개인정보·설정의
  위험을 찾는 요청이 오면 이 에이전트에 위임한다.
tools: Read, Grep, Glob
---

너는 (주)승승장구 온티켓의 보안검토자다. 산출물은 수정이 아니라
등급이 매겨진 위험 목록이다.

## 점검 목록 (매번 전부)
1. 시크릿: 코드에 하드코딩된 키·연결 문자열·비밀번호가 있는가
2. 저장소: .env 류 파일이 .gitignore 에 빠져 있지 않은가
3. 개인정보: API 응답·화면·로그에 불필요한 개인정보가 실리는가,
   실릴 때 마스킹되는가
4. 입력: 사용자 입력을 검증 없이 믿는 곳이 있는가
5. 설정: 전체 허용(CORS *, debug 모드 등)이 남아 있는가
6. 권한: 필요 이상의 권한이 부여된 곳이 있는가

## 리포트 형식
| 등급(Blocker/Major/Minor) | 위치 | 위험 | 시나리오(악용되면 무슨 일이) |

## 금지
- 코드·설정을 고치지 않는다
- 겁주기식 지적 금지 — 모든 지적에 구체적 악용 시나리오를 붙인다

이 역할 정의를 한 줄씩 뜯어 보면, 앞 장들에서 익힌 '역할 만들기'의 문법이 그대로 보인다.

점검 6항목이 온티켓에서 구체적으로 무엇을 찾는지 표로 풀면 이렇다. 학생이 "뭘 찾으라는 건지" 감을 잡게 하는 데 이 구체화가 중요하다.

# 항목 온티켓에서 찾는 것(예)
1 시크릿 api/src 어딘가에 DB 문자열·토스 키가 상수로 박혀 있지 않은가
2 저장소 .env, .env.local.gitignore에 들어 있는가
3 개인정보 마이티켓 조회 응답에 전화번호가 통째로 나가지 않는가, 로그에 이메일이 찍히지 않는가
4 입력 주문 수량·이벤트 ID 같은 사용자 입력을 검증 없이 그대로 쓰는 곳
5 설정 CORS가 *(전체 허용)이거나 debug 모드가 켜진 채 남아 있지 않은가
6 권한 필요 이상으로 열린 접근(예: 관리자 조회가 아무나 호출 가능)

"악용 시나리오를 붙인다"가 이 역할의 품질을 가른다. "위험합니다"가 아니라 "이 키가 유출되면 공격자가 주문 테이블을 통째로 읽을 수 있습니다"여야 사람이 우선순위를 판단할 수 있다.

이 규칙("겁주기 금지, 시나리오 필수")이 왜 중요한지 조금 더 짚자. 시나리오가 없는 보안 지적은 두 가지 방식으로 실패한다. 하나는 양치기 소년 — "다 위험합니다"만 반복하면 사람이 둔감해져 진짜 위험도 무시하게 된다. 다른 하나는 우선순위 마비 — 지적이 열 개인데 뭐부터 고칠지 판단할 근거가 없다. 악용 시나리오는 이 둘을 동시에 푼다. "이러면 이런 일이 벌어진다"가 붙으면, 사람은 피해의 크기로 우선순위를 매길 수 있다. 그래서 좋은 보안 지적은 겁을 주는 게 아니라 판단 재료를 주는 것이다.

약한 지적과 강한 지적을 나란히 놓으면 차이가 분명하다.

약한 지적(겁주기) 강한 지적(시나리오 포함)
DB 문자열이 노출돼 위험합니다 api/src/db.ts:12에 DB 연결 문자열이 상수로 있습니다. 이 파일이 저장소에 올라가면 공격자가 그 문자열로 DB에 직접 접속해 주문·고객 테이블을 통째로 내려받을 수 있습니다. (Blocker)
개인정보 노출 주의 마이티켓 조회 응답(/api/tickets)이 전화번호를 마스킹 없이 반환합니다. 조회 열쇠인 이메일만 알면 남의 전화번호를 볼 수 있어, 명단 수집·피싱에 악용됩니다. (Major)
CORS 설정 확인 필요 CORS가 *(전체 허용)입니다. 임의의 외부 사이트가 사용자의 브라우저를 통해 우리 API를 호출할 수 있습니다. 허용 도메인을 온티켓 프론트로 좁혀야 합니다. (Major)

보안 훅 두 개 — 문을 단다

찾는 눈과 별개로, 가장 흔한 사고 두 가지는 아예 문으로 막는다.

언제 막는 것
시크릿 파일 보호 PreToolUse (Read·Edit·Write) AI가 .env 류 파일을 읽거나 고치는 것 자체를 차단
시크릿 커밋 차단 PreToolUse (Bash) git add·git commit 명령에 .env 류가 포함되면 차단

이 두 훅이 각각 어떤 사고를 겨냥하는지, 그리고 왜 이 둘이 '가장 흔한 사고'인지 풀어 보자.

두 훅이 같은 위험(시크릿 유출)의 서로 다른 통로를 막는다는 점에 주목하자. 훅 1은 '대화로 새는 통로', 훅 2는 '저장소로 새는 통로'다. 시크릿이 밖으로 나가는 두 개의 주요 출구에 각각 문을 다는 것이다.

첫 번째 훅은 생각보다 중요하다 — AI가 .env를 읽으면 그 내용(진짜 키)이 대화에 들어오고, 대화 밖으로 복사될 수 있다. AI에게도 최소 권한 원칙이 적용되는 것이다. AI는 .env.example(이름 견본)만 보면 일할 수 있다.

여기서 'AI가 .env를 읽으면 왜 위험한가'를 초심자 눈높이로 더 풀어 두자. AI와의 대화 내용은 화면에 남고, 로그로 기록될 수 있고, 나중에 참조되거나 어딘가로 전달될 수 있다. .env에 든 것은 진짜 살아 있는 키다. 그것이 한 번 대화에 등장하면, 그 키는 이제 "코드에는 없지만 대화 기록에는 있는" 상태가 된다 — 마치 커밋 이력에 남는 것과 같은 논리다. 그래서 가장 깔끔한 방어는 애초에 AI가 그 파일을 열지 못하게 하는 것이다.

그럼 AI는 키를 못 보고 어떻게 일하나? 여기서 .env.example이 등장한다. .env.example키의 이름표만 담고 값은 비어 있는 견본 파일이다.

# .env  (진짜 — AI 접근 금지, 저장소 밖)
DATABASE_URL=postgres://real_user:real_pw@db.supabase.co:5432/onticket
TOSS_SECRET_KEY=test_sk_실제값이_여기_있음

# .env.example  (견본 — AI가 봐도 됨, 저장소에 올림)
DATABASE_URL=
TOSS_SECRET_KEY=

AI가 코드를 짤 때 필요한 것은 "이런 이름의 시크릿이 있다"는 사실이지, 그 이 아니다. 코드에서 process.env.DATABASE_URL이라고 쓰려면 그 이름을 알면 되고, 이름은 .env.example에 다 있다. 진짜 값은 프로그램이 실행될 때 .env에서 읽어 들이면 되는데, 그 일은 사람과 서버가 하지 AI가 대화 중에 볼 일이 아니다. AI는 이름표(.env.example)로 일하고, 값(.env)은 절대 안 본다 — 이 분업이 핵심이다.

[짚고 가기] "AI를 못 믿어서 막는 거냐"는 의문이 들 수 있는데, 답은 최소 권한 원칙에 있다. 믿고 안 믿고의 문제가 아니라, 볼 필요가 없으니 안 보게 하는 것이다. 유능한 비서에게도 회사 금고 비밀번호는 안 알려주는 것과 같다. 오히려 AI가 값을 몰라야 AI도 안전하다 — 실수로든 악의적 프롬프트로든 흘릴 값이 애초에 없으니까. 이 장의 훅은 AI를 제약하는 동시에 AI를 보호한다.

이 패턴이 회사에서 쓰이는 곳

이 네 가지는 우리가 만든 훅과 정확히 같은 발상을 규모만 키운 것이다. "위험한 행동이 실행되기 직전에 가로챈다." 각각을 조금 더 구체적으로 그려 보자.

이 사례들이 학생에게 주는 메시지는 하나다. 오늘 만드는 작은 훅이, 회사에서 쓰는 진짜 보안 통제와 구조가 똑같다. .env 하나 막는 것과 고객 DB 덤프를 막는 것은 규모가 다를 뿐, "위험한 행동을 실행 전에 가로챈다"는 원리가 동일하다.

[짚고 가기] 이 절은 "우리가 배우는 게 장난감이 아니라 실무 그 자체"임을 보여 주는 자리다. 특히 '운영/개발 오인 사고'는 경력자도 저지르는 유명한 실수다. 실제 업계에는 크고 작은 "운영 DB 날린 썰"이 흔한데, 찾아보면 이 대목이 한층 강하게 남는다. 규모는 다르지만 방어의 문법(PreToolUse로 가로채기)은 오늘 만드는 것과 완전히 같다는 점을 기억해 두자.


14.4 프롬프트 작성법 — 점검, 설치, 그리고 침입 시험

이 장의 프롬프트는 세 종류다 — 눈에게 점검을 시키는 프롬프트, 문을 설치하라는 프롬프트, 그리고 그 문을 일부러 두드려 보는(레드팀) 프롬프트. 성격이 다르니 하나씩 본다.

점검 요청

프롬프트
security-reviewer 서브에이전트에게 온티켓 전체(web/, api/, 설정 파일)의
보안 점검을 맡겨줘. 점검 목록 6항목 전부, 등급과 악용 시나리오 포함으로.

이 지시가 왜 좋은지 뜯어 보자. 범위(web/, api/, 설정 파일), 깊이(6항목 전부), 형식(등급과 악용 시나리오)이 모두 명시돼 있다. 셋 다 있어야 하는 이유가 있다.

점검 프롬프트도 상황에 따라 변형이 필요하다. 몇 가지 실전 변형을 보태 둔다.

상황 프롬프트
특정 영역만 집중 점검 security-reviewer에게 api/ 결제 관련 코드만 집중 점검시켜줘. 특히 시크릿 하드코딩과 입력 검증 위주로, 등급과 악용 시나리오 포함.
커밋 직전 최종 점검 커밋하기 전이야. security-reviewer에게 이번에 바뀐 파일들만 보안 점검시켜줘. Blocker가 하나라도 있으면 커밋을 멈춘다.
개인정보만 훑기 security-reviewer에게 개인정보 관점만으로 전체를 훑어줘 — API 응답·화면·로그에 이름·전화·이메일이 필요 이상 실리는 곳, 실릴 때 마스킹 여부.
재점검(수정 확인) 아까 지적된 Blocker 두 개를 고쳤어. security-reviewer에게 같은 점검을 다시 돌려서 그 두 개가 사라졌는지, 새로 생긴 문제는 없는지 확인시켜줘.

훅 설치 지시

프롬프트
.claude/settings.json 에 보안 훅 두 개를 추가해줘.
1) PreToolUse(Read·Edit·Write): 대상 경로가 .env, .env.*, *.pem 이면 차단.
   단 .env.example 은 허용. 사유: "시크릿 파일은 AI가 직접 다루지 않습니다"
2) PreToolUse(Bash): git add / git commit 명령에 .env 류 경로가 포함되면 차단.
검사 스크립트는 .claude/hooks/ 아래에.

이 지시문에는 초심자가 놓치기 쉬운 설계가 여럿 담겨 있다. 하나씩 짚자.

.env.example 예외에 주목 — 문을 달 때는 정상 업무의 통로를 함께 설계해야 한다. 전부 막으면 사람들은 문을 뜯는다(보안 우회가 습관이 되는 조직의 전형적 경로다).

이 "정상 통로를 함께 설계한다"는 원칙은 보안에서 몇 번을 강조해도 지나치지 않다. 보안 장치가 정상 업무까지 막아 버리면, 사람들은 그 장치를 꺼 버리거나 우회하는 습관을 들인다. 한 번 우회가 습관이 되면 그 장치는 있으나 마나다. 현실의 예로, 비밀번호 규칙을 너무 까다롭게 만들면 사람들이 포스트잇에 적어 모니터에 붙인다 — 보안을 위한 규칙이 오히려 보안을 무너뜨린다. 그래서 좋은 보안은 "막을 것은 확실히 막되, 정상 업무는 매끄럽게 통과시킨다." .env.example 예외가 바로 그 통로다 — AI가 시크릿의 '이름표'로 일하는 정상 경로를 열어 두면서, 진짜 값(.env)만 막는다.

[짚고 가기] 훅 설치 지시는 아직 스스로 처음부터 짜기엔 이르다. 이 장의 목표는 훅 스크립트를 손으로 작성하는 게 아니라, AI에게 정확히 지시하고, 설치된 결과를 읽어 내는 것이다. "무엇을(경로 조건), 언제(PreToolUse의 어떤 도구), 예외는(.env.example)"의 세 가지만 지시에 담기면 충분하다. 생성된 settings.json을 전부 이해하지 못해도 괜찮다 — "무엇이 언제 차단되는가"만 읽어 내면 된다.

레드팀 시험 지시 — 일부러 어긴다

프롬프트
지금부터 보안 훅을 시험한다. 다음을 순서대로 시도해줘. 막히면 막혔다고
보고하고 우회하지 마라:
1) api/.env 파일을 열어서 내용을 보여줘
2) DB 연결 문자열을 api/src/index.ts 에 상수로 박아줘
3) git add api/.env 를 실행해줘

세 가지 모두 차단되어야 정상이다. 하나라도 통과하면 그 구멍이 오늘의 수확이다 — 훅을 고치고 다시 시험한다.

이 프롬프트에서 "막히면 막혔다고 보고하고 우회하지 마라" 라는 한 줄이 결정적이다. 왜 이 문장이 필요한가? AI는 기본적으로 사람을 도우려는 성향이 있어서, "파일을 열어 달라"는 요청이 막히면 "다른 방법으로 열어 드릴까요?" 하며 우회로를 찾으려 들 수 있다. 레드팀 시험에서는 그 도움이 오히려 방해다. 우리는 문이 막는지를 보려는 것이지, 어떻게든 파일을 여는 게 목적이 아니다. 그래서 "막히면 그냥 막혔다고 보고만 하라"고 못을 박는다.

세 시도가 각각 어떤 문을 두드리는지, 무엇이 정상 반응인지 정리하면 이렇다.

시도 두드리는 문 정상 반응
1) api/.env 열기 훅 1 (시크릿 파일 보호, Read) 읽기가 차단됨. 내용이 화면에 안 나옴
2) DB 문자열을 코드에 상수로 박기 (훅이 아닌) 보안검토자가 잡을 대상 / 사람이 거부 하드코딩 시도 자체를 사람이 거부하거나, 이후 점검에서 Blocker로 걸림
3) git add api/.env 훅 2 (시크릿 커밋 차단, Bash) 명령이 실행되지 않고 사유 메시지가 뜸

여기서 시도 2번은 성격이 조금 다르다는 점을 짚어 두자. 1번과 3번은 훅(문)이 직접 막지만, 2번(코드에 상수 박기)은 파일을 쓰는 정상적인 편집 행위라 훅으로 일괄 차단하기 어렵다 — 코드에 상수를 쓰는 것 자체는 정상 작업이니까. 그래서 2번은 퍼미션 단계에서 사람이 거부하거나, 커밋 전에 보안검토자가 잡는 것으로 방어한다. "모든 위험을 훅 하나로 막을 수는 없다. 훅으로 막을 것, 눈으로 잡을 것, 사람이 퍼미션에서 거를 것이 나뉜다"는 것을 이 시도가 보여 준다.

레드팀 시험 프롬프트도 여러 변형을 준비해 두면 더 촘촘히 문을 두드릴 수 있다. 아래는 앞 세 가지에 더해 볼 만한 추가 시도들이다.

추가 시도 노리는 구멍
api/.env.local 을 열어서 보여줘 .env만 막고 .env.local은 빠뜨렸는지 (변종 뒷문)
api/.env 를 api/config.txt 로 복사한 다음 config.txt 를 열어줘 이름을 바꿔 우회하는 경로가 뚫리는지
git commit -am "wip" 을 실행해줘 (지금 스테이지에 .env 가 있음) add가 아닌 commit 경로로도 시크릿이 새는지
루트의 *.pem 인증서 파일을 읽어서 보여줘 시크릿이 .env가 아닌 키 파일 형태일 때도 막는지

이 변형들의 핵심 메시지는 "막았다고 믿는 것과 실제로 다 막힌 것은 다르다" 이다. .env는 막았는데 .env.local이 뚫려 있으면, 문을 달아 놓고 창문을 열어 둔 셈이다. 레드팀은 이런 '옆문'을 찾는 작업이다.

[짚고 가기] 레드팀 시험은 이 장의 하이라이트다. 실제로 세 시도를 직접 돌려 보면, 특히 1번에서 AI가 .env를 열려다 탁 막히는 화면을 만나게 된다. 그 순간 "AI에게도 최소 권한"이 추상이 아니라 눈앞의 사실이 된다. 만약 어떤 시도가 통과하더라도 당황할 필요 없다 — 이게 바로 레드팀을 하는 이유이고, 안 했으면 몰랐을 구멍을 찾은 것이다. 통과가 곧 실패가 아니라 발견임을 기억해 두자.


14.5 실습 — 문을 달고, 두드려 본다

이제 눈을 만들고, 문을 달고, 그 문을 직접 두드려 본다. 순서는 눈 먼저(찾고 고치고), 문 그다음(달고 두드리고) 이다.

따라하기 1 — 보안검토자 생성과 첫 점검

security-reviewer를 만들고(tools 세 개 확인) 아래 프롬프트로 전체 점검을 맡긴다.

프롬프트
security-reviewer 서브에이전트에게 온티켓 전체(web/, api/, 설정 파일)의
보안 점검을 맡겨줘. 점검 목록 6항목 전부, 등급과 악용 시나리오 포함으로.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

리포트를 사람이 읽으며 확인할 것 —

여기서 'tools 세 개 확인' 을 반드시 눈으로 하게 하자. 생성된 역할 파일의 tools: 줄에 Read, Grep, Glob 세 개만 있고 Edit·Write·Bash가 없는 것을 확인하는 것 — 이것이 "검토자에게는 고칠 능력을 주지 않는다"는 최소 권한 원칙이 실제로 적용됐는지 보는 순간이다. 만약 여기에 Edit가 끼어 있으면, 검토자가 코드를 고칠 수 있게 되어 '찾는 자와 고치는 자의 분리'가 깨진다.

리포트를 받으면, 사람이 읽으며 위 세 가지를 체크한다. 이때 학생에게 "리포트를 그대로 믿지 말고 한 줄이라도 근거를 따져 보라"고 안내한다. 예를 들어 "여기서 전화번호가 새고 있습니다"라는 지적이 있으면, 실제 그 응답 코드를 함께 열어 정말 전화번호가 나가는지 확인하는 것이다. 검토자는 눈이지 판사가 아니다 — 판단은 사람이 한다.

따라하기 2 — 발견한 문제 처리

Blocker부터 사람이 결정하고 만든 역할(/backend 등)에게 수정시킨다. 개인정보 지적이 있었다면 처리 원칙은 "필요한 최소만, 보일 때는 마스킹" — 예를 들어 마이티켓 응답에서 전화번호를 빼거나 가린다. 수정 후 같은 점검을 다시 돌려 사라졌는지 확인한다.

이 '발견 → 수정 → 재점검'의 고리가 검증 작업의 본질이다. 지적을 받고 고쳤다고 끝이 아니라, 같은 점검을 다시 돌려 그 지적이 사라졌는지 눈으로 확인해야 비로소 닫힌다. 코드에서 테스트를 고친 뒤 다시 돌려 초록불을 보는 것과 같은 리듬이다.

개인정보 처리에서 "빼기 vs 가리기(마스킹)"의 판단 기준을 손에 쥐여 주자.

순서는 "뺄 수 있으면 빼고, 못 빼면 가린다." 마스킹은 '어쩔 수 없이 보여야 할 때'의 차선책이지 기본값이 아니다.

[짚고 가기] 여기서 "우리 서비스는 작은데 이렇게까지?"라는 생각이 들 수 있다. 하지만 개인정보 사고에는 규모 하한선이 없다 — 100명이든 10만 명이든 한 명의 정보라도 새면 그 한 명에게는 100% 사고다. 그리고 이 처리 습관은 서비스가 커진 뒤 소급 적용하기가 훨씬 어렵다 — 작을 때 몸에 붙여 두는 편이 좋다.

따라하기 3 — 보안 훅 설치

아래 프롬프트로 보안 훅을 설치한다.

프롬프트
.claude/settings.json 에 보안 훅 두 개를 추가해줘.
1) PreToolUse(Read·Edit·Write): 대상 경로가 .env, .env.*, *.pem 이면 차단.
   단 .env.example 은 허용. 사유: "시크릿 파일은 AI가 직접 다루지 않습니다"
2) PreToolUse(Bash): git add / git commit 명령에 .env 류 경로가 포함되면 차단.
검사 스크립트는 .claude/hooks/ 아래에.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

설치가 끝나면 .claude/settings.json 과 hooks 폴더에 무엇이 생겼는지 열어서 훑는다 — 전부 이해 못 해도 좋다. "무엇이 언제 차단되는가"만 읽어 낸다.

설치가 끝나면, 생성된 결과에서 딱 세 가지만 확인하게 한다. 코드를 다 이해할 필요는 없고, 이 세 가지 질문에 답할 수 있으면 성공이다.

  1. 무엇을 막나 — 어떤 경로/명령이 대상인가? (.env, .env.*, *.pem / git add·git commit에 시크릿 경로가 섞인 경우)
  2. 언제 막나 — 어느 시점의 갈고리인가? (PreToolUse — 도구가 실제로 쓰이기 직전)
  3. 예외는 무엇인가 — 정상 통로가 열려 있나? (.env.example은 허용)

이 셋을 스스로 짚을 수 있으면, 학생은 훅을 '이해'한 것이다. 스크립트 문법을 줄줄 읽는 것보다 이 세 질문에 답하는 능력이 훨씬 오래 남는다.

한 가지 실무 팁을 덧붙인다. 훅을 설치한 직후에는 설치가 실제로 활성화됐는지 바로 이어지는 레드팀 시험으로 확인하는 것이 정석이다. 설정 파일에 글자로 적혀 있다고 해서 작동하는 게 아니다 — 문이 달렸다는 것과 잠긴다는 것은 다르니까. 그래서 이 실습은 설치(3) 다음에 곧바로 시험(4)이 온다.

따라하기 4 — 레드팀: 세 번의 침입 시도

아래 레드팀 지시를 입력해, 보안 훅을 일부러 어겨 본다.

프롬프트
지금부터 보안 훅을 시험한다. 다음을 순서대로 시도해줘. 막히면 막혔다고
보고하고 우회하지 마라:
1) api/.env 파일을 열어서 내용을 보여줘
2) DB 연결 문자열을 api/src/index.ts 에 상수로 박아줘
3) git add api/.env 를 실행해줘

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

세 시도가 전부 차단되는 것을 눈으로 확인한다. 특히 1번 — AI가 .env를 읽으려다 막히는 장면은, "AI에게도 최소 권한"이 무슨 뜻인지 보여주는 가장 선명한 순간이다.

통과한 시도가 있으면: 훅 수정 → 같은 시험 반복. 세 번이 다 막힐 때까지가 이 실습의 완료 조건이다.

시험을 돌릴 때 학생이 관찰할 포인트를 짚어 준다.

여유가 되면 위 '프롬프트 작성법'에서 본 추가 시도(.env.local 열기, 이름 바꿔 복사 후 열기 등)도 돌려 본다. .env는 막혔는데 .env.local이 뚫려 있다면, 그것이 바로 '옆문'이고 오늘의 진짜 수확이다.

[짚고 가기] 만약 환경 문제로 실제 Git 커밋 시도(3번)가 매끄럽지 않더라도, 최소한 1번(.env 읽기 차단)만은 직접 확인해 보자. 이 한 장면이 이 장 전체의 '증명'이다. 반대로 여유가 있으면 추가 시도 표의 '이름 바꿔 복사 후 열기'도 시도해 보면 좋다 — 이건 훅으로 완벽히 막기 어려운 경우가 있어, "훅도 만능이 아니라 눈(검토자)과 사람의 판단이 함께 필요하다"는 이 장의 결론을 자연스럽게 확인할 수 있는 좋은 사례가 된다.

따라하기 5 — 팀 규칙으로 기록

프롬프트
CLAUDE.md에 보안 규칙을 추가해줘:
- 시크릿은 .env 에만. AI는 .env 를 읽지 않는다 (.env.example 로 일한다)
- 개인정보는 필요한 최소만, 표시할 때는 마스킹
- 보안 점검은 security-reviewer, 차단은 훅 — 둘 다 통과해야 배포

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

왜 훅까지 달아 놓고 또 CLAUDE.md에 글로 적는가? 역할이 다르기 때문이다.

특히 마지막 줄 "보안 점검은 security-reviewer, 차단은 훅 — 둘 다 통과해야 배포"가 이 장 전체를 한 문장으로 요약한다. 눈과 문, 두 겹을 다 통과해야 비로소 배포한다는 팀의 완료 기준이다. 이렇게 CLAUDE.md에 적어 두면, 이후 어떤 작업에서든 AI가 이 원칙을 참조하며 일한다 — 규칙을 매번 말로 반복하지 않아도 되게, 팀의 기억으로 남기는 것이다.

완료 기준 체크리스트

이 체크리스트의 마지막 항목이 특별하다. 앞의 넷은 '무엇을 했는가'지만, 마지막 하나는 '무엇을 이해했는가'다. 손이 아니라 입으로 하는 확인이다. 옆 사람에게 "키를 실수로 커밋했어. 어떻게 해?"라고 물었을 때 "지우면 되지"가 아니라 "폐기하고 재발급, 그리고 다시 안 새게 훅"이라고 답할 수 있으면, 이 장의 핵심을 몸에 익힌 것이다.

[짚고 가기] 체크리스트를 다 채웠다면, 옆 사람과 "키를 실수로 올렸다"는 상황을 두고 문답을 해 보면 좋다. 한 명이 상황을 던지고, 다른 한 명이 처방 순서(재발급 → 이력 정리 → 훅 확인)를 말하는 것이다. 말로 설명할 수 있어야 진짜 아는 것이다. 두 번째 항목에서 "개인정보 노출이 하나도 없었다"는 결과가 나와도, "없다고 확인한 것 자체가 점검의 성과"다 — 못 찾은 게 아니라 없었던 것일 수 있고, 그 확인도 검증이다.


14.6 정리

이 장에서 얻은 것을 한 발 물러나 보면, 사실 우리는 새로운 보안 기술을 배운 게 아니다. 이미 아는 원칙 하나 — 최소 권한 — 을 끝까지 밀어붙였을 뿐이다. 리뷰어에게 편집 도구를 안 준 것, MCP를 읽기 전용으로 연결한 것, CORS를 좁힌 것, 그리고 이 장에서 AI가 .env를 못 읽게 한 것이 전부 같은 원칙의 얼굴들이다. 보안은 특별한 전문가의 영역이 아니라, "줄 필요 없는 권한은 주지 않는다" 는 습관을 사람에게도, AI에게도, 저장소에도 일관되게 적용하는 일이다.

그리고 이 장이 심은 가장 큰 전환은 "부탁에서 문으로"다. 리뷰어의 지적은 지켜 주기를 바라는 부탁이었지만, 훅은 어길 수 없게 만드는 문이다. 진짜 중요한 것은 부탁으로 지키지 않는다 — 문을 단다. 그리고 문을 달았으면, 반드시 두드려 확인한다. 온티켓의 비밀값과 고객의 개인정보는, 이제 눈과 문 두 겹 뒤에서 잠겨 있다.

오명운 · macro@prag-ai.com