安全开发-Nexus

前言

内网 DevSecOps 体系搭到这一步,还差一个更根本的问题:内网到不了公网,开发需要的一堆“外网的东西“从哪来? Go 依赖、SonarScanner 可执行文件、某个特定版本的 Go 编译器——默认全都去公网下载,内网全都不通。

这篇在内网部署 Nexus 制品仓库(https://nexus.kali-team.internal,3.96.1):Nexus 替你去上游取一份制品、缓存在本地,第二次起就不用出网了。全文按“概念 → 搭建 → 使用 → CI/CD 联动“展开。

1 背景概念

1.1 制品与代理仓库

源代码是自己写的,但把程序跑起来还要从外部下载一堆“拿来就能用的成品“:Go 第三方模块(example 项目有 16 个,含 gin 框架)、SonarScanner 可执行文件、特定版本的 Go 编译器(工具链,go/gofmt 等命令打包成的发行包)。这类统称制品(artifact)。

代理仓库(proxy repository) 就是架在客户端和公网之间的缓存层:客户端请求某个制品,本地没有就去 上游(upstream,公网源站) 取一份、缓存下来再返回;第二次再有人要同一个制品,直接命中缓存。对内网的三重价值:

  1. 可达:任务容器到不了公网,但一定到得了内网的 Nexus;
  2. 快且稳:同一版本的依赖缓存后是本地速度,还省公网流量;
  3. 可控:下载了什么、从哪下的、什么时候下的,全部记录在 Nexus 里,天然是审计点。

1.2 Nexus 与三种仓库类型

Nexus Repository(3.x) 是 Sonatype 的开源制品仓库,支持 Go 模块、Maven、npm、Docker 镜像以及 raw(通用文件) 等格式。本文只用三种能力:Go 模块代理, debian源 和 raw 文件代理(Docker 镜像功能不开——镜像归 Harbor 管)。

仓库分三种核心类型:

仓库类型说明
Proxy(代理)代理外部仓库,缓存下载的制品
Hosted(托管)存储内部私有制品
Group(组)聚合多个仓库,提供统一访问入口

本文建的 6 个仓库全是 Proxy;Hosted/Group 是“下一步“(见小结)。

1.3 Nexus 在体系里的位置

使用方下载什么Nexus 仓库配置在哪
Go 构建(Dockerfile / 本地)Go 模块依赖(gin 等)goproxyGOPROXY 环境变量
Go 模块完整性校验sumdb 校验数据go-sumdbGOSUMDB 环境变量(可选)
需要特定 Go 版本的流水线Go 工具链压缩包go-dev-dlsetup-go 的 go-download-base-url
SonarQube 流水线SonarScanner CLI 安装包sonar-scanner-clisonarqube.yaml 的 scannerBinariesUrl
Dockerfile apt 装包(构建镜像)Debian apt 包(索引文件 + .deb)debian / debian-securityDockerfile 的 sed 源改写

Harbor 管容器镜像,Nexus 管代码级制品,两个仓库分工明确。

2 搭建

版本:Nexus 3.96.1 / nxrm-ha chart 96.1.0,PostgreSQL 18.4 / bitnami chart 18.7.9,经 Rancher + Helm 部署。

2.1 总体思路:要哪些组件

部署分四块,顺序是“先数据库、后 Nexus“(没有库连,Nexus 起不来):

  • PostgreSQL 数据库:Nexus 的元数据(仓库定义、用户、权限、缓存索引)都存这里。给 middleware(中间件)命名空间单装一个共享 PostgreSQL,后面还被 Dependency-Track 复用;
  • Nexus 本体:官方 nxrm-ha Helm chart,放 sonatype 命名空间;
  • chart 源:内网到不了 bitnami 官方 chart 源,PostgreSQL 的 chart 走 daocloud 镜像源(同步了官方 chart 的国内镜像);Nexus 用 Sonatype 官方 chart 源;
  • 入口:域名 nexus.kali-team.internal,证书由内网 CA 签发。

2.2 安装 PostgreSQL(共享数据库)

添加 chart 源(Rancher 界面)。 地址填 daocloud 镜像源 oci://docker.m.daocloud.io/bitnamicharts(OCI 是容器镜像的发布格式规范,Helm chart 现在也能打成 OCI 产物放在镜像仓库兼容的地址上——daocloud 把 bitnami 的 chart 同步到这个地址,内网可达),名称起 postgresql:

Rancher 添加 OCI chart 仓库

图 1:Rancher「添加 Chart 仓库」页:经 daocloud OCI 镜像源(oci://docker.m.daocloud.io/bitnamicharts)添加 PostgreSQL 的 chart 仓库

安装并改关键项。 商店里找到 postgresql chart,版本 18.7.9(对应的 PostgreSQL 实际版本是 18.4,见下面的安装输出),点安装:

Rancher 安装 postgresql chart

图 2:bitnami postgresql chart 18.7.9 的安装页(安装进 middleware 命名空间)

默认值大部分不用动,只改下面几处(- 为 chart 默认值,+ 为本次部署值):

@@ auth 段 @@
-  database: ''
+  database: nexus                    # 预创建名为 nexus 的数据库(Nexus 的元数据库)
-  postgresPassword: ''
+  postgresPassword: <POSTGRES_PASSWORD>   # postgres 超级用户密码(强密码,本文占位)
@@ primary.configuration 段 @@
-  configuration: ''
+  configuration: |
+    listen_addresses = '*'           # 监听所有网卡(默认只监听本机)
+    max_connections = 500            # 最大并发连接数(默认 100)
+    shared_buffers = 256MB           # PG 内存缓冲(默认约为内存的 1/4)
@@ primary.resources / replica.resources 段 @@
-  resources: {}
+  resources:
+    limits:
+      cpu: 2                         # 资源上限:最多用 2 核 / 2Gi 内存
+      memory: 2Gi
+    requests:
+      cpu: 1                         # 资源保底:预留 1 核 / 2Gi 内存
+      memory: 2Gi
配置原因
auth.database: nexus安装时预创建 nexus 数据库,Nexus 的元数据全部住在这个库里
auth.postgresPasswordpostgres 超级用户密码,强密码,只存在于 values 与 k8s secret(K8s 内置的“密钥存储“对象)里,本文占位
configuration(三个 PG 参数)最关键的是 listen_addresses='*'——PostgreSQL 默认只监听 pod 自己的本机,而 Nexus 是“隔壁 pod“,走 Service 域名来连,必须放开监听;max_connections=500 给并发留余量(Nexus 和 Dependency-Track 共享这个库);shared_buffers=256MB 是 PG 读数据页的内存缓冲
resourcesrequests 是保底、limits 是上限。PostgreSQL 是吃内存的组件,不设上限可能被 OOM kill 或把节点内存吃光,2Gi 是这台小节点上求稳的值(diff 里第二段 resources 属于 replica 备库段,本次 replicaCount: 1 只起主库,备库配置留着是为了将来上高可用省事)

读安装输出。 安装命令末尾输出:

Release "postgresql" has been upgraded. Happy Helming!
NAME: postgresql
NAMESPACE: middleware        # 装进了 middleware 命名空间
STATUS: deployed             # 部署成功
REVISION: 2                  # 修订号:首次安装是 1,改 values 再跑一次 → 2(本次是升级)
NOTES:
CHART NAME: postgresql
CHART VERSION: 18.7.9
APP VERSION: 18.4.0          # PostgreSQL 实际版本
⚠ WARNING: Since August 28th, 2025, only a limited subset of
    images/charts are available for free.
...
PostgreSQL can be accessed via port 5432 on the following DNS names
    from within your cluster:
    postgresql.middleware.svc.cluster.local - Read/Write connection
To get the password for "postgres" run:
    export POSTGRES_PASSWORD=$(kubectl get secret --namespace middleware \
      postgresql -o jsonpath="{.data.postgres-password}" | base64 -d)

两个点:postgresql.middleware.svc.cluster.local 是数据库的集群内域名——K8s 里每个 Service 都有这种 <服务名>.<命名空间>.svc.cluster.local 形式的域名,别的 pod 直接用它就能连上(2.3 里 Nexus 的配置用的就是它);⚠ bitnami 警告:2025-08-28 起 bitnami 只对“有限子集“的镜像/chart 免费,本次安装不受影响,将来升级 chart 遇到“拉不到镜像“时先想到这条。

怎么验证:kubectl get pods -n middleware 能看到 postgresql-0 处于 Running;NOTES 里的命令能读出数据库密码(secret 里是 base64 编码,base64 -d 解码);终极验证在 2.3——Nexus 能连上库并正常启动。

2.3 安装 Nexus

同样在 Rancher 加一个 chart 源,地址是 Sonatype 官方 https://sonatype.github.io/helm3-charts/,更新完成后安装 nxrm-ha chart(版本 96.1.0,对应 Nexus 3.96.1;ha 是 high availability 的缩写,指“支持多副本部署“):

Rancher 安装 nxrm-ha chart

图 3:Sonatype 官方 nxrm-ha chart 96.1.0 的安装页(对应 Nexus 3.96.1,装进 sonatype 命名空间)

chart 默认值是按“云上生产“给的(16 核 16G、3 副本),本环境节点小,按实际缩水;同时把“连外部数据库 + 内网域名 + 本地存储“三件事配掉:

@@ ingress 段 @@
-  enabled: false
+  enabled: true
-  defaultRule: false
+  defaultRule: true
-  host: null
+  host: nexus.kali-team.internal
-  ingressClassName: null
+  ingressClassName: traefik
@@ pvc 段 @@
-  storage: 2Gi
+  storage: 20Gi
-  volumeClaimTemplate:
-    enabled: false
+  volumeClaimTemplate:
+    enabled: true
@@ storageClass 段 @@
-  name: nexus-storage
-  provisioner: provisionerName
+  name: local-path
+  provisioner: rancher.io/local-path
@@ secret.db 段 @@
-    host: nxrm_db_host
+    host: postgresql.middleware.svc.cluster.local
-    password: nxrm_db_password
+    password: <POSTGRES_PASSWORD>
-    user: nxrm_db_user
+    user: postgres
   dbSecret:
-    enabled: false
+    enabled: true
@@ secret.nexusAdminSecret 段 @@
     adminPassword: <NEXUS_ADMIN_PASSWORD>
-    enabled: false
+    enabled: true
@@ nexus.env 段 @@
-      install4jAddVmParams: '-Xms2703m -Xmx2703m'
+      install4jAddVmParams: '-Xms1024m -Xmx1024m'
       nexusDBName: nexus
       nexusDBPort: 5432
@@ nexus.resources 段 @@
       limits:
-        cpu: 16
-        memory: 16Gi
+        cpu: 2
+        memory: 2Gi
       requests:
-        cpu: 8
-        memory: 8Gi
+        cpu: 1
+        memory: 2Gi
@@ nexusNs 段 @@
-    enabled: true
-    name: nexusrepo
+    enabled: false
+    name: sonatype
@@ config / supportContent 段 @@
   config:
-    enabled: false
+    enabled: true
     mountPath: /sonatype-nexus-conf
   supportContent:
-    enabled: false
+    enabled: true
     path: /nexus-data/tmp
     sizeLimit: 50Gi
@@ 副本数 @@
-  replicaCount: 3
+  replicaCount: 1
配置原因
ingress经域名 nexus.kali-team.internal 对内网暴露(ingress 走集群现有的网关,证书由内网 CA 签发);defaultRule: true 启用 chart 自带的默认路由规则,把入口流量转到 Nexus 的 8081 端口
secret.db数据库连接:host 指向 2.2 装好的 PostgreSQL 的集群内域名,user / password 与装 PG 时相同(占位);dbSecret.enabled: true 让 chart 把数据库密码存进 k8s secret 再注入容器,密码不以明文出现在 pod 定义和日志里
secret.nexusAdminSecret初始 admin 密码也写进 secret(取回命令见下,值本文占位)
pvc + storageClass20Gi 持久卷(PVC 是给 pod 申请的“跨重启不丢的磁盘“;chart 默认 2Gi 对制品仓库太小);volumeClaimTemplate 是“每个副本各自一块 PVC“,将来扩副本、上高可用的前提;local-path(rancher.io/local-path)是 k3s 自带的本地存储类,数据落在节点本地磁盘
install4jAddVmParams: '-Xms1024m -Xmx1024m'Nexus 是 Java 程序,-Xms/-Xmx 是 JVM 堆内存的初始与最大值;容器内存上限 2Gi,要给元空间、线程栈留余量,1G 是安全线
resources + replicaCount: 1资源额度从默认 16C/16Gi、3 副本缩到 2C/2Gi、单副本;数据库已经外置(共享 PostgreSQL),将来把 replicaCount 改 3 即可多副本高可用,不用迁数据
nexusNs: enabled: false关掉 chart 自带的“自动建新命名空间“开关,资源统一放 sonatype 命名空间
config / supportContent两个持久化目录:config 存 admin 密码文件、license 等配置,supportContent 是临时/上传暂存(限 50Gi);都开启持久化,容器重建配置不丢
nexus.docker.enabled: false(保持默认)Nexus 本身可以当 Docker 镜像仓库,本环境镜像归 Harbor 管,不重复开

安装完成后,取回 admin 密码:

# 做什么:读回 chart 存好的初始 admin 密码
# 怎么做:chart 把密码存在 sonatype 命名空间的 nxrm-ha-adminsecret 里;
#         secret 内容是 base64 编码的,-o jsonpath 取出后 base64 -d 解码
# 为什么:密码不落文章、不进 shell 历史,只在凭据管理里留存
kubectl get secret nxrm-ha-adminsecret -n sonatype \
  -o jsonpath='{.data.nexus-admin-password}{"\n"}' | base64 -d
# 输出即初始 admin 密码(本文占位 <NEXUS_ADMIN_PASSWORD>;
# 建议首次登录后立即修改,并按团队凭据规范存放)

然后浏览器打开 https://nexus.kali-team.internal,用 admin 账号登录,看到 Dashboard(Usage Center) 页面即 Nexus 起来了:

Nexus Dashboard

图 4:安装成功后浏览器进入的 Nexus Dashboard(Usage Center)首页——管理界面的入口

Nexus 的登录也接入了 Gitea OIDC 体系:开发者用 Gitea 账号登录一次,进 Nexus 的 Web 界面不用再输密码。

2.4 Blob Store:制品文件的存放位置

建仓库之前先认识 Blob Store:Nexus 里“仓库“是目录 + 元数据,真正的文件内容存在 Blob Store 里。每个仓库单独建一个:文件独立存放,磁盘可以按仓库单独扩容、迁移,某个仓库换盘/换机器时只动它自己的目录——这也是官方 chart 的推荐布局(每个 Blob Store 是 /nexus-data 数据根目录下的一个子目录)。

操作(Nexus 界面):

  1. 打开 Blob Stores,初始列表是空的:

Blob Stores 空列表

图 5:安装成功后的 Blob Stores 页:列表为空,需要手工创建存储库

  1. 点 Create Blob Store:类型选 File(本地磁盘型;Nexus 还支持 S3 等云对象存储类型,本环境全用本地磁盘),名称 goproxy,路径 /nexus-data/blobs/goproxy:

创建 goproxy Blob Store

图 6:创建 Blob Store:类型 File、名称 goproxy、路径 /nexus-data/blobs/goproxy(数据根目录下的子目录)

  1. 同样方式再建一个 go-sumdb,建完两个都是 Started(就绪)状态:

Blob Stores 均就绪

图 7:Blob Stores 列表:go-sumdb 与 goproxy 均为 Started 状态

顺序提醒:Blob Store 必须先于仓库创建——建仓库时要在已有的 Blob Store 里选一个,没建就没得选。

2.5 创建六个代理仓库

Repositories 页同样是空列表起步:

Repositories 空列表

图 8:Repositories 页:空列表,准备创建第一个代理仓库

点 Create Repository,第一个要选的是 Recipe(配方):Nexus 为每种制品格式预置了“表单模板“——选 go 就是 Go 模块的表单,选 raw 就是通用文件的表单。本部署共建 6 个仓库:

仓库Recipe上游用途
goproxygo (proxy)https://goproxy.cnGo 模块依赖代理
go-sumdbraw (proxy)https://goproxy.cn/sumdb/sum.golang.org校验数据库(sumdb)查询代理
go-dev-dlraw (proxy)https://go.dev/dl/Go 工具链下载代理
sonar-scanner-cliraw (proxy)https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/SonarScanner 安装包代理
debianapt (proxy)https://deb.debian.org/debian/Docker 构建 apt 主源代理
debian-securityapt (proxy)https://deb.debian.org/debian-security/Debian 安全更新源代理

goproxy(Go 模块代理)。 Recipe 选 go (proxy)——go 命令只认这套协议,raw 顶替不了:

选择 go (proxy) Recipe

图 9:Create Repository:Recipe 下拉选择 go (proxy)——Go 模块代理的专用模板

填写:名称 goproxy,上游(remote)https://goproxy.cn,Blob Store 选 2.4 建的 goproxy:

创建 goproxy 仓库

图 10:创建 goproxy 代理仓库:上游填 https://goproxy.cn,Firewall 选 Disabled(本环境没有 IQ 服务)

上游为什么是 goproxy.cn 而不是官方 proxy.golang.org:goproxy.cn 是公共 Go 模块代理(国内维护),内网出口到它更快更稳,“Nexus 从 goproxy.cn 取、goproxy.cn 从官方取“双重缓存更可靠。表单里的 Firewall 是 Nexus 的“仓库防火墙”:要连商业版 IQ 服务,在制品入库前做安全策略检查;本环境没有 IQ 服务,只能选 Disabled。

go-sumdb(校验数据库代理)。 Go 下载模块后,为了防“代理递给你一个被换过的包“,会到校验数据库(sumdb,checksum database)里核对模块的哈希(能唯一识别文件内容的摘要值,内容变一个字节哈希就变)——官方数据库是 sum.golang.org,内网到不了,所以要代理。sumdb 的查询就是普通 HTTP GET(按路径取一个小文本文件),不属于任何包管理器协议,所以用 raw (proxy)(通用文件代理)表单:

选择 raw (proxy) Recipe

图 11:Create Repository:Recipe 下拉选择 raw (proxy)——通用文件代理,这里用于 sumdb 查询

填写:名称 go-sumdb,上游 https://goproxy.cn/sumdb/sum.golang.org(goproxy.cn 在这个路径下聚合了官方 sumdb),Strict Content Type Validation 不勾选——开启时 Nexus 会对存入文件的“内容类型“(Content-Type)做严格校验,sumdb 查询返回的是短文本,勾了会把缓存文件拦掉:

创建 go-sumdb 仓库

图 12:创建 go-sumdb 代理仓库:上游填 https://goproxy.cn/sumdb/sum.golang.org,Strict Content Type Validation 未勾选

验证代理生效。 打开 goproxy 仓库的 Browse 页,模块已经列出来了:

goproxy Browse 页

图 13:goproxy 仓库的 Browse 页:pelletier/go-toml v2.2.2(example 项目的间接依赖)已缓存在库中——代理生效的直接证据

这个模块此前在 Nexus 里不存在,是这次请求从 goproxy.cn 取回来缓存的,再跑一次秒回——全部命中缓存。

go-dev-dl(Go 工具链代理)。 CI/CD 里装 Go 经常要下载不同版本的工具链(go/gofmt 等命令打包的 tar 包),官方下载目录 https://go.dev/dl/ 内网访问不稳且慢,所以代理:

创建 go-dev-dl 仓库

图 14:创建 go-dev-dl 代理仓库:Recipe raw (proxy),上游填 https://go.dev/dl/(Go 官方工具链下载目录)

建完浏览,go1.26.3 的工具链压缩包已缓存(团队 CI 引用的就是这个版本):

go-dev-dl Browse 页

图 15:go-dev-dl 的 Browse 页:go1.26.3 工具链压缩包已缓存

sonar-scanner-cli(扫描器安装包代理)。 SonarQube 流水线要下载 SonarScanner CLI 可执行文件,官方站 sonarsource.com 内网不通,代理它:跑工作流时免出网、省流量:

创建 sonar-scanner-cli 仓库

图 16:创建 sonar-scanner-cli 代理仓库:上游填 https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/

浏览可见扫描器包已缓存:

sonar-scanner-cli Browse 页

图 17:sonar-scanner-cli 的 Browse 页:sonar-scanner-cli-8.1.0.6389 安装包已缓存(流水线 scannerFile 引用的就是它)

这个仓库在权限上比较特别:为了让流水线免凭据下载,给 anonymous 角色(“未登录“身份的统称)授予了该仓库的读权限(权限 ID nx-repository-view:sonar-scanner-cli:*);其余仓库保持默认“必须登录”。

debian / debian-security(apt 源代理)。 构建镜像基于 debian bookworm,Dockerfile 里 apt 装包,官方源 deb.debian.org 内网不通,再建两个 apt (proxy) 仓库(Nexus 的 apt 专用表单,理解成“专门代理 Debian 源的 raw 仓库“即可),上游分别指向官方主源与安全更新源。apt 请求不带凭据,所以这两个仓库同样开匿名读(只代理公开内容)。

六个仓库建完,Repositories 列表的最终状态:

Nexus Repositories 列表

图 18:Nexus Repositories 列表:goproxy(Go 模块代理,已缓存 589.6 MB)/ go-sumdb(校验数据库代理)/ go-dev-dl(工具链代理)/ sonar-scanner-cli(扫描器安装包存放)/ debian / debian-security(apt 主源与安全更新源代理)全部 Online

3 使用:把 Go 项目指到内网 Nexus

3.1 四个 Go 环境变量,各司其职

把 Go 项目指向 Nexus = 把下面四个环境变量配正确:

变量干什么本环境的值
GOPROXY模块下载走哪个渠道。逗号分隔 = “前面 404 再试后面”;特殊值 direct = “直接去源码仓库(git)拿”本地开发 …/goproxy/,direct;流水线 …/goproxy/(不带 ,direct)
GOSUMDB下载后到校验数据库核对模块哈希流水线指向内网 go-sumdb 代理,在线校验保持开启
GOPRIVATE声明“这些模块路径是私有的“:同时跳过代理和校验库(私有模块公共代理里没有)未设置(本例模块都是公开的)
GOINSECURE对指定域名跳过 TLS 证书校验(给自签证书的自家服务用)未设置(Nexus 由内网 CA 正规签发,系统已信任)

3.2 怎么配

层级一:临时(当前终端)——shell 环境变量,先试通再固化:

# 做什么:让当前 shell 里的 go 命令走 Nexus
# 怎么做:export 成环境变量
# 为什么:先试通再固化;关掉终端即失效,不影响别人
export GOPROXY="https://nexus.kali-team.internal/repository/goproxy/,direct"
export GOSUMDB="sum.golang.org https://nexus.kali-team.internal/repository/go-sumdb/"

层级二:持久(本用户所有 go 命令,推荐)——go env -w 写进 Go 自己的用户级配置文件(路径用 go env GOENV 可查,与 shell 无关):

go env -w GOPROXY="https://nexus.kali-team.internal/repository/goproxy/,direct"
go env -w GOSUMDB="sum.golang.org https://nexus.kali-team.internal/repository/go-sumdb/"

GOSUMDB 的两种写法(最容易绕晕的一点,单独说):

# 写法一(推荐,流水线在用):sumdb 查询指向 Nexus 的 go-sumdb 代理
go env -w GOSUMDB="sum.golang.org https://nexus.kali-team.internal/repository/go-sumdb/"

# 写法二(不推荐):关掉在线校验
go env -w GOSUMDB="off"
  • 写法一 两个字段(推荐):格式是 "校验库域名 + 代理基础 URL"。Go 要查 sum.golang.org 时不再直连公网,而是按这个基础 URL 拼出查询地址(大致是 <基础URL>/sum.golang.org/lookup/<模块>@<版本>),请求走内网的 go-sumdb 仓库转发到 goproxy.cn——“在线校验在内网也通了”。流水线与 Dockerfile 都用这一写法。
  • 写法二 off(不推荐):off 是“不去校验数据库在线核对“,但不等于“完全不校验“——go.sum 文件记录的哈希在构建时仍会逐个本地比对,依赖内容被换掉、哈希对不上,构建直接失败。但 go.sum 是仓库里自己维护的文件:条目缺失、和恶意改动一起提交、新拉一个没有历史记录的模块时,它拦不住——sumdb 的在线校验(官方签名的校验数据)才是防“模块被篡改 / 供应链投毒“的最后一道防线,内网也不能把这条通道断掉:go-sumdb 代理就是为此而建。只有“代理确实无法部署、且团队明确接受完整性降级“时才允许用 off,并把风险写进文档——本环境不采用。
  • 坑(必读):不要写成“带路径的完整 URL“当单值——例如 GOSUMDB="https://nexus…/go-sumdb/sumdb/sum.golang.org" 会直接报错(malformed verifier id)。GOSUMDB 接受的是“域名(可选再加公钥/基础 URL)“,带路径的 URL 不是合法域名。那个带路径的 URL 是“Go 最终查询到的地址”,不是“要填的值“。

关于 GOPROXY 末尾的 ,direct:带上它,模块在 Nexus 里 404 时,go 会退回“直接去源码仓库(git)拿“——在内网到不了公网的场景里,这个回退基本必然失败,它的价值更多是让报错信息说清楚“是代理里没有这个模块“而不是含糊的网络错。流水线(dependency-track.yaml)选择不写 ,direct:失败就失败,绝不偷偷出网;本地开发可以保留,无实质差别。

3.3 怎么验证:三层验证

以 example 项目为例(16 个依赖,含 gin 框架):

# 客户端:在项目目录里跑依赖下载
cd ~/IdeaProjects/example   # 换成你自己的 Go 项目目录
go mod download -x          # -x 打印执行细节,方便看"卡在哪一步"

# 检查当前生效的配置
go env GOPROXY GOSUMDB
# 预期输出(与 10.12 配的值一致):
# https://nexus.kali-team.internal/repository/goproxy/,direct
# sum.golang.org https://nexus.kali-team.internal/repository/go-sumdb/
  • 客户端层:go mod download 成功——内网到不了公网,这是唯一路径,成功即证明代理链路通了;
  • 服务端层:Nexus 的 goproxy Browse 页(图 13)里,16 个模块陆续出现——每一条都是“这次请求从上游取回并缓存“的结果;
  • 缓存层:再跑一次 go mod download,秒级完成(全部命中缓存,不再请求上游)。

再回 goproxy 仓库的 Browse 页看一眼,缓存已经备齐:

goproxy Browse 全量缓存

图 19:goproxy 的 Browse 页:代理缓存的模块路径列齐——golang.org、github.com、k8s.io 等顶层路径;example 项目的 16 个依赖及其间接依赖都在这个仓库的缓存里

3.4 扩展场景:有私有模块时怎么配

example 的 16 个依赖全是公开模块,所以 GOPRIVATE / GOINSECURE 都没设。但团队将来难免要把私有模块(代码放在自家 Gitea 上、公网代理里不可能有的 Go 模块)引进来,核心原则一句话:私有模块不能走公开代理、也不能去公开校验库查哈希——代理里没有、校验库里也没有,硬走只会得到 404。

声明私有模块路径:GOPRIVATE

# 做什么:告诉 go "这个域名前缀下的模块路径都是私有的"
# 怎么做:填模块路径里的域名(本环境示例 hub.kali-team.internal)
# 为什么:命中后 go 对该模块同时跳过 GOPROXY 与 GOSUMDB,
#         直接用 git 去源码仓库取
export GOPRIVATE="hub.kali-team.internal"

填的是“模块路径的域名“,不一定是 Web 域名。 模块路径是 Go 项目引用一个模块时,在 go.mod 的 require 行里写的那个地址——比如 Gitea 上 dev-sec-ops/example 组织里的代码,被引用时写 hub.kali-team.internal/dev-sec-ops/example,hub.kali-team.internal 就是模块路径的第一段。Gitea 的惯例是模块路径默认用代码托管域名,所以本环境两者恰好重合——但规则本身是“看模块路径“:如果哪天用独立域名给模块寻址(vanity path),要填那个域名,而不是 Web 域名。GOPRIVATE 支持逗号分隔多个、也支持通配符(如 *.example.com)。

git 认证:两种写法

跳过代理之后,go 靠 git 从源码仓库取私有模块——于是需要凭据。本环境(Gitea 托管)两种写法都可行:

# 写法一:HTTPS + Gitea 访问令牌
# 做什么:git 每次要访问 "https://hub.kali-team.internal" 前,
#         自动改写成"带令牌"的地址
# 怎么做:git config 的 url.<新前缀>.insteadOf "<旧前缀>"——git 的"URL 改写"机制,
#         发送请求前把旧前缀替换成新前缀,令牌随之带上
# 为什么:不用每次手动输凭据;<GITEA_TOKEN> 是 Gitea 的访问令牌
#         ("让程序代表你操作仓库"的凭据,Gitea 个人设置里创建、长期有效,
#          按密码级别管理:只进凭据管理,不进文章 / 代码)
git config --global url."https://<GITEA_TOKEN>@hub.kali-team.internal".insteadOf "https://hub.kali-team.internal"
# 写法二:SSH 公钥(本机公钥已加入 Gitea)
# 做什么:git 去 "https://hub.kali-team.internal/" 取模块时,改走 SSH 通道
# 怎么做:同样的 insteadOf 改写;SSH 通道是 Gitea 的 NodePort 32222
# 为什么:机器不用持有令牌(长期凭据),认证靠公钥;本机同步内网仓库用的就是这条通道
git config --global url."ssh://[email protected]:32222/".insteadOf "https://hub.kali-team.internal/"

怎么选:长期跑在机器上的自动化(开发机、Runner)推荐 SSH——公钥没有“过期“一说,一台机器一把钥匙、互不影响;HTTPS 令牌更简单(一行配置),但令牌是长期凭据,要按密码的纪律保管(泄露即轮换)。流水线里要取私有模块,思路不变:job 级 env 设 GOPRIVATE,再在步骤里加一段上面的 git config。

GOINSECURE:只给自签证书的域名用,本环境用不到:它专门服务于“证书自签、系统不信任“的服务。Nexus 与 Gitea 的证书都是内网 CA 正规签发、系统信任库已包含该 CA,没有跳过校验的必要——而且跳过校验是安全降级,不该顺手就开。

3.5 Go 模块下载报错怎么读:排查顺序

go mod download 的报错五花八门,但第一行错误基本都标明了卡在哪一层。按“客户端配置 → 网络 → 凭据 → 服务端代理“的顺序逐层排除,大多数问题能在前三层定位:

报错关键字(第一行)卡在哪先做什么
取模块时 404 Not Found代理里没有这个模块先怀疑自己:模块路径拼写错误(大小写敏感);是私有模块却报 404 → GOPRIVATE 没设;公开模块代理里没有 → 模块不存在 / 已改名 / 上游也没有
malformed verifier idGOSUMDB 写法非法改成 3.2 的两字段写法
lookup …: no such host / connection refused / i/o timeout网络层:根本到不了代理查 go env GOPROXY 的地址端口;内网域名要能解析(hosts / DNS);用 curl 直接访问代理基础 URL——返回任何 HTTP 状态码都说明网络通,连不上才是网络问题
git 取私有模块报 401 / 403git 认证没配或失效检查 3.4“git 认证“里的 insteadOf 改写是否存在、令牌是否有效
checksum mismatch下载内容的哈希与 go.sum 记录不符依赖被篡改、或 go.sum 本身损坏——不要顺手删掉 go.sum 重新生成(那等于放弃这道防线),先核对 go.sum 的来源与模块版本
module lookup disabled by GOPROXY=off代理通道被关了本环境不该出现;查 go env GOPROXY 为什么是 off

第二步(看完报错必做):go env GOPROXY GOSUMDB GOPRIVATE GOINSECURE 核对实际生效值——常见情况是“改了没生效“:shell 环境变量与 go env -w 写的不是一处,临时 export 关掉终端就没了。

第三步(客户端排完才走这步):到服务端查——Nexus 的 goproxy Browse 页(图 13)看该模块有没有新条目;代理本身报错的话,再看仓库的上游配置。

4 CI/CD 联动:流水线怎么用 Nexus

4.1 全景:谁在用哪个仓库

三条现有流水线(docker.yaml / sonarqube.yaml / dependency-track.yaml)和将来的工具链安装,都以 Nexus 为下载源:

使用方下载什么Nexus 仓库配置在哪
Dockerfile 构建阶段Go 模块依赖(gin 等)goproxyENV GOPROXY=… GOSUMDB="sum.golang.org https://nexus.kali-team.internal/repository/go-sumdb/"(当前依赖已预烤进 gomod-cache 镜像,此 env 是现场需要取件时的兜底)
dependency-track.yamlcyclonedx-gomod 及其模块依赖goproxyjob 级 env + “准备 Go 模块依赖“步骤
sonarqube.yamlSonarScanner CLI 安装包sonar-scanner-cliscannerBinariesUrl + scannerFile
Dockerfile apt 步骤Debian apt 包(索引文件 + .deb)debian / debian-securityDockerfile 的 sed 源改写
(将来)需要特定 Go 版本的流水线Go 工具链压缩包go-dev-dlsetup-go 的 go-download-base-url

4.2 dependency-track.yaml:模块依赖走 goproxy

实际文件的相关片段(已入库 example 仓库):

jobs:
  dependency-track:
    runs-on: ubuntu-host
    env:
      # 做什么:本 job 里所有 Go 模块下载走 Nexus 的 goproxy
      # 为什么:任务容器到不了公网;
      #         不写 ,direct——失败就失败,绝不偷偷出网
      GOPROXY: "https://nexus.kali-team.internal/repository/goproxy/"
      GOSUMDB: "sum.golang.org https://nexus.kali-team.internal/repository/go-sumdb/"  # sumdb 校验走 Nexus go-sumdb 代理,保持在线校验不关闭(go.sum 本地哈希校验始终生效)
    steps:
      # … Init / Checkout / Verify Certificate(与另两条流水线一致,此处省略)…
      - name: 准备 Go 模块依赖
        run: |
          go version        # 确认任务容器里可用的 Go 版本(1.25.14)
          go mod download   # 先把项目依赖批量拉好(经代理,缓存进任务容器)
      - name: 生成 CycloneDX SBOM
        uses: https://hub.kali-team.internal/actions/cyclonedx-gomod-generate-sbom@v2
        with:
          # cyclonedx-gomod 的 Go 模块版本:action 用 `go run` 现场编译,
          # 它这个模块本身也经 Nexus goproxy 下载
          version: "v1.10.0"
          args: "app -json -output bom.json -packages -files -licenses -main ."

重点在 env 两行:写在 job 级,意味着本 job 所有步骤(包括生成 SBOM 那步——它自己就要编译 Go 代码)都走同一个内网下载源——整条流水线零公网连接。go mod download 步骤是“把依赖提前“:先把项目的 16 个模块批量拉好,后面生成 SBOM 的步骤不用再拉。补一句容易忽略的:常规构建(模块已在 gomod-cache 预烤缓存里、go.sum 完整)根本不会触发 sumdb 查询,go-sumdb 代理只在“全新模块要校验“时出场——兜底通道平时安静,但必须活着。

4.3 sonarqube.yaml:scanner 来自 raw 代理

实际文件的相关片段(已入库 example 仓库):

      - name: SonarQube Scan 静态代码扫描
        uses: https://hub.kali-team.internal/actions/sonarqube-scan-action@v9
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}       # SonarQube 用户级 API token(长期有效)
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} # https://sonarqube.kali-team.internal
          NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/kali-team-ca.crt  # 信任内网 CA
        with:
          # 做什么:scanner 的下载源改为 Nexus 的 sonar-scanner-cli 仓库
          # 为什么:官方站 sonarsource.com 内网不通;代理更快更稳、省流量
          scannerBinariesUrl: https://nexus.kali-team.internal/repository/sonar-scanner-cli/
          # 做什么:显式指定要下载的文件名
          # 为什么:raw 仓库没有目录列表接口,
          #         不能"运行时列目录挑一个",必须写死文件名
          scannerFile: sonar-scanner-8.1.0.6389-linux.tgz
          projectBaseDir: .
          # 官方 action 原本会对下载包做 GPG 签名校验;内网 Nexus 里的包
          # 没有签名可验(是从官方站取回缓存的同一文件),兼容需要关掉
          skipSignatureVerification: true

三个值得注意的点:一是全程没传任何用户名密码——靠的是 2.5 给这个仓库开的匿名读权限,流水线侧比“机器账号“更省事(且该仓库只存公开扫描器包,无敏感内容);二是 scannerFile 显式写死是 raw 仓库“无目录列表“逼出来的;三是 action 用 wget 下载、解压后执行——scanner 本体约 9MB,大头(JRE 运行时)已在任务镜像里预装。

4.4 go-dev-dl:Go 工具链这样下

场景:任务容器自带默认 Go 版本(1.25.14);如果某个项目要更新的版本(比如 1.26.3),流水线需要现场安装。setup-go 这个 action 的 go-download-base-url 输入就是为此设计的:

      - name: 安装 Go 1.26.3
        # 注意:setup-go 官方包托管在 github.com,内网到不了;
        # 在本环境使用前需自托管进 actions 组织
        uses: actions/setup-go@v7
        with:
          go-version: '1.26.3'   # 要装的 Go 版本
          # 做什么:把工具链下载源从官方站改为 Nexus
          # 为什么:第一次请求由 Nexus 转发到 go.dev/dl 并缓存(图 15 即缓存证据),
          #         之后装同一版本直接命中缓存,不再依赖外网
          go-download-base-url: 'https://nexus.kali-team.internal/repository/go-dev-dl/'

现状说明:example 仓库的三条流水线目前没走这条路——任务容器已预装 Go,Docker 构建拉 golang 镜像走 Harbor 镜像代理、依赖走预烤缓存。go-dev-dl 仓库属于“先建好、要用即用“,图 15 的 go1.26.3 缓存就是它已就绪的证据。

4.5 Dockerfile:apt 源走两个代理

构建镜像(debian bookworm 基座)在 Dockerfile 里 apt 装包,官方源内网不通。Debian 12 的源文件是 deb822 格式的 /etc/apt/sources.list.d/debian.sources,在 Dockerfile 里直接 sed 把源域名改写到 Nexus 的两个代理仓库:

RUN set -eux; \
    sed -i \
      -e 's|http://deb.debian.org/debian|https://nexus.kali-team.internal/repository/debian|g' \
      -e 's|https://deb.debian.org/debian|https://nexus.kali-team.internal/repository/debian|g' \
      /etc/apt/sources.list.d/debian.sources

两个细节:

  • 一条替换同时覆盖两个源:-security 是域名后的前缀延续,替换域名后 deb.debian.org/debian-security/... 变成 …/repository/debian-security/...,主源与安全更新源各进各的仓库,不需要第二条规则;
  • http / https 两条都改写:镜像里的源文件两种写法都存在,漏一条就有那一路继续出公网。

效果和其余仓库一样:apt 请求的索引文件与 .deb 从 Nexus 取,首次取回缓存、第二次构建命中本地。两个前提:构建阶段信任内网 CA(https 源要过 TLS 校验);apt 不带凭据,所以这两个仓库开了匿名读。

4.6 价值:内网统一下载入口

回看六个仓库,它们背后的模式是同一个:凡是“公网来的“,先取回内网缓存一份,再用:

  1. 可达:任务容器只能到内网——把“下载源“一改(GOPROXY / scannerBinariesUrl / go-download-base-url / apt 源改写),整条流水线就通了,业务逻辑一行不动;
  2. 一份取回,全员共享:第一次付“出网取件“的成本,之后第 N 条流水线、第 N 个开发者都是本地速度,公网流量也省了;
  3. 可控可审计:团队下载了什么、从哪下、何时下的,Nexus 仓库和日志里都有记录,制品出了问题能追到具体条目;
  4. 可切换:上游地址是配置项。哪天官方代理慢了,改仓库的上游地址即可,下游全部无感——下游只认内网入口。

Harbor 管容器镜像、Nexus 管代码级制品,两个仓库分工明确,内网的依赖下载通道就此齐了。

小结

这一篇给内网 DevSecOps 体系补上了依赖下载通道:Nexus 制品仓库(3.96.1)装进 k3s,配好六个代理仓库(goproxy / go-sumdb / go-dev-dl / sonar-scanner-cli / debian / debian-security),Go 依赖、校验数据、工具链、扫描器、apt 源这些“公网来的“东西,从此在内网都能取到并缓存。

  • 搭建:共享 PostgreSQL 先行(middleware 命名空间,bitnami chart,关键是库名、三个 PG 参数、2Gi 资源上限),Nexus 随后(sonatype 命名空间,官方 nxrm-ha chart,关键是外置数据库、20Gi local-path PVC、1G JVM 堆、单副本起步);界面里先建 Blob Store 再建六个代理仓库;
  • 使用:四个环境变量各司其职;sumdb 两种写法——两字段代理写法(推荐,在线校验保持开启)与 off(不推荐,本环境不采用);验证三层:客户端下载成功、服务端 Browse 有条目、再跑命中缓存;
  • CI/CD 联动:dependency-track.yaml 的 job env 把 GOPROXY 指到 goproxy(整条流水线零公网)、sonarqube.yaml 的 scanner 走 raw 代理(文件名必须显式写死)、go-dev-dl 为将来的“特定版本 Go 安装“备好、Dockerfile 的 apt 源 sed 改写走 debian 代理;
  • 坑:raw 无目录列表、GOSUMDB 格式、strictContentTypeValidation 位置、Blob Store 顺序、上游不稳、Bitnami 警告(见“常见问题与坑“)。

下一步可选:建 Hosted 仓库发布内部 Go 模块与内部二进制包(团队的“自己的仓“)、用 Group 仓库把“代理 + 托管“聚合成统一入口(客户端 GOPROXY 只配一行);再往后可以接 Nexus 的 OIDC 登录细化与漏洞扫描能力(Nexus 有内置漏洞扫描插件,商业版)。至此内网各平台就位:Gitea(代码)→ Harbor(镜像)→ Nexus(制品)→ SonarQube(代码质量)→ Dependency-Track(依赖安全)。

参考