왜 CI에 보안 스캔이 필요한가

배포 직전이나 운영 중에 발견되는 취약점은 수정 비용이 기하급수적으로 커진다. 코드 리뷰만으로는 SQL 인젝션, 하드코딩된 시크릿, 취약한 의존성을 일관되게 걸러내기 어렵다. 사람은 놓치지만 파이프라인은 놓치지 않는다. 핵심은 보안 검증을 이벤트가 아니라 기본 절차로 만드는 것이다. PR을 열 때마다 자동으로 스캔이 돌면, 취약점은 머지되기 전에 개발자에게 되돌아온다.

SAST와 DAST의 역할 구분

둘은 대체재가 아니라 보완재다. SAST는 소스 코드를 정적으로 분석해 코딩 단계의 결함을 찾고, DAST는 실행 중인 애플리케이션을 외부에서 공격해 런타임 취약점을 찾는다.

구분SASTDAST
대상소스 코드(화이트박스)구동 중인 앱(블랙박스)
시점빌드 이전배포된 환경
강점정확한 위치·라인 특정실제 익스플로잇 가능성
약점오탐 많음느리고 커버리지 편차

대표 도구로 SAST는 Semgrep·CodeQL, 의존성 검사는 Trivy·Grype, DAST는 OWASP ZAP이 실무에서 자주 쓰인다.

SAST를 PR 파이프라인에 넣기

SAST는 빠르고 결정적이라 모든 PR에서 돌려도 부담이 적다. GitHub Actions 기준으로 Semgrep과 의존성 스캐너를 함께 실행하는 예시다.

name: security-sast
on:
  pull_request:
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Semgrep (코드 취약점)
        run: |
          pip install semgrep
          semgrep --config=p/owasp-top-ten \
                  --severity=ERROR \
                  --error --sarif -o semgrep.sarif .
      - name: Trivy (의존성/시크릿)
        uses: aquasecurity/trivy-action@0.28.0
        with:
          scan-type: fs
          scanners: vuln,secret
          severity: HIGH,CRITICAL
          exit-code: '1'
      - name: SARIF 업로드
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

--severity=ERROR와 exit-code: '1'로 심각도 높은 항목만 빌드를 실패시키는 것이 요령이다. 처음부터 모든 경고를 차단하면 개발자가 스캔 자체를 우회하게 된다.

DAST를 배포 후 단계에 넣기

DAST는 앱이 떠 있어야 하므로 스테이징 배포 이후, 혹은 CI 안에서 임시 컨테이너를 띄운 뒤 실행한다. ZAP 베이스라인 스캔은 능동 공격 없이 수동적으로 훑어 야간 스케줄에 적합하다.

name: security-dast
on:
  schedule:
    - cron: '0 18 * * *'   # 매일 야간 실행
jobs:
  zap:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 앱 기동
        run: docker compose up -d && sleep 20
      - name: ZAP baseline
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'http://localhost:8080'
          rules_file_name: '.zap/rules.tsv'
          fail_action: true

능동 스캔(active scan)은 실제 페이로드를 주입하므로 절대 운영 환경에 겨누면 안 된다. 반드시 격리된 스테이징에서만 돌린다.

게이트 정책: 무엇으로 빌드를 막을 것인가

모든 발견을 차단 조건으로 걸면 파이프라인이 상시 빨간불이 되어 신뢰를 잃는다. 심각도와 신규성으로 나누는 것이 현실적이다.

  • 차단(fail): CRITICAL/HIGH이면서 이번 PR에서 새로 유입된 항목
  • 경고(warn): MEDIUM 이하, 또는 기존 코드의 부채
  • 무시(ignore): 검증된 오탐 — 인라인 주석이나 .semgrepignore로 관리

기존 취약점(baseline)을 스냅샷으로 잡아두고 증가분만 차단하면, 레거시 부채에 발목 잡히지 않으면서 새 취약점 유입을 막을 수 있다.

운영 시 주의점

오탐 관리가 성패를 가른다. 예외 처리는 코드 옆에 근거와 함께 남겨 추적 가능하게 한다.

# 검증된 오탐: 내부 신뢰 입력, 외부 노출 없음 (JIRA-1234)
query = f"SELECT * FROM logs WHERE id = {internal_id}"  # nosemgrep: sqli

그 외 실무 체크포인트는 다음과 같다.

  • SAST 결과는 SARIF로 통일해 코드 스캐닝 탭에서 라인 단위로 확인한다.
  • DAST는 느리므로 PR이 아닌 스케줄/머지 후 단계로 분리한다.
  • 도구·룰셋 버전을 고정(pin)해 룰 업데이트로 인한 갑작스러운 빌드 실패를 막는다.
  • 스캐너가 발견한 시크릿은 코드에서 지우는 것으로 끝나지 않는다 — 반드시 해당 자격증명을 폐기·교체한다.

보안 스캔의 목표는 완벽한 차단이 아니라 측정 가능한 지속적 개선이다. 게이트를 낮게 시작해 신뢰를 쌓고, 심각도 기준을 점진적으로 조여가는 편이 결국 더 안전한 코드베이스로 이어진다.