🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

GitHub Actions 보안과 모범 사례 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 28. · 18 Views

GitHub Actions 보안과 모범 사례 완벽 가이드

GitHub Actions를 안전하게 사용하기 위한 보안 설정과 모범 사례를 다룹니다. GITHUB_TOKEN 권한 제한부터 포크 PR 보안까지, 실무에서 꼭 알아야 할 보안 지식을 초급자도 이해할 수 있게 설명합니다.


목차

  1. GITHUB_TOKEN 권한 제한
  2. Dependabot 보안 업데이트
  3. CodeQL 코드 스캔
  4. OpenSSF Scorecard
  5. 워크플로우 감사 로그
  6. 포크 PR 보안 고려

1. GITHUB TOKEN 권한 제한

김개발 씨는 회사의 오픈소스 프로젝트에 GitHub Actions 워크플로우를 처음 설정했습니다. 코드가 잘 돌아가는 것을 확인하고 뿌듯해하던 중, 보안팀에서 연락이 왔습니다.

"워크플로우 토큰 권한이 너무 넓게 설정되어 있어요. 이대로 두면 위험합니다."

GITHUB_TOKEN은 GitHub Actions가 저장소와 상호작용할 때 사용하는 자동 생성 토큰입니다. 마치 회사 출입증처럼, 이 토큰이 있어야 워크플로우가 저장소의 코드를 읽거나 이슈를 생성하는 등의 작업을 수행할 수 있습니다.

하지만 출입증에 모든 구역 접근 권한을 부여하면 위험하듯, 토큰 권한도 필요한 만큼만 부여해야 합니다.

다음 코드를 살펴봅시다.

# .github/workflows/ci.yml
name: CI Pipeline

# 저장소 전체에 적용될 기본 권한 설정
permissions:
  contents: read    # 코드 읽기만 허용
  pull-requests: write  # PR 코멘트 작성 허용

jobs:
  build:
    runs-on: ubuntu-latest
    # 특정 job에만 적용할 권한 (더 세밀한 제어)
    permissions:
      contents: read
      packages: write   # 패키지 배포 허용
    steps:
      - uses: actions/checkout@v4
      - name: Build and Test
        run: npm run build && npm test

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 팀에서 운영하는 오픈소스 프로젝트의 CI/CD 파이프라인을 담당하게 되었는데, 보안팀의 지적을 받고 당황했습니다.

"권한이 넓다니, 대체 무슨 말이지?" 선배 개발자 박시니어 씨가 다가와 화면을 살펴봅니다. "아, permissions 설정을 안 했구나.

이러면 기본값으로 꽤 많은 권한이 부여돼요." 그렇다면 GITHUB_TOKEN이란 정확히 무엇일까요? 쉽게 비유하자면, GITHUB_TOKEN은 마치 회사 출입 카드와 같습니다.

신입사원이 입사하면 출입 카드를 받지만, 모든 층과 모든 방에 들어갈 수 있는 건 아닙니다. 자기 팀 사무실, 회의실, 휴게실 정도만 출입이 가능하죠.

마찬가지로 GITHUB_TOKEN도 워크플로우가 필요한 작업만 수행할 수 있도록 권한을 제한해야 합니다. GitHub Actions가 처음 나왔을 때는 어땠을까요?

초기에는 GITHUB_TOKEN에 기본적으로 넓은 권한이 부여되었습니다. 저장소의 코드를 읽고 쓰고, 이슈를 만들고, 패키지를 배포하는 것이 모두 가능했습니다.

편리하긴 했지만, 보안 관점에서는 큰 문제였습니다. 만약 워크플로우에 취약점이 있거나 악의적인 코드가 실행된다면, 공격자가 이 넓은 권한을 악용할 수 있었습니다.

바로 이런 문제를 해결하기 위해 permissions 키워드가 도입되었습니다. permissions를 사용하면 워크플로우나 개별 job에 필요한 권한만 명시적으로 부여할 수 있습니다.

코드를 읽기만 하면 되는 빌드 작업에는 contents: read만, PR에 코멘트를 달아야 하면 pull-requests: write를 추가하는 식입니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 워크플로우 최상단의 permissions 블록을 보면, 이 워크플로우 전체에 적용될 기본 권한을 설정합니다. contents: read는 저장소 코드를 읽을 수 있다는 의미이고, pull-requests: write는 PR에 코멘트를 작성할 수 있다는 의미입니다.

그 아래 jobs.build.permissions를 보면, 특정 job에만 다른 권한을 부여할 수 있습니다. 이 빌드 job은 패키지를 배포해야 하므로 packages: write 권한이 추가되었습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 코드 품질 검사 워크플로우를 만든다고 가정해봅시다.

이 워크플로우는 코드를 체크아웃하고, 린트를 실행하고, 결과를 PR 코멘트로 남깁니다. 이 경우 contents: read와 pull-requests: write만 있으면 충분합니다.

패키지 배포 권한이나 이슈 생성 권한은 전혀 필요 없습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 권한이 부족해서 워크플로우가 실패하면 permissions를 아예 삭제해버리는 것입니다. 이러면 기본값으로 돌아가서 필요 이상의 권한이 부여됩니다.

에러가 나면 어떤 권한이 부족한지 로그를 확인하고, 딱 그 권한만 추가하는 것이 올바른 방법입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 바로 워크플로우 파일을 수정했습니다. "최소 권한 원칙이라...

보안의 기본이군요!" **최소 권한 원칙(Principle of Least Privilege)**은 보안의 기본 중의 기본입니다. GITHUB_TOKEN 권한 설정은 이 원칙을 실천하는 첫 번째 단계입니다.

여러분의 워크플로우도 지금 바로 점검해 보세요.

실전 팁

💡 - 새 워크플로우를 만들 때는 항상 permissions를 명시적으로 설정하는 습관을 들이세요

  • 저장소 설정에서 GITHUB_TOKEN의 기본 권한을 read-only로 변경할 수 있습니다

2. Dependabot 보안 업데이트

어느 월요일 아침, 김개발 씨의 메일함에 GitHub 알림이 가득 차 있었습니다. "Your repository has a known security vulnerability." 주말 사이에 사용 중인 라이브러리에서 심각한 보안 취약점이 발견된 것입니다.

수십 개의 프로젝트를 하나하나 업데이트해야 한다고 생각하니 막막해졌습니다.

Dependabot은 GitHub가 제공하는 자동 의존성 관리 도구입니다. 마치 집안의 화재 경보기가 연기를 감지하면 자동으로 알려주듯, Dependabot은 프로젝트에서 사용하는 라이브러리에 보안 취약점이 발견되면 자동으로 알려주고, 심지어 업데이트 PR까지 만들어줍니다.

다음 코드를 살펴봅시다.

# .github/dependabot.yml
version: 2
updates:
  # npm 패키지 의존성 관리
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"      # 매주 확인
      day: "monday"
      time: "09:00"
      timezone: "Asia/Seoul"
    open-pull-requests-limit: 10
    # 보안 업데이트는 별도로 자동 생성됨

  # GitHub Actions 버전 관리
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

김개발 씨는 회사에서 10개가 넘는 Node.js 프로젝트를 관리하고 있습니다. 각 프로젝트마다 수십 개의 npm 패키지를 사용하는데, 이것들을 일일이 최신 버전으로 관리하는 건 사실상 불가능에 가깝습니다.

박시니어 씨가 커피를 건네며 말합니다. "Dependabot 설정해뒀어?

그거 한 번 설정해두면 알아서 다 해줘." 그렇다면 Dependabot이란 정확히 무엇일까요? 쉽게 비유하자면, Dependabot은 마치 아파트 관리사무소의 시설 점검 서비스와 같습니다.

입주민이 일일이 보일러나 배관 상태를 확인하지 않아도, 관리사무소에서 정기적으로 점검하고 문제가 발견되면 알려주죠. Dependabot도 마찬가지로 프로젝트의 의존성을 정기적으로 점검하고, 보안 취약점이나 업데이트가 필요한 패키지를 발견하면 자동으로 PR을 생성합니다.

Dependabot이 없던 시절에는 어땠을까요? 개발자들은 npm audit이나 snyk 같은 도구를 직접 실행해서 취약점을 확인해야 했습니다.

문제는 이걸 정기적으로 하는 사람이 거의 없다는 것입니다. 바쁜 업무에 치이다 보면 의존성 관리는 뒷전으로 밀리기 일쑤였습니다.

그러다 어느 날 갑자기 보안 감사에서 "이 라이브러리에 치명적인 취약점이 있습니다"라는 지적을 받고 허둥지둥 대응하곤 했습니다. 바로 이런 문제를 해결하기 위해 Dependabot이 등장했습니다.

Dependabot을 활성화하면 두 가지 유형의 자동 업데이트를 받을 수 있습니다. 첫째는 보안 업데이트로, 사용 중인 패키지에서 알려진 취약점(CVE)이 발견되면 즉시 수정 버전으로 업데이트하는 PR을 생성합니다.

둘째는 버전 업데이트로, 새 버전이 나오면 정기적으로 업데이트 PR을 만들어줍니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 package-ecosystem: "npm"은 npm 패키지를 관리하겠다는 의미입니다. directory: "/"는 package.json이 있는 위치를 나타냅니다.

schedule 부분을 보면, 매주 월요일 오전 9시(한국 시간)에 업데이트를 확인합니다. 그 아래 package-ecosystem: "github-actions"도 눈여겨볼 필요가 있습니다.

GitHub Actions에서 사용하는 액션들도 하나의 의존성입니다. actions/checkout@v3에서 v4로 업데이트가 필요할 때 Dependabot이 알려줍니다.

실제 현업에서는 어떻게 활용할까요? 많은 팀에서 보안 업데이트와 일반 버전 업데이트를 다르게 처리합니다.

보안 업데이트 PR은 즉시 리뷰하고 머지하지만, 일반 버전 업데이트는 한 달에 한 번 정도 모아서 처리합니다. Dependabot PR에 자동으로 레이블을 붙이고, CI가 통과하면 자동 머지되도록 설정하는 팀도 많습니다.

하지만 주의할 점도 있습니다. Dependabot이 만든 PR이라고 무조건 머지해서는 안 됩니다.

메이저 버전 업데이트의 경우 breaking change가 있을 수 있어서 코드 수정이 필요할 수 있습니다. 또한 Dependabot PR을 너무 오래 방치하면 충돌이 쌓여서 나중에 해결하기 어려워집니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. dependabot.yml 파일을 추가한 후, 김개발 씨의 월요일 아침이 달라졌습니다.

이제 보안 취약점이 발견되면 Dependabot이 자동으로 PR을 만들어주고, 김개발 씨는 리뷰하고 머지만 하면 됩니다. 자동화는 개발자의 친구입니다. Dependabot을 활용하면 보안 업데이트라는 중요하지만 귀찮은 작업을 자동화할 수 있습니다.

아직 설정하지 않았다면, 지금 바로 dependabot.yml 파일을 추가해보세요.

실전 팁

💡 - 보안 업데이트 PR에는 security 레이블을 자동으로 붙이고, 우선순위를 높여서 처리하세요

  • Dependabot PR에 대해 CI가 통과하면 자동 머지되도록 GitHub Actions를 설정할 수 있습니다

3. CodeQL 코드 스캔

코드 리뷰 시간, 박시니어 씨가 김개발 씨의 PR을 보다가 심각한 표정을 지었습니다. "이 부분, SQL 인젝션 취약점이 있어요." 김개발 씨는 깜짝 놀랐습니다.

분명히 잘 작동하는 코드인데, 보안 취약점이 숨어있다니요.

CodeQL은 GitHub에서 제공하는 코드 분석 엔진입니다. 마치 공항 보안 검색대가 가방 속 위험물을 X-ray로 찾아내듯, CodeQL은 코드를 정적 분석하여 SQL 인젝션, XSS, 버퍼 오버플로우 같은 보안 취약점을 자동으로 찾아냅니다.

다음 코드를 살펴봅시다.

# .github/workflows/codeql.yml
name: "CodeQL Analysis"

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 9 * * 1'  # 매주 월요일 오전 9jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      security-events: write   # 보안 이벤트 기록 권한
      contents: read
    strategy:
      matrix:
        language: [ 'javascript', 'typescript' ]
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
      - uses: github/codeql-action/analyze@v3

김개발 씨는 사용자 입력을 받아 데이터베이스를 조회하는 기능을 구현했습니다. 테스트도 통과하고, 기능도 잘 작동합니다.

하지만 박시니어 씨의 눈에는 위험한 코드가 보였습니다. "여기 보세요, 사용자 입력을 그대로 쿼리에 넣고 있잖아요.

공격자가 악의적인 입력을 넣으면 데이터베이스가 통째로 날아갈 수도 있어요." 그렇다면 CodeQL이란 정확히 무엇일까요? 쉽게 비유하자면, CodeQL은 마치 건물의 소방 점검과 같습니다.

건물이 멀쩡해 보여도 소방관이 점검하면 "이 배선은 화재 위험이 있습니다", "이 비상구는 규정에 맞지 않습니다" 같은 문제를 찾아냅니다. CodeQL도 마찬가지로 겉보기에 잘 작동하는 코드에서 숨겨진 보안 취약점을 찾아냅니다.

코드 리뷰만으로는 왜 부족할까요? 사람이 코드 리뷰를 할 때는 주로 기능이 제대로 작동하는지, 코드 스타일이 일관적인지에 집중합니다.

보안 취약점은 기능적으로는 문제가 없어 보이기 때문에 놓치기 쉽습니다. 더구나 모든 개발자가 보안 전문가는 아닙니다.

SQL 인젝션, XSS, CSRF 같은 취약점을 정확히 파악하려면 전문 지식이 필요합니다. 바로 이런 문제를 해결하기 위해 CodeQL이 등장했습니다.

CodeQL은 코드를 데이터베이스처럼 취급하고, SQL과 비슷한 쿼리 언어로 취약점 패턴을 검색합니다. GitHub에서 이미 수천 개의 취약점 쿼리를 만들어두었기 때문에, 개발자는 설정만 하면 이 모든 점검을 자동으로 받을 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 on 트리거를 보면, push와 pull_request 이벤트에서 분석이 실행됩니다.

특히 pull_request에서 실행하면 취약한 코드가 main 브랜치에 머지되기 전에 발견할 수 있습니다. schedule을 추가한 이유는 새로운 취약점 패턴이 추가되었을 때 기존 코드도 검사하기 위해서입니다.

permissions 부분을 보면 security-events: write가 있습니다. 이 권한이 있어야 CodeQL이 발견한 취약점을 GitHub Security 탭에 기록할 수 있습니다.

strategy.matrix를 사용해서 여러 언어를 동시에 분석하고 있습니다. JavaScript와 TypeScript를 함께 사용하는 프로젝트라면 둘 다 검사해야 합니다.

실제 현업에서는 어떻게 활용할까요? 많은 기업에서 CodeQL 분석을 필수 체크로 설정합니다.

CodeQL이 취약점을 발견하면 PR을 머지할 수 없도록 브랜치 보호 규칙을 설정하는 것입니다. 이렇게 하면 "실수로" 취약한 코드가 프로덕션에 배포되는 것을 원천 차단할 수 있습니다.

하지만 주의할 점도 있습니다. CodeQL은 강력하지만 완벽하지는 않습니다.

**거짓 양성(False Positive)**이 발생할 수 있습니다. 실제로는 안전한 코드인데 취약점으로 잘못 분류하는 경우입니다.

이런 경우 코드에 주석으로 무시 표시를 하거나, CodeQL 설정에서 특정 규칙을 비활성화할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

CodeQL을 설정한 후, 김개발 씨의 다음 PR에서 SQL 인젝션 취약점이 자동으로 감지되었습니다. "와, 이거 박시니어 씨 없었으면 그냥 넘어갈 뻔했네요." 보안은 자동화가 핵심입니다. 사람은 실수할 수 있지만, 자동화된 도구는 매번 같은 기준으로 검사합니다.

CodeQL을 설정해두면 보안 전문가가 옆에 없어도 기본적인 취약점은 걸러낼 수 있습니다.

실전 팁

💡 - CodeQL 분석 결과를 PR의 필수 체크로 설정하여 취약한 코드가 머지되는 것을 방지하세요

  • 거짓 양성이 발생하면 코드에 // codeql-ignore 주석을 달아 무시할 수 있습니다

4. OpenSSF Scorecard

김개발 씨의 팀에서 새 오픈소스 라이브러리를 도입하려고 합니다. npm에서 검색해보니 비슷한 기능의 라이브러리가 여러 개 있습니다.

다운로드 수만 보고 고르자니 찜찜합니다. "이거 보안적으로 안전한 건지 어떻게 알 수 있을까요?"

OpenSSF Scorecard는 오픈소스 프로젝트의 보안 상태를 자동으로 평가하는 도구입니다. 마치 식당의 위생 등급처럼, 0점부터 10점까지 점수로 프로젝트의 보안 수준을 한눈에 파악할 수 있게 해줍니다.

자신의 프로젝트에도 적용하면 보안 모범 사례를 얼마나 잘 따르고 있는지 객관적으로 확인할 수 있습니다.

다음 코드를 살펴봅시다.

# .github/workflows/scorecard.yml
name: OpenSSF Scorecard

on:
  branch_protection_rule:
  schedule:
    - cron: '0 9 * * 1'  # 매주 월요일 분석
  push:
    branches: [ main ]

permissions: read-all

jobs:
  analysis:
    runs-on: ubuntu-latest
    permissions:
      security-events: write  # 결과를 Security 탭에 기록
      id-token: write         # Scorecard API 인증용
    steps:
      - uses: actions/checkout@v4
      - uses: ossf/scorecard-action@v2
        with:
          results_file: results.sarif
          results_format: sarif
          publish_results: true  # 결과 공개
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

김개발 씨는 팀 회의에서 새 라이브러리 도입 건을 논의하고 있습니다. 후보군은 세 개인데, 어떤 기준으로 골라야 할지 막막합니다.

스타 수? 다운로드 수?

마지막 커밋 날짜? 박시니어 씨가 말합니다.

"OpenSSF Scorecard 점수 확인해봤어? 거기서 보안 점수 낮은 건 거르는 게 좋아." 그렇다면 OpenSSF Scorecard란 정확히 무엇일까요?

쉽게 비유하자면, OpenSSF Scorecard는 마치 신용 평가 점수와 같습니다. 은행에서 대출을 해줄 때 개인의 신용 점수를 확인하듯, 개발자도 라이브러리를 도입하기 전에 그 프로젝트의 "보안 신용 점수"를 확인할 수 있습니다.

점수가 높을수록 보안 모범 사례를 잘 따르고 있다는 의미입니다. 왜 스타 수나 다운로드 수만으로는 부족할까요?

인기 있는 라이브러리가 반드시 안전한 것은 아닙니다. 과거에 악의적인 메인테이너가 인기 있는 npm 패키지에 악성 코드를 삽입한 사건이 여러 번 있었습니다.

또한 관리가 제대로 안 되는 프로젝트는 보안 취약점이 발견되어도 패치가 늦거나 아예 안 되는 경우도 있습니다. 바로 이런 문제를 해결하기 위해 OpenSSF(Open Source Security Foundation)에서 Scorecard를 만들었습니다.

Scorecard는 여러 가지 기준으로 프로젝트를 평가합니다. 브랜치 보호가 설정되어 있는지, 코드 리뷰가 이루어지고 있는지, 의존성이 고정되어 있는지, 보안 정책 파일이 있는지 등을 종합적으로 점검합니다.

각 항목별로 0~10점을 부여하고, 전체 점수도 보여줍니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 on 트리거를 보면 branch_protection_rule이 있습니다. 브랜치 보호 규칙이 변경될 때 자동으로 재분석하라는 의미입니다.

schedule로 정기 분석도 설정했습니다. permissions: read-all은 최상단에 선언되어 있는데, Scorecard가 저장소의 다양한 설정을 읽어야 하기 때문입니다.

하지만 job 레벨에서 security-events: write와 id-token: write만 추가로 부여하여 최소 권한 원칙을 지키고 있습니다. publish_results: true를 설정하면 분석 결과가 공개 API를 통해 조회 가능해집니다.

다른 개발자들이 여러분의 프로젝트 점수를 확인할 수 있게 됩니다. 실제 현업에서는 어떻게 활용할까요?

기업에서 새 의존성을 추가할 때 Scorecard 점수를 기준으로 삼는 경우가 많습니다. "Scorecard 7점 이상인 라이브러리만 사용한다" 같은 내부 정책을 세우는 것입니다.

또한 자사 오픈소스 프로젝트의 점수를 높이기 위해 노력하기도 합니다. 높은 점수는 프로젝트의 신뢰성을 보여주는 지표가 됩니다.

하지만 주의할 점도 있습니다. Scorecard 점수가 낮다고 해서 반드시 위험한 프로젝트는 아닙니다.

개인 프로젝트나 소규모 프로젝트는 브랜치 보호 같은 설정이 과하게 느껴질 수 있습니다. 점수는 참고 자료일 뿐, 최종 판단은 프로젝트의 실제 코드와 활동 내역을 보고 해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 세 개의 후보 라이브러리 중 하나는 Scorecard 점수가 3점이었고, 나머지 둘은 7점과 8점이었습니다.

"3점짜리는 일단 제외하고, 7점이랑 8점 중에서 기능 비교해서 고르면 되겠네요." 보안은 선택이 아니라 필수입니다. OpenSSF Scorecard를 활용하면 보안 관점에서 더 나은 의사결정을 할 수 있습니다. 여러분의 프로젝트에도 Scorecard를 적용해서 보안 점수를 확인해보세요.

실전 팁

💡 - 새 의존성을 추가할 때 scorecard.dev 사이트에서 해당 프로젝트의 점수를 미리 확인하세요

  • 자신의 프로젝트 점수를 높이려면 브랜치 보호, 코드 리뷰 필수화, SECURITY.md 파일 추가 등을 설정하세요

5. 워크플로우 감사 로그

어느 날 프로덕션 서버에 이상한 코드가 배포되었습니다. 담당자를 찾아보니 아무도 모르는 일이었습니다.

"누가, 언제, 어떻게 이 배포를 승인한 거죠?" 팀 전체가 당혹스러워하는 상황. 이런 일이 발생하지 않으려면 어떻게 해야 할까요?

워크플로우 감사 로그는 GitHub Actions에서 발생하는 모든 활동의 기록입니다. 마치 건물의 CCTV가 모든 출입을 기록하듯, 감사 로그는 누가 워크플로우를 실행했는지, 어떤 시크릿에 접근했는지, 언제 배포가 이루어졌는지를 모두 기록합니다.

문제가 발생했을 때 원인을 추적하는 데 필수적입니다.

다음 코드를 살펴봅시다.

# .github/workflows/deploy.yml
name: Production Deploy

on:
  workflow_dispatch:  # 수동 실행만 허용
    inputs:
      environment:
        description: '배포 환경'
        required: true
        type: choice
        options:
          - staging
          - production

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}  # 환경별 승인 필요
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: |
          echo "Deploying to ${{ inputs.environment }}"
          echo "Triggered by: ${{ github.actor }}"
          echo "Run ID: ${{ github.run_id }}"
          # 실제 배포 스크립트

김개발 씨의 팀에 큰 사고가 발생했습니다. 금요일 저녁에 누군가 프로덕션에 검증되지 않은 코드를 배포했고, 주말 내내 서비스 장애가 발생했습니다.

월요일 아침 회의에서 모든 사람이 "저 아닌데요"라고 말합니다. 박시니어 씨가 한숨을 쉬며 말합니다.

"감사 로그 확인해봤어? 거기 다 남아있을 거야." 그렇다면 워크플로우 감사 로그란 정확히 무엇일까요?

쉽게 비유하자면, 감사 로그는 마치 비행기의 블랙박스와 같습니다. 비행기에 문제가 생기면 블랙박스를 분석해서 무슨 일이 있었는지 정확히 파악할 수 있습니다.

마찬가지로 감사 로그를 보면 누가 워크플로우를 트리거했는지, 어떤 브랜치에서 실행되었는지, 어떤 환경 변수가 사용되었는지 모두 알 수 있습니다. 로그만 남기면 충분한 걸까요?

그렇지 않습니다. 로그는 사후 추적에 필요하지만, 사전 예방도 중요합니다.

그래서 environmentworkflow_dispatch를 함께 활용합니다. environment를 설정하면 특정 환경에 배포하기 전에 지정된 리뷰어의 승인을 받아야 합니다.

workflow_dispatch는 자동 실행을 막고 수동으로만 워크플로우를 실행할 수 있게 합니다. 바로 이런 조합으로 "누가 승인했는지"까지 기록에 남기는 것입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 on: workflow_dispatch를 보면, 이 워크플로우는 push나 PR로는 트리거되지 않습니다.

누군가가 GitHub UI나 API를 통해 직접 실행해야 합니다. inputs로 배포 환경을 선택하게 했는데, staging과 production 중 하나를 골라야 합니다.

environment: ${{ inputs.environment }} 부분이 핵심입니다. GitHub 저장소 설정에서 production 환경에 대해 "배포 전 리뷰어 승인 필요"를 설정해두면, 아무리 워크플로우를 실행해도 지정된 리뷰어가 승인하기 전에는 deploy job이 실행되지 않습니다.

github.actor와 github.run_id를 로그에 남기고 있습니다. 이렇게 하면 배포 로그만 봐도 누가 언제 이 배포를 트리거했는지 알 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 대부분의 기업에서는 프로덕션 배포에 최소 두 가지 제약을 겁니다.

첫째, 특정 브랜치(보통 main이나 release)에서만 배포 가능. 둘째, 지정된 승인자의 승인 필요.

이 두 가지만 해도 "몰래 배포"는 거의 불가능해집니다. GitHub Enterprise를 사용하는 기업이라면 더 상세한 감사 로그를 조직 레벨에서 확인할 수 있습니다.

어떤 멤버가 어떤 저장소의 어떤 워크플로우를 실행했는지, 어떤 시크릿에 접근했는지까지 모두 기록됩니다. 하지만 주의할 점도 있습니다.

감사 로그는 일정 기간이 지나면 삭제됩니다. GitHub Free에서는 90일, Enterprise에서는 더 오래 보관됩니다.

중요한 로그는 별도의 저장소에 백업해두는 것이 좋습니다. 또한 로그가 있다고 해서 보안이 완벽한 것은 아닙니다.

로그는 사후 추적용이지, 사전 예방을 대체하지는 않습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

감사 로그를 확인한 결과, 인턴 개발자가 실수로 잘못된 브랜치를 배포한 것으로 밝혀졌습니다. 이 사건 이후 팀에서는 프로덕션 environment에 시니어 개발자 승인을 필수로 설정했습니다.

기록이 없으면 증거도 없습니다. 워크플로우 감사 로그는 문제가 발생했을 때 원인을 빠르게 파악하고, 재발 방지 대책을 세우는 데 핵심적인 역할을 합니다. 특히 프로덕션 배포 워크플로우에는 반드시 environment와 승인 절차를 설정해두세요.

실전 팁

💡 - 프로덕션 environment에는 반드시 리뷰어 승인을 설정하고, 배포 기록을 추적하세요

  • workflow_dispatch를 사용하면 누가 워크플로우를 실행했는지 명확하게 기록됩니다

6. 포크 PR 보안 고려

김개발 씨가 관리하는 오픈소스 프로젝트에 외부 기여자의 PR이 올라왔습니다. 코드를 살펴보니 기능적으로는 문제가 없어 보입니다.

하지만 박시니어 씨가 경고합니다. "잠깐, 이거 포크에서 온 PR이야.

워크플로우가 자동으로 실행되면 안 돼."

포크 PR은 외부 기여자가 저장소를 복제(fork)하여 수정한 후 보내는 Pull Request입니다. 마치 낯선 사람이 보낸 택배와 같아서, 내용물을 확인하기 전에 무작정 열어보면 위험할 수 있습니다.

포크 PR이 트리거하는 워크플로우에서는 시크릿 접근을 제한하고, 신뢰할 수 없는 코드가 실행되지 않도록 주의해야 합니다.

다음 코드를 살펴봅시다.

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  # 안전한 작업: 포크 PR에서도 실행 가능
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
      - run: npm test

  # 위험한 작업: 메인테이너 승인 후에만 실행
  integration-test:
    runs-on: ubuntu-latest
    # 포크 PR이 아닐 때만 자동 실행
    if: github.event.pull_request.head.repo.full_name == github.repository
    environment: integration  # 승인 필요
    steps:
      - uses: actions/checkout@v4
      - name: Run with secrets
        env:
          API_KEY: ${{ secrets.INTEGRATION_API_KEY }}
        run: npm run test:integration

김개발 씨의 오픈소스 프로젝트는 꽤 인기가 있어서 외부 기여가 활발합니다. 어느 날 새로운 기여자가 PR을 올렸는데, 워크플로우 파일을 수정하는 내용이 포함되어 있었습니다.

얼핏 보면 CI 속도를 개선하는 것 같았지만, 자세히 보니 시크릿을 외부 서버로 전송하는 코드가 숨어 있었습니다. 박시니어 씨가 설명합니다.

"이게 바로 Pwn Request 공격이야. 포크 PR을 통해 악의적인 코드를 실행하려는 거지." 그렇다면 포크 PR은 왜 위험할까요?

일반적인 PR은 같은 저장소의 다른 브랜치에서 옵니다. 이 경우 PR 작성자는 이미 저장소에 push 권한이 있는 신뢰할 수 있는 사람입니다.

하지만 포크 PR은 다릅니다. 누구나 저장소를 포크하고 PR을 보낼 수 있습니다.

전혀 모르는 사람이 보낸 코드가 여러분의 CI 환경에서 실행되는 것입니다. 문제는 GitHub Actions가 PR에 반응하여 자동으로 워크플로우를 실행한다는 점입니다.

만약 이 워크플로우가 시크릿에 접근할 수 있다면, 악의적인 PR 작성자가 그 시크릿을 탈취할 수 있습니다. 바로 이런 문제를 방지하기 위해 GitHub는 포크 PR에 대한 보안 정책을 가지고 있습니다.

기본적으로 포크 PR이 트리거하는 워크플로우에서는 시크릿에 접근할 수 없습니다. GITHUB_TOKEN의 권한도 read-only로 제한됩니다.

하지만 pull_request_target 이벤트를 사용하면 이 제한을 우회할 수 있어서, 설정을 잘못하면 위험해질 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

lint-and-test job은 시크릿을 사용하지 않는 안전한 작업입니다. 코드를 체크아웃하고, 린트를 실행하고, 테스트를 돌립니다.

이런 작업은 포크 PR에서도 자동으로 실행해도 됩니다. integration-test job은 다릅니다.

if 조건을 보면, PR의 헤드 저장소가 현재 저장소와 같을 때만 실행됩니다. 즉, 포크 PR이 아닌 내부 PR에서만 자동 실행됩니다.

추가로 environment: integration을 설정해서 승인 절차를 거치도록 했습니다. secrets.INTEGRATION_API_KEY처럼 시크릿을 사용하는 작업은 반드시 이런 보호 장치가 필요합니다.

실제 현업에서는 어떻게 활용할까요? 많은 오픈소스 프로젝트에서는 포크 PR에 대해 "첫 기여자 승인" 정책을 사용합니다.

저장소 설정에서 "Require approval for first-time contributors"를 활성화하면, 처음 기여하는 사람의 PR은 메인테이너가 승인해야 워크플로우가 실행됩니다. 또한 pull_request_target 이벤트는 매우 조심해서 사용해야 합니다.

이 이벤트는 포크 PR에서도 시크릿에 접근할 수 있게 해주는데, 잘못 사용하면 보안 구멍이 됩니다. 가능하면 사용을 피하고, 꼭 필요하다면 PR의 코드를 체크아웃하지 않고 안전한 작업만 수행해야 합니다.

하지만 주의할 점도 있습니다. 보안을 너무 강화하면 외부 기여자의 경험이 나빠질 수 있습니다.

PR을 올렸는데 CI가 전혀 돌지 않으면 기여자는 자신의 코드가 제대로 작동하는지 알 수 없습니다. 따라서 시크릿이 필요 없는 기본적인 테스트는 자동으로 실행하고, 시크릿이 필요한 통합 테스트만 승인 후 실행하는 균형이 필요합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 악의적인 PR을 발견한 후, 김개발 씨는 워크플로우를 재점검했습니다.

시크릿을 사용하는 모든 job에 if 조건을 추가하고, 첫 기여자 승인 정책을 활성화했습니다. "오픈소스는 열려 있지만, 보안까지 열어두면 안 되는구나." 열린 기여와 보안은 양립할 수 있습니다. 포크 PR의 위험성을 이해하고 적절한 보호 장치를 설정하면, 외부 기여를 받으면서도 안전하게 프로젝트를 운영할 수 있습니다.

여러분의 오픈소스 프로젝트도 지금 바로 점검해보세요.

실전 팁

💡 - 포크 PR에서는 시크릿이 필요한 워크플로우가 자동 실행되지 않도록 if 조건을 추가하세요

  • pull_request_target 이벤트는 가능하면 피하고, 사용해야 한다면 PR 코드를 체크아웃하지 마세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#GitHub Actions#CI/CD#Security#DevOps#Workflow#GitHub Actions,CI/CD,DevOps

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.