코드 저장소에 커밋 한 번 할 권한이, 프로덕션 서버 root가 되는 길 — GitLab 공유 러너의 함정
코드 저장소에 커밋 한 번 할 권한이, 프로덕션 서버 root가 되는 길 — GitLab 공유 러너의 함정
분류: Security · 클라우드·컨테이너 · CI/CD 파이프라인 탈취 [정보성]
덱. 뉴질랜드 보안업체 Pulse Security는 GitLab의 CI/CD(코드를 커밋하면 자동으로 빌드·배포까지 이어주는 파이프라인) 환경에서, 낮은 권한의 사용자 계정 하나로 시작해 결국 프로덕션 서버에 root로 접속하는 공격 체인을 실습 환경에서 시연했습니다. 각 단계는 개별적으로는 GitLab의 “정상 기능”이지만, 여러 프로젝트가 하나의 실행 인프라(러너)를 공유하면서 격리가 충분하지 않을 때 이 기능들이 이어 붙어 심각한 권한 상승 경로가 됩니다.
리드
CI/CD는 코드 저장소와 배포 인프라 사이를 자동으로 이어주는 접착제 같은 계층입니다. 그런데 이 접착제가 여러 팀·프로젝트 사이에서 실행 자원을 공유하도록 설계돼 있으면, “저장소에 코드를 커밋할 수 있는 권한”이 “그 배포 인프라 전체를 실행할 수 있는 권한”으로 슬며시 번져 나갈 수 있습니다. 이 글이 다루는 사례는 인턴 계정 수준의 낮은 권한 하나가 어떻게 조직의 가장 민감한 프로덕션 자격증명까지 도달하는지를 단계별로 보여줍니다.
핵심 사실 요약
- 배경. GitLab의 파이프라인은 실제로는 “러너(runner)“라는 실행기가 처리합니다. 러너는 프로젝트 단위, 그룹 단위, 또는 인스턴스 전체 단위로 공유되도록 설정할 수 있는데, 인스턴스 전체 공유 러너는 GitLab을 쓸 수 있는 사람이면 누구나(개인 저장소를 만들 수 있는 정도의 권한만 있으면) 접근할 수 있습니다.
- 1단계 — 오염된 파이프라인(Poisoned Pipeline, 업계에서 CICD-SEC-04로 분류하는 유형). 개발자 권한만 있고 CI/CD 설정을 볼 권한이 없는 사용자라도, 자신의 브랜치에서 파이프라인 설정 파일을 자유롭게 고칠 수 있다면 그 파이프라인이 접근하는 모든 자격증명에 간접적으로 접근하게 됩니다. 원문에서 시연한 방식은 환경변수 전체를 압축하고 인코딩한 뒤 빌드 로그로 출력시키는 것으로, GitLab이 민감한 변수를 로그에서 자동으로 가려주는 마스킹 기능을 우회했습니다. 이렇게 배포용 클라우드 자격증명을 통째로 빼낼 수 있었습니다. GitLab은 기본적으로 CI 변수를 “보호된 브랜치”에서만 노출하도록 막아 두고 있지만, 러너 자체는 기본값으로 모든 브랜치에서 실행되므로 설정에 따라 여전히 위험이 남습니다.
- 2단계 — 공유 “docker-in-docker(DIND)” 러너 공격. DIND는 파이프라인 안에서 새 컨테이너 이미지를 빌드할 수 있게 해주는 기능인데, 이를 동작시키려면 러너의 컨테이너를 **Privileged(권한 모드)**로 띄워야 합니다. 이 모드는 도커가 제공하는 격리 보호 장치를 사실상 무력화합니다. 연구진은 실제 진단 과정에서 이렇게 Privileged 모드로 설정된 DIND·쿠버네티스 러너를 여러 번 만났다고 밝힙니다.
- 컨테이너 탈출. 개인 저장소를 만든 뒤 악성 파이프라인을 실행해 러너 컨테이너 안에서 역방향 셸(공격자가 지정한 주소로 접속을 걸어 명령을 주고받는 셸)을 얻습니다. 권한 모드로 떠 있는 컨테이너에서는 호스트가 사용하는 저수준 블록 디바이스(LVM 볼륨 등)를 컨테이너 안에서 직접 마운트할 수 있어, 별도의 취약점이나 특수 도구 없이도 호스트 전체 파일시스템에 쓰기 접근이 열립니다. 이 접근으로 호스트의 관리자 계정에 접속용 키를 심어 넣어 컨테이너 밖, 즉 호스트 자체에 직접 로그인합니다.
- 대기와 수확. 호스트에 들어간 뒤에는 다른 값어치 있는 파이프라인(예: 프로덕션 배포 작업)이 실행되기를 기다렸다가, 그 컨테이너가 사용하는 환경변수를 조회합니다. 배포 파이프라인이 담고 있던 개인키·서버 주소 같은 값을 이렇게 확보해, 그 키로 실제 프로덕션 서버에 접속하는 데까지 성공했습니다.
- 보너스 — 더 단순한 셸 실행기(shell executor). 도커를 쓰지 않고 명령을 직접 셸에서 실행하는 방식의 러너에서는 컨테이너 탈출 단계 자체가 필요 없었습니다. 낮은 권한으로 러너 프로세스 목록을 지켜보다가, 다른 잡이 실행되는 순간 그 프로세스의 환경변수 파일을 직접 읽는 것만으로 같은 수준의 정보를 얻을 수 있었다고 원문은 설명합니다.
- 저자는 이 문제가 GitLab 고유의 결함이라기보다, “여러 민감도의 작업을 하나의 공유 실행 인프라에 격리 없이 몰아넣는” 설정·아키텍처 선택의 문제라고 명확히 구분합니다. 원문에 따르면 GitLab 문서는 Privileged 모드가 갖는 의미를 명시하고 있으며, 워크로드 간 격리를 기본 제공 기능으로 내세운 적이 없다고 밝힙니다.
대응 방향
원문이 제안하는 대응은 크게 세 갈래입니다.
- 러너를 민감도별로 분리한다. 프로덕션 배포처럼 민감한 작업만 처리하는 러너는 별도로 두고, 접근 권한을 엄격히 제한된 소수 인원으로만 좁힙니다. 일반 개발 작업과 같은 러너를 공유하지 않는 것이 핵심입니다.
- 저장소·브랜치 보호를 강화한다. 보호된 브랜치와 보호된 변수 설정을 켜서, 낮은 권한 사용자가 파이프라인 설정을 마음대로 바꿔 민감한 변수에 접근하는 경로 자체를 줄입니다.
- 탐지·인증 통제를 함께 둔다. 다중 인증, IP 허용목록, 감사 로깅처럼 CI/CD 인프라 접근 자체를 좁히고 기록하는 통제를 배포 인프라에 준하는 수준으로 적용합니다.
짧은 논평
이 사례를 관통하는 원칙은 저희가 다른 신뢰 경계 붕괴 사례에서도 반복해서 짚는 것과 같습니다 — 한 계층에서 얻은 접근권한(여기서는 “저장소에 커밋할 수 있다”)이 훨씬 더 민감한 다른 계층(여기서는 “프로덕션 배포를 실행할 수 있다”)까지 그대로 넘어갈 수 있을 때, 그 사이의 격리 부재 자체가 공격 표면이 됩니다. 개별 신뢰 관계 하나하나는 각자 논리적으로 타당해 보여도(“저장소 커밋 권한은 파이프라인 실행 권한이다”, “파이프라인 실행 권한은 러너 자원 사용 권한이다”), 이 관계들을 사슬로 이으면 원래 의도하지 않았던 지점까지 권한이 흘러갑니다. CI/CD는 코드와 인프라 사이의 접착제인 동시에, 그 접착제를 통해 신뢰가 얼마나 멀리까지 새어 나가는지를 보여주는 좋은 사례이기도 합니다. 자동화 시스템을 늘릴수록, 그 시스템이 실제로 무엇에 접근할 수 있는지를 주기적으로 점검하는 일이 함께 늘어야 하는 이유입니다.
References / 참고자료
- Denis Andzakovic, Pulse Security, “OMGCICD: Attacking GitLab CI/CD via Shared Runners” — 이 글이 재구성한 원 분석. GitLab 공유 러너를 이용한 권한 상승 공격 체인 전 과정을 실습 환경에서 시연. https://pulsesecurity.co.nz/articles/OMGCICD-gitlab
이 글은 위 원 분석을 우리 관점에서 다시 쓰고 짧은 논평을 더한 정보성 초안입니다. 특정 문장·명령어·CI 설정 파일을 그대로 옮기지 않았으며, 작동하는 재현 절차나 완성형 페이로드는 싣지 않았습니다. ⚠️미확인 표시 항목은 발행 전 독립 팩트체크에서 확증·정정되어야 합니다.