生产环境 DevOps 实战 | 第 21 天:Harbor Robot Account 实战——为 Tekton CI 配置自动化凭证

第 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"
}

拿到 namesecret 后,生成 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 允许更长)。生产环境建议:

  1. CI 推送账号有效期 90 天,到期前自动轮换;
  2. 写一个轮换脚本,在 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
  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-botrobot$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 从创建到落地、再到轮换的全链路实战。核心要点回顾:

  1. Robot Account 是 CI/CD 访问 Harbor 的标准姿势:项目级隔离、最小权限、强制过期、不可登录 UI,彻底告别「流水线里写管理员密码」的坏味道。
  2. 权限按场景拆分:Tekton 推送账号只给 push(必要时加 pull),Argo CD / 集群拉取账号单独建、只给 pull,删除操作一律不进 CI 凭证。
  3. 凭证载体是 dockerconfigjson Secret:通过 DOCKER_CONFIG 环境变量映射给 kaniko,一行配置即可打通「构建 → 推送」;Argo CD 侧的 imagePullSecrets 负责「拉取」闭环。
  4. 过期与轮换必须自动化:90 天短周期 + 脚本轮换 + Prometheus 告警,避免「证书无声过期导致凌晨发布失败」的生产事故。
  5. 审计可追溯:robot 凭证精确到项目和操作,结合 Harbor 操作日志与 K8s 审计,满足合规要求。

下一篇(第 22 天)我们将进入 Argo CD 深入篇,从「Argo CD 安装与配置」开始,一步步把 GitOps 部署链路完整搭起来——敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容