写点什么

CI/CD 构建中能保护好 SSHKEY 吗?

作者:极狐GitLab
  • 2023-11-29
    江西
  • 本文字数:4169 字

    阅读完需:约 14 分钟

CI/CD 构建中能保护好 SSHKEY吗?

背景


使用极狐 GitLab CI/CD,在部署方面,主要有两种方式:


部署到 K8S 集群


  • Push 模式:流水线通过 kubectl 执行命令部署,这需要把 K8S 的权限给流水线,存在安全风险

  • Pull 模式:使用极狐GitLab Agent for Kubernetes 或 ArgoCD,通过 GitOps 的方式“监听”极狐 GitLab 的变化,触发部署



部署到服务器


目前仍有不少企业因为行业性质或者场景所限,没有使用 K8S 等云原生技术,还在采用传统的服务器方式进行部署。一般使用 ssh、scp、rsync 等命令部署到服务器。极狐 GitLab 也提供了基于 SSH keys 的部署。详见:Using SSH keys with JiHu GitLab CI/CD


需要说明的是,如果是使用专用的编译机进行编译构建,然后部署到指定的服务器,只需要实现编译机和部署服务器的免密 SSH 登陆即可,相对简单。但如果使用容器进行编译构建,然后部署到服务器,就需要按照上面文档中提到的,配合极狐 GitLab CI/CD 环境变量,将 SSH_PRIVATE_KEY 等变量存储到极狐 GitLab Project、Group 或 Instance 中,实现复用。且可以通过极狐 GitLab CI/CD 环境变量的 Mask 设置,掩藏这些变量在 CI/CD 日志中的显示。详见:极狐GitLab CI/CD variables


但遗憾的是 Mask 功能目前是有限制的,对于 SSH_PRIVATE_KEY 这种多行的变量无法直接使用 Mask 功能。这样开发人员就可以在.gitlab-cti.yml 文件的脚本中执行 echo $SSH_PRIVATE_KEY,在流水线的日志中输出 SSH Keys,存在密钥泄露风险。


The value of the variable must:

  • Be a single line.

  • Be 8 characters or longer, consisting only of:

Characters from the Base64 alphabet (RFC4648).

The @ and : characters (In GitLab 12.2 and later).

The . character (In GitLab 12.10 and later).

The ~ character (In GitLab 13.12 and later).

  • Not match the name of an existing predefined or custom CI/CD variable.




这个问题在极狐 GitLab 的 Issue 上挂了有一年多 ,看样子短时间没法解决。有没有其他方式 Mask SSH_PRIVATE_KEY?于是开始了各种折腾。


方案


编码存储


SSH Keys 不能直接 Mask,但 Mask 的要求里面是支持 Base64 的。所以把 SSH Keys 先用 Base64 编码,存到 CI/CD 环境变量中,这样就可以 Mask 了,然后在.gitlab-ci.yml 中解码,就可以在不影响功能的前提下实现效果。看看操作步骤:


  • Base64 编码 SSH_PRIVATE_KEY


# 输入示例   echo "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCAQEAsWchpjSe6HW8dS/DdmokMqET2+eCvD8ysOeju3Ur3cbXtZF1*****pbPfj6i+faMGc1wbP+Svh8P5bcWTJZvZcP87D/HRmSFz6xcT014=-----END RSA PRIVATE KEY-----" | base64 # 输出示例:Base64编码LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBc1djaHBqU2U2SFc4ZFMvRG*****cjRzNmVjY25ZRUZxb1NSTGVNU2xMb1ZreU5VZEpQUjJRa1djQzRkVDVQZwpwYlBmajZpK2ZhTUdjMXdiUCtTdmg4UDViY1dUSlp2WmNQODdEL0hSbVNGejZ4Y1QwMTQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
复制代码


  • 将输出的 Base64 编码作为 SSH_PRIVATE_KEY 存储在极狐 GitLab CI/CD 环境变量,并 Mask



  • 修改.gitlab-ci.yml


before_script:    - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'    - eval $(ssh-agent -s)    # - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -    - echo "$SSH_PRIVATE_KEY" | base64 -d | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts
复制代码


  • 运行测试,大功告成


这看上去是个不错的方案,但真的保证了 SSH_PRIVATE_KEY 安全么?我们本着 Geek(作死)精神,测试一下:


  • 修改.gitlab-ci.yml,通过各种方式看看能不能打印出 SSH_PRIVATE_KEY


before_script:    - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'    - eval $(ssh-agent -s)    # --- test begin ---    - echo "$SSH_PRIVATE_KEY"     - echo "$SSH_PRIVATE_KEY" >> output.txt    - cat output.txt    - echo "$SSH_PRIVATE_KEY" | base64 -d    # --- test end ---    - echo "$SSH_PRIVATE_KEY" | base64 -d | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts
复制代码


  • 运行测试



直接输出 SSH_PRIVATE_KEY 是被 Mask 了,但执行 echo "$SSH_PRIVATE_KEY" | base64 -d,居然把 SSH_PRIVATE_KEY 打印了出来,所以这个方法还是存在一定的问题。


逐行存储


SSH Keys 头部和尾部的-----BEGIN RSA PRIVATE KEY-----、-----END RSA PRIVATE KEY-----不能 Mask,但里面的内容,每一行可以单独作为一个环境变量存储并 Mask,使用的时候再进行拼接,看看操作步骤:


  • 将 SSH_PRIVATE_KEY 每一行拆分成一个变量,进行存储,有多少行就要存多少变量,为了偷懒,此处只列了 3 行,实际上我这个 SSH_PRIVATE_KEY 除去头尾,有 26 行……




  • 修改.gitlab-ci.yml,并加入一些测试


before_script:    - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'    - eval $(ssh-agent -s)    # 拼接SSH_PRIVATE_KEY    - |      SSH_PRIVATE_KEY=$'-----BEGIN RSA PRIVATE KEY-----\n'      SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY$SSH_PRIVATE_KEY1$'\n'      SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY$SSH_PRIVATE_KEY2$'\n'      SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY$SSH_PRIVATE_KEY3$'\n'      SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY$'-----END RSA PRIVATE KEY-----'    # --- test begin ---    - echo "$SSH_PRIVATE_KEY"     - echo "$SSH_PRIVATE_KEY" >> output.txt    - cat output.txt    # --- test end ---    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts
复制代码


  • 运行测试



方案行是行,实际操作起来要存 26 个变量然后还要拼起来,实在是太土了,能不能减少行数,存一行。


合并存储


Mask 不支持空格,只支持 @:.~,那我们尝试把 SSH_PRIVATE_KEY 除了头尾的部分合并成一行,把换行符替换成支持的符号,如.,然后再与头尾进行拼接。操作步骤如下:


  • 合并 SSH_PRIVATE_KEY


# 输入示例echo "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCAQEAsWchpjSe6HW8dS/DdmokMqET2+eCvD8ysOeju3Ur3cbXtZF1*****pbPfj6i+faMGc1wbP+Svh8P5bcWTJZvZcP87D/HRmSFz6xcT014=-----END RSA PRIVATE KEY-----" | tr -d '\r' | tr "\n" "."
#输出示例-----BEGIN RSA PRIVATE KEY-----.MIIEogIBAAKCAQEAsWchpjSe6HW8dS/DdmokMqET2+eCvD8ysOeju3Ur3cbXtZF1.LMb2Rq68/FPXsteLr4Y1ECKoy/YhFpyDw1h3cLm2WBUtRjt/Tq0ASbQCWAVkDsmx.uy28WofwfEKktzy3FmDSCXbvcOQgjChAmMbALWyH****=.-----END RSA PRIVATE KEY-----.
复制代码


  • 将除头尾部分存入环境变量并 Mask



  • 修改.gitlab-ci.yml


before_script:    - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'    - eval $(ssh-agent -s)    # 拼接SSH_PRIVATE_KEY    - SSH_PRIVATE_KEY=$'-----BEGIN RSA PRIVATE KEY-----\n'$SSH_PRIVATE_KEY$'\n-----END RSA PRIVATE KEY-----'    # --- test begin ---    - echo "$SSH_PRIVATE_KEY"     - echo "$SSH_PRIVATE_KEY" >> output.txt    - cat output.txt    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | tr "." "\n"     # --- test end ---    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | tr "." "\n" | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts
复制代码


  • 运行测试



和“编码存储”的方案一样,跑的通,但依旧可以通过对应的方式,打印出 SSH_PRIVATE_KEY。


到这里,可以隐约猜到 Mask 变量的原理是简单做了一个是否包含字符串的判断。如果与环境变量的值匹配就显示[MASKED],如果不匹配就直接将变量显示出来。这也是为什么目前只允许值是单行且没有太多特殊符号的环境变量才可以 MASK 的原因。


打马赛克


为了验证上一步留下来的猜想,我设计了一个实验:


  • 恢复环境变量中的 SSH_PRIVATE_KEY 为原始内容,并且不做 Mask,当然也无法 Mask



  • 新建一个环境变量,值为 SSH_PRIVATE_KEY 的一部分内容,这里设置的是 SSH_PRIVATE_KEY 内容的第一行,然后设置为 Mask



  • 恢复.gitlab-ci.yml 文件,需要注意的是这里面没有任何关于 MOSAIC 环境变量的使用


before_script:    - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'    - eval $(ssh-agent -s)    - echo "$SSH_PRIVATE_KEY"     - echo "$SSH_PRIVATE_KEY" | tr -d '\r'| ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts
复制代码


  • 运行测试



正如猜想一样,即便没有使用 MOSAIC 环境变量,但它依然作为判断是否包含字符串而被执行了。


利用这个特性,我们可以通过设置几个马赛克变量,给 SSH_PRIVATE_KEY 的部分内容打码,就可以一定程度上防止 SSH_PRIVATE_KEY 的泄露。


结论


作为一般场景下使用,上面的四种方式任意选一个都可以实现基本的安全防护,正所谓防君子不防小人。如果要进一步提高安全性,还是如官方所说,上专业的密钥管理工具,如 Vault,或者期待下极狐 GitLab 在管理密钥这块功能的完善。

发布于: 58 分钟前阅读数: 11
用户头像

极狐GitLab

关注

开源开放,人人贡献 2021-05-19 加入

开放式一体化DevOps平台,助力行业高速协同增长!

评论

发布
暂无评论
CI/CD 构建中能保护好 SSHKEY吗?_DevOps_极狐GitLab_InfoQ写作社区