安全开发-Gitea
目录
用 Gitea Actions 在这套 DevSecOps 内网里把 CI/CD 真正跑起来:自托管 Runner 怎么建、composite Action 为什么自建、docker/sonarqube/dependency-track 三条流水线逐段拆解,以及 Harbor/Nexus/SonarQube/DT 的平台联动 图 1:Gitea 主界面(hub.kali-team.internal) 在 Rancher 的 Gitea chart 里修改 Service 配置,改完的页面: 图 2:Rancher 里 Gitea chart 的 Service Settings 页:SSH 服务改为 NodePort、端口映射到宿主机 32222 对应的 values 改动(diff 格式: 最后点击安装,终端输出: 装好 Gitea,只是有了“代码的家“。在这套 DevSecOps 体系里,它还承担六项职能: 图 3:Gitea SSH / GPG 密钥页:本节登记的 加完公钥,这台宿主机就能以 SSH 方式克隆 最容易被绕晕的一点:Gitea 自己并不执行流水线。 一套 Actions 跑起来有三个角色: GitHub 官方也提供“托管 Runner“(你什么都不用装,GitHub 出机器)。自托管的区别主要在两点: Gitea 官方的 Runner 实现叫 Runner 是 Go 写的,任务容器是 Docker 的,启动前有两个坑必须先堵上,否则它起不来或连不上。 问题 1:Runner 访问 Gitea(https)要校验证书。 我们的 Gitea 证书是内网 CA 签的,而 Go 程序默认只信系统 CA 库。解法是把“系统 CA + 内网 CA“合并成一个文件,用环境变量 问题 2:任务容器解析不了内网域名。 任务容器默认走 Docker 的 bridge 网络,它的 我推荐 设置侧的 Runner 列表页(空闲即在线、未执行任务): 图 4:仓库 → 设置 → Actions → Runner: 图 5:Rancher「应用程序 → Chart 仓库」里的 actions chart(gitea-charts 仓库、版本 0.1.2)页面:Gitea Actions Helm chart for Kubernetes,Quick-start 即下面三步的 先获取 Runner Token(与 3.2 相同) 方式和 3.2 一样:在 Gitea 侧添加 Runner(实例 / 组织 / 仓库三级的 Actions 设置里都可以),页面会给出注册 token。区别只有一处:token 不再写进配置文件,而是放进 Secret。 再创建 Secret 把下面内容存成token用占位符,真值以 Gitea 界面给出的为准),hub为命名空间: 容易踩的坑:Secret 的 namespace 必须和 chart 部署的 namespace 一致。Secret 是“只认本命名空间“的对象,两边不一致,Runner 就读不到 token,注册会一直重试。 添加 chart 仓库,拉默认 values 模板 为什么先拉模板再改?chart 的默认值很多,逐条手敲容易漏也容易错;导出完整模板后只改需要的字段,其余一律用默认值,最稳。 填写 values.yaml 下面只列要改的字段(其余保持模板默认值),每行都注释了“为什么这么设“: 部署 图 6:Gitea「dev-sec-ops」组织 → 设置 → 工作流 → 运行器: 任务镜像 两个“为什么这么麻烦“的解释: 这里踩过一个很隐蔽的坑:这台 docker CLI 不加载 cli-plugins。buildx 的设计是“docker CLI 的插件“(放进 构建并推到一个私有 project(robot 账号登录,见Harbor 一节): GitHub/Gitea 流水线里,一步经常写成: 这叫 Action:一个可复用的“工具包“,别人把“克隆代码““登录镜像仓库”“构建镜像“这些通用操作封装好,你用一行就引用进来。 但 Action 本质是“别人写的、会在你的流水线里执行的代码“,这就是著名的供应链攻击面:引用外部 Action = 允许别人写的代码在你的 CI 环境里跑(能读到你仓库的 Secrets)。内网场景更现实:任务容器根本访问不了 解法干脆:把需要的 Action 自己实现一份,托管在自己的 Gitea 上。我在 Gitea 建了 三种触发方式对应三种使用场景:网页手动(调试/临时发版)、打 tag 推送(正式发版)、API 触发(跟其它系统联动)。 以 图 7:dev-sec-ops/example 仓库页面,克隆地址使用 SSH 公钥认证的 32222 端口(SSH 的 NodePort) 先有一个条件判断( 然后是执行步骤,按顺序拆: 第 1 步 Init:把 Gitea 的证书装进系统信任库 用 第 2 步 Checkout code:克隆代码 用 5 节自建的 第 3 步 Verify Certificate:信任 dev-sec-ops 证书 + 注入 docker 客户端证书目录 最后两行是内网 Docker 的经典细节:Docker 客户端有个约定, 第 4 步 Login to Harbor:登录镜像仓库 第 5 步 Set up QEMU:注册 binfmt(多架构预留,本环境跳过) 第 6 步 Go Build Cache for Docker:缓存(占位) GitHub 上这步会把 第 7 步 Set up Docker Buildx 第 8 步 Build and push:核心构建 第 9 步 Inspect image:确认推送成功 最后两步 Move cache 与 Trigger Watchtower:前者是“搬缓存目录“(带 本节数据与截图来自真实运行(2026-10-15 起)。 图 10:Gitea Actions 里 docker.yaml 的运行记录(run #6,手动触发,main 分支),状态「成功」,总耗时 1m25s,setup 与 build-and-push 两个 job 均为 success 第二个,Harbor 出现镜像: 图 11:Harbor 的 dev-sec-ops 私有 project:流水线推送的 example-server 与自建的 runner-images、gomod-cache 都在 第三个, 图 12:SonarQube 上 example 项目(显示名「项目例子」)的 Overview:Quality Gate Passed,三个维度的未关闭问题数与评级 前言
workflow + job + step 写法基本能直接搬过来用。Jenkins 功能强但插件体系重、配置要靠 Jenkinsfile,Woodpecker 轻但生态小,Gitea Actions 学习成本最低。
1 Gitea:安装与公钥配置
1.1 代码托管平台:流水线的源头
1.2 安装 Gitea:官方 Helm Chart 一键部署
hub.kali-team.internal 访问。
- 是原值,+ 是改后值):@@ -282,9 +282,9 @@
loadBalancerClass: null
loadBalancerIP: null
loadBalancerSourceRanges: []
- nodePort: null
+ nodePort: 32222
port: 22
- type: ClusterIP
+ type: NodePort
serviceAccount:
annotations: {}
automountServiceAccountToken: falselevel=WARN msg="--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher"
Release "gitea" has been upgraded. Happy Helming!
NAME: gitea
LAST DEPLOYED: Sun Sep 13 01:32:10 2026
NAMESPACE: default
STATUS: deployed
REVISION: 2
DESCRIPTION: Upgrade complete
NOTES:
1. Get the application URL by running these commands:
http://hub.kali-team.internal/
1. Review these warnings:
- Gitea uses 'memory' for caching which is not recommended for production use. See https://docs.gitea.com/next/administration/config-cheat-sheet#cache-cache for available options.
- Gitea uses 'leveldb' for queue actions which is not recommended for production use. See https://docs.gitea.com/next/administration/config-cheat-sheet#queue-queue-and-queue for available options.
- Gitea uses 'memory' for sessions which is not recommended for production use. See https://docs.gitea.com/next/administration/config-cheat-sheet#session-session for available options. 1.3 Gitea 在 DevSecOps 中的作用
1.4 添加公钥
ssh-keygen 是系统自带的生成密钥对命令;-t ed25519 指定密钥类型,ed25519 是较新的椭圆曲线算法,短小、快速、安全;-C 后面的文字是给密钥加的备注,方便以后区分“这把钥匙是给哪台服务器用的“):➜ ~ ssh-keygen -t ed25519 -C "hub.kali-team.internal"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/kali-team/.ssh/id_ed25519): /home/kali-team/.ssh/kali-team.internal
Enter passphrase for "/home/kali-team/.ssh/kali-team.internal" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/kali-team/.ssh/kali-team.internal
Your public key has been saved in /home/kali-team/.ssh/kali-team.internal.pub
The key fingerprint is:
SHA256:GcSGRR0uq1cEVqiEVbmWJtbm35DdQY1sWqnhTE9fp9Y hub.kali-team.internal
The key's randomart image is:
+--[ED25519 256]--+
| o.*B=o. . + |
| . oo=o. o O +|
| . +ooo+ X +o|
| + B* = = E|
| . *S .o o . |
| ...o . . |
| . .. o |
| . . . |
| |
+----[SHA256]-----+
➜ ~
➜ ~ cat .ssh/kali-team.internal.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP1ovd63A23WxDJy58RylWDuC+9Sy2DOF4Op+uJR8K58 hub.kali-team.internal
➜ ~hub.kali-team.internal),保存即可。
hub.kali-team.internal 公钥已在列表(另有一把 arch 密钥)hub.kali-team.internal 上的仓库了,克隆地址会带上 32222 端口,正是 1.2 节改出来的 SSH NodePort。仓库页面见 6.2 节的图 7。 2 概念:Action Runner 到底在干什么
2.1 三张角色的分工:Gitea 派单,Runner 干活,任务容器动手
角色 干什么 Gitea(hub) 读代码仓库里的流水线 YAML,把任务挂进待办队列,把结果和日志展示出来 Runner(运行器) 常驻进程,每隔几秒问 Gitea“有没有我的活“,有就领回来执行并回报结果 任务容器 Runner 执行流水线时按配置新建一个 Docker 容器,流水线的命令都在这个容器里跑 2.2 三个关键概念:labels、Secrets、事件上下文
runs-on: <标签>,意思是“我要一个能干这个活的机器“,Gitea 把 job 派给标签匹配的 Runner。标签值(如 ubuntu-host)可以附带一个镜像地址,表示“任务装进这个镜像的容器里跑“。github.*):平台自动注入的“情报“,如 ${{ github.event_name }}、${{ github.ref }} 这类写法,是这次运行由什么触发、在哪个分支/标签上等信息,让流水线能“看情况办事“。 2.3 自托管 Runner 和 GitHub 托管 Runner 的区别
github.com,托管 Runner 那种“去公共库拉东西“的模式在内网行不通。所以 Runner 必须在内网自托管,而且流水线里引用的一切资源(镜像、依赖、Action)都得是内网可达的。 3 部署自托管 Runner:act_runner
act_runner(gitea/act_runner)。它兼容 GitHub Actions 的 workflow 语法,所以 GitHub 上写过的流水线大多能直接搬过来。 3.1 先解决两个“内网特有“的问题
SSL_CERT_FILE 指给 Go 程序(这是 Go 官方支持的证书加载方式):# 合并:系统 CA 库 + 内网 CA(step 中间 CA + 根 CA)拼成一个 bundle
cat /etc/ssl/certs/ca-certificates.crt kali-team-ca.crt > ca-bundle-kali.pem/etc/hosts 和 DNS 跟宿主机完全隔离,宿主机里写的 hub.kali-team.internal 等内网域名容器里查不到,内网自建的 DNS 又不解析这些域名。解法:启动 Runner 时让它给每个任务容器追加 --add-host 参数(Docker 的“手动往 /etc/hosts 写条目“开关):# runner/config.yaml(节选)
container:
# 任务容器加入的 Docker 网络。不指定时 Runner 每个任务随机建一个网络,越积越多
network: "action_default"
# 字符串形式,条目之间用空格分隔
options: "--add-host=hub.kali-team.internal:10.111.13.105 --add-host=mirror.kali-team.internal:10.111.13.105 --add-host=nexus.kali-team.internal:10.111.13.105 --add-host=sonarqube.kali-team.internal:10.111.13.105 --add-host=rancher.kali-team.internal:10.111.13.99 --add-host=ca.kali-team.internal:10.111.13.99"
runner:
capacity: 1 # 同一时间最多并行跑 1 个任务(小机器保守起见)
timeout: 3h # 单任务超时;Gitea 服务端也有 3h 默认超时,取较短者
labels:
# 标签:<标签名>:<任务容器镜像>
- "ubuntu-host:docker://mirror.kali-team.internal/dev-sec-ops/gitea/runner-images:ubuntu-latest"action_default 这个网络要提前建好(docker network create action_default)。dev-sec-ops/gitea/runner-images:ubuntu-latest,就是 4 要自己构建的“任务容器镜像“,镜像走内网 Harbor 的 pull-through。 3.2 启动 Runner(docker compose / docker run 二选一)
docker compose(对应仓库的 runner/docker-compose.yml)。宿主机没装 compose 插件的话,用一条等价的 docker run 也行:export REGISTRATION_TOKEN=<在 Gitea 界面获取的注册 token>
export RUNNER_NAME=kali76-runner-01
export RUNNER_LABELS="ubuntu-host:docker://mirror.kali-team.internal/dev-sec-ops/gitea/runner-images:ubuntu-latest"
docker run -d --name kali76-runner \
--network action_default \
--add-host hub.kali-team.internal:10.111.13.105 \
-e CONFIG_FILE=/config.yaml \
-e GITEA_INSTANCE_URL="https://hub.kali-team.internal/" \
-e GITEA_RUNNER_REGISTRATION_TOKEN="$REGISTRATION_TOKEN" \
-e GITEA_RUNNER_NAME="$RUNNER_NAME" \
-e GITEA_RUNNER_LABELS="$RUNNER_LABELS" \
-e SSL_CERT_FILE=/etc/ssl/certs/ca-bundle-kali.pem \
-v $(pwd)/config.yaml:/config.yaml \
-v $(pwd)/data:/data \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/ca-bundle-kali.pem:/etc/ssl/certs/ca-bundle-kali.pem:ro \
mirror.kali-team.internal/docker.io/gitea/act_runner:latest/var/run/docker.sock 是 Docker 的“控制接口“,Runner 靠它才能在宿主机上创建/销毁任务容器;data 目录存注册结果(.runner 状态文件里有 Runner 的服务端 ID 和随机密钥,丢了就得重新注册)。
Runner registered successfully 即注册成功,之后 Runner 进入守护状态,按 fetch_interval(默认 60s)周期性来 Gitea 取任务。 3.3 验证 Runner 在线

kali76-runner-01(仓库级 Runner、标签 ubuntu-host、版本 v0.6.1)状态空闲(Idle),空闲 = 在线且当前没有任务在跑 3.4 生产化部署:用官方 Helm Chart(helm-actions)在 k3s 上跑 Runner
docker run 在单机上把 act_runner 跑起来了,学习和单机小环境没问题,但生产部署短板明显:进程挂了不会自动拉起、没法扩副本、状态依赖宿主机目录、没有资源限制。act_runner 用 官方 Helm Chart(helm-actions) 包装成 StatefulSet 跑进 k3s:自动重启、可扩副本、持久化存储、资源限制、由集群统一管理。1.2 已经用 Helm 在 k3s 上装了 Gitea 服务端本身,本节装的是 Runner。
helm repo add / helm show values / helm upgrade --install 三步ubuntu@nuc-node:~$ kubectl create secret generic runner-secret \
--from-literal=runner-token=jmOxTd7nNfuCfwDoLqDFawbcmO9Y9Ztt3oGPOIOX -n hub
secret/runner-secret created
ubuntu@nuc-node:~$helm repo add gitea-charts https://dl.gitea.com/charts/ && helm repo update # 加官方 chart 仓库并刷新索引
helm show values gitea-charts/actions > values.yaml # 把 chart 的默认 values.yaml 导到本地@@ -1,7 +1,7 @@
-enabled: false
-existingSecret: ''
-existingSecretKey: ''
-giteaRootURL: ''
+enabled: true
+existingSecret: runner-secret
+existingSecretKey: runner-token
+giteaRootURL: http://gitea-http.hub.svc.cluster.local:3000
global:
imagePullSecrets: []
imageRegistry: ''
@@ -48,6 +48,9 @@
level: debug
cache:
enabled: false
+ runner:
+ labels:
+ - "ubuntu-host:docker://mirror.kali-team.internal/dev-sec-ops/gitea/runner-images:ubuntu-latest"
container:
require_docker: true
docker_timeout: 300shelm install --labels=catalog.cattle.io/cluster-repo-name=gitea-charts --namespace=hub --timeout=10m0s --values=/home/shell/helm/values-actions-0.1.2.yaml --version=0.1.2 --wait=true action /home/shell/helm/actions-0.1.2.tgz
level=WARN msg="--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher"
NAME: action
LAST DEPLOYED: Wed Oct 28 07:52:44 2026
NAMESPACE: hub
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
---------------------------------------------------------------------
SUCCESS: helm install --labels=catalog.cattle.io/cluster-repo-name=gitea-charts --namespace=hub --timeout=10m0s --values=/home/shell/helm/values-actions-0.1.2.yaml --version=0.1.2 --wait=true action /home/shell/helm/actions-0.1.2.tgz
---------------------------------------------------------------------time="2026-10-28T08:25:23Z" level=info msg="Log level debug requested, setting up report caller for further debugging" func="[initLogging]" file="[daemon.go:236]"
time="2026-10-28T08:25:23Z" level=info msg="log level set to debug" func="[initLogging]" file="[daemon.go:240]"
time="2026-10-28T08:25:23Z" level=info msg="Starting runner daemon" func="[func5]" file="[daemon.go:42]"
time="2026-10-28T08:25:23Z" level=info msg="Docker is ready" func="[func5]" file="[daemon.go:86]"
time="2026-10-28T08:25:23Z" level=info msg="labels updated to: [ubuntu-host:docker://mirror.kali-team.internal/dev-sec-ops/gitea/runner-images:ubuntu-latest]" func="[func5]" file="[daemon.go:127]"
time="2026-10-28T08:25:23Z" level=info msg="runner: action-runner-0, with version: v2.0.1, with labels: [ubuntu-host], declare successfully" func="[func5]" file="[daemon.go:148]"
action-runner-0(ID 2、v2.0.1、组织级、标签 ubuntu-host)状态空闲——Helm 部署的 Runner 已注册并在线# .gitea/workflows/test.yaml(放法与 6 章相同)
name: test
on: [workflow_dispatch] # 手动触发
jobs:
hello:
runs-on: ubuntu-host # 必须与本集群 Runner 注册的 label 一致
steps:
- run: echo "hello from k3s runner"runs-on 的值必须和本集群 Runner 的注册 label 对上:注册时声明了与 3.2 相同的 ubuntu-host label(values 模板里有对应 label 参数,以模板为准),就能直接复用第 4 章的内网任务镜像;若注册的是 chart 默认 label,就把这一行改成对应 label,任务镜像用该 label 指向的镜像。跑通即说明“Gitea 派单 → 集群内 Runner 领单 → 任务容器干活“整条链路可用。 4 任务容器镜像:自建一个“ubuntu-latest“
runs-on: ubuntu-latest,用的是 GitHub 官方维护的镜像;内网没有这个源,所以我自己造了一个功能对等的镜像,推上 Harbor,标签里指向它。runner-images:ubuntu-latest 里预埋的东西(都是“流水线干活要用、但内网拉不到“的):工具 为什么任务里需要 怎么来的 Docker CLI 任务里要 docker login / docker buildx build拷贝 Runner 宿主机的二进制(29.8.2,与 daemon 同版本,最稳) buildx 插件 增强版 Docker 构建(多阶段、多平台、buildkit 缓存) 宿主机 Go 1.27.1 源码编译(v0.38.0) Go 工具链 Go 项目要 go mod/go buildgolang 基础镜像自带(1.25) JRE SonarScanner 扫描要 Java 运行时 预装 curl / wget / git / openssl 下载、克隆、证书 基础镜像自带 sudo(shim) 不少 action 脚本无脑 sudo xxx用 exec "$@" 直通 shim 兜底docker buildx build,最终是连到宿主机 Docker 守护进程干活的。daemon 是宿主机上的,客户端和 daemon 版本差太多可能出兼容问题,最稳的就是“同一个机器、同一个版本“。试过在容器里 go install 最新 docker CLI,但它依赖要 Go ≥ 1.26,容器里只有 1.25,编译不过,直接拷二进制最干脆。buildx 同理:最新版 v0.38.0 也要 Go ≥ 1.26,容器编不了,就到宿主机(Arch,Go 1.27.1)上源码编译好再放进去。GOPROXY? 编译 buildx 要拉一堆 Go 依赖,内网到不了 proxy.golang.org,所以走 Nexus 的 goproxy(后面会说到),这也提前埋下“构建环境也要接 Nexus“这条线。~/.docker/cli-plugins/ 就能 docker buildx 调用),但本环境这套 docker 29.8.2 无论如何不加载插件。解法是写一个 wrapper 脚本放 /usr/local/bin/docker(PATH 最前):遇到 buildx 子命令转给独立编译的 buildx 二进制,其余子命令原样透传给真正的 docker CLI。docker build -t mirror.kali-team.internal/dev-sec-ops/gitea/runner-images:ubuntu-latest .
echo "$MIRROR_TOKEN" | docker login mirror.kali-team.internal -u "$MIRROR_USER" --password-stdin
docker push mirror.kali-team.internal/dev-sec-ops/gitea/runner-images:ubuntu-latest 5 自托管 Actions:把常用 action 搬上 Gitea
5.1 什么是 Action,为什么要自托管
- uses: actions/checkout@v4github.com,uses: actions/checkout@v4 直接拉不到。actions 组织,将常用的action镜像下来:仓库 标签 干什么 actions/checkoutv4 克隆当前仓库到工作区 actions/login-actionv2 docker login 登录 Harboractions/setup-qemu-actionv3 注册 QEMU binfmt(多架构构建用;单架构自动跳过) actions/cachev4 缓存占位(本机场景由 buildkit 缓存挂载替代,见 6) actions/setup-buildx-actionv5 设置/选择 buildx builder actions/build-push-actionv6 docker buildx build 构建并推送镜像actions/sonarqube-scan-actionv9 从 Nexus 下载 SonarScanner 并执行扫描 6 写流水线(一):docker.yaml,从提交到镜像
6.1 概念铺垫:buildx / buildkit 与多阶段构建
docker buildx 是客户端插件,buildkit 是真正干活的引擎,支持多阶段构建、多平台构建、分层缓存。buildx 有几种驱动,我们选 docker 驱动(直接用 Docker 守护进程内置的 buildkit,零额外镜像),因为内网镜像源里没有 moby/buildkit 镜像(这是一个“被迫“的取舍,见 9)。FROM(阶段),构建时每个阶段只用自己和前一阶段的产物,最终镜像可以很小很干净。 6.2 触发条件(
on: 段)on:
workflow_dispatch: # 网页上手动点"运行",带输入
inputs:
build_server:
description: 'Build server'
default: false
type: boolean # 是否构建 server 服务
version:
description: 'Version tag'
default: 'latest' # 打的镜像 tag
watchtower:
description: '触发 Watchtower 更新'
default: false
type: boolean
push:
tags:
- "v*" # 打 v 开头的 git tag(如 v1.0.0)时自动触发
repository_dispatch: # 通过 API 发事件触发(外部系统联动用)dev-sec-ops/example 仓库为例,clone 地址走 SSH 公钥认证的 32222 端口(SSH 的 NodePort):
6.3 Job 1:setup——“该打什么 tag“由一个轻 job 算出来
jobs:
setup:
runs-on: ubuntu-host # 找带 ubuntu-host 标签的 Runner(就是我们的 Runner)
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- name: Get version
id: get-version
run: |
if [[ "${{ github.event_name }}" == "push" ]] && [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/} # 打 tag → 版本号 = tag 名(去掉 refs/tags/ 前缀)
else
VERSION="${{ github.event.inputs.version }}" # 手动触发 → 用输入框填的
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT # 写进 $GITHUB_OUTPUT 才成为 step 输出${{ }} 是 Actions 的表达式;github.event_name / github.ref / github.event.inputs.* 是 2 说的“事件上下文“。$GITHUB_OUTPUT 是 Runner 提供的一个文件,步骤往里写 key=value,后续步骤/ job 就能引用,git 版本管理、镜像 tag 这类跨步骤传递的“临时变量“都靠它。outputs 段把 step 的输出“上提“成整个 job 的输出,供下一个 job 用。 6.4 Job 2:build-and-push——真正干活的环节
if:),决定这条 job 什么时候跑: build-and-push:
runs-on: ubuntu-host
needs: setup # 等 setup 完成(依赖顺序)
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- service: "server"
build_input: "build_server"
# 只有三种情况才构建 server:打 tag 推送、repository_dispatch、手动触发且 build_server=true
if: |
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) ||
(github.event_name == 'repository_dispatch') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.build_server == 'true' || github.event.inputs.build_server == true))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-certificatesopenssl s_client 把 Gitea 的证书链拉下来,装进 update-ca-certificates 管的系统信任库。为什么?后面的 checkout 走 https 克隆,任务容器必须信任内网 CA 才能握手成功。 - 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 }} # 访问仓库的 Gitea 访问令牌(scopes=all)checkout action 克隆代码。token 是仓库 Secret 里存的访问令牌,克隆时认证用。fetch-depth: 0 拉完整历史(扫描/构建可能要看全量 commit)。sudo cp kali-team-ca.crt /usr/local/share/ca-certificates/
sudo update-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/etc/docker/certs.d/<registry 域名>/ca.crt 会作为访问该 registry 的额外 CA。这样 docker login/push mirror.kali-team.internal 就算忘了全局信任,也能通过证书校验。 - name: Login to Harbor Registry 登录Harbor Registry
uses: https://hub.kali-team.internal/actions/login-action@v2
with:
registry: https://mirror.kali-team.internal
username: ${{ secrets.MIRROR_USER }}
password: ${{ secrets.MIRROR_TOKEN }}MIRROR_USER / MIRROR_TOKEN 是 Harbor 的 robot 账号(8 讲)。登录成功后凭据写进任务容器的 ~/.docker/config.json,后面的 push 自动带上。 - name: Set up QEMU
uses: https://hub.kali-team.internal/actions/setup-qemu-action@v3
with:
image: mirror.kali-team.internal/docker.io/tonistiigi/binfmt:latestqemu-aarch64 后,内核就能在 amd64 上“执行“ arm64 二进制,多架构构建才用得上。linux/amd64(单架构),且注册 binfmt 需要特权容器,我们的 Runner 没开 privileged,所以 action 里容错成“注册失败只打 warning 不中断“。 - name: Go Build Cache for Docker
uses: https://hub.kali-team.internal/actions/cache@v4
with:
path: /tmp/.buildx-cache-${{ runner.os }}-buildx-${{ matrix.service }}
key: ${{ runner.os }}-buildx-${{ matrix.service }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-buildx-${{ matrix.service }}-
${{ runner.os }}-buildx-path 目录按 key 存到中心缓存服务。本环境 Runner 没有中心缓存服务,且任务容器的 /tmp 不跨任务保留,所以 cache@v4 实现成“建目录 + 打日志“的占位。真正的跨任务缓存被“烤“进了镜像,见 6.6 的 gomod-cache。 - name: Set up Docker Buildx
uses: https://hub.kali-team.internal/actions/setup-buildx-action@v5
with:
config-inline: |
[registry."mirror.kali-team.internal"]
insecure = true # buildkitd 官方镜像不含内网 CA,对 mirror 跳过 TLS 校验docker 驱动下 buildkit 跑在 Docker 守护进程里,所以这段内联配置实际上不生效(TLS 信任靠 Verify Certificate 步骤注入的 /etc/docker/certs.d/ CA)。保留它只是对齐上游 action 的接口。v5 的行为是 driver=docker 时不新建 builder,直接 docker buildx use default,因为 docker 驱动是“单例“,守护进程自带的 default builder 删不掉、再建会报错(详见 9 坑 #5)。 - name: Build and push Docker image for ${{ matrix.service }}
uses: https://hub.kali-team.internal/actions/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
build-args: |
BUILDKIT_INLINE_CACHE=1
target: ${{ matrix.service }}
tags: |
mirror.kali-team.internal/dev-sec-ops/example-${{ matrix.service }}:${{ needs.setup.outputs.version }}
mirror.kali-team.internal/dev-sec-ops/example-${{ matrix.service }}:latest
cache-from: type=local,src=/tmp/.buildx-cache-${{ runner.os }}-buildx-${{ matrix.service }}
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=maxtarget 指的是 Dockerfile 里某一个“阶段“(配合多阶段构建,只构建 server 这一个最终镜像);tags 打两个 tag(版本号 + latest);cache-from/to 是 buildkit 的本地目录缓存。docker 驱动下 buildkit 跑在宿主机,type=local 的路径指宿主机文件系统;而流水线的 Move cache 步骤跑在任务容器里,两边对不上,所以这套 cache-from/to 实际不跨任务生效(Move cache 带了 continue-on-error: true 不会挂)。真正跨任务缓存由 gomod-cache 镜像承担(6.6)。 - name: Inspect image
run: |
docker buildx imagetools inspect mirror.kali-team.internal/dev-sec-ops/example-${{ matrix.service }}:${{ needs.setup.outputs.version }}imagetools inspect 不去拉镜像,只向 registry 要 manifest(镜像的“目录清单“),能打印出来就证明镜像确实在 Harbor 里。continue-on-error,作用见第 8 步的澄清);后者是可选“自动更新“。Watchtower 是一个“容器看门狗“镜像,发现 registry 有新镜像就自动拉新、替换、重启。这里给它一个 HTTP API,构建完可选地通知它“去更新“。本环境暂未部署 Watchtower,所以这步只在手动触发且勾选 watchtower 时才执行。 6.5 对应的 Dockerfile:多阶段
# 构建阶段:golang 镜像里编译 Go 程序;依赖走 Nexus goproxy(7 讲)
FROM --platform=$TARGETPLATFORM mirror.kali-team.internal/docker.io/golang:1.25-bookworm AS builder
WORKDIR /app
ENV GOPROXY="https://nexus.kali-team.internal/repository/goproxy/" \
GOSUMDB="sum.golang.org https://nexus.kali-team.internal/repository/go-sumdb/" \
CGO_ENABLED=0
COPY kali-team-ca.crt /usr/local/share/ca-certificates/kali-team-ca.crt
RUN update-ca-certificates
COPY go.mod go.sum ./
# buildkit(docker 驱动)禁止 RUN --network=host,构建容器又解析不了内网域名(见 9 坑 #6);
# 所以在能联网的环境预先把 go mod download 的产物烤进 gomod-cache 镜像(推到 Harbor),
# 这里用 COPY --from 把依赖缓存和编译缓存"搬"进来
COPY --from=mirror.kali-team.internal/dev-sec-ops/gitea/gomod-cache:go1.25-gin1.10 \
/go/pkg/mod /go/pkg/mod
COPY --from=mirror.kali-team.internal/dev-sec-ops/gitea/gomod-cache:go1.25-gin1.10 \
/root/.cache/go-build /root/.cache/go-build
RUN go mod download
COPY main.go .
RUN go build -ldflags '-s -w' -trimpath -o /app/example .
# 运行时基础:精简 debian + 手动信任内网 CA
# (bookworm 精简镜像没有 update-ca-certificates,用 openssl x509 -hash 预算 subject hash 建软链)
FROM --platform=$TARGETPLATFORM mirror.kali-team.internal/docker.io/library/debian:bookworm AS runtime-base
WORKDIR /app
COPY kali-team-ca.crt /etc/ssl/certs/kali-team-ca.crt
RUN ln -s kali-team-ca.crt /etc/ssl/certs/6ecaa1df.0 && \
ln -s kali-team-ca.crt /etc/ssl/certs/15763dfa.0
# 非 root 运行用户(安全基线:容器内进程不跑 root)
RUN groupadd -g 10001 app && useradd -u 10001 -g 10001 -d /app -s /usr/sbin/nologin app
LABEL com.centurylinklabs.watchtower.enable="true"
# 应用阶段:只从 builder 拷一个静态二进制
FROM runtime-base AS server
WORKDIR /app
USER app
COPY --from=builder /app/example /app/example
EXPOSE 8080
ENTRYPOINT ["/app/example"] 6.6 为什么要有
gomod-cache 镜像:把“构建期网络“整个拿掉go mod download 退化成“离线校验一下缓存是否齐全“的 no-op,构建 100% 不依赖构建期网络。 7 实测验证:三条流水线跑起来

dev-sec-ops project 的 Repositories 页出现三行。example-server(本次流水线推的,tags 1.0.0 和 latest,指向同一 digest sha256:4cd55…e3513,约 49.6 MiB,2026-10-15 14:00 UTC 推送;digest 是镜像内容的“指纹“,内容相同指纹就相同,用来精确指认一个镜像)、gitea/gomod-cache(构建期缓存镜像)、gitea/runner-images(任务镜像):
sonarqube.yaml(run #8,成功):sonarqube job 耗时 21s、状态 success。SonarQube example 项目(显示名「项目例子」)出现扫描记录:Quality Gate Passed,61 行代码,Security / Reliability / Maintainability 三个维度 open issues 为 0 / 0 / 1(评级 A),Coverage 0.0%(示例项目没写单测,符合预期),最后分析 2026-10-15 14:12 UTC:
后续
参考