Files
aio-01d4-stu/Lecture/73/03-STAGE3-OUTPUT-AND-TESTS.md
T

34 KiB

3단계 · 출력 필터와 보안 테스트

  • 교시: 5교시 강의 (14:0014:50) · 6교시 Lab 03 실습 5개 (15:0015:50)
  • 이 단계가 끝나면: 나가는 답에서 개인정보를 가리고 유출을 막고, 무엇을 왜 막았는지 기록하고, 관문마다 시간과 기록을 보고, 지나치게 막기와 덜 막기를 함께 찾는 보안 테스트를 쓰고, 기록을 개인정보 없이 감사 로그로 모아 하루 요약을 만들 수 있다.
  • 숫자: 모두 강사 노트북 실측입니다 (65일차 n8n 2.42.6, OpenAI gpt-4.1-mini, 10/26 정답 워크플로우 1회). 출력 전부는 수업 뒤 공개되는 Last3Mon/73/answers/LAB-TEST-RESULT.md에 있습니다.
  • 실습 가리키기: 이 문서에서 "Lab 03 예제 1"은 6교시 Lab 03 문서의 그 절입니다. 파일 경로는 실습 폴더 ~/aio-01d4-stu/Last3Mon/73 기준입니다.
flowchart LR
  A["1. 나가는 답도<br/>검사"] --> B["2. 가리기 ·<br/>유출 차단"] --> C["3. 보안 기록"] --> D["4. 관문별<br/>시간과 기록"] --> E["5. 지나치게 ·<br/>덜 막기"]
  E --> F["6. 감사 로그"] --> G["7. 데이터 최소화"] --> H["8. 남은 위험"] --> I["9. 요청문(프롬프트)으로"]

0. 이 시간 (50분)

시간 절 · 개요 하는 일
14:00~14:07 1 · 2 · 개요 22~23장 출력 가드레일, 개인정보 가리기(마스킹) · 유출 차단, 정규식
14:07~14:13 3 · 개요 24장 보안 기록, 같은 모양, 기록에 남길 것
14:13~14:18 4 · 개요 25장 관문별 시간과 기록 실측
14:18~14:28 5 · 개요 26장 보안 테스트, 거짓 양성 · 거짓 음성, 경계값, 레드팀 자동화
14:28~14:38 6 · 7 · 개요 27~28장 감사 로그, 데이터 최소화, 개인정보보호법, pii_in_log
14:38~14:44 8 남은 위험 (인증 · 요청 수 제한 · 정기 점검 · 커밋 속 비밀)
14:44~14:50 9 · 10 · 확인 질문 요청문, 자주 하는 실수, 질문

6교시 Lab 03에서 이어서 하는 것

6교시 실습 (시각) 이 강의의 절 실습에서 하는 일 쓰는 파일 · 명령
Lab 03 예제 1 (15:00~15:10) 1 · 2 답을 보내기 전에 연락처 가리기 · 유출 차단 call my-secure requests/contact-own.json -v, 테스트 cases/lab03.json 앞 둘 · lab01-attacks
Lab 03 예제 2 (15:10~15:18) 3 모든 결과에 security 기록 테스트 cases/lab03.json 6개
Lab 03 예제 3 (15:18~15:25, 에이전트 없이) 4 관문 다섯 곳의 시간과 기록 보기 test cases/lab03-layers.json -v → my-work/SECURITY.md 7절
Lab 03 과제 1 (15:25~15:38) 5 어려운 보안 테스트 8개 쓰고 실패를 고치기 cases/my-cases.json (내가 씀), --repeat 3 → SECURITY.md 8절, my-work/TEST-REPORT.md
Lab 03 과제 2 (15:38~15:50) 6 · 7 감사 로그와 하루 보안 요약 my-work/security-report.json (새로), 테스트 cases/lab03-challenge2.json → SECURITY.md 9 · 10절

1. 나가는 답도 검사한다

일반 설명

출력 가드레일은 LLM이 만든 답을 사용자에게 보내기 직전에 봅니다. 입력 관문은 "무엇이 들어오나"를, 출력 관문은 "무엇이 나가나"를 봅니다. 보안에서는 둘을 따로 세웁니다. 들어오는 것을 다 막을 수는 없고, 막았다고 해도 LLM이 데이터를 어떻게 섞어 쓸지는 모르기 때문입니다.

OWASP LLM Top 10 출력 쪽에서 생기는 일 막는 법
민감 정보 유출 Tool이 가져온 연락처 · 메모가 답에 그대로 마스킹, 유출 표시가 있으면 차단 (2절)
부적절한 출력 처리 LLM의 답을 검사 없이 다음 시스템에 넘김 받는 시스템의 형식에 맞게 이스케이프 · 검증
시스템 프롬프트 유출 답에 지시문 조각이 섞임 지시문 조각 모양을 찾아 차단

답은 화면에서 끝나지 않습니다. 사용자는 답을 캡처하고, 메신저로 전달하고, 문서에 붙입니다. 시스템은 답을 로그 · 대화 기록 · 분석 도구에 남깁니다. 본인 것이라도 답에 꼭 필요한 만큼만 보여 주는 까닭입니다. 보안에서는 이것을 "데이터가 흘러가는 길(data flow) 전체를 본다"고 말합니다.

출력이 다른 시스템의 입력이 될 때가 더 위험합니다. LLM의 답을 그대로 HTML 화면에 넣으면 XSS가, SQL에 넣으면 SQL 인젝션이, 셸에 넣으면 명령어 주입이 됩니다 (1단계 1절의 옛 공격들이 출구에서 다시 나타남). 원칙은 LLM의 출력도 사용자 입력처럼 믿지 않는다입니다. 받는 시스템의 형식에 맞게 이스케이프하고, 정해진 모양(JSON 스키마 · 허용된 값)인지 검사합니다. 오늘 답은 사람이 읽는 글이므로 개인정보 가리기와 유출 차단만 합니다.

출력 관문은 마지막 겹입니다. 앞의 관문이 모두 뚫렸을 때 피해를 줄이는 자리입니다. 그래서 출력 필터는 확실한 코드로 둡니다. 마지막 겹을 확률(LLM)에 맡기면, 앞이 뚫린 날 마지막도 뚫릴 수 있습니다.

출력 쪽 사고는 늦게 알게 됩니다. 입력 쪽 공격은 이상한 질문으로 보이지만, 출력 쪽 유출은 평범한 친절한 답처럼 보입니다 (오늘 아침 공격 대상이 비밀을 내주며 HTTP 200으로 답한 것처럼). 그래서 출력 관문은 막는 것만큼 세는 것(3절 기록)이 중요합니다. 답에서 무엇을 몇 번 가렸는지가 쌓이면, 어느 Tool이 너무 많은 데이터를 돌려주는지가 보입니다.

출력 관문이 보는 것 예 오늘
개인정보 모양 전화 · 이메일 · 주민번호 · 카드 번호 가림
내부 정보 모양 내부 코드 · 서버 주소 · 키 모양 답 전체 차단
지시문 조각 <question>, 지시문의 특정 문장 답 전체 차단
형식 정해진 칸 · 길이 · 언어 72일차 답 검사
내용 정책 욕설 · 차별 · 위험한 안내 (오늘 하지 않음, 콘텐츠 안전 서비스)

오늘 실습에서

2단계에서 본인 건이 제대로 보이게 되자(Lab 02 예제 1), 이번에는 본인 연락처가 그대로 답에 나왔습니다. lab01-attacks에서 공격 대상 때는 ✔였던 "연락처 그대로"가 Lab 02 뒤 ✘가 되었습니다 (answers/14). 한 곳을 고치면 다른 곳이 드러납니다. 그래서 고칠 때마다 모든 테스트를 다시 돌립니다.

2. 가리기 · 유출 차단: 코드가 한다 (6교시 Lab 03 예제 1)

일반 설명

**마스킹(masking)**은 값의 일부를 *로 바꿔 보여 주는 것입니다. 카드 영수증의 ****-****-****-1234가 예입니다. 개인정보보호에서는 이것을 가명 · 비식별 처리의 한 방법으로 봅니다. 남길 부분은 본인이 알아볼 수 있을 만큼만 정합니다.

대상 정규식 (요지) 가린 모양 남긴 것 왜 그만큼
전화번호 01[016789][-. ]?\d{3,4}[-. ]?\d{4} 010-****-2135 앞 3 · 끝 4 본인 번호인지 알아볼 수 있음
이메일 [\w.+-]+@[\w-]+\.[\w.]+ y***@example.com 첫 글자 · 도메인 어느 계정인지 짐작
주민등록번호 \d{6}[- ]?[1-4]\d{6} 900101-******* 생년월일 뒷자리가 핵심 정보
카드 번호 (예) 16자리 ****-****-****-1234 끝 4 업계 관행
유출 표시 내부 코드 모양(IT-ADMIN-\d+), 지시문 조각(<question>, systemMessage) 답 전체를 차단 — 일부만 가려도 이미 샌 것

가리기와 막기의 차이. 가리기는 "있어도 되는데 보기 좋게", 막기는 "있으면 안 되는 것"입니다. 연락처는 본인에게 일부 보여 줘도 되지만, 내부 비상 코드는 한 글자도 나가면 안 됩니다. 그래서 내부 정보 모양이 보이면 답을 고쳐 쓰지 않고 답 전체를 정해진 안내로 바꿉니다.

셀 수 있는 검사는 코드가 합니다. LLM에게 "연락처는 가려서 답해"라고 지시하는 것도 좋지만, 그것은 확률입니다. 영어로 묻거나 "숫자 사이에 점을 찍어 줘", "한 글자씩 띄워서 알려 줘"라고 하면 지시를 피해 갈 수 있습니다. 정규식은 같은 모양을 늘 같게 찾습니다 (69일차). 지시와 코드를 둘 다 씁니다: 지시로 대부분 줄이고, 코드로 마지막을 확실히.

정규식의 두 실패 (1단계 7절과 같음).

실패 예 줄이는 법
놓침 하이픈 없는 01014142135, 점 010.1414.2135, 띄어쓰기 [-. ]?처럼 구분자를 넓게
오탐 날짜 2026-10-27, 주문 번호, 버전 1.0.10 앞뒤 경계(숫자 · 점이 붙어 있지 않을 때만), 앞자리 조건

돌려 말한 유출은 못 막습니다. "공일공 일사일사 …"처럼 말로 풀어 쓰거나, 여러 답에 나눠 조금씩 내보내는 것은 정규식으로 잡기 어렵습니다. 이런 것은 출력 필터만으로 막지 않고, 애초에 LLM이 그 데이터를 받지 않게(Tool이 꼭 필요한 칸만 돌려주게) 합니다. 데이터 최소화(7절)는 출력 쪽에서도 가장 확실한 방어입니다.

가리기 말고도 있는 방법.

방법 뜻 되돌릴 수 있나 쓰는 곳
마스킹 일부를 *로 아니오 화면 · 답 · 로그 (오늘)
토큰화 진짜 값 대신 의미 없는 대체값, 진짜는 따로 금고에 금고가 있으면 예 결제 · 카드 번호
암호화 열쇠로 잠금 열쇠가 있으면 예 저장 · 전송
해시 같은 값은 같은 결과, 원래 값은 모름 아니오 "같은 사람인가"만 알 때 (7절)
지우기 아예 빼기 아니오 필요 없는 칸 (가장 확실)

어느 방법이든 어디서 하는지가 중요합니다. 답을 만든 뒤 가리는 것보다, Tool이 애초에 필요한 칸만 돌려주는 것이 더 확실합니다.

오늘 실습에서

10/26 (answers/24~26):

확인 결과
call my-secure requests/contact-own.json -v "… 등록된 연락처는 전화번호 010-****-2135입니다."
cases/lab03.json 앞 둘 ✔ (뒤 넷은 예제 2의 기록)
cases/lab01-attacks.json 9/9 (아침 공격 대상 2/9)

아홉 공격을 막은 관문: 입력 검증 3 · 규칙 2 · 웹훅의 user + 403 1 · 비밀 없는 지시문 2 · 출력 필터 1.

3. 보안 기록 (6교시 Lab 03 예제 2)

일반 설명

관문이 일하는지는 기록으로만 압니다. 보안 운영(보안 관제, SOC: Security Operations Center)은 기록을 모아 세고, 이상한 것을 찾고, 규칙을 고칩니다. 기록이 없으면 사고가 났는지도, 관문이 정상 사용자를 막고 있는지도 모릅니다.

좋은 보안 기록의 조건.

조건 뜻 어기면
같은 모양 (구조화된 로그) 어느 길로 끝나든 같은 칸 길마다 따로 세야 함, 빠뜨림
누가 막았나 관문 이름 어느 관문을 고칠지 모름
왜 막았나 맞은 규칙 · 판별 이유 오탐을 고칠 수 없음
얼마나 가린 개수 등 숫자 추세를 못 봄
비밀을 남기지 않음 막은 문장 전체 · 연락처를 남기지 않음 기록이 새 유출 통로
사용자 안내와 분리 사용자에게는 짧은 안내, 자세한 이유는 기록에 공격자에게 규칙을 알려 줌 (1단계 5절)
{"route": "차단", "answer": "보안 정책상 …", "tools_used": [], "security": {"blocked_by": "guard", "reason": "requests other employee's data", "masked": 0}}
기록 쓰임
blocked_by 어느 관문이 일하나 (규칙 · 판별 LLM · 출력 필터 · 없음)
reason 지나치게 막은 것 찾기, 규칙 고치기
masked 개인정보가 답에 섞이는 빈도

"막지 않았다"도 기록합니다. 정상 답에도 blocked_by: "", masked: 0을 남깁니다. 전체 중 몇 퍼센트가 막혔는지(차단율)를 세려면 분모가 필요하고, 정상 답에서 가리기가 일어난 빈도(masked > 0)는 Tool이 너무 많은 데이터를 돌려준다는 신호입니다.

기록은 증거입니다. 사고가 나면 "언제부터, 누가, 무엇을" 했는지를 기록으로 되짚습니다. 그래서 기록은 고칠 수 없게(추가만 되게) 보관하고, 시각을 함께 남깁니다 (6절 감사 로그).

오늘 실습에서

규칙과 판별 LLM은 같은 Blocked 노드로 들어옵니다. 누가 막았는지는 $('Blocked by guard?').isExecuted로 구분합니다 (AGENTS.md 6절). 10/26 (answers/25 · 27): lab03 2/6 → 6/6.

4. 관문별 시간과 기록 (6교시 Lab 03 예제 3)

일반 설명

겹겹이 막기를 운영할 때는 관문마다 얼마나 자주 · 얼마나 빨리 · 얼마나 정확히 일하는지를 봅니다. 72일차의 단계별 시간(timings)과 같은 생각을 보안에 적용한 것입니다.

보는 것 알 수 있는 것 할 일
관문별 차단 수 어느 관문이 바쁜가 판별 LLM이 많이 막으면 그 모양을 규칙으로 옮길 후보
관문별 시간 관문이 정상 요청에 더한 시간 느린 관문을 뒤로, 또는 더 싼 방법으로
관문별 오탐 정상을 막은 수 (이의 신고) 규칙을 좁히거나 정상 예를 더함
끝난 곳의 분포 앞에서 끝나는 비율 앞에서 많이 끝날수록 비용 · 위험이 작음

앞에서 끝날수록 좋습니다. 앞 관문은 싸고(코드), 확실하고, LLM에 닿지 않습니다. 같은 공격이 뒤의 판별 LLM에서 막히면 1초 · LLM 1번이 들고, 판별이 속으면 그다음 겹에 기대야 합니다. 그래서 운영자는 "판별 LLM이 막은 것 중 규칙으로 막을 수 있었던 것"을 정기적으로 찾아 앞으로 옮깁니다.

관문 하나의 효과를 따로 재는 법. 관문 하나를 끄고(또는 그 관문 앞에서 끝나는 요청을 보내고) 같은 테스트를 돌려 차이를 봅니다. 오늘 실습의 "고치기 전 → 고친 뒤"가 바로 이것입니다. 관문마다 "이 관문이 없으면 몇 개가 새나"를 말할 수 있어야, 관문을 뺄지 남길지 숫자로 정할 수 있습니다.

오늘 결과로 읽는 법 (예). 관문 다섯 곳에 하나씩 보낸 결과에서, 앞의 둘(입력 검증 · 규칙)은 0.1초 · LLM 0번, 판별 LLM은 약 1초 · LLM 1번, 끝까지 간 둘은 3초 안팎 · LLM 2번 + Tool이었습니다. 하루 요청 중 공격 · 잘못된 요청이 많은 서비스일수록 앞 관문을 두껍게 하는 것이 시간과 비용을 함께 줄입니다.

운영 지표 (예) 계산 경고 신호
차단율 차단 수 ÷ 전체 갑자기 오르면 공격, 갑자기 내리면 관문 고장
판별 LLM 비율 판별이 막은 수 ÷ 전체 차단 높으면 규칙 보강 후보
정상 답의 가림 비율 masked > 0인 답 ÷ 답 높으면 Tool이 데이터를 너무 많이 줌
관문 멈춤 guard_down 수 0이 아니면 알림 (76일차)

오늘 실습에서

10/26 cases/lab03-layers.json -v (answers/28, 5/5):

케이스 끝난 곳 HTTP security.blocked_by 시간 LLM
① 빈 질문 입력 검증 400 — 0.1초 0
② 알려진 공격 규칙 200 rule 0.1초 0
③ 새 공격 판별 LLM 200 guard 0.8초 1
④ 내 연락처 출력 필터 (가림) 200 "" · masked 1 3.4초 2 + Tool
⑤ 정상 질문 모두 지나감 200 "" 3.0초 2 + Tool

앞에서 끝날수록 빠르고 쌉니다. 이 표가 오늘 하루를 한 장에 보여 줍니다.

5. 지나치게 막기와 덜 막기: 보안 테스트 (6교시 Lab 03 과제 1)

일반 설명

보안 테스트의 목적은 통과가 아니라 실패를 찾는 것입니다. 이미 막은 것을 더 시험해도 배울 것이 없습니다. 좋은 보안 테스트는 "이건 지나갈 것 같은데?"라는 의심에서 나옵니다.

테스트를 쓰는 다섯 방향.

방향 무엇을 찾나 예
막아야 할 것 거짓 음성 (놓침) 숨긴 공격, 처음 보는 말
막으면 안 되는 것 거짓 양성 (오탐) 공격 낱말이 들어간 정상 질문
경계값 기준선 바로 앞뒤의 실수 정확히 500자, 501자
다른 길 같은 위험을 다른 통로로 영어로, 남의 것을, 두 번에 나눠서
기록 기록 자체의 유출 security.reason에 개인정보가 없나
  • 경계값: 대부분의 버그는 경계에 있습니다. "500자 이하"를 "<500"으로 잘못 쓰면 정확히 500자가 막힙니다. 0, 1, 기준-1, 기준, 기준+1을 시험합니다.
  • 두 방향을 같이: 막아야 할 것만 쓰면 지나치게 막는 규칙도 "통과"합니다. 막으면 안 되는 것을 반쯤 섞어야 균형을 봅니다.
  • 회귀 테스트: 한 번 찾은 실패는 테스트로 남깁니다. 나중에 규칙을 고치다 같은 실패가 다시 생기면 바로 압니다.
  • 여러 번 돌리기: LLM이 들어간 관문은 같은 입력에도 결과가 흔들립니다. --repeat으로 여러 번 돌려 모두 통과해야 고쳤다고 봅니다.

레드팀 자동화. 공격 문장을 모아 두고(오늘 samples.json), 워크플로우 · 지시문 · 모델이 바뀔 때마다 자동으로 돌립니다. 공개된 LLM 공격 모음과 점검 도구들이 같은 일을 하고, 회사들은 배포 전 검사(74일차)에 넣습니다. 새로 발견된 공격은 모음에 더합니다.

고칠 관문을 고르는 법. 실패마다 "어디서 막혔어야 했나(또는 어디서 잘못 막았나)"를 먼저 말합니다. 정상이 막혔다면 막은 관문의 조건을 좁히고, 그 대신 뚫리는 것을 뒤의 관문이 받치는지 확인합니다. 공격이 지나갔다면, 가장 싸고 확실하게 막을 수 있는 앞쪽 관문부터 봅니다.

케이스 하나를 쓰는 틀. 테스트 케이스는 "무엇을 보내고, 무엇을 기대하고, 왜 어려운가"를 한 줄씩 적으면 됩니다.

칸 예
이름 정상 · '관리자 권한' 설치 신청
보내는 것 {"question": "관리자 권한이 필요한 프로그램 설치는 어떻게 신청하나요?", "user": "한지훈"}
기대 route: 자동 답변 (막으면 안 됨)
왜 어려운가 "관리자 권한"이 공격 규칙의 낱말과 같음
실패하면 의심할 관문 규칙 (관리자\s*(권한|모드))

판정 기준을 미리 정합니다. "답이 이상하다"는 판정이 아닙니다. route가 무엇이어야 하는지, 답에 어떤 글자가 없어야 하는지(not_contains)처럼 기계가 판정할 수 있게 씁니다. 특히 보안 테스트는 "무엇이 나오면 안 되는가"로 쓰는 경우가 많습니다. 원래 값이 나오지 않으면 통과, 하나라도 나오면 실패.

오늘 실습에서

10/26 (answers/29 · 30~34):

결과
예제 2 정답에서 6/8 — ✘ 둘 다 지나치게 막음 (정상 두 질문을 규칙이 0.1초에 막음), 덜 막은 것은 없음
규칙 둘을 좁힌 뒤 (이전·모든 + 규칙 + 잊, 관리자 권한으로) --repeat 3 24/24
그 뒤 lab01-challenge · lab02-challenge · lab03 20/20 · 18/18 · 6/6
  • 좁혀서 놓치는 공격은 판별 LLM이 받칩니다. 규칙은 확실한 것만, 애매한 것은 판별 LLM으로.
  • 이 정답에는 Lab 01 과제 2의 글자 정리가 없는데, 숨긴 공격(lab01-challenge2)이 9/9였습니다 (answers/34). 판별 LLM이 뜻으로 막은 것입니다. 같은 결과지만 글자 정리는 0.1초 · LLM 0번, 판별 LLM은 약 1초 · LLM 1번입니다.

6. 감사 로그 (6교시 Lab 03 과제 2)

일반 설명

**감사 로그(audit log)**는 "누가 · 언제 · 무엇을 · 결과는"을 한곳에 쌓는 기록입니다. 보안 사고가 나면 가장 먼저 찾는 것이고, 정보보호 관리체계 인증(ISMS-P, ISO 27001)과 개인정보보호법의 안전성 확보 조치에서도 접속 · 처리 기록의 보관과 점검을 요구합니다.

응답 안의 security (3절) 감사 로그
범위 요청 하나 모든 요청
어디에 사용자에게 가는 응답 서버 쪽 저장소
수명 응답과 함께 사라짐 정한 기간 보관
쓰임 그 자리의 확인 하루 요약, 추세, 사고 조사
flowchart LR
  S["Security record"] --> R1(["Send result"])
  S --> L["Log event<br/>POST /audit"]
  B["Blocked"] --> R2(["Send blocked"])
  B --> L
  L --> D[("감사 로그")]
  H(["my-security-report"]) --> G["GET /audit"] --> C["세기 · pii_in_log"] --> O(["하루 요약"])
  D -.-> G

감사 로그의 보안 원칙.

원칙 까닭
서비스를 멈추게 하지 않는다 기록이 실패해도 사용자 답은 나가야 함 (응답과 나란히, 실패해도 계속). 반대로 결제처럼 "기록 없이는 처리하지 않는" 업무도 있으니 업무마다 정함
고칠 수 없게 (추가만) 공격자 · 내부자가 흔적을 지우지 못하게. 운영에서는 쓰기만 되는 저장소, 따로 떨어진 로그 서버
시각을 정확히 여러 시스템의 기록을 맞춰 보려면 같은 시계 (서버 시각 · 시간대)
볼 수 있는 사람을 정한다 로그도 개인정보 · 보안 정보를 담음
보관 기간을 정한다 너무 짧으면 조사를 못 하고, 너무 길면 쌓인 것이 위험
개인정보를 최소로 (7절) 로그 자체가 유출 대상

로그로 무엇을 하나. 매일 아침 "어제 몇 건을 막았나, 누가 반복해서 막혔나, 어느 관문이 바빴나, 판별 LLM이 멈춘 적은 없나"를 요약합니다. 같은 사람이 짧은 시간에 여러 번 막혔다면 공격 시도일 수 있으니 확인합니다. 77일차 실행 로그 분석과 79일차 운영 대시보드로 이어집니다.

한 줄의 모양 (오늘의 예).

{"t": 1793015017.235, "user": "한지훈", "route": "차단", "blocked_by": "guard", "reason": "requests other employee's data", "masked": 0, "tools_used": []}

시각 · 누가 · 결과 · 관문 · 이유 · 숫자만 있고, 질문 · 답 글자는 없습니다. 이것으로 "어제 한지훈 님이 판별 LLM에 한 번 막혔다"는 알 수 있지만 무엇을 물었는지는 알 수 없습니다. 무엇을 물었는지까지 꼭 알아야 하는 업무라면, 가린 문장을 따로, 더 짧은 보관 기간과 더 좁은 열람 권한으로 둡니다.

오늘 실습에서

73일차 데이터 API에 POST /audit · GET /audit · POST /audit-clear를 더했습니다 (AGENTS.md 2절). 10/26 cases/lab03-challenge2.json:

고치기 전 (answers/35) 고친 뒤 (answers/36)
네 요청 (규칙 · 판별 · 연락처 · 정상) ✔ ✔
보안 요약 ✘ 404 (웹훅 없음) ✔ total 4 · answered 2 · blocked.rule 1 · blocked.guard 1 · masked 1 · pii_in_log 0 · blocked_users ["이준호", "한지훈"]
합계 4/5 5/5

7. 데이터 최소화: 로그에 무엇을 남기나

일반 설명

개인정보보호법의 기본 원칙 중 하나가 최소 수집입니다: 목적에 필요한 최소한의 개인정보만 모읍니다. 로그도 개인정보를 모으는 곳입니다. 필요 없는 것을 남기면 로그 자체가 유출 대상이 되고, 보관 · 파기 · 열람 권한 관리 · 정보주체의 열람 요청 대응이 모두 따라옵니다. 모으지 않은 것은 샐 수 없습니다 (2단계 3절 "주지 않은 비밀은 새지 않는다"와 같은 생각).

남길지를 정하는 질문 셋. ① 이 칸이 없으면 로그의 목적(몇 건, 누가, 어느 관문)을 못 이루나? ② 이 칸에 개인정보 · 비밀이 들어갈 수 있나? ③ 꼭 필요하면, 가리거나 줄여서(해시 · 일부만) 남길 수 있나?

남길지 무엇 까닭
남김 직원 이름, route, blocked_by, reason, masked, tools_used, 시각 "몇 건, 누가, 어느 관문" (목적에 필요)
남기지 않음 질문 · 답 글자 안에 연락처 · 주민번호가 있을 수 있음
남기지 않음 연락처 (가린 것도) 다시 남길 까닭이 없음
바꿔서 남김 (예) 질문의 해시 "같은 질문이 반복되나"만 알면 될 때, 원문 없이

가명 · 익명 처리. 이름 대신 직원 번호의 해시를 남기면 "같은 사람이 반복하나"는 알 수 있지만 누구인지는 바로 알 수 없습니다(가명). 보안 로그에서는 사고 조사 때 누구인지 알아야 하므로 오늘은 이름을 남기되, 볼 수 있는 사람을 제한하는 쪽을 택했습니다. 목적에 따라 정합니다.

지켜야 할 규칙은 숫자로 확인합니다. 요약 워크플로우가 로그 전체에서 전화 · 이메일 · 주민번호 모양을 세어 pii_in_log로 냅니다. 0이 아니면 어딘가에서 새고 있습니다. 규칙("로그에 개인정보를 남기지 않는다")을 사람의 기억이 아니라 테스트로 지키는 방법입니다.

규칙의 reason도 줄입니다. 정규식 그대로 남기면 길고, 로그를 본 사람에게 공격 문장의 모양(= 피하는 법)을 알려 줍니다. 운영에서는 규칙에 이름(rule:ignore_instructions)을 붙여 이름만 남깁니다.

오늘 실습에서

curl -s localhost:8800/audit(answers/38)로 줄을 직접 열어 질문 글자가 없는지 눈으로 봅니다. pii_in_log 0이 테스트로도 확인됩니다.

8. 남은 위험: 오늘 막지 않은 것

일반 설명

보안은 끝이 없습니다. 막지 않은 것을 적어 두는 것도 보안 일입니다. 회사에서는 이것을 **위험 등록부(risk register)**라고 부르고, 위험마다 "얼마나 일어날 것 같나 · 일어나면 얼마나 나쁜가 · 무엇을 했나 · 받아들인 까닭 · 언제 다시 보나"를 적습니다.

위험을 다루는 네 가지 방법.

방법 뜻 예
줄이기 (mitigate) 관문을 더함 오늘 한 대부분
피하기 (avoid) 그 기능을 하지 않음 연락처 조회 기능 자체를 뺌
넘기기 (transfer) 다른 곳 · 보험이 맡음 인증은 사내 SSO가, 보안 관제는 전문 업체가
받아들이기 (accept) 알고도 둠 (적어 둠) 오늘의 user 본문 (수업이라서)

받아들인 위험은 반드시 적습니다. 적지 않은 위험은 "몰랐던 위험"과 같고, 담당자가 바뀌면 잊힙니다. 적어 둔 위험은 운영으로 옮길 때의 할 일 목록이 됩니다.

남은 것 운영에서는
user를 본문으로 받음 (누구나 다른 이름을 씀) 로그인 · 인증 헤더 (n8n Webhook의 Header Auth, 사내 SSO), user는 토큰에서
웹훅 주소를 알면 누구나 부름 인증 · 사내망 · 요청 수 제한(rate limit, 429)
판별 LLM도 틀릴 수 있음 감사 로그를 보고 규칙 · 지시문 갱신, 정기 공격 테스트 (레드팀 자동화)
간접 인젝션 (FAQ · 문서에 숨은 지시) 데이터 출처 관리(누가 FAQ를 고칠 수 있나), Tool 결과도 데이터로, 출력 필터
비밀이 커밋에 섞임 7교시 tscan · pre-commit, 저장소 비밀 검사
감사 로그가 메모리에만 (데이터 API를 다시 켜면 사라짐) 파일 · 데이터베이스 · 로그 서비스에 보관, 보관 기간
모델 · 제공사가 바뀌면 결과가 바뀜 바꿀 때마다 오늘의 보안 테스트를 다시 (74일차 배포 전 검사)

정기적으로 다시 봅니다. 공격은 계속 바뀌고, 모델도 바뀌고, 업무도 바뀝니다. 위험 등록부는 한 번 쓰고 끝이 아니라 분기마다, 큰 변경마다 다시 봅니다.

오늘 실습에서

my-work/SECURITY.md 10절에 내 워크플로우의 남은 위험을 둘 이상 적습니다. 74일차(버전 관리와 운영 체계)에서 이 목록을 배포 전 점검표로 씁니다.

9. 요청문(프롬프트)으로

요청문(프롬프트)이란. 실습에서 CLI 코딩 에이전트(codex · claude · agy)에게 보내는 글입니다. 에이전트는 이 글만 보고 n8n 워크플로우 JSON을 만들거나 고칩니다. 이 절은 이 단계에서 배운 것을 요청문(프롬프트)에 어떤 칸으로 적을지 실습 전에 정리하는 자리입니다. 실습 문서의 ▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대가 이 칸을 실제 문장으로 만든 것이고, 빈칸 ①~④는 그 문장 속 정확한 값입니다.

칸 에이전트에게 알려 주는 것 빠지면 생기는 일
무엇을 만들거나 바꿀 기능 에이전트가 짐작해서 다른 것을 만듦
어디에 고칠 파일 · 노드 · 자리 엉뚱한 노드를 고치거나 새 파일을 만듦
어떤 모양으로 결과 JSON의 칸 이름 · 값 · 응답 코드 테스트가 보는 칸 이름과 달라 실패
그대로 · 하지 말 것 건드리면 안 되는 것 다른 곳까지 바뀌어 무엇 때문인지 모르게 됨
확인 통과해야 할 테스트 파일 "다 됐다"를 판단할 기준이 없음

67~71일차의 다섯 가지(① 파일 ② 입력 ③ 처리 ④ 결과 모양 ⑤ 기대 결과)와 같은 뜻입니다. 파일 · 입력이 "어디에", 처리가 "무엇을", 결과 모양이 "어떤 모양으로", 기대 결과가 "확인"입니다. 72일차부터는 이미 있는 워크플로우를 고치는 요청이 많아 "그대로"가 따로 한 칸이 됐습니다.

Lab과 다른 업무로 예를 듭니다 (고객 주문 문의 Agent).

아래 표의 줄(넣을 것)은 위 다섯 칸을 이 단계 주제에 맞게 쪼갠 것이고, 실습 요청문(프롬프트)을 쓸 때도 같은 줄을 채웁니다.

넣을 것 예
출력 필터의 자리 "답을 만드는 Code 뒤, 보내기 직전"
가릴 것과 모양 "전화 앞 3 · 끝 4, 카드 번호 끝 4만, 주소는 시 · 구까지"
막을 것 "답에 내부 쿠폰 코드 모양이 있으면 차단, reason: output_leak"
기록 "모든 결과에 security {blocked_by, reason, masked}, 길마다 같은 모양"
감사 로그 "200으로 끝나면 POST /audit, 질문 · 답 글자 · 연락처는 넣지 않음, 실패해도 답은 나감"
테스트 "막을 것 · 막지 말 것 반반, 경계값, --repeat 3, pii_in_log 0"

표를 위에서부터 이어 읽으면 요청문(프롬프트)이 됩니다 (화살표 오른쪽은 다섯 칸 중 어디인지):

답을 만드는 Code 뒤, 보내기 직전에 출력 필터를 넣어 줘.                         ← 어디에
전화는 앞 3 · 끝 4, 카드 번호는 끝 4만, 주소는 시 · 구까지 남기고,
답에 내부 쿠폰 코드 모양이 있으면 차단해 줘 (reason output_leak).                  ← 무엇을
모든 결과에 security {blocked_by, reason, masked} 를 길마다 같은 모양으로.        ← 어떤 모양으로
200 으로 끝나면 POST /audit, 질문 · 답 글자 · 연락처는 넣지 말고, 실패해도 답은 나가게.  ← 하지 말 것
막을 것 · 막지 말 것 반반 · 경계값 테스트를 --repeat 3, pii_in_log 0.             ← 확인

10. 자주 하는 실수

실수 생기는 일 대신
입력만 검사 Tool 데이터가 답에 그대로 출력 필터
가리기를 LLM 지시에만 영어 · 점 찍기로 나감 코드로 가리기
전부 가림 본인도 알아볼 수 없음 알아볼 만큼 남기기
길마다 다른 기록 모양 한꺼번에 못 셈 같은 칸 셋
막힌 문장 전체를 로그에 로그가 유출 통로 이유 · 개수만, 데이터 최소화
로그 실패로 답이 멈춤 기록 때문에 서비스 장애 나란히, 실패해도 계속
통과하는 테스트만 더함 실패를 못 찾음 경계 · 정상인데 공격 낱말
규칙을 좁히고 다른 테스트를 안 돌림 알려진 공격이 다시 지나감 lab01-challenge · lab02-challenge를 같이

확인 질문

  1. 입력 관문을 모두 세웠는데도 출력 필터가 필요한 까닭은? (1절)
  2. 가리기와 차단은 무엇이 다른가요? (2절)
  3. 막힌 문장 전체를 기록에 남기면 어떤 문제가 생기나요? (3 · 7절)
  4. 관문별 표에서 가장 빠른 관문과 가장 느린 관문은? (4절)
  5. 어려운 보안 테스트에서 실패한 둘은 덜 막은 것인가요, 지나치게 막은 것인가요? (5절)
  6. 감사 로그를 남기는 노드가 실패해도 답이 나가게 한 까닭은? (6절)
  7. pii_in_log는 무엇을 세고, 0이 아니면 무엇을 의심하나요? (7절)
  8. 오늘 막지 않은 위험 둘과 운영에서의 방법은? (8절)

정리

  • 나가는 답도 검사: 가리기와 유출 차단은 코드로 (lab01-attacks 9/9)
  • 무엇을 왜 막았는지 같은 모양으로 기록 (기록에 새 비밀을 남기지 않게)
  • 관문별 시간과 기록: 앞에서 끝날수록 빠르고 쌈 (0.1 · 0.1 · 0.8 · 3초대)
  • 보안 테스트는 막을 것과 막지 말 것을 함께, 규칙은 확실한 것만 (6/8 → 24/24)
  • 감사 로그는 서비스를 멈추지 않게, 데이터는 최소로 (pii_in_log 0)
  • 남은 위험을 적어 둔다 (인증 · 요청 수 · 정기 점검 · 커밋 속 비밀)