第 21/60 天
引言
在第 15 天我们通过 kaniko 在 Tekton 中构建出了容器镜像,在第 16 天完成了 Harbor 的安装与 HTTPS 配置。但到现在为止,还有一个关键的「最后一公里」没有打通:流水线凭什么凭证把镜像推进 Harbor?
很多团队在初期图省事,直接在流水线里硬编码 Harbor 管理员的用户名密码,或者创建一堆共享的「CI 专用」人工账号。这种做法的隐患是显而易见的:
- 管理员密码泄漏:一旦 Jenkins/Tekton 的 Secret 泄漏,攻击者直接拿到 Harbor 全部项目的管理权;
- 权限过大:CI 只需要推镜像到指定项目,却拥有删除、复制、管理成员的权限;
- 无法审计:多个人工账号共用,出问题后根本查不到是哪个流水线、哪个团队的操作;
- 无法轮换:密码写死在几十个 Pipeline 的 YAML 里,轮换一次密码等于做一次全量发布。
Harbor 从 2.0 开始提供了 Robot Account(机器人账号) 机制,专门解决这类「程序访问」场景。本文围绕「生产环境 Tekton + Argo CD + Harbor 三件套」中的 CI 环节,讲透 Robot Account 的完整实战:如何创建、如何在 Tekton 中配置推送、如何在 Argo CD 中配置拉取、以及过期轮换与安全加固的最佳实践。
核心概念
什么是 Robot Account
Robot Account 是 Harbor 为非人类调用方(CI 流水线、部署工具、备份脚本)设计的独立凭证体系。它不同于普通用户账号:
| 维度 | 普通用户 | Robot Account |
|---|---|---|
| 认证方式 | 用户名 + 密码(或 LDAP/OIDC) | 用户名 + Token(本质是加长随机串) |
| 权限范围 | 全局、可访问所有被授权的项目 | 仅限指定项目(push/pull/pull 之外可选 delete/read) |
| 登录入口 | 可登录 Harbor Web UI | 不能登录 Web UI |
| 命名 | 任意用户名 | 自动生成前缀,如 robot$ci-builder+push-bot |
| 过期时间 | 通常不过期 | 必须设置过期日期(最长 1000 天) |
| 审计 | 记录在常规操作日志 | 同样记录,且能精确到哪个 robot |
Robot Account 的权限模型
Harbor 2.x 的 Robot Account 支持细粒度权限控制,在创建时可以勾选:
- push:推送镜像/制品到该项目
- pull:从该项目拉取镜像/制品
- delete:删除该项目中的制品
- read:读取项目元数据(如查看 tag 列表)
生产环境推荐的最小权限原则:
- CI 构建机(Tekton):只给
push,最多加pull(构建过程需要回读已推送 tag 做幂等判断) - CD 部署机(Argo CD / 集群节点):只给
pull - 绝不给 CI 账号
delete权限——清理工作交给第 19 天讲到的 Harbor 回收日程(GC)或专门的管理员流程
Robot Account 在 Tekton 中的认证形态
Kubernetes 中访问私有镜像仓库的标准方式是 imagePullSecrets 或构建时的 docker config.json。Robot Account 的凭证(用户名 + token)可以直接编码进 docker config JSON:
{
"auths": {
"harbor.example.com": {
"username": "robot$ci-builder+push-bot",
"password": "xxxxxxxx",
"auth": "cm9ib3QkY2ktYnVpbGRlcitwdXNoLWJvdDp4eHh4eHh4eA=="
}
}
}
其中 auth 字段是 username:password 的 Base64 编码。Tekton 的 kaniko/docker 任务会读取 Secret 里 config.json 这个 key,从而完成向 Harbor 的认证推送。
实战步骤
下面我们完整走一遍「创建 Robot Account → 配置 Tekton Secret → 流水线推送 → Argo CD 拉取 → 过期轮换」的流程。假设环境:Harbor 地址 harbor.example.com,项目名 demo-project,Kubernetes 集群已安装 Tekton Pipelines。
Step 1:通过 API 创建 Robot Account(Shell)
Harbor 官方推荐用 API 管理 Robot Account,这样整个过程可以脚本化、可复现。先登录拿到会话 ID:
#!/bin/bash
set -euo pipefail
HARBOR_URL="https://harbor.example.com"
ADMIN_USER="admin"
ADMIN_PASS="REPLACE_WITH_ADMIN_PASSWORD"
PROJECT_NAME="demo-project"
# 1. 登录获取会话 ID
SESSION_ID=$(curl -sk -c /tmp/harbor_cookie.txt -u "${ADMIN_USER}:${ADMIN_PASS}"
-X POST "${HARBOR_URL}/api/v2.0/users/login"
-H "Content-Type: application/json" | jq -r '.["session_id"] // empty')
# 2. 查询项目 ID(Robot Account 必须挂在具体项目下)
PROJECT_ID=$(curl -sk -b /tmp/harbor_cookie.txt
"${HARBOR_URL}/api/v2.0/projects?name=${PROJECT_NAME}"
-H "X-Harbor-CSRF-Token: ${SESSION_ID}" | jq -r '.[0].project_id')
echo "PROJECT_ID=${PROJECT_ID}"
# 3. 创建 robot account(10 年 3650 天,权限仅 push+pull)
curl -sk -b /tmp/harbor_cookie.txt
-X POST "${HARBOR_URL}/api/v2.0/robots"
-H "Content-Type: application/json"
-H "X-Harbor-CSRF-Token: ${SESSION_ID}"
-d "{
"name": "ci-builder",
"duration": 3650,
"level": "project",
"disable": false,
"permissions": [{
"kind": "project",
"namespace": ${PROJECT_ID},
"access": [
{"resource": "repository", "action": "push"},
{"resource": "repository", "action": "pull"},
{"resource": "artifact", "action": "read"},
{"resource": "tag", "action": "list"}
]
}]
}" | tee /tmp/robot_response.json
Step 2:解析创建结果(JSON)
创建成功后 API 会返回 robot 的用户名和 token,格式如下(注意 token 只显示这一次,务必保存):
{
"id": 3,
"name": "robot$ci-builder+push-bot",
"secret": "S7kz9XpL2vQw1RtY4uMn8AbCdEfGhIj",
"creation_time": "2026-09-01T08:00:00.123Z",
"expires_at": "2036-08-29T08:00:00.123Z",
"disable": false,
"duration": 3650,
"level": "project"
}
拿到 name 和 secret 后,生成 docker config JSON(Python 一步到位,顺便把 Base64 也算了):
#!/usr/bin/env python3
import base64
import json
import subprocess
# 从环境变量读取,避免明文写进脚本
robot_name = "robot$ci-builder+push-bot"
robot_secret = "S7kz9XpL2vQw1RtY4uMn8AbCdEfGhIj"
registry = "harbor.example.com"
auth_str = f"{robot_name}:{robot_secret}"
b64_auth = base64.b64encode(auth_str.encode("utf-8")).decode("utf-8")
dockerconfig = {
"auths": {
registry: {
"username": robot_name,
"password": robot_secret,
"auth": b64_auth,
}
}
}
# 输出 kubectl 可用的 Secret YAML(注意 $ 符号需要转义处理)
print(json.dumps(dockerconfig, indent=2))
注意:robot$ci-builder+push-bot 中的 $ 在 Shell 中会被当作变量引用,写脚本时建议用单引号包裹,或者干脆在 ${} 外拼接字符串时使用 Python/heredoc 方式规避。
Step 3:在 Kubernetes 中创建 dockerconfigjson Secret(YAML)
把上一步生成的 docker config 保存为 /tmp/dockerconfig.json,然后创建 Kubernetes Secret:
kubectl -n tekton-pipelines create secret docker-registry harbor-push-cred
--docker-server=harbor.example.com
--docker-username='robot$ci-builder+push-bot'
--docker-password='S7kz9XpL2vQw1RtY4uMn8AbCdEfGhIj'
--docker-email=ci@example.com
--dry-run=client -o yaml | tee /tmp/harbor-push-secret.yaml
生成的 Secret 长这样(等价于用 dockerconfigjson 手动写):
apiVersion: v1
kind: Secret
metadata:
name: harbor-push-cred
namespace: tekton-pipelines
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: "eyJhdXRocyI6eyJoYXJib3IuZXhhbXBsZS5jb20iOnsidXNlcm5hbWUiOiJyb2JvdCRjaS1idWlsZGVyK3B1c2gtYm90IiwicGFzc3dvcmQiOiJTN2t6OVhwTDJ2UXcxUnRZNHVNbjhBYkNkRWZHaElqIiwiYXV0aCI6InJtOXZ..."
用 kubectl apply -f /tmp/harbor-push-secret.yaml 应用即可。如果希望流水线能自动把 Secret 同步到各个 Runner 命名空间,可以为它打上标签,配合 Tekton 的 workspaces 或后续的 cluster-resource 机制分发。
Step 4:编写带推送功能的 Tekton Task(YAML)
接下来写一个 kaniko 构建 Task,通过 workspace 挂载 docker config,构建后推送到 Harbor:
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: build-and-push
spec:
params:
- name: image
description: 目标镜像完整名称,如 harbor.example.com/demo-project/app
- name: dockerfile
default: ./Dockerfile
- name: context
default: .
workspaces:
- name: source # 源码工作区
- name: dockerconfig # 挂载 harbor-push-cred Secret 的 .dockerconfigjson
steps:
- name: build-and-push
image: gcr.io/kaniko-project/executor:v1.20.0
args:
- --dockerfile=$(params.dockerfile)
- --context=$(workspaces.source.path)/$(params.context)
- --destination=$(params.image)
- --skip-tls-verify=false
env:
- name: DOCKER_CONFIG
value: /kaniko/.docker
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
volumes:
- name: docker-config
secret:
secretName: harbor-push-cred
items:
- key: .dockerconfigjson
path: config.json
关键点:DOCKER_CONFIG 指向 /kaniko/.docker,而该目录下的 config.json 正是由 Secret 的 .dockerconfigjson 字段映射而来。kaniko 会自动读取它完成认证,然后在 --destination 指定的地址推镜像。
Step 5:定义 PipelineRun 并触发(YAML)
把源码、docker config、参数一次性灌进 Task:
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: build-push-run-$(context.pipelineRun.uid)
namespace: tekton-pipelines
spec:
pipelineSpec:
workspaces:
- name: source
- name: dockerconfig
tasks:
- name: checkout
taskRef:
name: git-clone
params:
- name: url
value: https://github.com/example/demo-app.git
- name: revision
value: main
workspaces:
- name: output
workspace: source
- name: build-push
taskRef:
name: build-and-push
runAfter: [checkout]
params:
- name: image
value: harbor.example.com/demo-project/app:$(params.tag)
workspaces:
- name: source
workspace: source
- name: dockerconfig
workspace: dockerconfig
params:
- name: tag
value: v1.0.0
workspaces:
- name: source
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
- name: dockerconfig
secret:
secretName: harbor-push-cred
# 按需接入第 12-13 天讲过的 Triggers,
# 由 Git 推送事件自动触发本 PipelineRun
# triggers:
# - name: push-event
# ...
推完之后验证:
# 列出 Harbor 项目中的镜像
curl -sk -u 'robot$ci-builder+push-bot:S7kz9XpL2vQw1RtY4uMn8AbCdEfGhIj'
"https://harbor.example.com/api/v2.0/projects/demo-project/repositories?page_size=10" | jq .
Step 6:为 Argo CD / 集群节点配置拉取凭证(YAML)
推送完成,部署侧(Argo CD 管理的目标集群)需要能拉取镜像。两种方式:
方式 A:直接给 Deployment 加 imagePullSecrets
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: production
spec:
replicas: 3
template:
spec:
imagePullSecrets:
- name: harbor-pull-cred
containers:
- name: app
image: harbor.example.com/demo-project/app:v1.0.0
方式 B:在 Argo CD 管理的 Git 仓库中,用 Kustomize 的 SecretGenerator 统一注入
# kustomization.yaml(配置仓库里)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
secretGenerator:
- name: harbor-pull-cred
type: kubernetes.io/dockerconfigjson
files:
- .dockerconfigjson=/secrets/harbor-pull.json
patches:
- path: add-image-pull-secret.yaml
add-image-pull-secret.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
spec:
template:
spec:
imagePullSecrets:
- name: harbor-pull-cred
拉取专用的 Robot Account 建议单独建一个 robot$deploy+pull-bot,只给 pull 权限,与推送账号分离。这样即使节点被攻破,攻击者也无法往仓库里写恶意镜像。
Step 7:过期管理与自动轮换(Shell + CronJob)
Robot Account 有硬性过期时间(创建时指定 duration,最长 1000 天但 API 允许更长)。生产环境建议:
- CI 推送账号有效期 90 天,到期前自动轮换;
- 写一个轮换脚本,在 CI 里作为定时 Job 运行:
bash
#!/bin/bash
# 每分钟/每天检查,通过 API 更新 token 并重新生成 Secret
curl -sk -X PATCH "${HARBOR_URL}/api/v2.0/robots/${ROBOT_ID}"
-H "Content-Type: application/json"
-H "X-Harbor-CSRF-Token: ${SESSION_ID}"
-d '{"secret": "'"${NEW_SECRET}"'"}'
# 然后重新生成 dockerconfig 并 kubectl apply,流程同 Step 2/3 - 也可以把 Robot Account 的过期检测接入 Prometheus 告警(第 47 天会讲监控),提前 7 天告警「robot 即将过期」。
常见问题
1. 为什么 Robot Account 的 token 创建后只能看到一次?
Harbor 出于安全考虑,POST /api/v2.0/robots 返回的 secret 明文仅此一次,之后任何 API 都只返回脱敏值。所以创建后必须立即保存到 Secret 管理工具(Vault / Sealed Secrets / SOPS),或者干脆用脚本直接生成 docker config 并落成 Secret。丢失后只能通过 PATCH 更新 secret 重新获取。
2. robot$ci-builder+push-bot 中的 $ 导致 Shell 报错怎么办?
$ 在双引号里会被当成变量开头,导致用户名被截断(例如变成 robot)。解决办法:一律用单引号包裹,如 'robot$ci-builder+push-bot';在 YAML 里则不需要转义,因为 YAML 不解析 $。构建 docker config JSON 用 Python 的 json.dumps 最稳妥。
3. kaniko 推送时报 unauthorized: authentication required 怎么办?
按顺序排查:① Secret 是否真的挂载到了 /kaniko/.docker/config.json(进 Pod kubectl exec 检查);② DOCKER_CONFIG 环境变量路径是否与挂载路径一致;③ Robot Account 是否在该项目下有 push 权限;④ 如果是私有 Harbor + 自签名证书,kaniko 需要 --registry-certificate 参数指定 CA(参考第 16 天 HTTPS 配置);⑤ 检查 robot 是否已过期或已被 disable。
4. 一个 Robot Account 能被多个 Tekton Task 共用吗?
可以,只要命名空间能访问同一个 Secret(跨命名空间需要复制 Secret 或用外部 secrets 控制器)。但更推荐按流水线/按项目细分:例如 robot$ci-a-push-bot、robot$ci-b-push-bot,每个只拥有自己项目的权限。这样某个账号泄漏时,影响面被限制在单个项目,审计日志也清晰。
5. Robot Account 和 Argo CD 仓库凭据有什么区别?
Argo CD 的 Repository 凭据用于访问 Git 源码仓库(HTTPS 用户名密码或 SSH key),与镜像仓库无关。Argo CD 部署时拉镜像靠的是目标集群里的 imagePullSecrets(即本文的 Robot Account),两者是不同层级的认证。别混为一谈——在 Argo CD UI 里配置的不是 Harbor 的 robot,而是 Git 的 access token 或 SSH 私钥。
总结
本文围绕「生产环境 Tekton + Argo CD + Harbor」三件套中的凭证环节,完成了 Robot Account 从创建到落地、再到轮换的全链路实战。核心要点回顾:
- Robot Account 是 CI/CD 访问 Harbor 的标准姿势:项目级隔离、最小权限、强制过期、不可登录 UI,彻底告别「流水线里写管理员密码」的坏味道。
- 权限按场景拆分:Tekton 推送账号只给
push(必要时加pull),Argo CD / 集群拉取账号单独建、只给pull,删除操作一律不进 CI 凭证。 - 凭证载体是
dockerconfigjsonSecret:通过DOCKER_CONFIG环境变量映射给 kaniko,一行配置即可打通「构建 → 推送」;Argo CD 侧的imagePullSecrets负责「拉取」闭环。 - 过期与轮换必须自动化:90 天短周期 + 脚本轮换 + Prometheus 告警,避免「证书无声过期导致凌晨发布失败」的生产事故。
- 审计可追溯:robot 凭证精确到项目和操作,结合 Harbor 操作日志与 K8s 审计,满足合规要求。
下一篇(第 22 天)我们将进入 Argo CD 深入篇,从「Argo CD 安装与配置」开始,一步步把 GitOps 部署链路完整搭起来——敬请期待。















暂无评论内容