安全开发-SonarQube

前言

Rancher 集群、Gitea 代码与流水线、Nexus 制品源就位之后,还差一环:你自己写的代码有没有 bug、有没有安全漏洞、有没有难维护的“坏味道“——这是 SonarQube 的活。

SonarQube 是 SonarSource 公司的开源代码质量平台:Web 面板(下称 Server)展示每个项目代码的健康状况,并自带质量门禁机制。本文做三件事:

  1. 搭建:Rancher + 官方 Helm chart 部署,重点两件事——内网自签 CA 证书的信任(caCerts)、Gitea OIDC 单点登录接入;
  2. 使用:手动创建项目、创建 token、看懂面板数据;
  3. CI/CD 联动:把 sonarqube.yaml 流水线逐段拆掉,让每次推送 / PR 自动做一遍 SAST(静态应用安全测试)——不运行代码、只“读“代码就能发现问题。

1 背景概念:SonarQube 扫什么

1.1 SAST:不运行代码,只“读“代码就发现问题

静态分析:程序不运行,工具把源代码解析成语法结构,再按一套规则库找出潜在问题,业内把“静态应用安全测试“缩写成 SAST。一次 SonarQube 扫描输出三类问题,优先级从高到低:

类型英文是什么例子
漏洞Vulnerability安全洞,攻击者可能利用SQL 注入:把用户输入直接拼进 SQL 语句
缺陷Bug逻辑错误,运行时会出错或行为不对空指针:一个可能为空的对象没检查就直接用
坏味道Code Smell不致错但让代码难读难维护一个 300 行的函数、魔法数字满天飞

漏洞第一优先(攻击者正盯着),缺陷其次(程序会坏),坏味道是“健康管理“(慢慢清,别堆积)。有些问题只有运行时才暴露——那要靠测试或动态扫描。

1.2 质量门禁:扫描的判定标准

质量门禁(Quality Gate) 是一组阈值(例如“新增漏洞 = 0、覆盖率 ≥ X%“),每次扫描后自动判定 Passed / Failed。流水线里门禁还能“卡“住流程:扫描不通过就不许合并 / 发布(本环境目前只到“扫描出报告”,尚未接成卡点,详见“常见问题与坑“)。

1.3 Server 与 Scanner 的分工:谁存结果、谁干活

  • Server(服务端):Web 服务(本环境 sonarqube.kali-team.internal),存分析结果,提供面板、门禁判定和权限管理。
  • SonarScanner CLI(扫描器):跑在 CI 侧的命令行工具,用 Java 写(需要 JRE 运行),按配置读代码、做分析、把结果传给 Server。

两半靠一个唯一标识对上号——仓库根目录的 sonar-project.properties 文件:

sonar.projectKey = example        # 项目唯一标识:必须与 SonarQube Server 上创建的项目 Key 完全一致
sonar.projectName = 项目例子       # 显示名称:面板上看到的名字
sonar.sources = .                 # 扫描范围:扫哪些目录(这里是整个仓库)

JRE 这里要记一下:流水线跑在内网任务容器里、到不了公网下载 JRE,所以任务镜像预装了 JRE;Scanner 本体很小(约 9MB),从 Nexus 的 sonar-scanner-cli 仓库下载。

1.4 SAST 的定位:对照 SCA 与 DAST

SAST(SonarQube)SCA(Dependency-Track)DAST(动态,本环境未部署)
看什么你自己写的代码你引用的第三方依赖的已知漏洞运行中的系统(黑盒发请求探测)
输入源代码SBOM(软件物料清单)HTTP 请求 / 响应
发现什么注入风险、逻辑缺陷、坏味道依赖组件的 CVE / KEV运行服务的真实攻击面
典型动作修代码升级依赖版本修配置 / 堵接口

SonarQube 看代码,Dependency-Track 看依赖。在流水线里两者是各自独立的 workflow:sonarqube.yaml 扫自己的代码,dependency-track.yaml 传 SBOM 查依赖——互不干扰,合起来构成“代码 + 依赖“两道安检。

2 搭建

版本:SonarQube Community 26.7.0.124771 / chart 2026.4.1,经 Rancher + 官方 Helm chart 部署。

2.1 总体思路:要哪些组件

组件本环境的来源
SonarQube 本体官方 chart sonarqube(chart 2026.4.1,对应 Community 版 26.7.0.124771 构建),在 Rancher 界面里安装
数据库复用 Nexus 安装时带出来的共享 PostgreSQL(middleware 命名空间),只为它建一个专属库 sonarqube
CA 证书kali-team-root-ca.crt——内网根证书,搭集群时就已获取
Ingress域名 sonarqube.kali-team.internal,TLS 由集群网关统一终结

chart 来自 SonarSource 官方 chart 仓库(https://sonarsource.github.io/helm-chart-sonarqube)。Community(社区版)免费,Enterprise 等是付费版——本环境用社区版。

Rancher 里的 sonarqube chart

图 1:Rancher 界面里的 sonarqube chart(2026.4.1),Community 版 26.7.0.124771——本环境从这里安装

这次部署有两处“内网特色“(官方文档示例里不会强调的):

  1. 证书全是自签的:SonarQube 的 OIDC 登录要连 Gitea 的 https 端点,不信任内网 CA 就会握手失败——chart 专门提供了 caCerts 配置来覆盖默认 CA 库;
  2. 数据库不随 chart 带:复用 middleware 命名空间的共享 PostgreSQL,用 jdbcOverwrite 指过去。

2.2 在共享 PostgreSQL 上建 sonarqube 库

kubectl 本身没有 psql 客户端,所以临时起一个 pod(--rm 表示退出后自动删除)当“跳板“,在里面执行建库 SQL:

# 从现成的 secret 读取共享 PostgreSQL 的超级用户密码
#    secret 是 k8s 里的凭据对象,密码以 base64 编码存放,管道里 base64 -d 解码
export POSTGRES_PASSWORD=$(kubectl get secret --namespace middleware postgresql \
  -o jsonpath="{.data.postgres-password}" | base64 -d)

# 起一个临时 postgresql-client pod 执行"建库"
#    --rm              退出后自动删 pod
#    --restart='Never' 不重启
#    --tty -i          分配终端、接标准输入
#    --command -- ...  覆盖容器默认启动命令,直接跑 psql
kubectl run postgresql-client --rm --tty -i --restart='Never' \
  --namespace middleware \
  --image registry-1.docker.io/bitnami/postgresql:latest \
  --env="PGPASSWORD=$POSTGRES_PASSWORD" \
  --command -- psql --host postgresql -U postgres -d postgres -p 5432 \
  -c "CREATE DATABASE sonarqube;"

密码从集群现成的 secret 读、不手敲(不进 shell 历史,文内占位);postgresql 是共享库在集群内的服务名,5432 是 PostgreSQL 默认端口。实际输出:CREATE DATABASE 一行成功,临时 pod 自动删除。

2.3 把内网 CA 证书放进 Secret

为什么需要:内网的证书都是自签的——公网体系里证书由人人信任的公共 CA 签发,内网里大家信任的是自建 CA。SonarQube(Java 程序)配置 OIDC 登录时要主动连 Gitea 的 https 端点,它会用自己的 CA 信任库校验对方证书——内网 CA 不在库里,握手直接被拒(证书错误)。

官方 Helm chart 文档有专门一节讲这个场景(“Use custom cacerts”,就是针对离线环境 / 内部工具 / 自签证书说的):思路是把要信任的证书先放进一个 k8s Secret,再用 chart 的 caCerts 配置指向它,覆盖默认 CA 库:

# chart 文档的示例写法(本环境 Secret 名用 kali-team-root-ca)
apiVersion: v1
kind: Secret
metadata:
  name: my-cacerts
stringData:
  cert-1.crt: |
    xxxxxxxxxxxxxxxxxxxxxxx   # 证书的 PEM 内容

本环境实际执行(证书文件是搭集群时已获取的根证书,放在集群节点上):

kubectl create secret generic kali-team-root-ca \
  -n sonarqube \
  --from-file=kali-team-root-ca.crt=kali-team-root-ca.crt
# 输出:secret/kali-team-root-ca created

怎么验证:Secret 建好只是第一步,真正的验证在 SonarQube 起来之后——OIDC 登录走通,就说明服务端成功连上了 Gitea 的 https 端点且证书校验通过。

2.4 Helm values 改动

部署时改动的 values(chart 的部署参数;diff 形式,+ 为相对 chart 默认值新增 / 改动的行):

@@ caCerts @@
 caCerts:
-  enabled: false
+  enabled: true
+  secret: kali-team-root-ca
@@ community @@
 community:
   buildNumber: 26.7.0.124771
-  enabled: false
+  enabled: true
@@ deploymentStrategy @@
 deploymentStrategy:
-  type: Recreate
+  type: RollingUpdate
+  maxSurge: 1
+  maxUnavailable: 0
@@ ingress @@
 ingress:
-  enabled: false
+  enabled: true
   hosts:
-    - name: sonarqube.your-org.com
+    - name: sonarqube.kali-team.internal
@@ jdbcOverwrite @@
 jdbcOverwrite:
-  enabled: false
-  jdbcPassword: sonarPass
-  jdbcUrl: jdbc:postgresql://myPostgres/myDatabase
-  jdbcUsername: sonarUser
+  enabled: true
+  jdbcPassword: <POSTGRES_PASSWORD>
+  jdbcUrl: jdbc:postgresql://postgresql.middleware.svc.cluster.local:5432/sonarqube
+  jdbcUsername: postgres
@@ persistence @@
 persistence:
-  enabled: false
+  enabled: true
-  size: 5Gi
+  size: 10Gi
@@ resources.limits @@
 resources:
   limits:
     cpu: 800m
-    memory: 6144M
+    memory: 4196M
   requests:
     cpu: 400m
@@ 末尾新增 @@
+monitoringPasscode: community
配置原因
caCertsenabled: true + secret 指向前面建好的 kali-team-root-ca——让 SonarQube 信任内网 CA;验证方式:OIDC 登录可用
communitychart 同时支持 Community(免费)与 Enterprise(付费),enabled: true 选社区版;buildNumber 钉住具体构建号 26.7.0.124771(与图 1 chart 页一致,版本钉死避免意外漂移)
deploymentStrategychart 默认 Recreate(先删旧 pod 再起新 pod——中间有短暂“服务不可用“窗口);改成 RollingUpdate(滚动更新:先起新的、就绪后再删旧的),maxSurge: 1(更新期间最多多出一个新 pod)、maxUnavailable: 0(新的没就绪前旧的不下线,服务不断)
ingress启用并填域名 sonarqube.kali-team.internal;流量经集群统一网关终结 TLS,证书由内网 CA 签发——这也是“浏览器直接打开域名就是绿锁“的原因
jdbcOverwriteSonarQube 的分析数据必须落 PostgreSQL,chart 默认的 jdbcUrl 是占位符 myPostgres/myDatabase——必须覆盖:enabled: true,URL 指向共享库的集群内地址 postgresql.middleware.svc.cluster.local:5432,库名 sonarqube(2.2 建的)、用户名 postgres(本环境直接用共享库超级用户)、密码用 2.2 读到的值(占位)。整篇搭建里“数据放哪“只由这一段决定
persistenceenabled: true + 10Gi——申请一块 PVC(Persistent Volume Claim,跨重启不丢的“磁盘“)存 SonarQube 自身的文件类数据,pod 重建不丢;storageClass 不指定,用集群默认
resources.limits.memory思路是“内存给多点,java 开发的,避免执行任务的时候 OOM“(OOM = Out Of Memory,内存不够被系统杀掉)。SonarQube 是 Java 服务,分析任务吃内存,上限给低了 pod 会被 OOM 杀掉反复重启。chart 默认 6144M,本环境最终值 4196M(约 4.1Gi)——这台小规格节点的实际内存是上限
monitoringPasscodechart 必填参数,需要指定添加,本环境对应社区版填 community;不填 chart 校验不过

其余 values 保持 chart 默认(安全上下文、探针、日志级别等),此处不逐项展开。

2.5 安装与验证

在图 1 的页面点部署(Deploy),等 pod 就绪。验证两层:

  1. pod 层:Rancher 工作负载页里 sonarqube 的 pod 状态 Running,重启次数稳定;
  2. 服务层:浏览器打开 https://sonarqube.kali-team.internal 出现登录页(浏览器信任内网 CA、证书有效——这一步同时验证了 ingress 与域名证书)。

到这里搭建完成,进入网页做“首次登录与初始配置“。

3 首次登录与初始配置

3.1 用默认 admin 账号登录

全新 SonarQube 内置一个管理员账号:用户名 admin,默认密码 admin。登录后第一眼看到的是“创建项目“引导页(图 2——此时界面还是英文)。

默认密码务必在首次登录后修改,新值按团队凭据规范存放(文章只占位,不落明文)。

首次登录后的创建项目引导页

图 2:初始 admin/admin 登录后看到的创建项目引导页(此时界面还是英文)

3.2 安装中文语言包与 OIDC 插件

SonarQube 的扩展机制是 Marketplace(插件市场):进 Administration → Marketplace 搜索、安装,状态 Install Pending(等待重启生效),重启后自动加载。要装两个:

  1. 中文语言包:界面变中文(后面的截图都是中文界面);
  2. OpenID Connect 插件:OIDC 是建立在 OAuth2 之上的标准身份登录协议,实现“一次登录、处处通行“——SonarQube 的 OIDC 登录模块不是天生就有的,先装插件(第 4 章)。

Marketplace 安装中文语言包

图 3:Marketplace 里的中文语言包,状态 Install Pending(等待重启生效)

Marketplace 安装 OpenID Connect 插件

图 4:Marketplace 里的 OpenID Connect(OIDC)插件,同样 Install Pending

重启后引导页变成中文界面:

中文界面生效后的引导页

图 5:服务重启后语言包生效,引导页已是中文

4 配置 OIDC 登录:用 Gitea 账号进 SonarQube

4.1 为什么能复用 Gitea 单点登录

Gitea 内置了 OIDC 提供方(IdP):各平台在 Gitea 组织里建一个 OAuth2 应用(客户端),拿到一对 Client ID/Secret 填进自己,之后开发者用 Gitea 账号登录一次、进所有平台都不用再输密码。SonarQube 的接入方式与其它平台相同:Gitea 侧建应用、SonarQube 侧填设置。

4.2 先看“说明书“:OIDC 发现端点

Gitea 在 https://hub.kali-team.internal/.well-known/openid-configuration 暴露一份 JSON“说明书“(发现端点):issuer、各端点地址、支持的 scope 全列在里面——打开这个链接就知道怎么填了:SonarQube 的 OIDC 表单字段基本都能从这份 JSON 里找到对应值。

4.3 Gitea 侧:创建 OAuth2 应用

在 dev-sec-ops 组织 → 设置 → 应用 → 创建 OAuth2 应用程序:

  • 应用名称:SonarQube;
  • 重定向 URL:https://sonarqube.kali-team.internal/oauth2/callback/oidc——SonarQube 侧的固定“回调地址“,OIDC 插件的默认回调,照抄即可;
  • 客户端类型:机密客户端(confidential——能安全保管客户端密钥的客户端,Web 服务属此类)。

创建后 Gitea 颁发 Client ID(半公开的标识)与 Client Secret(等价于密码),占位 <client-id> / <client-secret>,只写进 SonarQube 的 OIDC 设置。

Gitea 组织里的 SonarQube OAuth2 应用

图 6:Gitea dev-sec-ops 组织的 OAuth2 应用里创建的 SonarQube 应用:重定向 URL 指向 SonarQube 的 OIDC 回调(Client ID / Secret 已打码)

4.4 SonarQube 侧:填 OIDC 设置

Administration → General Settings → 权限(OIDC 插件提供的表单),关键项:

字段值
Issuer(签发方)https://hub.kali-team.internal——声明“我的 IdP 是 Gitea“,SonarQube 会拿这个地址去自动读取 4.2 的发现端点
Client ID / Client Secret<client-id> / <client-secret>(4.3 创建的应用的身份凭据)
Scopesopenid email profile groups——openid 是基础身份(必需)、email / profile 是邮箱与资料、groups 关键:把用户的组信息带进令牌,平台按组授权限
签名算法RS256——Gitea 给令牌签名用的算法(发现端点里能查到),SonarQube 用它验“令牌没被篡改“

SonarQube 的 OIDC 设置

图 7:SonarQube Administration → General Settings → 权限里的 OIDC 配置:Issuer 指向 hub.kali-team.internal,scopes 为 openid email profile groups(Client ID / Secret 已打码)

4.5 两个通用设置

同一个 General Settings 页还有两处要看(图 8):

  1. Server base URL:填 https://sonarqube.kali-team.internal。配置 Server base URL,不然 OIDC 跳转的时候会默认跳到 localhost——OIDC 流程里 SonarQube 需要知道“我对外叫什么地址“,不配就默认 localhost,用户从 Gitea 授权回来后会被送回自己电脑,登录闭环直接断掉。
  2. JRE auto-provisioning:关闭。这个开关打开时,SonarQube 会按需要从公网下载 JRE 供 scanner 使用——内网到不了公网,索性关掉;任务镜像已预装 JRE,scanner 不需要向服务端要。

通用设置:JRE auto-provisioning 关闭与 Server base URL

图 8:General Settings:JRE auto-provisioning 关闭(内网无法从公网下载 JRE)、Server base URL 填内网域名(否则 OIDC 跳转默认回 localhost)

4.6 验证:用“Gitea Connect“登录

登出 admin,回到登录页——表单已经不一样了:没有账号 + 密码,只有“通过 Gitea Connect 登录“和”更多选项“(图 9)。点“通过 Gitea Connect 登录”,浏览器跳到 Gitea,授权后自动回 SonarQube,此时已以 Gitea 身份登录——顶部菜单能看到当前身份(图 10),全程没输任何密码。

登录页的 Gitea Connect 入口

图 9:OIDC 生效后,登录页没有密码框,只有“通过 Gitea Connect 登录“与“更多选项“

以 Gitea 身份登录成功

图 10:从 Gitea 授权回来后已以 Gitea 身份登录(顶部菜单显示当前身份),进入引导页

验证清单:登录页出现 Gitea 入口 ✓ → 跳转授权后无需密码回到 SonarQube ✓ → 顶部菜单显示 Gitea 身份 ✓。三条都过,说明 OIDC 链路(含 2.3 的 caCerts 证书信任)走通。

5 使用:创建项目与令牌

5.1 创建项目(手动)

引导页里有几种建项目方式,最常见的是接代码平台集成后由平台自动建;本环境没接,手动创建:填名称、Key,绑定分支(图 11),“选择质量门禁“直接用默认策略(内置阈值,1.2)。

创建项目对话框

图 11:手动创建项目对话框:名称「项目例子」、Key example、绑定 main 分支

Key 必须与仓库对上(最重要的规则,否则扫描结果挂不上号):

$ cat sonar-project.properties   # example 仓库根目录的文件
sonar.projectKey = example
sonar.projectName = 项目例子
sonar.sources = .

文件里的 sonar.projectKey 必须与创建的项目 Key 完全一致(这里都是 example)。

5.2 创建令牌(扫描器的凭据)

向导下一步推荐“使用 GitHub CI 分析项目“(图 12):核心是两个环境变量 SONAR_TOKEN(令牌)与 SONAR_HOST_URL(SonarQube 地址)。Gitea Actions 与 GitHub Actions 语法兼容,官方的 sonarqube-scan-action 可以直接用(内网自托管版见下文)。

向导推荐使用 GitHub CI 分析项目

图 12:向导的“使用 GitHub CI 分析项目“步骤说明:流水线里传入 SONAR_TOKEN 与 SONAR_HOST_URL

点进去创建令牌(图 13),记下生成的密钥(文内占位 <SONARQUBE_ANALYSIS_TOKEN>;截图本身不含令牌值)。

创建项目令牌对话框

图 13:创建项目令牌对话框(令牌尚未生成,图中不含令牌值)

令牌怎么存(红线):这是一个用户级 API token,长期有效,等价于“账号的钥匙“。本环境只放进 example 仓库的 Gitea Secrets(仓库级加密环境变量,日志里打印自动打码):

Secret值
SONAR_TOKEN<SONARQUBE_ANALYSIS_TOKEN>
SONAR_HOST_URLhttps://sonarqube.kali-team.internal

不进代码、不进文章、不进聊天——谁拿到令牌,谁就能以你的名义向 SonarQube 提交分析结果。

5.3 用 SonarScanner CLI 手动扫描(没有流水线时)

暂时没有流水线的场景(新项目没接 CI、开发机一次性验证、流水线坏了本地复现),用 SonarScanner CLI 直接扫——就是流水线里那个 scanner,区别只是“由你在命令行驱动“。三步:

第 1 步:获取 scanner

官方下载站内网到不了,从 Nexus raw 仓库 sonar-scanner-cli 取——raw 仓库没有目录列表,必须显式写全文件名:

# 做什么:从内网 Nexus 仓库下载 scanner
# 怎么做:wget 显式文件名(raw 仓库不能"列目录挑文件")
# 为什么:官方站内网不可达;Nexus 里这份是官方站取回缓存的同一文件
wget https://nexus.kali-team.internal/repository/sonar-scanner-cli/sonar-scanner-8.1.0.6389-linux.tgz
# 解压到 /opt,并把 bin 目录加进 PATH,之后 sonar-scanner 命令直接可用
tar -xzf sonar-scanner-8.1.0.6389-linux.tgz -d /opt/
export PATH="$PATH:/opt/sonar-scanner-8.1.0.6389-linux/bin"

两个前提:scanner 是 Java 程序,开发机要先装一个 JRE(JDK 发行版里就带;流水线任务镜像已预装,1.3);Java 不读系统信任库(详见“常见问题与坑“),开发机若没装内网 CA,系统信任库与 Java 的 cacerts 两处都要导入,思路同 6.2 的“双通道“。

第 2 步:写 sonar-project.properties

scanner 靠这个文件对上目标;example 用的是最小形式(上面三行),完整字段按需取用:

字段必填作用
sonar.projectKey是项目唯一标识:必须与 SonarQube 上创建的项目 Key 一致,对不上结果就挂不到项目下
sonar.projectName建议面板显示名
sonar.projectVersion建议项目版本,随分析记录展示
sonar.sources是扫描哪些目录,多个用逗号分隔;. = 整个仓库(example 的写法)
sonar.exclusions可选排除模式,如 **/vendor/**,**/node_modules/**——生成代码 / 第三方代码不该占扫描指标
sonar.sourceEncoding可选源码编码,本环境统一 UTF-8
sonar.language可选手动指定语言;可省略——scanner 会自动识别(example 的 Go 没写这条,照样识别)

第 3 步:执行扫描

# 做什么:扫描当前目录的代码,结果提交给 SonarQube
# 怎么做:命令行 -D 传两个参数——服务器地址 + 令牌(-D 是 scanner 传配置
#         的方式,命令行参数优先于 properties 文件)
# 为什么:令牌是 scanner 连接 SonarQube 的凭据;<SONARQUBE_ANALYSIS_TOKEN> 为本文占位符
sonar-scanner \
  -Dsonar.host.url=https://sonarqube.kali-team.internal \
  -Dsonar.token=<SONARQUBE_ANALYSIS_TOKEN>

跑成功后,5.1 创建的项目页面上会出现一条新的分析记录——和流水线扫的是同一个页面、同一份数据:质量门禁判定、三维度评级、问题列表都在这里看。

5.4 配置质量门禁:点哪里、配什么

这一节只讲操作,分项目级和系统级两层。

项目级:给本项目选一个门禁

项目列表 → 点进项目 → Project Settings → Quality Gate → 从下拉里选一个。内置的 Sonar way 是出厂推荐阈值,也可以选自定义门禁;改动只影响当前项目。

系统级:在 Quality Gates 页自定义条件

管理员入口 → Quality Gates → 新建 / 编辑门禁,逐条设置条件。本环境参考口径:

条件阈值含义
新代码覆盖率≥ 80%新增代码里被单元测试覆盖的比例
新代码重复率≤ 3%新增代码里互相复制的比例
维护性等级A评级 A–E,A 最好
安全等级A同上

为什么都是“新代码“:门禁条件可按“全部代码“或“新代码”(“新代码期“内的变动,按版本基线 / 时间窗口划定)定义——对老项目设全量门禁,存量问题会把它永远钉在 Failed,先管增量才可落地。

提醒:门禁配置后每次扫描都判定(含 5.3 的手动扫描);本环境现状是判定只展示在页面上,Failed 不会让流水线失败(详见“常见问题与坑“)。

6 CI/CD 联动:sonarqube.yaml 逐段讲

6.1 SAST 在流水线里的位置

.gitea/workflows/sonarqube.yaml 的触发条件:打版本 tag(v* push)、PR 事件、repository_dispatch(webhook)、workflow_dispatch(手动)——每次代码变动都自动做一遍静态扫描。

在流水线里:docker.yaml 管构建镜像、dependency-track.yaml 管依赖扫描(SCA)、sonarqube.yaml 管自己的代码扫描(SAST)——三条并行、各管一段。

6.2 四个步骤逐段看

全文(example 仓库真实文件,注释保留原样):

on:
  # Trigger analysis when pushing to your main branches, and when creating a pull request.
  push:
    tags:
      - "v*"
  pull_request:
      types: [opened, synchronize, reopened]
  repository_dispatch:  # 通过 Webhook 触发
  workflow_dispatch:   # 支持手动立即触发
name: Main Workflow
jobs:
  sonarqube:
    runs-on: ubuntu-host
    env:
      RUNNER_TOOL_CACHE: /toolcache
    steps:
      - name: Init
        run: |
          echo -n | openssl s_client -showcerts -connect hub.kali-team.internal:443 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >> hub.kali-team.internal.crt
          sudo cp hub.kali-team.internal.crt /usr/local/share/ca-certificates/
          sudo update-ca-certificates
      - name: Checkout code
        uses: https://hub.kali-team.internal/actions/checkout@v4
        env:
          GIT_SSL_NO_VERIFY: true
        with:
          submodules: true
          fetch-depth: 0
          token: ${{ secrets.TOKEN }}
      - name: Verify Certificate
        run: |
          sudo cp kali-team-ca.crt /usr/local/share/ca-certificates/
          sudo mkdir -p /etc/docker/certs.d/mirror.kali-team.internal
          sudo cp kali-team-ca.crt /etc/docker/certs.d/mirror.kali-team.internal/ca.crt
          sudo update-ca-certificates
      - name: SonarQube Scan 静态代码扫描
        uses: https://hub.kali-team.internal/actions/sonarqube-scan-action@v9
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # 创建项目的时候创建的长期有效的Token
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} # SonarQube服务器地址
          NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/kali-team-ca.crt # 信任dev-sec-ops证书
          # SONAR_ROOT_CERT: ${{ secrets.SONAR_ROOT_CERT }}  #sonar是java写的,他不读取系统证书,所以还要加到环境变量,PEM格式的证书
        with:
            scannerBinariesUrl: https://nexus.kali-team.internal/repository/sonar-scanner-cli/ # 下载SonarScanner CLI的URL,使用私有Nexus仓库
            scannerFile: sonar-scanner-8.1.0.6389-linux.tgz # 显式文件名(Nexus raw 仓库无目录列表接口,实测 GET / 返回 404)
            projectBaseDir: . # 项目根目录,默认为当前工作目录
            skipSignatureVerification: true  # 不知道为什么内网有时候访问hkps://keyserver.ubuntu.com提示证书错误

前三步与另外两条流水线相同:Init 抓内网 Gitea 证书进系统信任库;Checkout 克隆代码;Verify Certificate 把内网 CA 同时放进系统信任库与 Docker 证书目录(/etc/docker/certs.d/),让 git、docker 都认内网 https。重点讲最后一步:

Scanner 从哪来?Nexus 仓库。 官方站在公网,scanner 压缩包(约 9MB)从 Nexus raw 仓库 sonar-scanner-cli 取,scannerBinariesUrl 指向它。raw 仓库没有目录列表接口(GET / 直接 404),必须在 scannerFile 里显式写死文件名。大头的 JRE 完全不下载:任务镜像已预装。

Java 不读系统证书。 scanner 是 Java 程序,用自己的证书库(keystore,$JAVA_HOME/lib/security/cacerts),不读 Linux 系统信任库——前面导再多次,Java 也看不见。解法分两路:node 部分靠 NODE_EXTRA_CA_CERTS(node.js 的“额外信任证书“环境变量);Java 部分由 action 内部用 keytool(JDK 自带的证书管理工具)把 kali-team-ca.crt 导入 cacerts。两路都通,https 握手才成功。

项目怎么对上号? projectBaseDir: . 告诉 scanner 从当前目录开始扫,它在该目录读 sonar-project.properties:文件里 sonar.projectKey=example 必须与 Server 侧项目一致,结果就挂到那个项目下。

skipSignatureVerification: true。 官方 action 默认还会对下载包做 GPG 签名校验(防篡改的签名机制)。内网 Nexus 的包是自己传的,没有签名可验;验签还要连公网密钥服务器(文件注释里的实测现象:内网访问 hkps://keyserver.ubuntu.com 有时报证书错误)——所以关掉。完整性改由“内网 Nexus + 内部管控流程“保证。

6.3 实测:run #8

在 Gitea Actions 页面手动触发 sonarqube.yaml:sonarqube job 耗时 21s,状态 success。随后 SonarQube 上 example 项目(显示名「项目例子」)出现扫描记录:

SonarQube 项目列表

图 14:SonarQube 项目列表:项目例子(61 行代码,Go)最新分析 Quality Gate Passed,Security / Reliability / Maintainability 三维度 A/0、A/0、A/1,Coverage 0.0%——与下表同一份数据

指标实测值怎么读
Quality GatePassed扫描通过门禁标准
代码行数61example 项目很小
Security / Reliability / Maintainability0 / 0 / 1(open issues)安全漏洞 / 稳定性缺陷 / 可维护性坏味道三维度未关闭问题数,评级 A;那个“1“是坏味道
Coverage0.0%单元测试覆盖的代码比例——示例项目没写单测,0% 符合预期
最后分析时间2026-10-15 14:12 UTC即本次 run 完成时间

目前流水线停在“扫描并出报告“:门禁用的是内置默认策略,判定只展示在 SonarQube 页面上,尚未接成“卡住流水线“的硬卡点(详见“常见问题与坑”)。

7 常见问题与坑

  1. OIDC 连 Gitea 报证书错误(内网自签 CA)。 现象:配置 / 调试 OIDC 时 SonarQube 连 hub.kali-team.internal 证书校验失败。根因:SonarQube 是 Java 程序、用自带 CA 信任库,内网 CA 不在默认库里。解法:chart 的 caCerts——根证书先建 Secret,caCerts.enabled: true + secret 指向它覆盖默认库(2.3、2.4 的 caCerts 项)。
  2. OIDC 授权后跳转到 localhost。 根因:Server base URL 没配,SonarQube 以为自己在 localhost,回调地址算错。解法:General Settings 里填真实内网域名。
  3. Scanner 连不上 SonarQube 的 https(Java 不读系统证书)。 根因:scanner 是 Java 程序,用自己的 cacerts;解法是双通道——action 内 keytool 导入内网 CA 到 cacerts,node 侧走 NODE_EXTRA_CA_CERTS(6.2 的证书步骤)。
  4. Scanner 下载 404(Nexus raw 无目录列表)。 不写 scannerFile 时 action 无法列目录找文件(6.2 的第一步)。教训:raw 仓库取文件,永远显式写文件名。
  5. apt-get remove wget 误删扫描器的下载工具。 流水线 Init 里曾有一句删 wget 的“瘦身“,而 scan-action 靠 wget 从 Nexus 拉 scanner——删完下载直接失败。7dce1ad 删掉该行后一次跑通。教训:给任务容器做工具面改动前,先确认每个 action 实际用到哪些命令。
  6. 初始 admin 默认密码(admin/admin)。 全新 SonarQube 用 admin/admin 可登;首次登录后必须修改,新值按团队凭据规范存放;本文与截图一律占位 / 打码。
  7. 质量门禁尚未卡流水线。 现状是“扫描并出报告“:门禁用内置默认策略,即使判定 Failed,流水线也不会因此失败。要“扫不过不许合并 / 发布“,需让流水线在门禁不通过时失败(官方 scan-action 支持等待门禁判定并据此置失败,可作为后续接入项)。
  8. chart 要求指定 monitoringPasscode。 不填 chart 校验不过(2.4 的第 8 项);需要指定添加,本环境用 community。
  9. 内存上限给低了 Java 服务 OOM。 思路是“内存给多点,java 开发的,避免执行任务的时候 OOM“(2.4 的第 7 项);若 pod 反复被杀(重启次数不停涨),第一反应查 resources.limits.memory。

8 小结

这一篇给内网 DevSecOps 体系补上代码质量 / 安全一环:SonarQube 装进 k3s(官方 chart + 共享 PostgreSQL + 自签 CA 覆盖),接入 Gitea OIDC 统一登录,sonarqube.yaml 让每次推送 / PR 自动跑一遍 SAST 扫描——21 秒出结果,质量门禁判定在页面上可见。

  • 概念:SAST 不运行代码就能找漏洞 / 缺陷 / 坏味道;质量门禁是扫描的判定标准;Server 存结果、Scanner 干活,sonar-project.properties 的 projectKey 是两者对上的唯一标识;
  • 搭建:共享 PostgreSQL 上临时 pod 建库、自签 CA 进 Secret 用 caCerts 覆盖、values 五个重点(community 版、jdbcOverwrite、ingress、persistence、内存);
  • 初始配置 + OIDC:改 admin 默认密码、Marketplace 装中文与 OIDC 插件、按发现端点填 Issuer / Client / scopes、配 Server base URL(否则跳转回 localhost);
  • 使用:手动建项目(Key 与仓库 properties 对齐)、令牌只进 Gitea Secrets;
  • CI/CD:scanner 走 Nexus raw 仓库(文件名显式写死)、Java 证书双通道(keytool + NODE_EXTRA_CA_CERTS)、实测 run #8:21s、Quality Gate Passed、61 行、0/0/1;
  • 坑:自签 CA、localhost 跳转、Java 证书、raw 无目录列表、wget 被误删、默认密码、门禁未卡点、monitoringPasscode、OOM。

下一步可选:把质量门禁接成流水线硬卡点(扫不过不许合并,官方 action 支持等待门禁判定)、配更严的门禁策略(New Code 周期、覆盖率阈值)、接入更多项目(key 规则通用——sonar-project.properties 的 key 与服务端项目一致即可)。至此内网五平台齐位:Gitea(代码)→ Harbor(镜像)→ Nexus(制品)→ SonarQube(代码质量)→ Dependency-Track(依赖安全),流水线覆盖“代码 → 镜像 → 依赖 → 质量“全链。

参考