본문 바로가기
IT/Cloud

[Cloud] GitHub Actions 자체 호스팅 러너 보안 강화: 백도어 방지 및 2026년 최신 가이드

by 수누다 2026. 4. 16.

CI/CD 파이프라인의 약한 고리, 자체 호스팅 러너

몇 달 전에 팀 내에서 꽤 아찔한 일이 있었습니다. 동료 엔지니어가 운영하던 GitHub Actions 자체 호스팅 러너(Self-hosted Runner)가 공급망 공격(Supply Chain Attack)의 타깃이 될 뻔했거든요. 다행히 조기에 발견했지만, 그때 이후로 저도 홈랩이랑 실무 환경 전체를 다시 들여다보게 됐습니다.

솔직히 말씀드리면, 저도 처음에는 "GitHub에서 제공하는 거니까 그냥 쓰면 되겠지"라고 생각했었어요. 근데 자체 호스팅 러너는 얘기가 완전히 다릅니다. 여러분 서버 위에 올라가는 순간, 그 보안 책임은 100% 여러분 몫이거든요.

이번 글에서는 GitHub Actions 자체 호스팅 러너 보안을 실질적으로 강화하는 방법을 단계별로 정리해 드릴게요. 2025~2026년 기준으로 GitHub에서 권고하는 최신 가이드라인과 제가 직접 적용해본 경험을 섞어서 써봤습니다.

GitHub Actions 자체 호스팅 러너 보안 아키텍처 전체 개요 다이어그램

▲ GitHub Actions 자체 호스팅 러너의 전체 보안 아키텍처 개요 — 러너, 리포지토리, 네트워크, 시크릿 관리 레이어별 보안 포인트를 한눈에 보여줍니다.


자체 호스팅 러너(Self-hosted Runner)란 무엇인가요?

GitHub Actions에는 두 가지 러너 타입이 있습니다.

  • GitHub 호스팅 러너(GitHub-hosted Runner): GitHub이 관리하는 클라우드 VM. 워크플로우 실행 후 바로 폐기됩니다.
  • 자체 호스팅 러너(Self-hosted Runner): 여러분이 직접 서버를 준비하고, 그 위에 러너 에이전트를 설치해서 운영하는 방식입니다.

쉽게 말해, 자체 호스팅 러너는 "내 서버를 GitHub CI/CD 파이프라인에 연결하는 것"입니다. 비용 절감, 내부망 접근, 특수 하드웨어 활용 등 장점이 많죠. 근데 바로 그 유연성 때문에 보안 리스크도 같이 따라옵니다.

왜 위험한가요?

핵심 문제는 워크플로우 파일(.yml)이 코드 저장소에 존재한다는 것입니다. 누군가 PR(Pull Request)을 통해 악성 워크플로우를 심으면, 그게 여러분 서버 위에서 실행될 수 있어요. GitHub 호스팅 러너라면 그 VM은 바로 폐기되지만, 자체 호스팅 러너는 여러분 서버가 그대로 남아있고, 내부 네트워크에도 접근 가능하죠.

구분 GitHub 호스팅 러너 자체 호스팅 러너
인프라 관리 GitHub 책임 사용자 책임
보안 패치 자동 직접 관리
실행 후 환경 VM 폐기 (격리) 서버 유지 (잔류 위험)
내부망 접근 불가 가능 (위험 요소)
비용 분당 과금 자체 서버 비용
공급망 공격 위험 낮음 높음

GitHub Actions 자체 호스팅 러너 보안 강화 — 단계별 실전 가이드

1단계: 러너 접근 범위를 최소화하세요

제가 처음 자체 호스팅 러너를 설정할 때 Organization 레벨로 등록했었어요. 편하긴 한데, 나중에 생각해보니 이게 꽤 위험한 설정이더라고요. 모든 리포지토리의 워크플로우가 그 러너에 접근할 수 있으니까요.

권장 설정: 러너를 특정 리포지토리 레벨에만 등록하거나, Runner Group으로 접근을 제한하세요.

GitHub Enterprise나 Organization을 쓰신다면 Runner Group(러너 그룹) 기능을 꼭 활용하세요.

  1. GitHub Organization → Settings → Actions → Runner groups
  2. 새 그룹 생성 후, 접근 허용할 리포지토리만 선택
  3. "Allow public repositories" 옵션은 반드시 비활성화
# 러너 등록 시 특정 리포지토리 레벨로 등록하는 예시
# Organization 레벨 대신 리포지토리 레벨 토큰 사용
./config.sh \
  --url https://github.com/your-org/specific-repo \
  --token YOUR_REPO_LEVEL_TOKEN \
  --name "secure-runner-01" \
  --labels "self-hosted,linux,secure"

2단계: 에페머럴 러너(Ephemeral Runner) 도입 — 이게 진짜 핵심입니다

이거 처음 알았을 때 "왜 진작 이걸 안 썼지?" 싶었어요. 에페머럴(Ephemeral, 일회성)이라는 단어처럼, 하나의 잡(Job)을 처리하고 나면 러너가 자동으로 폐기되는 방식입니다. GitHub 호스팅 러너처럼요.

이렇게 하면 이전 잡에서 심어진 악성 코드나 환경 오염이 다음 잡으로 이어지지 않습니다. 공급망 공격 방어에 가장 효과적인 방법 중 하나거든요.

# 에페머럴 모드로 러너 실행
./config.sh \
  --url https://github.com/your-org/your-repo \
  --token YOUR_TOKEN \
  --ephemeral  # 이 플래그 하나로 일회성 러너가 됩니다

./run.sh

컨테이너 환경이라면 Actions Runner Controller(ARC)를 활용하면 Kubernetes 위에서 에페머럴 러너를 자동으로 스케일링할 수 있어요. 저도 홈랩 k3s 클러스터에 ARC 올려서 쓰고 있는데, 진짜 편하더라고요.

# Helm으로 Actions Runner Controller 설치
helm install arc \
  --namespace "arc-systems" \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

# 에페머럴 러너 스케일셋 설정 (values.yaml)
githubConfigUrl: "https://github.com/your-org/your-repo"
githubConfigSecret: "arc-github-secret"

containerMode:
  type: "dind"  # Docker-in-Docker로 격리 강화

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        resources:
          limits:
            cpu: "2"
            memory: "4Gi"
GitHub Actions 에페머럴 러너 라이프사이클 다이어그램 - 잡 실행 후 자동 폐기 과정

▲ 에페머럴 러너의 라이프사이클 — 잡 수신 → 실행 → 자동 폐기 과정을 통해 환경 오염을 원천 차단합니다.

3단계: 워크플로우 권한(Permissions)을 최소화하세요

이게 은근히 놓치기 쉬운 부분이에요. GitHub Actions 워크플로우는 기본적으로 꽤 넓은 권한을 가질 수 있거든요. 최소 권한 원칙(Principle of Least Privilege)을 워크플로우에도 적용해야 합니다.

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

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# 워크플로우 전체 기본 권한을 최소화
permissions:
  contents: read  # 코드 읽기만 허용

jobs:
  build:
    runs-on: [self-hosted, linux, secure]
    
    # 잡별로 필요한 권한만 추가
    permissions:
      contents: read
      packages: write  # 패키지 빌드가 필요한 경우에만
    
    steps:
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
        with:
          persist-credentials: false  # 크리덴셜 잔류 방지!
      
      - name: Build
        run: make build

특히 persist-credentials: false 옵션, 저도 처음엔 그냥 넘겼다가 나중에 알고 깜짝 놀랐어요. 이게 없으면 체크아웃 후 GitHub 토큰이 git 설정에 남아있을 수 있거든요.

4단계: 외부 액션(Third-party Actions) SHA 핀닝(Pinning)

공급망 공격(Supply Chain Attack)에서 가장 많이 노출되는 지점이 바로 여기입니다. uses: some-action/checkout@main처럼 브랜치 태그를 쓰면, 그 액션 저장소가 해킹당했을 때 여러분 파이프라인도 같이 당하게 돼요.

해결책: 액션을 커밋 SHA 해시로 고정(Pin)하세요.

# ❌ 이렇게 쓰면 위험합니다
- uses: actions/checkout@v4
- uses: actions/setup-node@main

# ✅ 커밋 SHA로 고정하는 것이 안전합니다
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0

SHA 해시를 일일이 관리하기 귀찮으시다면 Dependabot을 활용하세요. .github/dependabot.yml에 설정하면 액션 버전 업데이트를 자동으로 PR로 올려줍니다.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    # SHA 핀닝된 액션도 자동으로 업데이트 PR 생성
    groups:
      actions:
        patterns:
          - "*"

5단계: 시크릿(Secrets) 관리와 환경 보호 규칙

시크릿 관리도 허술하면 다 무너져요. 몇 가지 중요한 포인트를 짚어드릴게요.

  • 환경(Environment) 보호 규칙 설정: Production 환경에는 반드시 Reviewer 승인 단계를 추가하세요.
  • 시크릿 범위 최소화: Organization 시크릿보다는 리포지토리 시크릿, 그것보다는 환경 시크릿이 더 안전합니다.
  • OIDC(OpenID Connect) 활용: 장기 자격증명(Long-lived Credentials) 대신 단기 토큰을 사용하세요.
# OIDC로 AWS 인증하는 예시 — 장기 Access Key 불필요!
jobs:
  deploy:
    runs-on: [self-hosted, linux]
    environment: production  # 환경 보호 규칙 적용
    
    permissions:
      id-token: write  # OIDC 토큰 발급에 필요
      contents: read
    
    steps:
      - name: Configure AWS Credentials via OIDC
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502  # v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
          aws-region: ap-northeast-2
          # Access Key/Secret Key가 전혀 필요 없습니다!

6단계: 러너 서버 자체 하드닝(Hardening)

러너가 올라가는 서버 자체도 단단하게 만들어야 하거든요. 이건 일반적인 리눅스 서버 보안과 겹치는 부분도 있는데, 자체 호스팅 러너 특화 포인트만 짚어드릴게요.

# 러너를 전용 비권한 사용자로 실행
sudo useradd -m -s /bin/bash github-runner
sudo usermod -aG docker github-runner  # Docker 사용이 필요한 경우만

# 러너 디렉토리 권한 설정
sudo chown -R github-runner:github-runner /opt/actions-runner

# systemd 서비스로 등록 (루트 실행 방지)
cd /opt/actions-runner
sudo ./svc.sh install github-runner  # 전용 유저로 서비스 설치
sudo ./svc.sh start
# /etc/systemd/system/actions.runner.*.service 에서 확인
# User=github-runner 로 설정되어 있어야 합니다

# Docker 소켓 접근 제한 (필요한 경우 rootless Docker 고려)
# /etc/docker/daemon.json
{
  "userns-remap": "github-runner",  # 사용자 네임스페이스 리매핑
  "no-new-privileges": true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

그리고 네트워크 측면에서는, 러너 서버가 외부 인터넷에 직접 노출되지 않도록 해야 해요. GitHub Actions 자체 호스팅 러너는 아웃바운드 연결만 사용합니다. 인바운드 포트를 열 필요가 없거든요. 방화벽 규칙을 아웃바운드 443(HTTPS)만 허용하는 식으로 좁힐 수 있습니다.

GitHub Actions 자체 호스팅 러너 보안 강화 설정 체크리스트 대시보드

▲ 자체 호스팅 러너 보안 강화 체크리스트 — 네트워크, OS, 워크플로우, 시크릿 관리 등 레이어별 적용 현황을 확인하세요.


⚠️ 실제로 삽질했던 문제들과 해결법

문제 1: pull_request 이벤트와 포크 리포지토리

이거 진짜 주의하셔야 해요. 퍼블릭 리포지토리에서 자체 호스팅 러너를 쓰면 포크(Fork)된 리포지토리의 PR도 트리거될 수 있거든요. 외부 기여자가 악성 워크플로우를 PR에 담아서 보내면... 생각만 해도 아찔하죠.

해결책: 퍼블릭 리포지토리에는 자체 호스팅 러너를 절대 사용하지 마세요. GitHub도 공식적으로 이를 권고하지 않아요. 꼭 써야 한다면 pull_request_target 이벤트 대신 pull_request를 쓰고, 환경 보호 규칙으로 외부 기여자 PR은 수동 승인 후 실행되도록 설정하세요.

# 외부 기여자 PR 실행 제한 예시
on:
  pull_request:
    branches: [main]

jobs:
  build:
    # 포크 PR의 경우 시크릿 접근 불가 (GitHub 기본 동작)
    # 자체 호스팅 러너는 내부 PR만 처리하도록 조건 추가
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: [self-hosted, linux]

문제 2: 러너 업데이트 누락

홈랩 운영하다 보면 러너 에이전트 업데이트를 깜빡하기 쉬워요. 저도 몇 달 방치했다가 나중에 보니 꽤 여러 버전이 밀려있더라고요. 자동 업데이트 설정을 켜두는 게 좋습니다.

# 러너 자동 업데이트 설정 확인
# .runner 파일에서 disableUpdate 옵션 확인
cat /opt/actions-runner/.runner

# 자동 업데이트가 비활성화되어 있다면
# 주기적으로 업데이트 스크립트를 cron으로 실행
# 에페머럴 러너라면 이미지 자체를 최신으로 유지하는 것이 더 좋습니다

문제 3: 워크플로우 로그에 시크릿 노출

워크플로우 실행 로그가 공개되는 경우, 시크릿이 실수로 echo 되거나 에러 메시지에 포함되는 경우가 있어요. GitHub은 등록된 시크릿 값을 자동으로 마스킹해주지만, 파생된 값(예: base64 인코딩된 시크릿)은 마스킹이 안 될 수 있거든요.

# 파생 값도 마스킹하려면 add-mask 명령을 사용하세요
- name: Mask derived secret
  run: |
    DERIVED_SECRET=$(echo "${{ secrets.MY_SECRET }}" | base64)
    echo "::add-mask::$DERIVED_SECRET"  # 이 값도 로그에서 마스킹됨
    echo "DERIVED_SECRET=$DERIVED_SECRET" >> $GITHUB_ENV

보안 검증 — 설정이 제대로 됐는지 확인하기

설정을 다 했다면 실제로 제대로 동작하는지 확인해봐야 하거든요. 제가 주기적으로 체크하는 항목들입니다.

GitHub의 보안 권고 확인

리포지토리 → Security → Code scanning alerts에서 워크플로우 파일의 보안 문제를 스캔할 수 있어요. CodeQL이나 actionlint를 CI에 넣어두면 자동으로 체크됩니다.

# actionlint로 워크플로우 파일 정적 분석
# 로컬에서 실행
docker run --rm -v $(pwd):/repo rhysd/actionlint:latest -color /repo/.github/workflows/

# CI에 통합
- name: Lint GitHub Actions workflows
  uses: raven-actions/actionlint@v2
  with:
    files: .github/workflows/*.yml

보안 강화 체크리스트 최종 점검

  • ✅ 러너가 특정 리포지토리/그룹에만 등록되어 있는가?
  • ✅ 에페머럴 모드로 실행되는가?
  • ✅ 워크플로우 기본 권한이 read-all 또는 그 이하인가?
  • ✅ 외부 액션이 SHA 해시로 핀닝되어 있는가?
  • ✅ Dependabot으로 액션 업데이트를 추적하는가?
  • ✅ 프로덕션 환경에 보호 규칙이 설정되어 있는가?
  • ✅ 장기 자격증명 대신 OIDC를 사용하는가?
  • ✅ 러너가 비권한 사용자로 실행되는가?
  • ✅ 러너 서버의 인바운드 포트가 닫혀 있는가?
  • ✅ 퍼블릭 리포지토리에 자체 호스팅 러너를 사용하지 않는가?
GitHub Actions 자체 호스팅 러너 보안 강화 10가지 핵심 체크리스트 인포그래픽

▲ GitHub Actions 자체 호스팅 러너 보안 강화 요약 인포그래픽 — 10가지 핵심 체크리스트를 한눈에 확인하세요.


마무리 — CI/CD 보안은 한 번이 아닙니다

긴 글 읽어주셔서 감사합니다. 정리하자면, GitHub Actions 자체 호스팅 러너 보안의 핵심은 이렇습니다.

  1. 접근 최소화: 러너 범위를 필요한 리포지토리로만 제한
  2. 에페머럴 러너: 잡 실행 후 환경을 폐기해서 오염 방지
  3. 최소 권한: 워크플로우 권한을 필요한 것만 허용
  4. 공급망 공격 방어: 외부 액션을 SHA로 핀닝하고 Dependabot으로 관리
  5. 시크릿 보호: OIDC 활용, 환경 보호 규칙 설정
  6. 서버 하드닝: 비권한 사용자 실행, 네트워크 제한

CI/CD 보안은 "한 번 설정하면 끝"이 아니거든요. 공급망 공격 기법은 계속 진화하고 있고, GitHub도 꾸준히 새로운 보안 기능을 추가하고 있어요. 저도 이 글 쓰면서 다시 한 번 제 홈랩 설정을 점검했는데, 고쳐야 할 부분이 몇 개 보이더라고요.

💡 다음 글에서는 Kubernetes 기반의 Actions Runner Controller(ARC)를 활용한 에페머럴 러너 자동 스케일링 설정을 더 자세히 다룰 예정이에요. 홈랩에 k3s 올려서 직접 구성해본 경험을 공유해드릴 거니까 기대해주세요.

궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있으면 댓글로 남겨주세요. 제 경험이 여러분의 파이프라인을 조금 더 안전하게 만드는 데 도움이 됐으면 좋겠습니다. 🎉