보안

코드를 커밋하고 배포하는 CI 봇을 위한 최소 권한

광범위한 쓰기 권한과 무제한 배포 권한을 가진 자동화 봇은 한 번의 잘못된 실행을 전체 시스템 장애로 만듭니다. 영향 범위를 줄이는 방법은 다음과 같습니다.

이 글은 영어 원문을 AI 모델이 번역한 것입니다. 표현이 원문과 다를 수 있습니다. 영어 원문 보기

코드를 커밋하고, 브랜치를 푸시하고, 배포를 트리거하는 자동화 봇은 사람의 개입 없이 프로덕션 접근 권한을 가진 머신 계정입니다. 쉬운 설정 방식은 봇에게 수명이 긴 토큰, 전체 리포지토리에 대한 쓰기 접근 권한, 그리고 무엇이든 배포할 수 있는 권한을 부여합니다. 이 방식은 처음 시도에 성공하므로 널리 퍼집니다. 이는 또한 단 한 번의 오작동, 프롬프트 주입 또는 유출된 토큰만으로도 아무도 알아채기 전에 어떤 파일이든 다시 쓰고 어떤 서비스든 출시할 수 있다는 것을 의미합니다. 이 게시물은 그 폭발 반경을 줄이는 방법에 관한 것입니다. 즉, 각 봇에 고유한 ID, 수명이 짧은 자격 증명, 좁은 경로의 허용 목록, 그리고 서명된 출력을 부여하여, 잘못된 실행이 모든 것 대신 일부에만 피해를 주도록 하는 것입니다.

봇의 폭발 반경이 사람보다 더 큰 이유

광범위한 접근 권한을 가진 사람이라도 여전히 사람의 속도에 제약을 받습니다. 그들은 몇 개의 파일을 열고, 생각하고, 변경 사항을 만들고, 동료가 pull request를 검토합니다. 실수는 작고 발견할 수 있을 만큼 느린 경향이 있습니다.

봇에는 그러한 제동 장치가 전혀 없습니다. 봇은 밀리초 단위로 작동하고, 접근할 수 있는 모든 리포지토리에서 동일한 작업을 반복하며, 변경 사항이 잘못되었는지 확인하기 위해 잠시 멈추지 않습니다. 로직에 결함이 있거나 계정이 탈취되면, 설정을 편리하게 만들었던 동일한 광범위한 권한 부여가 이제 공격자가 토큰의 전체 범위에 걸쳐 기계의 속도로 움직일 수 있게 합니다. 편리한 기본 설정과 최악의 사고는 좋은 날과 나쁜 날의 관점에서 본 동일한 권한 집합입니다.

따라서 설계 목표는 "봇을 신뢰하는 것"이 아닙니다. 봇은 그 뒤에 판단력이 없고 규칙과 입력된 값만 있기 때문에 사람이 신뢰받을 수 있는 의미에서 신뢰받을 수 없습니다. 목표는 봇이 할 수 있는 일에 상한선을 두어 완전히 손상된 실행조차도 작게 유지되도록 하는 것입니다. 모든 자동화 계정을 신뢰할 수 있는 행위자가 아닌 제한된 기능으로 취급하십시오.

각 봇에 고유한 ID 부여하기

첫 번째 실수는 여러 작업에 걸쳐 하나의 강력한 계정을 공유하는 것입니다. 테스트 실행기, 릴리스 작업, 문서 게시자, 그리고 세 개의 자동화 스크립트가 모두 인증하는 단일 머신 ID는 완전한 단일 장애 지점(single point of total failure)입니다. 이 작업들 중 하나라도 손상되면 공유 계정이 할 수 있는 모든 권한의 합집합이 넘어가게 됩니다.

분리하세요. 모든 개별 작업은 해당 작업에 필요한 권한만 가진 자체 ID를 갖게 됩니다. 문서를 게시하는 ID는 결제 서비스를 배포할 수 없어야 합니다. 테스트를 실행하는 ID는 읽기 접근 권한만 가져야 하며 그 이상은 안 됩니다. 왜냐하면 테스트는 프로덕션에 어떤 것도 쓸 필요가 없기 때문입니다.

개별 ID는 감사 로그를 읽기 쉽게 만듭니다. 하나의 계정이 모든 것을 할 때 "ci 계정이 커밋을 푸시했습니다"라는 로그 라인은 거의 아무것도 알려주지 않습니다. 문서 봇이 자체 이름을 가질 때, 문서 봇이 docs 디렉터리 외부에서 무언가를 작성하는 것을 보여주는 항목은 경보를 울릴 수 있는 명백한 이상 징후입니다. ID별 범위 지정은 접근 로그를 작동하는 침입 신호로 바꿔줍니다.

정확한 경로 및 리소스로 권한 범위 지정

최소 권한은 권한 부여가 플랫폼이 아닌 작업과 일치함을 의미합니다. 변경 로그를 업데이트하는 봇은 전체 트리가 아닌 하나의 파일에 대한 쓰기 액세스 권한이 필요합니다. 하나의 서비스를 배포하는 봇은 프로젝트의 모든 서비스가 아닌 해당 서비스에 대한 배포 권한이 필요합니다.

여기서는 두 가지 제한이 대부분의 작업을 수행합니다. 봇이 작업할 수 있는 리소스를 제한하고, 쓸 수 있는 경로를 제한하세요. 배포 ID는 특정 서비스나 네임스페이스에 바인딩되어야 유효한 토큰이라도 다른 것을 건드릴 수 없습니다. 커밋 봇은 자신의 레인 외부의 변경 사항을 거부하는 브랜치 보호 규칙을 통하거나, diff가 벗어날 때 실행을 실패시키는 검사를 통해 디렉터리로 제한되어야 합니다.

# Per-bot permission model. Each identity gets only what its job requires.
identities:
  changelog-bot:
    repo_write: true
    path_allowlist:
      - "CHANGELOG.md"
      - "docs/releases/**"
    deploy: none

  service-deploy-bot:
    repo_write: false          # deploys, does not commit
    deploy:
      target: "web-frontend"   # this one service, nothing else
      environments: ["staging", "production"]

  test-bot:
    repo_write: false          # read-only; tests never write prod
    deploy: none

모델을 작성하는 이유는 '쓰기 액세스 권한을 부여한다'는 결정을 더 정밀하게 내릴 수 있다는 데 있습니다. 거의 모든 봇은 전체 권한보다 훨씬 적은 권한을 필요로 하며, 더 좁은 범위의 권한은 일단 설정되면 런타임에 아무런 비용이 들지 않습니다.

광범위한 접근과 최소 권한, 나란히 비교

두 설정은 평상시에는 비슷해 보입니다. 문제가 발생하면 완전히 달라집니다. 아래 표는 각 모델에서 동일한 자동화 봇을 보여줍니다.

속성 광범위한 접근 최소 권한
ID 모든 작업에 대해 하나의 공유 계정 작업당 하나의 ID
리포지토리 쓰기 전체 트리 허용 목록에 있는 경로만
배포 범위 모든 서비스, 모든 환경 하나의 지정된 서비스 및 환경
자격 증명 수명 만료 없는 장기 토큰 실행마다 발급, 몇 분 후 만료
유출된 토큰이 허용하는 것 무기한으로 전체 쓰기 및 배포 토큰이 만료될 때까지 하나의 경로 또는 하나의 서비스
잘못된 실행의 도달 범위 모든 파일, 모든 서비스 봇의 레인만
감사 신호 "ci 계정이 무언가를 했습니다" 지정된 행위자, 지정 범위를 벗어난 작업 알림
교체 부담 수동, 잊기 쉬움 없음, 자격 증명이 일시적임

가장 중요한 행은 유출 및 잘못된 실행 행입니다. 광범위한 접근 방식에서는 손상된 토큰 하나 또는 버그가 있는 실행 하나가 모든 것에 도달합니다. 최소 권한 방식에서는 동일한 실패가 단일 경로 또는 단일 서비스로 제한되며, 자격 증명 수명이 짧기 때문에 도난당한 토큰은 실행이 끝나면 거의 가치가 없어집니다.

단기 자격 증명으로 장기 시크릿 제거

시크릿에 저장된 수명이 긴 토큰은 상시적인 위험 요소입니다. 이 토큰은 만료되지 않고, 로그나 손상된 종속성을 통해 유출될 수 있으며, 만약 유출되더라도 도난당한 토큰이 실제 봇과 똑같이 보이는 호출을 생성하기 때문에 종종 그 사실을 알 수 없습니다. 교체는 수작업으로 해야 하는 귀찮은 일이므로, 토큰은 쌓이게 되고 그것을 필요로 했던 작업보다 더 오래 살아남게 됩니다.

더 나은 패턴은 각 실행마다 새로 발급되고 몇 분 후에 만료되는 자격 증명입니다. 많은 자동화 플랫폼은 실행에 대해 서명된 ID 어설션을 제시할 수 있고, 대상 시스템은 특정 호출자에 대한 해당 어설션을 신뢰하여 단기 액세스 토큰으로 교환하도록 구성할 수 있습니다. 영구적인 시크릿이 저장되지 않으므로 유출되거나, 교체하거나, 몇 달 후 저장소에서 잊힌 채 발견될 것이 없습니다. 신뢰는 정확한 ID와 종종 정확한 브랜치에 고정되므로, 다른 곳에서 온 어설션은 실제 권한에 도달하기 전에 교환에 실패합니다.

플랫폼이 실행별 자격 증명을 발급할 수 없어 저장된 토큰을 사용해야만 하는 경우, 짧은 교체 주기를 가진 범위가 지정된 토큰으로 만들고, 최소 권한 모델처럼 범위를 최대한 좁게 유지하십시오. 범위가 엄격하게 지정된 단기 키는 방어 가능한 차선책입니다. 범위가 넓고 만료되지 않는 것은 설계에서 배제해야 할 대상입니다.

단일 유출이 연쇄적으로 이어지지 않도록 콘텐츠 작성과 배포를 분리

편리하다는 이유로 모든 권한을 하나의 자격 증명에 묶는 것은 미묘한 함정입니다. 만약 동일한 토큰으로 코드를 커밋하고 배포를 트리거할 수 있다면, 해당 토큰이 유출될 경우 전체 체인을 넘겨주는 것과 같습니다. 즉, 공격자가 악의적인 변경 사항을 작성하고 두 번째 장벽 없이 한 번에 배포할 수 있습니다.

기능별로 토큰을 분리하십시오. 콘텐츠를 작성하는 자격 증명은 배포할 수 없어야 하며, 배포하는 자격 증명은 콘텐츠를 작성할 수 없어야 합니다. 이제 작성 측에서 유출이 발생하더라도 파일을 오염시킬 수는 있지만 자체적으로 라이브로 푸시할 수는 없으며, 배포 측에서 유출이 발생하더라도 기존 아티팩트를 다시 배포할 수는 있지만 새로운 악성 코드를 주입할 수는 없습니다. 공격자가 엔드 투 엔드 침해를 완료하려면 두 가지 모두가 필요하며, 이는 하나만 필요로 하는 것보다 훨씬 더 높은 장벽입니다.

# Two credentials, two jobs, no overlap.
content-writer:
  can_commit: true
  can_deploy: false      # writing alone cannot reach production

deployer:
  can_commit: false      # deploying alone cannot introduce new code
  can_deploy: true
  target: "web-frontend"

이것은 읽기와 쓰기를 분리하는 것과 동일한 논리를 한 단계 위에서 적용한 것입니다. 자격 증명에 따라 임무를 분할하는 것은 도난당한 단일 보안 비밀만으로 소스 변경부터 라이브 트래픽까지의 전체 과정을 실행할 수 없다는 것을 의미합니다.

커밋과 아티팩트에 서명하여 출처를 확인할 수 있도록 하세요

범위 지정은 봇이 할 수 있는 일을 제한합니다. 서명은 봇이 실제로 한 일을 증명할 수 있게 해줍니다. 봇이 커밋과 빌드하는 아티팩트에 서명하면 모든 출력물에 검증 가능한 출처 표시가 남게 되며, 서명되지 않았거나 잘못된 키로 서명된 모든 것은 눈에 띄게 외부의 것으로 구분됩니다.

좁은 권한 집합만으로는 혼란스럽거나 탈취된 봇이 허용된 범위 내에서 출력물을 생성할 여지가 여전히 남아있기 때문에 이는 중요합니다. 서명은 그 출력물을 확인할 수 있는 대상으로 바꿔줍니다. 배포 단계에서는 예상된 빌드 ID로 서명되지 않은 아티팩트의 배포를 거부할 수 있으므로, 공격자가 서명되지 않은 바이너리를 파이프라인에 몰래 넣더라도 배포되지 않고 게이트에서 거부됩니다. 서명된 커밋 기록이 있으면, 공격자가 봇의 서명 키를 가지고 있지 않기 때문에 릴리스 봇에서 온 것이라고 주장하는 위조된 커밋은 검증에 실패하게 됩니다.

봇의 출력물이 나중에 실행되는 무언가로 흘러 들어갈 때 서명은 설정할 가치가 있습니다. 이를 위에서 언급한 단기 수명 자격 증명과 결합하고, 서명 키 자체는 최소한으로 유지하며 분리하면, 출처 검증은 수동적인 신뢰 결정이 아닌 자동 확인 절차가 됩니다.

장단점, 그리고 단순하게 유지해야 할 때

이 중 어느 것도 공짜는 아닙니다. 솔직한 비용은 설정의 복잡성과 지속적인 구조입니다. 광범위한 토큰을 가진 하나의 공유 계정은 몇 번의 클릭으로 만들 수 있습니다. 최소 권한 버전은 여러 ID, 커밋 봇당 path allowlist, 배포 봇당 resource binding, 단기 자격 증명을 위한 federation trust, 분리된 콘텐츠 및 배포 토큰, 그리고 자체 검증 단계가 있는 서명 키입니다. 이는 실제 작업이며, 특히 실행별 자격 증명 신뢰는 느슨한 방향으로 미묘하게 잘못 설정하기 쉬워서, 작동하는 것처럼 보이지만 의도한 것보다 더 많은 호출자를 신뢰하게 될 수 있습니다. 이러한 신뢰 조건을 정확하게 작성하고, 잘못된 경로의 ID가 실제로 거부되는지 테스트하는 시간을 계획해야 합니다.

그 보상은 비용이 한 번 지불되면 대부분 사라지는 반면, 제거되는 위험은 상존하며 반복적이라는 점입니다. 교체할 토큰도 없고, 범위가 조용히 커지는 공유 계정도 없으며, 손상된 실행은 전체 시스템 대신 하나의 경로 또는 하나의 서비스에 국한됩니다.

판단은 여전히 필요합니다. 실제 데이터가 없고 수명이 며칠인 일회용 샌드박스는 전체 체계가 필요하지 않습니다. 왜냐하면 다음 주에 전체 프로젝트가 삭제될 때 폭발 반경이 이미 작기 때문입니다. 에뮬레이터에 대해 실행되는 로컬 스크립트는 프로덕션 자격 증명이 전혀 필요하지 않습니다. 전체 모델은 봇이 중요한 것에 대한 상시 접근 권한을 가질 때 그 비용의 가치를 합니다: 사람들이 의존하는 리포지토리, 실제 사용자에 대한 배포 경로, 프로덕션에서 실행되는 아티팩트. 실제 시스템에 대해 자체적으로 커밋하거나 배포하는 모든 자동화의 경우, 광범위한 권한 부여는 오늘 몇 분의 시간을 절약해주지만 그것이 존재하는 한 시스템 전반의 잠재적 위험을 초래합니다. 최소 권한은 한 번의 긴 오후 시간을 비용으로 요구하지만, 그 후 모든 잘못된 실행을 작은 범위로 유지합니다.

관련 글