ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • repo마다 신뢰도가 다르다 — 4-tier 분류와 2-layer egress allowlist
    IT 2026. 6. 2. 22:00
    repo마다 신뢰도가 다르다 — 4-tier 분류와 2-layer egress allowlist

    코드 지식 베이스를 만들 때 자주 놓치는 두 가지가 있다. 첫째, 모든 repo가 같은 신뢰도와 같은 IP 경계를 갖지 않는다. 여기서 IP 경계(Intellectual Property boundary)는 "이 코드가 외부로 나가도 되는가"를 가르는 선 — 비공개 기밀 코드는 외부 LLM API에 보내면 안 되고, 본인 공개 코드는 자유롭게 보내도 된다. 내가 직접 쓴 공개 코드와 기밀 코드는 명백히 다른 정책으로 다뤄야 한다.

    둘째, 외부 LLM API를 호출할 때 무엇이 어디까지 나갈 수 있는지를 제어하지 않으면 보안 사고로 직결된다. Qdrant에 들어가는 정보, claude API로 가는 페이지 내용, gemma에 던지는 prompt — 모두가 어떤 호스트에 어떤 형태로 도달하는지 명확해야 한다.

    deep-wiki를 시작하며 이 두 가지를 처음부터 정했다. 두 정책의 의미는 이렇다.

    • 4-tier 분류 — 모든 repo를 신뢰도 등급 4개로 나눠 정책을 분기한다. tier는 영어 그대로 "층/등급"을 의미한다. 같은 도구로 인덱싱해도 등급에 따라 외부 LLM 사용·산출물 공개 가능 여부가 달라진다.
    • 2-layer egress allowlistegress"외부로 나가는 트래픽"(반대말 ingress), allowlist"허용된 목적지 목록"(반대말 blocklist). 워커가 도달할 수 있는 외부 호스트를 명시 목록으로 제한하고, 이 검증을 두 단계로 이중화한다.

    이 글은 그 두 정책의 설계와 운영 기록이다.

    1. 배경 — "모든 코드가 같다"의 위험성

    1인 환경이라도 코드의 출처는 단일하지 않다. ~/projects/ 하위에 있는 22개 repo는 거의 본인 개인 작업물이지만, 실제로 내가 일상적으로 접하는 코드 풀에는 다른 출처가 섞여 있다:

    • 개인 공개 코드 — 본인 GitHub repo. 인덱싱·검증·외부 노출 모두 OK
    • 제한적 기밀 코드 — 본인이 제한적으로 다루도록 받은 비공개 코드. 외부 LLM에 보내려면 별도 동의 필요
    • 공개 OSS — git.tizen.org, github.com의 공개 repo. 인덱싱 OK, 산출도 공개 가능
    • 엄격 기밀 코드 — 최고 민감도 비공개 영역. 인덱싱 자체가 금지

    네 종류를 모두 같은 파이프라인에 던지면 사고 가능성이 높아진다. 엄격 기밀 코드를 실수로 claude API에 보내거나, 기밀 영역에서 추출한 정보를 공개 OSS 영역과 같은 Qdrant 컬렉션에 섞으면 정보 누출이 발생한다. "신뢰도 등급을 코드 자체에 붙이고, 파이프라인이 그 등급에 따라 분기"하는 게 필요했다.

    2. 4-tier 분류 — 등급마다 정책이 다르다

    diagram

    네 등급의 핵심 차이는 두 가지다. (a) 외부 LLM(claude API, ChatGPT API 등)에 prompt로 보내도 되는가, (b) 산출물이 공개 가능한가. T0과 T1은 둘 다 OK. T2는 외부 LLM은 본인 명시 동의 후에만, 산출은 비공개. T3은 처음부터 끝까지 차단.

    이 분류는 단순한 정책 표가 아니라 코드 안에 박힌 실제 가드다. scripts/enumerate_personal.py는 T0 repo만 열거하고, scripts/index_repo.sh는 host allowlist를 매칭하지 못하면 시작 자체를 거부한다. "인하우스 영역을 우연히 인덱싱하는 사고"가 코드 레벨에서 불가능하게 만든다.

    산출물 경로도 이중화된다. wiki-output/{public,private}/<repo>/로 갈라져서 T0/T1은 public에, T2는 private에 들어간다. publish는 처음부터 차단된다 (로컬 git only). "산출이 우연히 외부로 나가는 사고"도 폴더 구조 레벨에서 막힌다.

    3. 2-layer egress allowlist — script 가드 + docker network

    tier 분류만으로는 부족하다. 워커 프로세스가 인덱싱 중에 어디로 나가는지를 제어해야 한다. "잘못된 호스트로 정보가 새는 한 줄"이 가장 흔한 사고 패턴이기 때문이다. 답은 두 layer로 합쳐 짠다.

    diagram

    한 줄로 정리하면 "sanity check를 두 번 한다"이다. 첫 번째 layer는 script가 stdin/argument를 검증해서 시작 자체를 막는다. 두 번째 layer는 컨테이너 네트워크 수준에서 packet 자체를 막는다. 첫 번째 layer를 우회하더라도 두 번째 layer가 잡는다.

    한쪽만 박는 게 안 되는 이유는 두 layer가 잡는 대상이 다르기 때문이다. 먼저 짚을 게 하나 있다 — script가 인자로 받는 건 인덱싱할 repo의 git URL(repo를 추가할 때마다 바뀌는 가변 값)이고, egress 목적지인 API endpoint(api.anthropic.com 등)는 코드에 고정돼 있다. 그래서 여기서 말하는 "잘못된 URL"은 API endpoint가 틀리는 게 아니라 — 그건 고정값이라 틀릴 일이 없다 — allowlist에 없는 호스트의 repo URL이 인자로 들어오는 경우다. 새 repo를 추가하다 오타를 내거나, 예상 못 한 호스트의 repo를 던지는 상황이다.

    이때 layer별 역할이 갈린다. Layer 1(script)만 있으면 — script는 자기에게 인자로 들어온 URL만 검사한다. 그래서 정상 URL로 인덱싱이 시작된 뒤에 의존성 라이브러리가 몰래 telemetry를 보내거나, 누군가 index_repo.sh를 거치지 않고 별도 스크립트로 fetch를 돌리면 — 그건 script의 시야 밖이라 그대로 샌다. Layer 2(컨테이너)만 있으면 — 네트워크 경계에서 모든 egress를 잡지만 '의도'를 모른다. Layer 1에서는 allowlist 밖 호스트의 URL을 넘겨도 거부하지 못하고, 워커가 일단 떠서 fetch를 시도하다 Layer 2에서 packet이 drop된다. 둘을 합치면 Layer 1이 잘못된 입력을 시작 시점에 "host X not in allowlist, add to ALLOWED_HOSTS" 같은 actionable한 메시지로 쳐내고, Layer 2가 script를 우회한 모든 egress를 네트워크 레벨에서 받아낸다.

    4. 코드로 보면 — index_repo.sh의 host 검증

    # scripts/index_repo.sh — Layer 1 가드
    # 인덱싱 시작 전 host가 우리 allowlist에 매칭되는지 확인.
    
    set -euo pipefail
    
    ALLOWED_HOSTS=(
        "github.com"
        "git.tizen.org"
        "api.anthropic.com"        # claude -p가 도달하는 단 한 곳
        # 기밀 영역은 명시 추가 — T2 등급만 (T3는 절대 추가 안 함)
    )
    
    target_url="${1:?usage: index_repo.sh }"
    host=$(echo "$target_url" | sed -E 's|^https?://([^/]+)/.*|\1|')
    
    # allowlist 매칭 — 매칭 안 되면 즉시 종료
    matched=0
    for allowed in "${ALLOWED_HOSTS[@]}"; do
        if [[ "$host" == "$allowed" ]]; then
            matched=1
            break
        fi
    done
    
    if [ "$matched" -eq 0 ]; then
        echo "[deep-wiki] DENIED: host '$host' not in allowlist" >&2
        echo "  add to ALLOWED_HOSTS in scripts/index_repo.sh if T0/T1/T2" >&2
        exit 1
    fi
    
    # tier 자동 추정
    tier="T1"   # 기본 — 공개 OSS
    if [[ "$target_url" =~ internal-host\.example ]]; then
        tier="T2"   # 제한적 기밀 호스트는 환경별로 치환
    fi
    if [[ "$target_url" =~ confidential|internal-only ]]; then
        echo "[deep-wiki] DENIED: T3 hint detected, indexing blocked" >&2
        exit 1
    fi
    
    echo "[deep-wiki] indexing $target_url (tier=$tier)"
    # ... 실제 인덱싱 단계로 진행
    
    # docker-compose.yml — Layer 2 가드
    # 워커 컨테이너의 network를 internal로 잠그고, egress allowlist만 별도 허용.
    
    services:
      deep-wiki-worker:
        image: deep-wiki/worker
        networks:
          - internal
          - egress
        environment:
          DEEP_WIKI_REDIS_URL: "redis://redis:6379/0"
    
      redis:
        image: redis:7
        networks:
          - internal
    
    networks:
      internal:
        # internal: true → 외부 어떤 호스트로도 도달 불가
        internal: true
      egress:
        # 별도 네트워크로 분리. iptables 규칙으로 allowlist만 통과
        driver: bridge
        driver_opts:
          com.docker.network.bridge.name: deep-wiki-egress
    

    Docker network internal: true는 컨테이너가 외부 인터넷으로 패킷을 보낼 수 없게 한다. 별도 egress 네트워크를 정확한 destination에만 향하게 설정하면, "이 패킷이 도달할 수 있는 호스트 목록"이 단단히 잠긴다. 매일 systemd timer가 egress traffic을 모니터링하고, allowlist 외 도메인으로 hit가 발생하면 즉시 텔레그램으로 알리고 컨테이너를 격리한다.

    5. 운영 결과 — 한 번도 발동되지 않은 알림이 가치

    이 정책을 잠근 후 한 달 동안 egress 모니터링 알림이 0번 발동됐다. 솔직히 말하면 1인 통제 환경에서 극적인 차단 사건은 일어나지 않는다 — URL을 인자로 넣는 사람도 나고, allowlist를 관리하는 사람도 나다. allowlist 밖 호스트를 던지면 script가 시작 단계에서 거부하지만, 그건 allowlist가 정의대로 동작한 것일 뿐 자랑할 사건은 아니다.

    그런데 "0번"이 곧 정책이 무용하다는 뜻은 아니다. 이 가드가 막는 실수는 두 가지 성질을 갖는다 — 조용하고, 비가역적이다. 기밀 코드가 외부 LLM에 한 번 실려 나가면 회수할 방법이 없고, 더 나쁜 건 나갔다는 사실조차 모를 수 있다는 점이다. 사후에 탐지할 수 없는 사고는 "얼마나 자주 일어나나"가 아니라 "한 번 일어나면 얼마를 잃나"로 정당화된다. egress 알림이 0번인 것도 위험이 애초에 없어서가 아니라, 상류 가드(Layer 1 + tier 분기)가 egress에 닿기 전에 다 걸러내기 때문이다. Layer 2의 침묵은 시스템이 설계대로 동작한다는 신호다.

    마무리 — 보안 가드의 두 가지 미덕

    보안 가드를 짤 때 두 가지 미덕이 있다. 첫째, "모든 자원이 같은 등급이 아니다"를 시스템 안에 박아야 한다. tier 분류는 운영의 코드화다. 둘째, "두 겹의 가드는 한 겹보다 훨씬 더 강하다" — 다른 layer에서 다른 의도로 잡으면 우회 가능 시나리오의 90%가 막힌다.

    1인 환경에서는 이런 가드가 과하다고 생각될 수 있다. "내가 직접 인덱싱하는데 누가 우회한단 말인가?" 그러나 우회는 사람만 하는 게 아니다. 의존성 라이브러리, 잘못된 인자, 시간이 지나며 잊힌 컨벤션 — 모두 우회의 원천이다. 보안 가드는 "미래의 내가 실수했을 때 잡아주는 도구"이지 외부 공격자를 막는 도구만이 아니다. 이 한 줄 사고 전환이 1인 환경에서도 진지한 보안 정책을 정당화한다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.