본문으로 건너뛰기

취약점 데이터 (Trivy DB)

v0.10.0부터 TRUSCA는 단일 임베디드 엔진 Trivy(Aqua Security)로 SBOM을 CVE에 대조합니다. 워커 이미지에 trivy 바이너리가 포함되어 있고, Trivy DB(NVD + OSV + GHSA + EPSS + KEV 통합 번들)는 첫 부팅 시 다운로드되어 주간 단위로 갱신됩니다. 본 페이지는 그 라이프사이클을 운영·감사하는 방법입니다.

대상 독자

배포를 운영하는 super_admin. docker-compose와 기본 셸 친숙도 가정. 설치본에서 마이그레이션 중이라면 v0.10.0 릴리스 노트의 DT 제거 절차를 먼저 확인하세요.

Dependency-Track 커넥터 대체

v0.10.0은 Dependency-Track(DT)과 /admin/dt 커넥터 페이지를 제거했습니다. Trivy DB가 같은 역할(SBOM ↔ CVE 매칭)을 4GB JVM + H2 DB 대신 ~500MB 풋프린트로 수행합니다. 결정 배경은 ADR-0001을 참조하세요.

라이프사이클 동작

워커 부팅 ──► trivy --download-db-only ──► /var/lib/trivy/db


Celery beat (주간) ─► trivy --download-db-only (refresh)


trivy sbom <sbom.json> ──► vulnerability_findings

구성 요소는 세 가지입니다.

  1. 부팅 시 다운로드. 워커는 Celery worker_ready 시그널 핸들러를 등록해 워커가 큐를 소비하기 시작하면 백그라운드 스레드에서 trivy --download-db-only를 호출합니다. 첫 다운로드는 약 500MB로, 일반 네트워크에서 1~3분이 소요됩니다. 다운로드는 백그라운드로 진행되므로 Celery는 즉시 태스크를 받기 시작합니다 — 부트스트랩과 경합하는 스캔은 Trivy 자체 파일 락에 잠깐 블로킹(분 단위 아님)된 뒤 새 manifest를 읽습니다. 캐시 볼륨이 이미 채워진 워커 재시작에서는 다운로드가 no-op입니다(Trivy가 manifest를 stat해 0으로 종료).
  2. 주간 갱신. Celery Beat 태스크(trustedoss.trivy_db_refresh, 일요일 03:00 UTC)가 동일 trivy --download-db-only를 실행합니다. Trivy의 manifest 계약은 부분 다운로드를 원자적으로 교체합니다 — 갱신 실패는 기존 DB를 그대로 둡니다. 취약점 재매칭 beat(trustedoss.vulnerability_rematch_enqueue, 6시간 주기)가 다음 tick에 새 어드바이저리를 픽업해 새 critical을 Slack / Teams 알림으로 노출합니다.
  3. 스캔별 사용. 각 source-scan 파이프라인은 trivy sbom <cyclonedx.json>을 호출해 컴포넌트를 로컬 DB에 대조합니다. 스캔당 네트워크 왕복은 없습니다.

갱신 실패(타임아웃, 네트워크 미도달, 미러 인증 오류)는 /notifications에 설정된 Slack·Teams 채널로 TRIVY-DB-REFRESH-FAILED / TRIVY-DB-REFRESH-TIMEOUT 알림을 발사합니다 — 운영자는 웹훅 이벤트로 staleness를 인지하며 조용히 누락되지 않습니다.

업스트림 DB 재빌드 주기는 약 6시간입니다. 주간 갱신은 신선도와 egress 바이트를 트레이드오프합니다. 더 빠른 주기가 필요하면 TRIVY_DB_REFRESH_HOURS를 낮추고(admin 패널 + next_refresh_at에 노출되는 안내값) apps/backend/tasks/celery_app.py의 Beat 스케줄을 일간 cron으로 수정하세요.

환경 변수

기본값설명
TRIVY_DB_REPOSITORYghcr.io/aquasecurity/trivy-dbDB를 받아오는 OCI 저장소. air-gapped 미러로 오버라이드.
TRIVY_DB_REFRESH_HOURS168 (주간)admin/health Trivy DB 패널에 next_refresh_at = last_update + 이 값으로 노출되는 안내 주기. 실제 beat 스케줄은 crontab(minute=0, hour=3, day_of_week='sun') — 더 빠른 주기는 apps/backend/tasks/celery_app.py에서 직접 수정.
TRIVY_CACHE_DIR/var/lib/trivyDB가 풀리는 디렉터리. 두 compose 파일 모두 공유 trivy-cache named volume을 여기 마운트 — 워커(rw, 다운로드·갱신)와 backend(ro, 관리자 health/disk 패널).
TRIVY_DB_BOOTSTRAP_ON_STARTtrue워커의 worker_ready 핸들러가 부팅 시 trivy --download-db-only를 실행할지. 별도 프로세스가 캐시 볼륨에 DB를 미러링하는 air-gapped 클러스터에선 false로 설정 — 워커는 네트워크 pull을 시도하지 않음.
TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS900부팅 시 다운로드 subprocess의 wall-clock 한도. 느린 회사 프록시에선 상향. 범위 [30, 3600].
TRIVY_DB_REFRESH_TIMEOUT_SECONDS900주간 beat refresh subprocess의 wall-clock 한도. 범위 [30, 3600].
TRIVY_TIMEOUT_SECONDS300trivy sbom의 스캔별 타임아웃. 대규모 모놀리스에선 상향.

모든 키는 apps/backend/core/config.py가 런타임에 읽으며, 워커·beat 컨테이너 재시작만으로 반영됩니다 — 리빌드 불필요.

동작 확인

설치 직후(또는 TRIVY_* 키 수정 후):

# 1. 워커에 DB가 있는지 확인
docker-compose -f docker-compose.yml exec worker \
ls -lh /var/lib/trivy/db/

# 2. DB 메타데이터(마지막 갱신 시각·출처) 확인
docker-compose -f docker-compose.yml exec worker \
trivy --quiet image --download-db-only --skip-update && \
trivy --version

# 3. 작은 스캔으로 동작 확인
docker-compose -f docker-compose.yml exec worker \
trivy sbom --format json /tmp/sample-cyclonedx.json | jq '.Results | length'

정상 설치는 db/ 하위 파일 수가 0이 아니고, Created 타임스탬프가 최근(≤ 7일)이며, 동작 확인 명령이 최소 1개의 Results 배열을 반환합니다.

관리자 UI 패널

/admin/health 하위의 Trivy DB 카드가 마지막 갱신 시각, 취약점 수, 신선도(fresh / stale / very_stale / unknown), 설정된 주기, 캐시 디렉터리, 미러 저장소를 노출합니다. 패널은 디스크의 metadata.json을 직접 읽으므로(라이브 네트워크 프로브 없음) air-gapped 클러스터에서도 유용합니다.

/admin/health — 캐시 디렉터리와 미러 저장소 푸터가 있는 Trivy 취약점 DB 카드 (방금 부팅된 워커의 empty-state 표시)

Air-gapped 운영

워커 호스트가 ghcr.io에 접근할 수 없다면 두 가지 지원 경로 중 운영 모델에 맞는 쪽을 선택하세요.

  • 경로 A — 사내 OCI 미러. trivy-db 사본을 네트워크가 접근 가능한 사내 OCI 레지스트리에 유지. 워커는 여전히 trivy --download-db-only(부트스트랩 + 주간 갱신)를 실행하되 미러에서 pull. 대부분의 엔터프라이즈에서 권장 — Trivy의 manifest 계약이 pull마다 원자적 교체와 무결성 검증을 유지.
  • 경로 B — 완전 오프라인 캐시 볼륨. 워커 내부 다운로드 경로 전체를 비활성화(TRIVY_DB_BOOTSTRAP_ON_START=false)하고, 연결된 호스트에서 sneaker-net으로 가져온 tarball로 캐시 볼륨을 사전 채움. 사내 레지스트리조차 air gap 반대편에 있을 때 사용.

경로 A — 사내 OCI 미러

1. DB 미러링

ghcr.io에 접근 가능한 호스트에서 실행:

# oras 설치 (1회)
curl -fsSL https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz \
| tar -xz oras
sudo mv oras /usr/local/bin/

# 업스트림 DB pull
oras pull ghcr.io/aquasecurity/trivy-db:2

# 사내 레지스트리에 re-push
oras push registry.internal.acme.com/trivy/trivy-db:2 \
db.tar.gz:application/vnd.aquasec.trivy.db.layer.v1.tar+gzip

Trivy DB는 멀티 아키 OCI 아티팩트로 :1·:2 태그가 있습니다. 포털은 현재 :2(Trivy v2 스키마)를 사용하므로, 포털 버전이 요구하는 동일 태그로 고정하세요.

2. 워커가 미러를 바라보게 설정

.env:

TRIVY_DB_REPOSITORY=registry.internal.acme.com/trivy/trivy-db

미러가 인증을 요구하면 워커 호스트에서 1회 로그인:

docker-compose -f docker-compose.yml exec worker \
trivy registry login --username svc-trivy --password-stdin \
registry.internal.acme.com < /run/secrets/trivy_registry_pw

Trivy는 컨테이너 내부 ~/.docker/config.json에 자격 증명을 저장합니다. 컨테이너 재시작 후에도 유지하려면 해당 경로를 호스트 볼륨에서 마운트하세요.

3. 재시작 + 재다운로드

docker-compose -f docker-compose.yml restart worker beat
docker-compose -f docker-compose.yml exec worker \
trivy --download-db-only

미러를 통한 첫 다운로드는 크기·SHA가 업스트림 blob과 일치해야 합니다 — Trivy는 교체 전 manifest를 검증합니다.

4. 미러 갱신 스케줄

미러 pull 명령을 인터넷 egress가 가능한 bastion에서 cron(보통 일 1회)으로 돌려, 사내 미러가 업스트림을 따라가게 하세요.

# /etc/cron.d/trivy-db-mirror
0 4 * * * trivyops oras pull ghcr.io/aquasecurity/trivy-db:2 \
&& oras push registry.internal.acme.com/trivy/trivy-db:2 db.tar.gz

이후 포털의 주간 갱신이 사내 미러에서 pull하므로, 포털 네트워크의 어떤 호스트도 인터넷 egress가 필요하지 않습니다.

미러 태그 드리프트

Trivy DB 태그는 캘린더 날짜가 아닌 스키마 버전을 따릅니다. Aqua가 :3(스키마 bump)을 게시해도 :2로 고정된 포털 워커는 포털 업그레이드 전까지 계속 동작합니다. 업그레이드 후 빈 Results를 피하려면 미러 갱신과 포털 업그레이드를 함께 조정하세요.

경로 B — 완전 오프라인 캐시 볼륨

사내 레지스트리조차 워커에서 air-gap된 경우(워커가 어떤 OCI 엔드포인트와도 통신할 수 없는 네트워크 분리 구간), 워커의 네트워크 pull 경로를 비활성화하고 연결된 호스트에서 캐시 볼륨을 사전 채우세요.

1. 워커 측 부트스트랩 + beat 비활성화

.env:

# worker_ready 부팅 다운로드 끔.
TRIVY_DB_BOOTSTRAP_ON_START=false

beat 이미지를 리빌드해 apps/backend/tasks/celery_app.py::_build_beat_scheduletrivy-db-refresh-weekly 항목을 주석 처리하거나, 더 간단히 — beat 스케줄을 그대로 둡니다. 네트워크 egress가 없으면 beat tick은 발사되지만 subprocess가 실패하고 실패 알림이 캐시 갱신을 상기시킵니다. 두 형태 모두 유효하며 첫 번째가 Slack 측에서 더 조용하고, 두 번째가 능동적인 알림을 줍니다.

2. 연결된 호스트에서 캐시 사전 채우기

ghcr.io 접근 가능한 호스트(빌드 서버, 개발자 노트북, 연결된 bastion)에서 실행:

# trivy + TRIVY_CACHE_DIR 레이아웃을 가진 일회성 컨테이너 기동.
docker run --rm \
-v trivy-cache-export:/var/lib/trivy \
-e TRIVY_CACHE_DIR=/var/lib/trivy \
ghcr.io/trustedoss/trusca-backend-worker:0.11.0 \
trivy --quiet image --download-db-only

# 채워진 볼륨을 tarball로 패킹.
docker run --rm \
-v trivy-cache-export:/cache:ro \
-v "$PWD":/out \
alpine \
tar -C /cache -czf /out/trivy-db.tar.gz .

3. air-gapped 워커 호스트로 전송 + 로드

trivy-db.tar.gz를 air gap을 가로질러(USB, 사내 아티팩트 스토어, 승인된 파일 전송 채널) 복사한 뒤 워커 캐시 볼륨에 로드:

# air-gapped 호스트에서. trivy가 read 중이지 않도록 워커 정지.
docker-compose -f docker-compose.yml stop worker

# tarball을 named 캐시 볼륨 마운트에 로드.
docker run --rm \
-v trustedoss_trivy-cache:/cache \
-v "$PWD":/in:ro \
alpine \
sh -c "cd /cache && tar -xzf /in/trivy-db.tar.gz"

# 워커 재시작. 부트스트랩 핸들러는 비활성화돼 있으므로 캐시를 그대로 사용.
docker-compose -f docker-compose.yml start worker

4. 갱신 주기

admin/health Trivy DB 패널이 last_updatefreshness를 노출합니다. 보안 정책에 맞는 주기(보통 주간)로 export-전송-로드 파이프라인을 스케줄링하세요. 운영자가 유지할 두 습관:

  • 스왑 이전에 패널의 last_update를 기록 — 알려진 양호 상태로 롤백 가능.
  • 저트래픽 시간대에 스왑. 워커가 잠시 정지하지만 실행 중 스캔(스캔 태스크의 워크스페이스 마무리가 보장)은 살아남고, 큐 대기 스캔은 재시작 완료까지 대기.
볼륨 이름

워커 측 named volume은 기본 docker-compose 네이밍(프로젝트 prefix trustedoss + docker-compose.ymltrivy-cache: 선언) 하에서 trustedoss_trivy-cache입니다. 다른 COMPOSE_PROJECT_NAME을 쓴다면 -v <prefix>_trivy-cache:/cache 인자를 조정하세요. 호스트에서 docker volume ls로 확정된 이름을 확인하세요.

데이터 출처

Trivy DB는 5개 공개 피드를 통합합니다. 출처별 커버리지 매트릭스·갱신 주기·생태계 매핑은 데이터 출처 reference를 참조하세요.

출처커버리지갱신
NVD (NIST)전체 CVE ID, CVSS v3업스트림 약 6시간
OSV (Google)생태계별 취약점 (npm, PyPI, Maven, Go 등)연속
GHSA (GitHub)어드바이저리 메타데이터, 수정 버전연속
EPSS (FIRST)30일 익스플로잇 확률일간
KEV (CISA)Known Exploited Vulnerabilities 카탈로그발표 시

트러블슈팅

먼저 확인할 로그
  • docker-compose logs --tail=200 worker | grep trivy — 부팅 시 다운로드 + 스캔별 호출.
  • docker-compose logs --tail=200 beat | grep trivy_db_refresh — 주간 갱신 스케줄.
  • 워커 컨테이너: ls -lh /var/lib/trivy/db/ + cat /var/lib/trivy/db/metadata.json.

첫 부팅에서 DB 다운로드 실패

증상: 워커 컨테이너가 crash loop, 로그에 trivy: failed to download vulnerability DB.

흔한 원인:

  • ghcr.io로의 outbound HTTPS 차단. 확인:
    docker-compose -f docker-compose.yml exec worker \
    curl -fsS https://ghcr.io/v2/ -o /dev/null -w "%{http_code}\n"
    200을 기대. 워커가 air-gapped면 미러로 전환 — Air-gapped 운영 참조.
  • 회사 프록시 미적용. Trivy는 워커 환경의 HTTP_PROXY / HTTPS_PROXY를 읽습니다. .env에 설정 후 재시작.
  • /var/lib/trivy 디스크 부족. DB는 ~500MB. 최소 2GB 여유 확보. docker-compose -f docker-compose.yml exec worker df -h /var/lib/trivy.

DB 손상 — unexpected EOF 또는 invalid manifest

이전 실행의 부분 다운로드가 잘린 아카이브를 남겼을 수 있습니다. 비우고 재다운로드:

docker-compose -f docker-compose.yml exec worker \
rm -rf /var/lib/trivy/db
docker-compose -f docker-compose.yml restart worker

Trivy는 다운로드 시 manifest 체크섬을 검증합니다 — 교체 성공이면 새 DB는 정상. 오류가 반복되면 업스트림 아티팩트를 의심 — 다른 네트워크에서 oras pull로 sanity check.

사설 미러 인증 실패

증상: trivy: failed to pull image: unauthorized.

워커 컨테이너 내부에 자격 증명이 있는지 확인:

docker-compose -f docker-compose.yml exec worker \
cat /root/.docker/config.json

파일이 없거나 미러 호스트 항목이 없으면 trivy registry login을 재실행(위 2단계 참조). docker-compose down 이후에도 자격 증명을 유지하려면 /root/.docker를 호스트 볼륨에서 마운트.

주간 갱신이 건너뜀

증상: /var/lib/trivy/db/metadata.jsonCreated 타임스탬프가 TRIVY_DB_REFRESH_HOURS보다 오래됨, 또는 /admin/health Trivy DB 패널이 freshness: stale / very_stale을 표시.

  • beat이 태스크를 스케줄링하는지:
    docker-compose -f docker-compose.yml logs --tail=200 beat | grep trivy_db_refresh
  • 워커가 큐를 소비하는지 (다른 장기 실행 태스크가 슬롯을 점유하지 않는지) 확인.
  • 일회성 갱신 강제:
    docker-compose -f docker-compose.yml exec worker \
    celery -A tasks.celery_app call trustedoss.trivy_db_refresh
  • 실패/타임아웃 갱신은 cve_id: TRIVY-DB-REFRESH-FAILED(또는 -TIMEOUT) Slack·Teams 알림을 발사합니다. 패널이 staleness를 표시하는데 알림을 받지 못했다면 /notifications 로그 확인 — 잘못 설정된 웹훅 URL이 실패를 가립니다.

부팅 다운로드가 막힌 듯/지연됨

증상: 새로 배포된 워커는 정상이지만 첫 스캔이 평소보다 3분 이상 늦음.

부팅 시 다운로드는 백그라운드 스레드에서 실행됩니다(roadmap) — 워커는 다운로드가 진행되는 동안에도 정상이며 큐를 소비합니다. 스캔별 trivy sbom 호출은 다운로드가 완료될 때까지 Trivy 자체 파일 락에 블록되며 보통 1~3분 안에 풀립니다. 네트워크가 느리면 .env에서 TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS(기본 900초, 범위 [30, 3600])를 상향한 뒤 워커 재시작.

부트스트랩이 실패하면 워커가 trivy_db_bootstrap_degraded를 status(failed/timeout) + 오류 tail과 함께 로깅합니다. 스캔은 캐시 볼륨의 기존 DB(있다면)로 계속 진행 — 워커가 크래시하지 않습니다. 네트워크 egress(curl https://ghcr.io/v2/)를 점검한 뒤 워커를 재시작(부트스트랩 재발사)하거나 위 수동 갱신 명령을 실행하세요.

대용량 SBOM에서 trivy sbom 타임아웃

스캔별 한도 상향:

# .env
TRIVY_TIMEOUT_SECONDS=900

워커 재시작. 기본 300초는 10 000개 컴포넌트까지의 SBOM을 커버합니다. 매우 큰 모노레포는 600900초가 필요할 수 있습니다.

알림

취약점 이벤트의 알림 트리거는 /notifications에서 설정합니다. kind enum은 6개 값:

kind트리거
scan_completed스캔 성공 종료
scan_failed스캔이 failed로 종료
cve_detected기존 프로젝트에 새 CVE 탐지 (Celery beat 재매칭 기반 — 재탐지 참조)
license_violation스캔에서 금지/조건부 라이선스 관측
approval_pending결정 대기 중인 컴포넌트 승인
policy_gate_failed빌드 게이트(POST /v1/scans/{id}/policy-gate)가 block 반환

채널: 이메일(SMTP), Slack 웹훅, MS Teams 웹훅. .env에서 설정(SMTP_*, SLACK_WEBHOOK_URL, TEAMS_WEBHOOK_URL).

참고