LabHub

GitLab CI/CD · 현장에서의 파이프라인 · 이론

파이프라인이 사고를 내는 자리

LabHub 에서 이어서 보기

한 줄 요약

파이프라인은 저장소에서 가장 강한 권한을 들고 임의의 코드를 실행하는 자리라, 사고는 대부분 "설정이 틀려서"가 아니라 "누가 그 실행을 조종할 수 있어서" 일어난다.

왜 이게 필요했나

CI 잡은 배포 자격 증명, 레지스트리 토큰, 클라우드 키를 손에 쥔 채 돈다. 그리고 그 잡이 무엇을 실행할지는 저장소 안의 파일이 정한다. 즉 저장소에 파일을 넣을 수 있는 사람은 사실상 그 자격 증명으로 무엇이든 할 수 있다. 이 사실을 늦게 깨달으면 "빌드 서버는 내부망이니까 괜찮다"는 말로 몇 년을 보내게 된다.

공급망 쪽 사건들이 이 점을 반복해서 보여 줬다. tj-actions/changed-files 사건에서 공격자는 저장소를 뚫지 않았다. 널리 쓰이던 액션의 태그를 악성 커밋으로 옮겼을 뿐인데, 그 태그를 참조하던 수많은 파이프라인이 그대로 악성 코드를 실행했다. 태그는 사람이 옮길 수 있는 이름표이고, 파이프라인은 그 이름표를 믿고 있었다.

어떻게 동작하나

방어는 네 군데에 건다.

첫째, 비밀 값. CI 변수는 저장소가 아니라 프로젝트 설정에 두고, 보호 변수로 표시해 보호 브랜치와 보호 태그에서 도는 잡에만 넘어가게 한다. 마스킹은 로그에서 값을 가려 주지만 방어가 아니라 예의에 가깝다 — 값을 base64 로 인코딩해 출력하면 마스킹은 통과한다. 진짜 방어는 그 값이 애초에 그 잡에 들어가지 않게 하는 것이다.

둘째, 포크에서 온 병합 요청. 남이 쓴 파이프라인 설정이 우리 러너에서 도는 상황이므로 기본적으로 비밀 값을 주지 않는다. 캐시도 신뢰 경계를 넘는다. 포크가 악의적인 의존성을 캐시에 심어 두면 이후 빌드가 그것을 그대로 쓰는 캐시 포이즈닝이 성립한다.

셋째, 남의 설정을 끌어오는 include. 다른 저장소의 템플릿을 가져올 때 ref 를 브랜치 이름으로 두면 그 브랜치가 바뀔 때마다 우리 파이프라인의 내용도 바뀐다. 커밋 SHA 로 고정해야 어제와 오늘이 같아진다. 잡이 쓰는 컨테이너 이미지도 같은 이유로 다이제스트나 불변 태그로 고정한다.

넷째, 되돌릴 수 있는 배포. 운영 배포 잡은 수동 승인(when: manualallow_failure: false)으로 두고, environment 를 지정해 무엇이 어디에 나갔는지 기록으로 남긴다. 그리고 배포에 쓰는 이미지 태그를 커밋 SHA 로 두면, 사고가 났을 때 "그때 나간 것"이 무엇인지 레지스트리와 Git 로그만으로 5분 안에 답이 나온다. 운영에 latest 만 있으면 이 질문에 아무도 답하지 못하고 되돌릴 대상도 없다.

실무에서 지키는 최소선

현실적으로 모든 것을 한 번에 하기는 어렵다. 처음 손대는 팀에는 순서를 정해 주는 편이 낫다. 먼저 배포 자격 증명을 보호 변수로 옮기고, 그다음 운영 배포에 수동 승인을 걸고, 그다음 이미지와 include 의 참조를 고정하고, 마지막으로 포크 병합 요청의 파이프라인 정책을 손본다. 앞의 둘은 하루면 되고 사고 대부분을 막는다.

한 가지 더. 파이프라인 실행 시간이 15분을 넘기 시작하면 사람들이 결과를 기다리지 않고 다른 일을 하러 간다. 그러면 실패를 늦게 발견하고, 늦게 발견한 실패는 이미 여러 커밋이 쌓인 뒤라 원인을 좁히기 어렵다. 보안과 속도는 따로 노는 주제처럼 보이지만, 아무도 안 보는 파이프라인은 게이트로서도 죽은 것이라 결국 같은 이야기가 된다.

다음 퀴즈에서 볼 것

보호 변수와 마스킹의 차이, include 와 이미지 참조를 고정하는 이유, 수동 승인이 승인 게이트가 되기 위한 조건을 구분해 본다.