GitHub Actions 작업은 시크릿을 훔치기 좋은 곳입니다. 배포 키, 레지스트리 토큰, 클라우드 자격 증명을 보유하고 있으며, 서드파티 액션, 패키지와 그 설치 스크립트처럼 1분 전에 다른 누군가가 작성한 코드를 실행합니다. GitHub의 시크릿 처리는 실수로부터 보호합니다. 이미 작업 안에서 실행 중인 코드를 막도록 설계된 것은 아닙니다.
이 글에서는 GitHub Actions에서 시크릿이 작동하는 방식, 실제로 시크릿이 유출되는 다섯 가지 경로, 그리고 각 경로를 막는 통제 방법을 다룹니다.
GitHub Actions 시크릿의 작동 방식
시크릿은 조직, 리포지토리, 환경의 세 수준에 존재합니다. 같은 이름이 여러 수준에 있으면 가장 구체적인 것이 우선합니다. 환경이 리포지토리보다, 리포지토리가 조직보다 우선합니다.
알아 두면 좋은 몇 가지 제한과 규칙은 다음과 같습니다.
- 크기와 개수. 조직 시크릿은 최대 1,000개, 리포지토리 시크릿 100개, 환경 시크릿 100개까지이며, 각각 최대 48 KB입니다. 워크플로는 모든 리포지토리 및 환경 시크릿을 볼 수 있지만, 조직 시크릿은 알파벳순으로 처음 100개만 볼 수 있습니다.
- 포크는 아무것도 받지 못합니다.
GITHUB_TOKEN을 제외하면, 포크에서 온 풀 리퀘스트로 트리거된 워크플로에는 시크릿이 전달되지 않습니다. - Dependabot은 포크처럼 취급됩니다. Dependabot이 트리거한 워크플로는 읽기 전용 토큰과 Dependabot 시크릿만 받으며, Actions 시크릿은 받지 못합니다.
- 재사용 가능한 워크플로에는 시크릿을 전달해야 합니다.
secrets: inherit는 같은 조직 또는 엔터프라이즈 안에서 모든 시크릿을 암묵적으로 전달합니다. 환경 시크릿은workflow_call을 통해 전달할 수 없습니다. - 환경이 접근을 통제합니다. 필수 검토자가 지정되어 있으면, 누군가 승인할 때까지 작업은 환경의 시크릿을 읽을 수 없습니다. 배포 브랜치 규칙은 애초에 어떤 브랜치가 환경을 사용할 수 있는지 제한합니다.
마스킹이 하는 일과 하지 않는 일
GitHub는 로그에서 시크릿 값을 가리지만, 이 가림 처리는 정확히 일치하는 값을 찾는 데 의존합니다. GitHub 자체 문서에 실패하는 경우가 나열되어 있습니다.
- JSON이나 YAML 같은 구조화된 데이터 안의 시크릿
- 다른 토큰으로 교환된 토큰처럼 시크릿에서 파생된 값(
::add-mask::로 등록하지 않은 경우) - base64 인코딩처럼 변환된 시크릿
마스킹은 실수로 시크릿을 출력하는 것을 막는 장치입니다. 시크릿을 먼저 인코딩하는 코드에는 아무런 효과가 없습니다.
워크플로에서 시크릿이 유출되는 경로
1. 손상된 액션이 시크릿을 출력합니다
2025년 3월, 공격자가 23,000개 이상의 리포지토리에서 사용하는 액션인 tj-actions/changed-files(CVE-2025-30066)를 손상시켰습니다. 공격자는 액션의 기존 버전 태그를 악성 커밋으로 옮겼고, 그 결과 @v45 같은 태그를 참조하는 모든 워크플로가 공격자의 코드를 실행했습니다. 전체 커밋 SHA로 고정된 워크플로만 안전했습니다.
페이로드는 러너 메모리에서 시크릿을 읽어 작업 로그에 출력했으며, 마스킹에 걸리지 않도록 base64로 두 번 인코딩했습니다. 공개 리포지토리에서는 누구나 그 로그를 읽을 수 있습니다. 이 침해는 다른 액션인 reviewdog/action-setup(CVE-2025-30154)에서 시작되었고, 그 이전에는 pull_request_target 워크플로를 통해 탈취된 개인 액세스 토큰에서 시작되었습니다.
2. pull_request_target 워크플로가 신뢰할 수 없는 코드를 실행합니다
pull_request_target은 풀 리퀘스트가 포크에서 왔더라도 기본 리포지토리의 컨텍스트에서, 그 시크릿과 쓰기 권한이 있는 토큰을 가지고 실행됩니다. 이는 워크플로가 풀 리퀘스트의 코드를 절대 실행하지 않는 경우에만 안전합니다. 워크플로가 풀 리퀘스트의 head를 체크아웃해 빌드하면, 풀 리퀘스트를 열 수 있는 누구나 워크플로가 가진 모든 시크릿을 읽을 수 있습니다. GitHub Security Lab은 이를 "pwn requests"라고 부릅니다.
GitHub는 이 범위를 좁혀 왔습니다. 2025년 12월부터 pull_request_target은 항상 기본 브랜치의 워크플로 파일을 사용합니다. 2026년 7월부터 actions/checkout은 알려진 위험한 패턴을 사용하는 pull_request_target 워크플로에서 포크 코드의 체크아웃을 거부하며, 2026년 9월부터 정식 제공된 워크플로 실행 보호 기능은 2026년 11월 2일부터 이벤트 정책이 없는 공개 리포지토리에서 pull_request_target을 기본적으로 비활성화합니다.
3. 신뢰할 수 없는 입력이 스크립트에 주입됩니다
워크플로 표현식은 셸이 실행되기 전에 확장됩니다. 다음과 같은 단계는
- run: echo "Title: ${{ github.event.issue.title }}"
이슈 제목에 담긴 내용이 무엇이든 실행합니다. 공격자가 셸 명령을 제목으로 한 이슈를 열면, 작업은 시크릿에 접근할 수 있는 상태로 그 명령을 실행합니다. 대신 신뢰할 수 없는 값은 환경 변수를 통해 전달해 셸이 이를 데이터로 취급하도록 하세요.
- env:
TITLE: ${{ github.event.issue.title }}
run: echo "Title: $TITLE"
4. 캐시와 아티팩트가 시크릿을 밖으로 옮깁니다
- 아티팩트. 2024년 Unit 42의 ArtiPACKED 연구는
actions/checkout이 기본적으로GITHUB_TOKEN을.git/config에 기록하므로, 작업 공간을 아티팩트로 업로드하면 아티팩트를 내려받을 수 있는 누구에게나 토큰이 유출된다는 것을 보여 주었습니다.upload-artifact는 2024년 9월부터 기본적으로 숨김 파일을 제외하며,actions/checkoutv6는 자격 증명을 체크아웃 디렉터리 바깥에 저장하지만, 고정된 이전 버전과 사용자 지정 업로드 단계에서는 여전히 유출됩니다. - 캐시. 리포지토리의 어떤 워크플로에서든 코드를 실행할 수 있는 공격자는 캐시를 가득 채워 실제 항목을 밀어내고, 같은 키로 오염된 항목을 심을 수 있습니다. 이후 권한이 있는 워크플로가 이를 복원합니다. 2026년 5월,
pull_request_target에서 시작해 러너 메모리에서 읽은 OIDC 토큰으로 끝나는 이 연쇄 공격으로 공격자는 42개 TanStack 패키지의 악성 버전 84개를 게시했습니다.
5. 종속성이 시크릿을 서버로 보냅니다
설치 스크립트는 작업의 나머지 부분과 같은 권한으로 실행됩니다. 2025년 9월 npm에서 처음 발견된 Shai-Hulud 웜은 npm install 중에 실행되어 환경에서 npm 토큰, GitHub 토큰, 클라우드 키를 수집하고 이를 공개 GitHub 리포지토리에 게시했습니다. 2025년 11월의 공격 물결은 preinstall 단계에서 실행되었고, 감염된 머신을 셀프 호스팅 러너로 등록해 공격자가 나중에 다시 들어올 수 있게 했습니다.
이 경로는 워크플로에 실수가 없어도 성립합니다. 종속성 트리 어디에든 손상된 버전 하나만 있으면 충분하며, 시크릿은 네트워크를 통해 빠져나갑니다.
시크릿을 보호하는 방법
저장된 클라우드 키를 OIDC로 대체하세요. AWS, Azure 또는 Google Cloud 키를 시크릿으로 보관하는 대신, 작업이 GitHub의 OIDC 발급자에게 수명이 짧은 토큰을 요청하고 이를 클라우드 제공업체와 교환하도록 하세요. 신뢰 정책이 어떤 리포지토리, 브랜치 또는 환경이 역할을 맡을 수 있는지 결정하며, 자격 증명은 작업과 함께 만료됩니다. 작업에는 permissions: id-token: write가 필요합니다. TanStack 사례에서 보듯 OIDC 토큰도 실행 중인 작업에서 탈취될 수 있지만, 탈취된 토큰의 가치는 만료되지 않는 키보다 훨씬 낮습니다.
액션을 전체 커밋 SHA로 고정하세요. GitHub 문서는 이것이 액션을 변경 불가능한 릴리스로 사용하는 유일한 방법이라고 설명합니다. 태그는 옮길 수 있지만 SHA는 옮길 수 없습니다. 2025년 8월부터 허용된 액션 정책으로 조직 전체에 SHA 고정을 강제하고 특정 액션을 차단할 수 있습니다. Dependabot과 Renovate로 고정된 SHA를 최신 상태로 유지할 수 있습니다.
GITHUB_TOKEN에는 필요한 최소 권한만 부여하세요. 기본값을 읽기 전용으로 설정하고 쓰기 범위는 작업별로 부여하세요. 새 조직과 리포지토리는 2023년 2월부터 읽기 전용이 기본값이지만, 오래된 조직과 리포지토리는 누군가 변경하지 않는 한 관대한 기본값을 유지하고 있습니다. GITHUB_TOKEN 권한: 모든 워크플로에 최소 권한 적용을 참조하세요.
시크릿의 범위를 필요한 단계로 한정하세요. 단계의 env:에 있는 시크릿은 해당 단계의 환경에만 존재합니다. 작업 수준에서 설정한 시크릿은 모든 서드파티 액션을 포함한 모든 단계에서 볼 수 있습니다.
프로덕션 시크릿은 환경 뒤에 두세요. 필수 검토자와 배포 브랜치 규칙이 있으면, 기능 브랜치의 워크플로나 풀 리퀘스트로 트리거된 워크플로는 이를 읽을 수 없습니다.
pull_request_target과 신뢰할 수 없는 입력은 코드 실행으로 취급하세요. 꼭 필요하지 않다면 pull_request_target을 피하고, 그 안에서 풀 리퀘스트 코드를 절대 빌드하지 말며, 이벤트 데이터는 환경 변수를 통해 전달하세요. CodeQL은 워크플로에서 인젝션과 pwn request 패턴을 검사할 수 있으며, 2025년 4월부터 GitHub Actions용으로 정식 제공되고 있습니다.
시크릿 스캔과 푸시 보호를 켜세요. 이 기능은 리포지토리에 커밋된 시크릿을 잡아냅니다. 위에서 다룬 것과는 다른 유출이지만 그만큼 흔합니다.
작업이 연결할 수 있는 곳을 제한하세요. 대부분의 작업에 필요한 목적지는 패키지 레지스트리, 컨테이너 레지스트리, 몇몇 API 등 소수에 불과합니다. 작업이 그곳에만 접근할 수 있다면, 악성 코드가 읽은 시크릿은 갈 곳이 없습니다. 이그레스 정책이 GitHub Actions에서의 데이터 유출을 막는 방법을 참조하세요.
노출이 있었다면 교체하세요. 워크플로가 손상된 액션이나 패키지를 실행했다면, 그 워크플로가 읽을 수 있었던 모든 시크릿이 탈취되었다고 가정하세요. 사용되었다고 생각되는 것만이 아니라 모두 폐기하고 재발급하세요.
CRACI의 역할
CRACI는 GitHub Actions 작업을 CRACI 러너에서 실행합니다. runs-on을 craci로 바꾸기만 하면 워크플로, 시크릿, 실행 기록은 GitHub에 그대로 남습니다. CRACI가 하는 두 가지 일은 위의 유출을 겨냥합니다.
- 실패 시 차단되는 이그레스 정책. 작업이 접근할 수 있는 패키지 소스, 레지스트리, 호스트를 설정합니다. 정책은 작업 시작 전에 검증되고 러너에서 적용되므로, 그 밖의 다른 곳으로의 연결은 차단되며, 정책 위반 시 알림을 받습니다.
- 작업이 한 일의 기록. CRACI는 캐시에서 복원된 패키지를 포함해 각 작업이 실제로 가져온 모든 패키지와, 작업의 네트워크 연결 추적을 기록합니다. 다음 손상된 패키지가 발표되면, 잠금 파일로 추측하는 대신 어떤 빌드가 그 패키지를 가져왔고 어디에 연결했는지 확인할 수 있습니다.
각 작업은 자체 가상 머신에서 실행되므로, 한 작업이 남긴 것이 다음 작업에 남아 있지 않습니다. CRACI는 코드의 시크릿을 스캔하지 않으므로, 그 용도로는 GitHub의 시크릿 스캔을 계속 사용하세요.
데모를 예약하고 여러분의 빌드 하나의 기록을 확인해 보세요.
