1. 이 프로젝트가 뭔가요?
just-shield는 한 문장으로 이렇습니다:
우리 저장소의 CI 설정 파일(.github/workflows)을 읽고, "바깥에서 받아 쓰는 부품(GitHub Action) 중에 오염됐거나 오염될 수 있는 게 있는지"를 실행 전에 검사해 주는 명령줄 도구.
받아서 실행
2. 적을 알기: TeamPCP 사건
TeamPCP는 실제로 존재하는 해커 그룹입니다 (Google 추적명 UNC6780). 이들의 수법은 단순하지만 치명적입니다:
- 표적을 직접 안 때립니다. 대신 모두가 믿고 쓰는 보안 도구(Trivy, KICS, LiteLLM)의 배포 계정을 훔칩니다.
- 버전 이름표를 바꿔치기합니다. 예를 들어 Trivy의 버전 태그 77개 중 76개를 악성 코드가 든 커밋으로 옮겨 꽂았습니다. 사용자는 어제와 똑같이
@v3을 받았는데 내용물만 바뀐 겁니다. - CI 안의 열쇠를 쓸어 담습니다. 악성 코드는 CI에서 실행되며 클라우드 키, 토큰 등을 외부 서버로 보냈습니다. 약 50만 개 자격증명이 털린 것으로 추정됩니다.
- 훔친 열쇠로 다음 피해자를 만듭니다. 훔친 토큰으로 47개 패키지를 60초 만에 추가 감염시켰습니다 — 웜(전염병)처럼 번집니다.
TeamPCP "전용 백신"을 만드는 게 아닙니다. 일반적인 공급망 방어 도구를 만들되, "TeamPCP의 실제 공격을 우리 도구가 막았을까?"를 채점 기준(시험 문제)으로 쓰는 겁니다. (결정 ①)
3. 지금까지 내린 결정 7개
인터뷰에서 큰 결정부터 차례로 내려왔습니다. 각 결정은 다음 결정의 전제가 됩니다.
1범위: 특정 그룹 전용이 아닌 일반 방어
정한 것: TeamPCP 전용 탐지기가 아니라, 공급망 공격이라는 일반 문제를 푼다. TeamPCP 사건은 검증 시나리오로 쓴다.
왜: 특정 그룹의 흔적(악성 도메인, 파일 해시)만 쫓는 도구는 그룹이 수법을 바꾸면 쓸모없어집니다.
2방어 지점: 받기 전에 검사한다
정한 것: 공격 체인(① 업스트림 오염 → ② 오염된 것 섭취 → ③ 열쇠 수집 → ④ 유출) 중 ②를 끊는다. 실행 중 감시(③④ 차단)는 v2 후보로 미룸.
왜: ②에서 막으면 ③④는 아예 일어나지 않습니다. 그리고 ②는 파일만 읽으면 검사할 수 있어서 만들기가 현실적입니다. 비유: 택배를 열기 전에 송장과 봉인을 확인하는 것.
3검사 대상: GitHub Actions부터
정한 것: v1은 .github/workflows의 워크플로 파일이 참조하는 GitHub Action만 검사. npm/PyPI 패키지(락파일)는 v2 후보.
왜: 액션은 시크릿 바로 옆에서 실행되는 가장 위험한 부품이고, 검사 대상이 YAML 몇 개로 좁아서 v1으로 완성 가능한 크기입니다.
4형태: CLI 엔진 먼저, 포장은 나중에
정한 것: 검사 로직(엔진)을 명령줄 도구로 먼저 완성. GitHub Action 래퍼(CI에서 자동 실행)는 v1.5에 얹는다.
왜: GitHub Action·VS Code 확장·AI 에이전트 연동은 전부 같은 엔진을 호출하는 "포장"입니다. 엔진 없이 포장부터 만들 수는 없습니다. CI에서는 래퍼 없이도 run: just-shield scan 한 줄로 쓸 수 있습니다.
5규칙 범위: 두 계층, 9개로 고정 (ADR-0001)
정한 것: "받는 게 진짜인가"(섭취 검증 6개) + "털려도 덜 털리게"(피해 반경 3개) = 9개. 그 외 일반 CI 보안 검사(스크립트 인젝션 등)는 일부러 안 한다 — 그 영역은 기존 도구 zizmor가 이미 잘함.
왜: 다 하려다 보면 기존 도구의 열화 복제품이 됩니다. 좁고 깊은 정체성이 생존 전략입니다.
6신뢰 경계: 누구를 의심할 것인가
정한 것: 내 저장소·내 조직의 액션 = 퍼스트파티 (검사 제외), GitHub 공식 actions/* = 서드파티지만 완화된 경고, 그 외 전부 = 예외 없이 엄격.
왜: TeamPCP에게 털린 곳이 바로 "믿을 만한 보안 회사들"이었습니다. 평판은 신뢰 근거가 아닙니다. 단, 경고를 남발하면 사용자가 경고 자체를 무시하게 되므로 등급을 나눕니다.
7한계 인정: 무엇을 보증하지 않는가
정한 것: SHA 핀 고정은 "내용물이 안 바뀐다"(불변성)를 보증할 뿐 "내용물이 착하다"(무해성)는 보증하지 않는다. 아무도 모르는 오염(제로데이)은 v1 보증 범위 밖이며, 이를 ADR에 명시했다. 대신 이미 알려진 감염 버전을 잡는 규칙 R9를 추가했다.
왜: "다 막아준다"고 암시하는 보안 도구는 통과 마크가 거짓 안심이 되어 오히려 위험합니다.
4. 확정된 검사 규칙 9개
4-1. 섭취 검증 — "받는 물건이 진짜인가" Tier 1
| 규칙 | 무엇을 잡나 | 쉬운 비유 |
|---|---|---|
| R1 가변 참조 | 액션을 @v3 같은 태그로 받는 것. 태그는 공격자가 옮겨 꽂을 수 있음 → 커밋 SHA(지문)로 받아야 함 | "이름표"가 아니라 "지문"으로 신원 확인 |
| R2 타이포스쿼팅 | aquasecurtiy처럼 한 글자 바꾼 짝퉁 이름의 액션 | 짝퉁 상표 감별 |
| R3 미검증 설치 | curl | sh처럼 검증 없이 인터넷에서 받아 바로 실행하는 스텝 | 출처 미상 설치파일 더블클릭 |
| R4 이미지 미고정 | image: foo:latest처럼 내용물이 바뀔 수 있는 컨테이너 참조 | 내용물 바뀌는 택배 상자 |
| R5 임포스터 커밋 온라인 | SHA로 핀했어도 그 커밋이 해당 저장소 족보(히스토리)에 없는 경우 — 강한 오염 신호 | 족보에 없는 가짜 호적 |
| R9 알려진 감염 버전 DB 동봉 | 보안 커뮤니티가 이미 "악성"으로 공표한 버전을 아직 쓰고 있는 경우 | 리콜 목록과 대조 |
4-2. 피해 반경 축소 — "털려도 덜 털리게" Tier 2
| 규칙 | 무엇을 잡나 | 쉬운 비유 |
|---|---|---|
| R6 시크릿 노출 | 서드파티 액션이 시크릿에 접근 가능한 잡에서 실행되는 것 | 금고 열어둔 방에 외부 수리기사 들이기 |
| R7 권한 과잉 | permissions 미선언 — 기본 토큰이 필요 이상으로 강력해짐 | 알바생에게 마스터키 지급 |
| R8 위험 트리거 | pull_request_target + 외부 PR 코드 체크아웃 조합 — 외부인이 시크릿 있는 환경에서 코드 실행 가능 | 낯선 사람을 금고 방에 초대 |
기본은 완전 오프라인(저장소 파일만 읽음). 네트워크가 필요한 검사(R5, R9 최신화)는 --online 옵션을 켰을 때만 실행. 이유: 방어 도구 자신이 "아무 데도 접속 안 한다"고 말할 수 있어야 신뢰받습니다.
5. 아직 정하지 않은 것
아무도 모르는 오염은 탐지가 불가능하지만, "나온 지 7일 안 된 버전은 일단 쓰지 말라"는 규칙(쿨다운)으로 위험 기간을 피해갈 수 있습니다. 오염은 보통 며칠 안에 발각되므로, 남들이 먼저 밟게 하는 전략입니다.
현재 추천: R10으로 추가 (온라인 규칙, 기본 7일). 트레이드오프: 길수록 안전하지만 버그 패치도 그만큼 늦게 받습니다.
그 뒤에 남아 있는 결정들 (난이도 순으로 정렬, 위가 쉬움):
| 결정 | 질문 내용 | 전문성 필요도 |
|---|---|---|
| 리포트 형식 | 검사 결과를 어떻게 보여줄까? (표 형태, 심각도 색깔, JSON 출력 등) | 낮음 — 취향 문제 |
| 실패 기준 | 어떤 심각도부터 CI를 빨갛게(빌드 실패) 만들까? | 낮음 |
| 예외 처리 | "이 경고는 알고 쓰는 거야"라고 무시 표시하는 방법 | 중간 |
| 구현 언어 | Python, Go, Rust 중 무엇으로 만들까? | 중간 — 팀이 아는 언어가 우선 |
| 테스트 전략 | TeamPCP 시나리오를 재현한 가짜 워크플로로 채점하는 방법 | 중간 |
어려운 결정(위협 모델, 규칙 범위, 신뢰 경계)은 이미 다 끝났습니다. 남은 것은 대부분 "도구를 어떻게 편하게 쓸까"에 대한 가벼운 결정이고, 모르겠으면 추천안을 그대로 받아들여도 안전합니다.
6. 용어 미니 사전
| 용어 | 쉬운 뜻 |
|---|---|
| 공급망 공격 | 나를 직접 해킹하는 대신, 내가 믿고 받아 쓰는 부품을 오염시켜서 나까지 감염시키는 공격 |
| CI/CD | 코드를 올리면 자동으로 빌드·테스트·배포해 주는 시스템. 배포 권한(열쇠)을 잔뜩 들고 있어서 해커의 표적 |
| GitHub Action | CI에서 받아 쓰는 남이 만든 부품 (예: 코드 체크아웃, 스캔 도구) |
| 태그 / SHA | 태그(@v3)는 옮겨 붙일 수 있는 이름표, SHA는 바꿀 수 없는 지문 |
| 태그 하이재킹 | 공격자가 이름표를 악성 코드에 옮겨 붙이는 것 |
| 임포스터 커밋 | 저장소의 정식 기록에 없는 가짜 커밋 |
| 섭취 전 검증 | 부품을 실행하기 전에 진짜인지 확인하는 것 — just-shield의 본업 |
| 피해 반경 | 뚫렸을 때 얼마나 털리는가의 범위. 권한을 줄이면 반경이 줄어듦 |
| 제로데이 | 아직 아무도 모르는 (그래서 목록 대조로 못 잡는) 오염 |
| ADR | "왜 이렇게 결정했는지" 기록한 문서. docs/adr/에 있음 |