Maven 빌드 → Docker 이미지 → Harbor(general) 저장 → NeuVector 스캔 → 통과 시 Harbor(checked) 승격 + GitOps 업데이트
general은 빌드 직후 원본, checked는 보안 스캔 통과 후에만 입장 가능한 "프로덕션 허용 저장소"다. NeuVector Admission Control이 checked 이미지만 클러스터에 배포되도록 강제한다.
각 스테이지를 클릭하거나 버튼으로 단계별 탐색
소스 코드를 컴파일하고 JAR/WAR 아티팩트를 생성한다. mvn clean package -DskipTests — 테스트는 별도 stage에서 돌리거나 CI 초기 단계에서 처리.
SUSE Private Registry(Harbor)에 두 개의 Project를 만들어 스캔 통과 전/후를 분리한다
general과 checked다. Jenkins가 빌드 후 harbor.example.com/general/myapp:v1.0에 푸시, 스캔 통과 후 Harbor API로 checked 프로젝트에 복사(artifact copy)한다.Harbor v2 API의 from 쿼리 파라미터로 프로젝트 간 이미지를 복사한다. 레이어를 재업로드하지 않고 메타데이터 참조만 복사되므로 빠르다.
# Harbor Basic Auth (robot account 권장) HARBOR_USER="robot$jenkins" HARBOR_PASS="<robot-token>" HARBOR_HOST="harbor.example.com" IMAGE_TAG="${BUILD_NUMBER}" # general → checked 복사 curl -sk -X POST \ "https://${HARBOR_HOST}/api/v2.0/projects/checked/repositories/myapp/artifacts?from=general%2Fmyapp%3A${IMAGE_TAG}" \ -u "${HARBOR_USER}:${HARBOR_PASS}" \ -H "Content-Type: application/json" \ -w "\n%{http_code}" # 201 이면 성공, 나머지는 실패 처리
from 파라미터 값은 URL 인코딩 필요. / → %2F, : → %3A. Jenkins pipeline에서 sh로 실행 시 curl 응답 코드 체크 후 실패 처리할 것.두 가지 방법: Jenkins 공식 플러그인(권장) vs REST API 직접 호출
Jenkins 공식 플러그인으로 스캔 임계값 초과 시 빌드 자동 실패. 별도 스크립트 없이 선언적 파이프라인에서 사용 가능.
// Jenkinsfile — NeuVector Plugin 사용 stage('NeuVector Scan') { steps { neuvector( // Jenkins 시스템 설정에 등록한 NeuVector 컨트롤러 이름 controllerEndpointUrlSelection: 'neuvector-prod', // NeuVector에 등록된 Harbor 레지스트리 이름 registrySelection: 'suse-harbor-general', repository: "myapp", tag: "${BUILD_NUMBER}", // 스캔 대기 타임아웃 (분) scanTimeout: 10, // 임계값 초과 시 빌드 FAIL numberOfHighSeverityToFail: 5, // High CVE 5개 초과 시 실패 numberOfMediumSeverityToFail: 15, // Medium CVE 15개 초과 시 실패 // 특정 CVE 번호가 있으면 무조건 실패 nameOfVulnerabilityToFailOne: 'CVE-2024-1234' ) } } // 플러그인이 임계값 초과 감지 시 currentBuild.result = 'FAILURE' 자동 설정
registrySelection에 그 이름 입력.플러그인 없이 curl로 직접 NeuVector API를 호출하는 방식. 세밀한 제어 가능하지만 코드량이 많다.
#!/bin/bash — NeuVector API 스캔 스크립트 NV_HOST="https://neuvector-svc.cattle-neuvector-system:10443" HARBOR_HOST="harbor.example.com" # 1. 인증 TOKEN=$(curl -sk -X POST "${NV_HOST}/v1/auth" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin"}' \ | jq -r '.token.token') # 2. 스캔 요청 (long-poll — 완료까지 대기) SCAN_RESULT=$(curl -sk -X POST "${NV_HOST}/v1/scan/repository" \ -H "Content-Type: application/json" \ -H "X-Auth-Token: ${TOKEN}" \ --max-time 300 \ -d "{ \"request\": { \"registry\": \"https://${HARBOR_HOST}\", \"repository\": \"general/myapp\", \"tag\": \"${BUILD_NUMBER}\", \"username\": \"robot\$jenkins\", \"password\": \"${HARBOR_ROBOT_TOKEN}\", \"scan_layers\": true } }") # 3. 결과 파싱 HIGH_COUNT=$(echo $SCAN_RESULT | jq '[.report.vulnerabilities[]|select(.severity=="High")]|length') CRITICAL_COUNT=$(echo $SCAN_RESULT | jq '[.report.vulnerabilities[]|select(.severity=="Critical")]|length') echo "High: ${HIGH_COUNT}, Critical: ${CRITICAL_COUNT}" # 4. 로그아웃 curl -sk -X DELETE "${NV_HOST}/v1/auth" -H "X-Auth-Token: ${TOKEN}" # 5. 임계값 판정 if [ $CRITICAL_COUNT -gt 0 ] || [ $HIGH_COUNT -gt 5 ]; then echo "SCAN FAILED: Critical=${CRITICAL_COUNT}, High=${HIGH_COUNT}" exit 1 # Jenkins 스테이지 실패 처리 fi
Critical CVE 존재? OR High CVE > 임계값? OR 특정 CVE 번호 포함?
Harbor checked 프로젝트로 이미지 복사
gitops-repo YAML 업데이트
Fleet 자동 배포 트리거
general 이미지 삭제 (선택)
개발팀에 CVE 목록 통보
클러스터에 배포되지 않음
복사해서 바로 사용 가능한 구조. 변수값만 환경에 맞게 교체.
pipeline { agent any environment { HARBOR_HOST = 'harbor.example.com' APP_NAME = 'myapp' GITEA_HOST = 'gitea.example.com' GITOPS_REPO = 'test/gitops-repo' YAML_PATH = 'k8s/deployment.yaml' IMAGE_TAG = "${BUILD_NUMBER}" } stages { // ─── Stage 1: Maven Build ────────────────────────────── stage('Maven Build') { steps { sh 'mvn clean package -DskipTests' } } // ─── Stage 2: Docker Build + Push to general ─────────── stage('Docker Build & Push (general)') { steps { withCredentials([usernamePassword( credentialsId: 'harbor-robot-creds', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS' )]) { sh ''' docker login -u "${HARBOR_USER}" -p "${HARBOR_PASS}" ${HARBOR_HOST} docker build -t ${HARBOR_HOST}/general/${APP_NAME}:${IMAGE_TAG} . docker push ${HARBOR_HOST}/general/${APP_NAME}:${IMAGE_TAG} ''' } } } // ─── Stage 3: NeuVector Scan ─────────────────────────── stage('NeuVector Image Scan') { steps { neuvector( controllerEndpointUrlSelection: 'neuvector-prod', registrySelection: 'suse-harbor-general', repository: "${APP_NAME}", tag: "${IMAGE_TAG}", scanTimeout: 10, numberOfHighSeverityToFail: 5, numberOfMediumSeverityToFail: 15 ) // 임계값 초과 → 자동으로 빌드 FAILURE 처리됨 } } // ─── Stage 4: Promote to checked ─────────────────────── stage('Promote to checked (Harbor API)') { when { // 스캔 통과한 경우(SUCCESS)에만 실행 expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' } } steps { withCredentials([usernamePassword( credentialsId: 'harbor-robot-creds', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS' )]) { script { // URL 인코딩: / → %2F, : → %3A def fromParam = "general%2F${APP_NAME}%3A${IMAGE_TAG}" def status = sh( script: """ curl -sk -o /dev/null -w "%{http_code}" -X POST \\ "https://${HARBOR_HOST}/api/v2.0/projects/checked/repositories/${APP_NAME}/artifacts?from=${fromParam}" \\ -u "${HARBOR_USER}:${HARBOR_PASS}" \\ -H "Content-Type: application/json" """, returnStdout: true ).trim() if (status != '201') { error("Harbor promote failed: HTTP ${status}") } echo "✅ Image promoted to checked/${APP_NAME}:${IMAGE_TAG}" } } } } // ─── Stage 5: GitOps Repo 이미지 태그 업데이트 ───────── stage('Update GitOps Repo') { when { expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' } } steps { withCredentials([string( credentialsId: 'gitea-token', variable: 'GITEA_TOKEN' )]) { sh """ rm -rf gitops-tmp git clone https://${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO}.git gitops-tmp cd gitops-tmp # deployment.yaml의 이미지 태그 업데이트 sed -i "s|image: ${HARBOR_HOST}/checked/${APP_NAME}:.*|image: ${HARBOR_HOST}/checked/${APP_NAME}:${IMAGE_TAG}|" ${YAML_PATH} git config user.email "jenkins@ci.local" git config user.name "Jenkins CI" git add ${YAML_PATH} git commit -m "ci: update ${APP_NAME} to ${IMAGE_TAG} [skip ci]" git push https://${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO}.git main """ } } } } post { always { sh 'docker rmi ${HARBOR_HOST}/general/${APP_NAME}:${IMAGE_TAG} || true' cleanWs() } failure { echo "❌ Pipeline failed at stage: ${currentBuild.result}" } success { echo "✅ Deployed: checked/${APP_NAME}:${IMAGE_TAG}" } } }
sed -i 패턴이 정확히 매칭되어야 한다. YAML에서 이미지 라인이 image: harbor.example.com/checked/myapp:TAG 형태여야 교체 가능. 처음 구성 시 명시적 full image path 사용 권장.Fleet이 감시하는 gitops-repo의 Deployment YAML 예시 — Jenkins가 이미지 태그만 업데이트한다
# gitops-repo/k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: myapp-prod spec: replicas: 2 selector: matchLabels: app: myapp template: metadata: labels: app: myapp # OTel Java agent auto-injection (SUSE Observability) annotations: instrumentation.opentelemetry.io/inject-java: "true" spec: imagePullSecrets: - name: harbor-checked-secret # checked 프로젝트 접근 시크릿 containers: - name: myapp # ↓↓↓ Jenkins가 sed로 이 라인의 태그만 교체 ↓↓↓ image: harbor.example.com/checked/myapp:42 ports: - containerPort: 8080
# gitops-repo/fleet.yaml — Fleet GitOps 설정 defaultNamespace: myapp-prod targetCustomizations: - name: downstream clusterSelector: matchLabels: management.cattle.io/cluster-display-name: k8s-downstream # gitops-repo 최상위에 fleet.yaml 존재해야 Fleet이 인식 # git push 발생 → Fleet Controller가 변경 감지 → k8s-downstream에 적용
image: 라인 태그 교체 → git commit → git push[skip ci]를 붙이지 않으면 gitops-repo의 push가 Jenkins를 다시 트리거할 수 있다. Gitea webhook 설정에서 gitops-repo는 Jenkins를 트리거하지 않도록 분리 구성 권장.클러스터 레벨 배포 게이트웨이 — general 이미지를 클러스터에 직접 올리는 것 자체를 차단
NeuVector는 Kubernetes Validating Admission Webhook으로 동작한다. Pod 배포 요청이 들어오면 NeuVector Controller가 규칙을 검사하고 허용/거부를 결정한다.
클릭해서 뒤집기
?from=)는 레이어를 재전송하지 않고 메타데이터 참조만 복사한다. 스토리지 효율적이고 빠르다.POST /v1/scan/repository는 비동기인가, 동기인가?--max-time을 충분히 크게(300초+) 설정해야 한다.when { expression { ... } }의 역할은?currentBuild.result == 'FAILURE'가 되어 Promote/GitOps 단계를 건너뛴다.imageScanned 조건이 필요한 이유는?imageScanned=true가 없으면 미스캔 이미지가 CVE 기준을 통과한 것처럼 처리될 수 있다.kubectl apply 방식으로 대상 클러스터에 적용한다.DELETE /v1/auth로 세션 정리해야 한다.파이프라인 실행 전 완료해야 할 항목들
imagePullSecrets 시크릿 등록kubectl create secret docker-registry harbor-checked-secret ...