Skip to content
Go back

Self-hosted GitLab에서 Renovate Bot으로 보안 취약점 패치 자동화하기

배경

프론트엔드 Monorepo(Next.js + TurboRepo)를 운영하면서 npm 의존성 보안 취약점 관리가 숙제였다. pnpm audit을 수동으로 돌리고, 취약 패키지를 하나씩 올리고, lock 파일 충돌을 해결하는 반복 작업. 이걸 자동화하고 싶었다.

목표는 단순했다:

GitHub에선 Dependabot이 기본 제공되지만, Self-hosted GitLab에는 없다. Renovate Bot을 Self-hosted로 올려서 해결했다.

전체 구조

파이프라인은 세 단계로 동작한다:

  1. GitLab Pipeline Schedule — 평일 오전 8시(KST) cron 트리거
  2. Renovate Bot — OSV 데이터베이스 기반 취약점 스캔 → 패치 브랜치 + MR 자동 생성
  3. 패치 로그 스크립트 — MR description을 파싱해 docs/severity-patches/YYYY-MM-DD.md 생성
Schedule (0 8 * * 1-5)
  → trigger_renovate (parent pipeline)
    → renovate job (child pipeline)
      → Renovate Bot 실행
      → generate-patch-log.mjs 실행
        → renovate/security-patches-20260521 브랜치에 패치 로그 push

Renovate 설정 — 취약점만 잡기

Renovate는 기본적으로 모든 의존성을 최신 버전으로 올리려 한다. 보안 패치만 원했기 때문에 일반 업데이트는 전부 비활성화하고 vulnerabilityAlerts만 켰다.

{
  "enabledManagers": ["npm"],
  "baseBranches": ["devel"],
  "branchPrefix": "renovate/",
  "osvVulnerabilityAlerts": true,
  "packageRules": [{ "matchPackagePatterns": ["*"], "enabled": false }],
  "vulnerabilityAlerts": {
    "enabled": true,
    "addLabels": ["critical", "fix"],
    "automerge": false,
    "commitMessagePrefix": "common: fix: ",
    "commitMessageTopic": "보안 패치 {{depName}} {{newVersion}}"
  }
}

핵심은 packageRules에서 "enabled": false로 전체를 끄고, vulnerabilityAlerts.enabled: true로 보안 패치만 허용한 것이다.

날짜 기반 브랜치명 — 삽질의 기록

Renovate가 만드는 브랜치명에 날짜를 넣고 싶었다. renovate/security-patches-20260521 형태로 매일 새 브랜치가 생기면 이력 관리가 편하다.

처음에는 branchPrefix를 동적으로 바꾸려 했다:

결국 찾은 방법은 groupName을 CI 환경변수로 오버라이드하는 것이었다:

script:
  - export TODAY=$(TZ=Asia/Seoul date +%Y%m%d)
  - export RENOVATE_BRANCH="renovate/security-patches-${TODAY}"
  - export RENOVATE_CONFIG='{"vulnerabilityAlerts":{"groupName":"security-patches-'"${TODAY}"'"}}'
  - renovate

branchPrefixrenovate.json"renovate/"로 고정하고, groupName에 날짜를 넣으면 최종 브랜치명이 renovate/security-patches-20260521이 된다.

여기서 한 가지 더 주의할 점이 있었다. renovate.jsongroupName이 이미 정의되어 있으면 repo config이 env config보다 우선해서 날짜가 무시된다. renovate.json에서 groupName을 제거해야 env config이 적용된다.

CI 환경의 함정들

UTC vs KST

CI 컨테이너는 UTC 시간을 사용한다. 한국 시간 오전 8시에 파이프라인이 돌면 UTC로는 전날 23시라서, date +%Y%m%d가 전날 날짜를 반환한다. TZ=Asia/Seoul을 붙여서 해결했다.

Detached HEAD에서 push

GitLab CI 러너는 detached HEAD 상태로 checkout한다. 패치 로그를 Renovate 브랜치에 push하려면 명시적으로 git fetch + git checkout이 필요했다.

인증 토큰

CI 러너의 기본 clone 토큰은 read-only다. Renovate 브랜치에 패치 로그를 push하려면 별도 토큰으로 remote URL을 재설정해야 했다.

const repoUrl = `https://oauth2:${TOKEN}@gitlab.example.com/group/repo.git`;
execFileSync('git', ['remote', 'set-url', 'origin', repoUrl]);

패치 로그 자동 생성

Renovate가 MR을 만들면, 후속 스크립트가 MR description을 GitLab API로 조회해서 패키지 업데이트 테이블과 CVE 정보를 파싱한다. 이걸 docs/severity-patches/2026-05-21.md 형태의 마크다운으로 변환해서 Renovate 브랜치에 push한다.

MR description 파싱
  → 패키지 목록 (이름, 이전 버전, 패치 버전)
  → CVE/GHSA ID + CVSS 심각도
  → pnpm audit 결과와 교차 매칭
  → 마크다운 패치 로그 생성

MR을 리뷰할 때 패치 로그도 같이 보이기 때문에, 어떤 취약점이 해결되는지 한눈에 파악할 수 있다.

Pipeline Schedule — Sidekiq 이슈

설정을 다 끝내고 스케줄을 등록했는데, 자동 트리거가 안 됐다. Play 버튼으로 수동 실행하면 잘 되는데, cron 시간에 자동으로 안 도는 현상.

원인은 GitLab의 PipelineScheduleWorker였다. Self-hosted GitLab은 Sidekiq 워커가 주기적으로 스케줄을 체크해서 파이프라인을 트리거하는 구조인데, gitlab.rb에서 이 워커의 cron 설정이 비활성화되어 있었다.

# /etc/gitlab/gitlab.rb
gitlab_rails['pipeline_schedule_worker_cron'] = "*/5 * * * *"

설정 후 gitlab-ctl reconfigure + Sidekiq 재시작으로 해결했다. 한 가지 주의할 점은 19 * * * **/19 * * * *의 차이다. 전자는 “매시 19분”, 후자는 “19분마다”인데, 이 차이를 놓치면 워커가 한 시간에 한 번만 도는 상황이 생긴다.

회고

자동화는 “돌아가게 만드는 것”보다 “안 돌았을 때 왜 안 도는지 알 수 있게 만드는 것”이 더 중요하다.

Renovate 설정 자체는 어렵지 않았다. 시간이 걸린 건 CI 환경의 세부사항들이었다 — detached HEAD, 토큰 권한, UTC 시간대, Sidekiq 워커 상태. 로컬에서 잘 되는 것과 CI에서 잘 되는 것은 별개라는 걸 다시 한번 체감했다.


Share this post on:

Comments


Previous Post
AI 에이전트 하네스 개발기 #1 — 20개 스택 × 3개 에이전트, 프로젝트 스캐폴더부터
Next Post
퀀트 플랫폼 개발기 #9 — 갭 필터를 비대칭으로 재설계하기