의존성 해석용 사설 레지스트리
소스 스캔은 클론한 저장소에서 cdxgen을 실행하고, cdxgen은 빌드가 하는 것과
같은 방식으로 mvn·npm·pip를 호출해 프로젝트의 의존성 트리를 해석합니다. 이
의존성 중 일부가 사설 레지스트리(사내 Nexus/Artifactory 미러, 범위가 지정된 npm
레지스트리, 사설 PyPI 인덱스)에 있다면 워커가 그 레지스트리의 자격증명을 가지고
있어야 합니다. 없으면 해석이 중간에 실패하고, 스캔이 만든 SBOM에는 접근하지 못한
의존성이 빠집니다.
이 기능은 사설 레지스트리와 다른 기능입니다. 그쪽(ER3)은 컨테이너 스캔에서 Trivy가 컨테이너 이미지를 pull할 때 인증합니다. 이 문서는 소스 스캔에서 cdxgen이 프로젝트 의존성을 해석할 때 인증하는 방법을 다룹니다. 두 자격증명은 저장 방식도, 쓰이는 도구도 다릅니다.
규칙
다음 네 파일 중 필요한 것만 한 디렉터리 아래에 둡니다.
| 파일 | 생태계 |
|---|---|
settings.xml | Maven |
.npmrc | npm |
pip.conf | pip |
.netrc | 범용 HTTP/Git 기본 인증. 예를 들어 cdxgen의 Go 분석기가 호출하는 git/go list가 접근하는 사설 Go 모듈 VCS 호스트 |
네 개를 다 둘 필요는 없습니다. cdxgen은 나머지가 없어도 개의치 않습니다. 워커는 이
디렉터리를 읽기 전용으로 /etc/trusca/registry라는 고정 경로에 마운트합니다.
경로를 설정 가능하게 두지 않고 고정한 이유는 임의 파일 읽기로 이어지지 않게
하기 위해서입니다. 프로젝트나 스캔 요청의 어떤 값도 이 경로에 무엇이 마운트되는지
정하지 못하고, 오직 운영자의 배포 설정만 정합니다.
파일 내용 예시
settings.xml (Maven)
Maven은 특정 저장소 조회에 어떤 <server> 자격증명을 보낼지 저장소의 <url>이
아니라 <id>로 판단합니다. 스캔 대상 프로젝트의 pom.xml은 원하는 <id>와
<url>을 가진 <repository>를 얼마든지 선언할 수 있고, 그 <id>가 여러분의
settings.xml에 있는 <server>의 id와 우연히 겹치면 Maven은 선언된 저장소와
실제 저장소를 구분하지 못하고 그 자격증명을 그쪽으로 보냅니다. 이런 이유로
<server>만 두는 방식에 기대면 안 됩니다. 대신 <mirror mirrorOf="*">로 모든
조회를 신뢰하는 URL 하나에 고정하세요. 이렇게 하면 프로젝트가 선언한
<repository>는 무시되고, <id>가 가로챌 대상 자체가 남지 않습니다.
<settings>
<mirrors>
<mirror>
<id>internal-mirror</id>
<mirrorOf>*</mirrorOf>
<url>https://nexus.internal.example.com/repository/maven-public/</url>
</mirror>
</mirrors>
<servers>
<server>
<id>internal-mirror</id>
<username>svc</username>
<password>S3cret</password>
</server>
</servers>
</settings>
mirror 없이 <server>만 두면 어떤 문제가 생기는지는 아래 "알려진 한계"를
참고하세요.
.npmrc (npm)
자격증명은 //호스트/경로/:_authToken= 형태로 정확한 레지스트리 호스트에
범위를 지정하세요. 범위를 지정하지 않은 _auth나 _authToken 줄은 이번
설치에서 npm이 조회하는 모든 레지스트리에 적용됩니다. 스캔 대상 프로젝트의
package.json이 publishConfig.registry 필드로 지정한 레지스트리도
포함되므로, 워커에 마운트하는 자격증명에는 범위를 지정하지 않은 형태를 절대
쓰지 마세요.
@myorg:registry=https://registry.internal.example.com/
//registry.internal.example.com/:_authToken=${NPM_TOKEN}
파일 넣기
Docker Compose
.env에 REGISTRY_CONFIG_HOST_PATH를 설정하고(기본값 ./secrets/registry) 그
경로에 파일을 둡니다.
mkdir -p ./secrets/registry
cp /path/to/settings.xml ./secrets/registry/settings.xml
cp /path/to/.npmrc ./secrets/registry/.npmrc
docker-compose.yml은 이 디렉터리를 스캔 파이프라인 워커(프로덕션은
worker-scan, dev는 celery-worker)에 읽기 전용으로 이미 마운트해 둡니다.
디렉터리가 없거나 비어 있어도 문제없습니다. 이 저장소의 cosign 키 마운트와 같은
방식입니다.
.netrc는 가리키는 환경변수가 없는 유일한 파일이라(아래 참고) 별도의 명시적
마운트가 한 줄 더 필요합니다. docker-compose.yml과 docker-compose.dev.yml
모두 디렉터리 마운트 바로 옆에 이 줄을 주석으로 넣어 두었습니다. 파일을 만든
뒤에만 주석을 해제하세요.
- ${REGISTRY_CONFIG_HOST_PATH:-./secrets/registry}/.netrc:/home/trustedoss/.netrc:ro
호스트에 파일이 없는 채로 주석만 해제하면 아무 일도 일어나지 않는 게 아니라 문제가 생깁니다. Docker는 없는 바인드 마운트 원본을 빈 디렉터리로 만들어 버리고, 그 디렉터리가 해당 경로를 파일로 기대하는 모든 것을 가립니다. 파일을 먼저 만드세요.
Helm
Secret을 직접 만들고, 차트가 스캔 워커에만 마운트하게 합니다.
kubectl create secret generic trustedoss-registry-config \
--from-file=settings.xml=./settings.xml \
--from-file=.npmrc=./.npmrc
worker:
scan:
extraVolumes:
- name: registry-config
secret:
secretName: trustedoss-registry-config
extraVolumeMounts:
- name: registry-config
mountPath: /etc/trusca/registry
readOnly: true
extraEnv:
MVN_ARGS: "--settings /etc/trusca/registry/settings.xml"
NPM_CONFIG_USERCONFIG: /etc/trusca/registry/.npmrc
PIP_CONFIG_FILE: /etc/trusca/registry/pip.conf
worker.scan.*은 스캔 파이프라인 워커에만 적용됩니다.
사설 인증기관에 쓰는, 백엔드·beat·워커 둘 다에 적용되는
차트 전체용 env.extraVolumes/env.extraEnv와는 다릅니다. cdxgen을 실행하지
않는 파드가 레지스트리 자격증명을 읽을 이유는 없습니다.
.netrc는 같은 Secret을 subPath를 지정해 한 번 더 마운트해서
/etc/trusca/registry가 아니라 $HOME/.netrc(이 이미지의 HOME은
/home/trustedoss)에 오도록 합니다.
extraVolumeMounts:
- name: registry-config
mountPath: /home/trustedoss/.netrc
subPath: .netrc
readOnly: true
도구가 그 파일을 보게 하기
디렉터리를 마운트하는 것만으로는 아무것도 바뀌지 않습니다.
/etc/trusca/registry를 저절로 읽는 도구는 없습니다. settings.xml,
.npmrc, pip.conf는 각각 해당 도구에게 어디를 볼지 알려 주는 변수가 따로
필요합니다.
| 변수 | 읽는 주체 | 효과 |
|---|---|---|
MVN_ARGS | cdxgen 자체의 Maven 호출 | mvn 명령줄에 붙는 추가 인자입니다. --settings /etc/trusca/registry/settings.xml로 설정하세요. |
NPM_CONFIG_USERCONFIG | npm(표준 npm 환경변수) | 대체 .npmrc 경로. |
PIP_CONFIG_FILE | pip(표준 pip 환경변수) | 대체 pip.conf 경로. |
필요한 만큼 .env에 추가합니다.
MVN_ARGS=--settings /etc/trusca/registry/settings.xml
NPM_CONFIG_USERCONFIG=/etc/trusca/registry/.npmrc
PIP_CONFIG_FILE=/etc/trusca/registry/pip.conf
MAVEN_SETTINGS라는 환경변수는 존재하지 않고, 설정해도 아무 효과가 없습니다.
Maven 자체는 ~/.m2/settings.xml이나 -s/--settings 명령줄 플래그만 읽습니다.
환경변수 형태는 없습니다. MVN_ARGS는 cdxgen이 대신 실행하는 mvn 호출에 추가
인자를 넘기는 cdxgen 자체의 방식이고, --settings <경로>가 바로 그 플래그입니다.
(cdxgen은 스캔 중인 pom.xml 옆에서 settings.xml을 발견하면 콘솔 출력에 이
방법을 그대로 안내합니다.)
NPM_CONFIG_USERCONFIG와 PIP_CONFIG_FILE은 cdxgen의 기능이 아닙니다. npm과
pip가 각자 문서화한, 대체 설정 파일 경로를 가리키는 환경변수입니다. cdxgen은
npm install·pip install을 호출할 때 워커의 환경을 그대로 물려줄 뿐이고, 이는
다른 서브프로세스와 다르지 않습니다.
.netrc는 자신을 가리키는 변수가 아예 없습니다. .netrc를 따르는 모든 도구
(git, go, curl 기반 페처)는 조건 없이 $HOME/.netrc를 읽으므로, 환경변수가
아니라 위에서 설명한 두 번째 명시적 마운트가 필요합니다.
알려진 한계
- 위 예시의
<mirror mirrorOf="*">없이settings.xml에<server>만 두면<url>이 아니라<id>로 매칭됩니다. Maven은 어떤<server>자격증명을 보낼지 저장소의<id>만 보고 정합니다. 스캔 대상 프로젝트의 악성pom.xml이<repository><id>internal-nexus</id><url>https://attacker.example.com/</url></repository>를 선언할 수 있고, 여러분의settings.xml에id가internal-nexus인<server>가 있다면(흔히 고를 법한 이름이라 실제로 겹치기 쉽습니다) Maven은 그 자격증명을 실제 저장소가 아니라 공격자의 URL로 보냅니다.<mirror mirrorOf="*">형태는 프로젝트가 무엇을 선언하든 모든 조회를 URL 하나에 고정합니다.<server>만 두지 말고 이 형태를 쓰세요. - 프로젝트나 팀별이 아니라 워커 하나에 자격증명 하나입니다. 조직별로 나뉘고
pull 대상 이미지와 매칭하는 컨테이너 이미지 레지스트리 자격증명(ER3)과 달리,
/etc/trusca/registry아래 것은 그 스캔 파이프라인 워커가 실행하는 모든 소스 스캔에 똑같이 보입니다. 팀마다 다른 사설 레지스트리 자격증명이 필요하다면 현재는 워커 전체가 하나를 공유합니다. 프로젝트 단위 범위 지정은 구현되어 있지 않습니다. - 읽기 전용이고, 스캔 입력에서 결정되지 않습니다. 마운트 경로는 배포 시점에 운영자가 고정합니다. 프로젝트 설정이나 API 필드, 스캔 트리거 파라미터 어느 것도 무엇이 마운트되고 읽히는지 바꿀 수 없습니다.
- 망 분리 환경에서도 이 문서가 필요합니다. 오프라인 설치 번들은 네트워크에 전혀 접근할 수 없는 호스트를 위해 워커 이미지와 Trivy 취약점 데이터베이스를 패키징하는 문제를 다루며, 이는 도달은 가능하지만 자격증명이 필요한 사내 미러에 인증하는 문제와는 다릅니다. 네트워크 구성에 따라 둘 다 필요할 수도, 하나만 필요할 수도 있습니다.