just-shield
CI 파이프라인이 받아 쓰는 GitHub Action이 진짜인지, 오염됐을 때 덜 털리는 구조인지를 실행 전에 검사하는 CLI. 북극성은 CI 자격증명 탈취 방지다 — TeamPCP(UNC6780) 캠페인을 대표 검증 시나리오로 사용한다.
의존 크레이트 0개, 기본 완전 오프라인. 설계 배경은 CONTEXT.md와 docs/adr/에 있다.
설치
패키지 매니저로 설치하면 체크섬 검증까지 자동이다:
# macOS / Linux (Homebrew)
# Windows (Scoop)
scoop bucket add kihyun1998 https://github.com/kihyun1998/scoop-bucket
scoop install just-shield
두 채널 모두 formula/manifest에 SHA256이 명시되어 있어 패키지 매니저가 설치 때 강제 검증하며, 새 릴리스가 나오면 자동 갱신된다.
직접 내려받으려면 릴리스 페이지에서 플랫폼에 맞는 아카이브를 받는다. Linux 바이너리는 musl 정적 링크라 시스템 라이브러리 의존이 없다.
컨테이너 기반 CI는 ghcr.io 이미지를 쓴다 — FROM scratch에 정적 바이너리 하나뿐인 수 MB 이미지다 (linux amd64·arm64). 참조는 반드시 다이제스트로 핀 고정한다(우리 R4 규칙 그대로) — 각 릴리스의 다이제스트는 릴리스 노트에 기록된다:
| 플랫폼 | 파일 |
|---|---|
| Linux x86_64 | just-shield-<버전>-x86_64-unknown-linux-musl.tar.gz |
| Linux arm64 | just-shield-<버전>-aarch64-unknown-linux-musl.tar.gz |
| macOS Apple Silicon | just-shield-<버전>-aarch64-apple-darwin.tar.gz |
| macOS Intel | just-shield-<버전>-x86_64-apple-darwin.tar.gz |
| Windows x86_64 | just-shield-<버전>-x86_64-pc-windows-msvc.zip |
| Windows arm64 | just-shield-<버전>-aarch64-pc-windows-msvc.zip |
내려받은 파일은 실행 전에 검증한다 — 이 도구가 설파하는 원칙(R3) 그대로다:
# ① 체크섬: 릴리스의 SHA256SUMS와 대조
# ② 빌드 출처 증명: 이 파일이 이 저장소의 릴리스 워크플로에서 만들어졌는지 GitHub이 보증
Rust 툴체인이 있으면 crates.io에서 바로 설치할 수도 있다 — 사전 빌드 바이너리가 없는 플랫폼의 만능 탈출구:
GitHub Action으로 사용
워크플로에 한 블록 추가하면 끝이다. 래퍼는 로직 없는 얇은 껍데기로, 릴리스 바이너리를 내려받아 SHA256SUMS 체크섬 검증을 통과한 경우에만 실행한다 — 검증 실패 시 즉시 실패한다.
jobs:
supply-chain:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# 우리 액션도 우리 규칙(R1)대로 커밋 SHA로 핀 고정해서 쓰라
- uses: kihyun1998/just-shield@<40자리 커밋 SHA> # 릴리스 태그의 커밋
with:
strict: true
입력: path(기본 .), strict, online, format(text|json|sarif), cooldown-days, output-file(출력 저장 경로), version(내려받을 릴리스 — 기본값은 핀 고정된 검증 릴리스). scan의 종료 코드가 그대로 잡 결과가 된다 — 위반이면 잡이 실패한다.
사용법
종료 코드: 0 통과 · 1 위반(🔴, --strict면 🟡 포함) · 2 사용법/입출력 오류.
검사 규칙 (구현 현황)
| 규칙 | 등급 | 내용 |
|---|---|---|
| R1 | 🔴/🔵 | 서드파티 액션의 가변 참조(태그/브랜치). GitHub 공식은 🔵 완화 |
| R2 | 🔵→🔴 | 유명 액션과 한 글자 차이(전치 포함) — 기본 🔵, --online 교차 검증(짝퉁은 태그 ≤2 · 원본 ≥10)으로만 🔴 격상 |
| R3 | 🔵 | curl | sh류 미검증 파이프 설치 (휴리스틱 — 항상 안내만, 체크섬 검증 시 침묵) |
| R4 | 🟡 | 다이제스트 없는 컨테이너 이미지 참조 (image:, container:, docker://) |
| R6 | 🟡 | 시크릿을 쓰는 잡에서 서드파티 액션 실행 |
| R7 | 🟡 | permissions 미선언 또는 write-all |
| R5 | 🔴 | (--online) 핀된 SHA가 저장소 정식 히스토리에서 도달 불가 — 임포스터 커밋 |
| R8 | 🔴 | pull_request_target/workflow_run + 외부 PR 체크아웃 조합 |
| R9 | 🔴 | 공개 권고에 악성으로 등재된 버전/커밋 사용 (동봉 DB 스냅숏, 오프라인 동작) |
| R10 | 🟡 | (--online) 발행 7일 미만 참조 — 미검증 기간(쿨다운) 회피, --cooldown-days로 조정 |
| LOCK | 🔴/🔵 | shield.lock 박제본 대비 태그 이동 (정확 버전 이동=🔴, 별칭/브랜치=🔵) |
| EGRESS | 🔴 | (observe) 잠근 잡이 egress.lock에 없는 목적지를 조회 — 유출 신호. 관찰 모드 참조 |
섭취 검사 규칙 10개 + 런타임 유출 정책(EGRESS) 구현 완료. R9의 권고 DB(data/advisories.txt)와 R2의 유명 액션 목록(data/popular-actions.txt)은 바이너리에 동봉된다 — 형식·갱신 절차는 각 파일 머리말 참조. 갱신 = 새 릴리스이므로 데이터만 바꿔치기하는 공격면이 없다.
신뢰 분류: 로컬·같은 소유자 = 퍼스트파티(검사 제외), actions/*·github/* = 공식(완화), 그 외 전부 서드파티(엄격). 판별 실패 시 서드파티 취급(fail-closed).
shield.lock
just-shield lock이 각 액션의 "태그 → 커밋 SHA" 대응을 저장소 루트의 shield.lock에 박제한다. 이후 scan --online은 현재 대응을 박제본과 대조해 태그 하이재킹(TeamPCP가 Trivy 76개 태그에 쓴 수법)을 권고 DB 등재 이전에 탐지한다. 락파일은 커밋해서 PR 리뷰 대상으로 관리하라 — 신뢰 변경이 코드 리뷰를 통과하는 구조다.
관찰 모드 + egress.lock — "못 나가게" (v2, Linux 러너)
scan이 섭취 전(들어오기 전)을 검사한다면, 관찰 모드는 실행 중(들어온 후)을 본다. 잡이 도는 동안 DNS 질의를 기록해 "이 잡이 실제로 어디와 통신했는가"를 모으고, 그 사실을 egress.lock(저장소 루트, shield.lock의 자매)에 박제한 허용 목록과 대조한다.
[!NOTE] 이것은 차단기가 아니라 정책층이다 (ADR-0006). 관찰은 DNS 수준이고 회피 가능하며(IP 직결·DoH는 못 봄) 보안 경계가 아니다 — 정책 작성을 위한 가시성 도구다. 강제 차단이 필요하면 step-security/harden-runner나 GitHub의 네이티브 egress 방화벽을 함께 쓰라. 우리의 자리는 "무엇을 허용할지"라는 정책을 리뷰 가능한 파일에 두는 것이고, 집행자가 누구든 허용 목록의 진실은
egress.lock에 있다.
워크플로에 한 블록이면 관찰이 켜진다:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: kihyun1998/just-shield@<40자리 커밋 SHA> # 릴리스 태그의 커밋
with:
mode: observe
- run: ./deploy.sh # 이 스텝들이 어디와 통신하는지 관찰된다
쓰는 흐름:
- 켠다 — 잡이 평소처럼 돌고, 끝나면 잡 요약에 통신한 도메인 목록 +
egress.lock초안이 뜬다. 빨간불 없음(거울일 뿐). - 박제한다 — 초안을 검토해
egress.lock에 커밋한다. 이 커밋의 뜻은 "이 잡은 여기 적힌 곳 말고 다른 데랑 통신하면 안 된다". - 평상시 — 등재된 곳만 통신하면 조용하다. 유지비 0.
- 발각 — 오염된 액션이 시크릿을 미등재 도메인으로 보내면 🔴 + 어느 잡·어느 도메인 + 토큰 회전 권고. 실제 TeamPCP 피해자들은 몇 주 뒤에 알았지만, 이건 몇 분 안에 안다.
잠금은 잡 단위 선택제다 — egress.lock에 적은 잡만 대조 대상(🔴 가능)이고, 안 적은 잡은 영원히 관찰 보고만 받는다. 권장 사용은 "시크릿 만지는 잡(release/deploy/publish)만 잠가라" — 가치가 큰 곳이 통신처가 가장 안정적이라 유지비도 가장 싸다.
# egress.lock — 잡별 허용 목적지. 정당하게 통신처가 늘면 한 줄 추가 PR.
[release]
ghcr.io
crates.io
*.blob.core.windows.net # 무작위 접두사는 명시적 와일드카드로 (사람이 직접)
탈출구 — 경고를 의도적으로 수용하기
경고가 난 줄 위(또는 같은 줄 끝)에 사유 필수 무시 주석을 단다:
# just-shield: ignore R1 -- 내부 보안팀 검증 완료, 2026-07 SHA 핀 예정
- uses: vendor/tool@v2
--뒤 사유가 없으면 무시가 적용되지 않고 그 사실이 🔵로 보고된다- 해당 행·해당 규칙에만 적용된다 (여러 규칙은
ignore R1, R7) - 무시된 항목은 사라지지 않는다 — ⚪ 등급으로 사유와 함께 리포트·JSON에 남는다 (침묵 ≠ 은폐)
조직 단위 신뢰는 저장소 루트의 .just-shield.conf에 선언한다:
# 한 줄에 하나, 해당 org의 액션은 퍼스트파티로 취급
trust-org partner-org
# R10 쿨다운 기준 일수 (기본 7, CLI --cooldown-days가 우선)
cooldown-days 14
JSON 출력 스키마 (version 1)
severity:high|medium|info(무시된 항목은suppressed배열에 별도 수록)file: 플랫폼과 무관하게/구분자로 정규화exit_code: 해당 실행의 종료 코드와 동일 (--strict반영)findings: (file, line, rule) 순 정렬 — 같은 입력이면 같은 순서
SARIF 출력
--format sarif는 SARIF 2.1.0 형식으로 출력한다 — GitHub 코드 스캐닝에 업로드하면 경고가 PR의 해당 코드 줄 위에 직접 표시된다.
- 심각도 매핑: 🔴 →
error, 🟡 →warning, 🔵 →note - 무시 주석으로 수용된 발견은 결과에서 사라지지 않고 SARIF
suppressions(사유 포함)로 표현된다 (침묵 ≠ 은폐) - 종료 코드는 텍스트/JSON 모드와 동일 — 출력 형식이 판정을 바꾸지 않는다
- 출력 전체가 스냅숏 테스트(
tests/snapshots/violation.sarif)로 고정된다
# PR마다 검사 → 위반을 PR 코드 줄 주석으로 표시 (전체 동작 예: just-shield-demo 저장소)
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # SARIF 업로드에 필요
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: kihyun1998/just-shield@066f5f60ce9611e71b86043668a763f0e84fdab9 # v0.1.3
with:
format: sarif
output-file: results.sarif
- uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
if: always() # 위반으로 scan이 실패해도 결과는 업로드
with:
sarif_file: results.sarif
실제 동작은 just-shield-demo에서 볼 수 있다 — 일부러 취약하게 만든 워크플로를 추가하는 데모 PR에 경고가 줄 단위로 달려 있다.
개발
채점표 게이트 (tests/corpus/, ADR-0002 원칙 ④): TeamPCP 캠페인을 재현한 공격 코퍼스는 전부 탐지돼야 하고(미탐 0), 실제 워크플로를 본뜬 양성 코퍼스에서 🔴 오탐이 하나도 없어야 한다. CI는 마지막에 just-shield로 자기 저장소를 검사한다(dogfood). 코퍼스 추가 절차는 tests/corpus/README.md.
모든 판정은 사실 기반이어야 한다 — 빌드를 깨뜨리는 🔴는 검증 가능한 사실에서만 나온다 (ADR-0002).
라이선스
MIT 또는 Apache-2.0 중 원하는 쪽을 선택해 사용한다 (Rust 생태계 관례의 이중 라이선스).