CODING 代码资产安全系列之 —— 构建全链路安全能力,守护代码资产安全
本文作者:王振威 - CODING 研发总监
CODING 创始团队成员之一,多年系统软件开发经验,擅长 Linux,Golang,Java,Ruby,Docker 等技术领域。近两年来一直在 CODING 从事系统架构和运维工作。
不同类型的企业资产有不同的管理办法,但守护资产的安全性无一例外都是重中之重,但对如何保障代码资产安全并没有形成统一认知。本文将就“代码资产的安全性”这一话题展开全面的阐述,尝试从代码管理的生命周期进行全链路分析,读者可以据此来审视自己企业的代码资产安全。
代码资产安全是什么
代码资产安全不等于信息安全
代码资产安全不等于信息安全,这是很容易理解的。整个企业的信息系统组成不仅仅是代码资产,甚至可以说大多数情况下不涉及代码资产。企业的信息系统往往由基础计算设施、网络平台、软件、数据库等方面组成。信息安全重点是关注上述信息设施在投产之后运行过程中的安全问题。而大多数软件运行的程序包是经由源代码编译的结果,跟源代码本身是分割开来的。信息安全关注的方面更为全面,代码资产安全只是其中的一部分,而且往往不是最为关注的一部分。
代码资产安全不等于代码安全
代码资产安全不等于代码安全,这不太容易理解。代码安全往往指代代码本身的安全性,如代码中是否有远程过程执行漏洞,注入漏洞等等。而代码资产安全是一个管理概念,强调管理过程的安全,而非代码本身安全,例如某研究机构需要研究某种计算机病毒,他们需要在源码库中存放对应病毒的源码。这病毒的源码就是这个机构的重要资产。
代码管理系统不审视源码中的漏洞或者恶意行为,而是必须忠实地确保存储的代码的原始文件。
代码资产管理是围绕代码仓库的全生命周期管理
代码资产管理的核心是代码仓库。仓库里存放着企业的全部代码,配置文件以及全部历史版本。守护代码资产安全的核心就是围绕代码仓库的三个关键环节构建起全链路的安全能力,这三个环节分别是检入,存储和检出。
检入安全
检入可以理解为开发者在开发环境上编辑好代码,并且把代码传送到代码仓库的过程。这个环节关注两个方面,分别是机密性和完整性。
机密性
机密性是指开发者把开发环境中的代码检入代码仓库的过程不被第三方窃取,一般通过传输过程加密来实现。Git 代码仓库最常用的是 HTTPS 和 SSH 传输协议。
HTTPS 协议是通过 HTTP 协议加上传输层安全协议(TLS)实现的。HTTP 协议是明文传输协议,这意味着如果没有 TLS,网络节点中的路由设备都可以轻松窃取代码。TLS 可以在 TCP 协议之上建立双向加密能力,配合 HTTP 协议上就是 HTTPS。HTTPS 客户端和服务端先通过非对称加密协商加密算法和密钥,再使用协商的算法和密钥来进行对称加密传输。本文不涉及具体算法的安全性介绍,不过随着密码学的发展,算法在与时俱进,我们可以认为加密算法本身是安全的。
然而这一过程并不完备,攻击者可以制作中间服务器,使得客户端在发起连接的时候误连接了中间服务器,从而跟这个中间服务器进行加密通信。这将导致即便是加密传输,但最终还是会被恶意服务器窃取,这就形成了中间人攻击。
行业推出了 CA(证书授权机构)机制应对此问题,即服务器在提供加密传输服务前,要把自己的公钥和服务的域名绑定,并且在全球公信的 CA 处登记。这样一来,HTTPS 客户端在尝试建立加密链接的时候,会要求服务器出示 CA 签发的证书,客户端可以使用预安装在操作系统或者浏览器内的 CA 公钥进行验证,确认服务器对域名的所有权,这样一来就可以确保不会有中间人攻击。有行业公信力 CA,也有企业内部 CA,而后者需要在客户端安装企业内部 CA 的证书文件。
知名安全机构 Qualys 可以在线对 HTTPS 服务器进行 SSL/TLS 多方面的报告评估,如下图为两家国内云计算公司推出的代码托管服务器的评估:
HTTPS 虽然解决了传输安全,但在认证用户身份这里,Git 代码仓库还是依赖 Basic Auth 机制来实现。Git 代码仓库会要求 HTTPS 客户端提供账号密码,并附在请求体中一并传输给服务器,由服务器来确认操作者身份。在传输过程中,账号和密码是被 TLS 一并加密传输的,我们不必担心传输过程的密码泄露问题。但开发者通常为了不必每次操作都输入账号密码,会让电脑记住密码,如果不妥善处理,可能会导致泄露。这里重点是一定不能把账号密码拼接在远程仓库访问地址里面,正确的做法是使用 Git 在各种操作系统下的 凭据管理器,如 macOS 是使用钥匙串管理,Windows 是使用 Git Credential Manager for Windows 来进行管理。
SSH 是一种常用于远程管理 Linux/Unix 服务器的安全加密协议,其功能非常多样。以 Git 为基础的代码托管也常使用这个协议进行加密代码传输。使用者提前把自己的公钥文件配置在服务器上后,可以在后续的传输过程中确认身份。
SSH 使用非对称加密(用户的公钥)确认身份,用对称加密传输数据。跟 HTTPS 不同的是,SSH 协议无法指定域名,所以无法引入 CA 机制来防止中间人攻击。
但 SSH 客户端在与未知服务器进行连接时,会提示服务器的公钥指纹信息,使用者应当对比服务供应商官方提供的公钥公告和命令行提示信息来确认服务器身份,确保不被中间人攻击。
如图展示腾讯云 CODING SSH 服务器的公钥指纹公示:
如图所示,SSH 客户端尝试连接服务器时给出的服务器公钥指纹确认:
在用户确认身份(输入 yes 并按下回车)后,SSH 客户端会把服务器的公钥信息记录在 ~/.ssh/known_hosts 中,下次即可直接连接,不再询问。
要点小结
- 代码的传输要使用双向加密协议,HTTPS 和 SSH 都可以
- HTTPS 协议需要关注服务器的证书签发方(CA)的权威性
- HTTPS 协议需要关注客户端是否安装了不受信任的 CA 文件(防止 CA 欺诈)
- 使用 Git 凭据管理器保管 Git HTTPS 协议的账号密码
- SSH 协议在使用的时候需要仔细比对服务器提供的公钥指纹跟服务提供商公告的公钥指纹是否完全一致,防止中间人攻击
- 客户端需要注意防止攻击者恶意篡改 ~/.ssh/known_hosts 文件内容或者 SSH 的客户端配置(可以通过忽略服务器公钥信任机制)
- 妥善保管 SSH 私钥文件(往往存放于 ~/.ssh/id_rsa),如 Linux 下确保此文件的权限是 400 等,防止他们读取
完整性
代码检入的完整性包含两个方面:
- 开发者一次提交的代码变动是否完整(内容不被篡改)
- 某次提交是否确为某开发者做出的变动(不被冒名顶替)
以 Git 为例子,这个代码版本控制软件已经从内生机制上确保了内容不被篡改。Git 采用一种类 Merkel 哈希树的机制来实现分层校验。
哈希是一种把任意数据映射成等长数据的算法,且不可逆。哈希算法有的特点是原始数据发生一点变化,映射的结果会产生较大变化,而且这一变化毫无规律。映射后的等长的数据被称为指纹。
哈希算法非常适合用来快速比较两段数据是否完全一致(指纹一致几乎可以推断原文一致)。在我们上文中提到的对比 SSH 服务器出示的公钥指纹,和服务提供商公告的指纹就是这种原理的应用。
Merkel 哈希树:
Git 对仓库中的每一个文件内容和其基本信息整合进行哈希。会将一个目录树下的所有文件路径和文件哈希值组合再哈希形成目录树的哈希。会把目录树和提交信息组合再哈希,此哈希结果就是 Git 的版本号。这意味着每次提交都产生一个完全不同的版本号,版本号即哈希。在给定一个版本号,我们可以认为这个版本背后对应的全部文件内容,历史记录,提交信息,目录结构都是完全一致的。对于确定的版本号就没有篡改的可能性。
哈希算法小概率会产生冲突(同一个指纹对应多个不同原始数据的情况),这时可能导致一致性校验失效。所以哈希算法也在与时俱进,如当下 MD5 算法已经几乎过时,Git 当前正在使用 SHA1 算法,未来可能会升级到更为安全的 SHA256 算法。
如图展示 Git 中的某个目录树的内容信息:
即便开发者自己提交的版本经过 Git 的层层哈希,可以确保内容不被恶意篡改,但仍然有被冒名顶替的危险。
因为 Git 在提交过程不需要验证用户身份,而且提交可以被不同的人在各种传输过程中传输和展示。设想攻击者冒充公司员工制造一个提交,却被公司其他员工认为是公司内部人士会有多可怕。目前基于 Git,业界的普遍做法是引入 GPG 签名机制。
GPG 是基于非对称加密算法的一个应用,其原理是使用私钥处理一段信息,得到一段新的信息,这段新的信息只能由私钥生成,而且可以使用对应的公钥来识别这段新的信息的生成来源,这段新的信息就被称为数字签名。
简单来说,信息发布者使用自己的私钥(私人印章)对要发布的信息(待签名文件)进行签名,并且把原始文件和数字签名一并发送给使用方。使用方持有发布方的公钥,对收到的数字签名和原始文件进行校验就可以确认确实是发布方发出的,未被冒名顶替。这类似给要发布的信息盖了个章。
如图展示 Git 中某个提交被开发者添加 GPG 签名的效果:
要点小结
- Git 本身的哈希机制可确保内容不被篡改
- 使用 GPG 为提交签名可防止冒名顶替
- 服务器端要校验 Git 提交邮箱声明和 GPG 签名
存储安全
存储安全是指当代码被检入到代码仓库后,如何保证数据的机密性,完整性和可用性。抛开基础设施的安全性不谈,对于代码存储来说,数据往往由数据库数据和代码库文件组成,这里重点讨论代码文件存储安全问题。
机密性
代码仓库中的代码大多直接存放于操作系统的磁盘中,在服务器软件进行读写操作的时候,不涉及网络传输的机密性风险,但直接写入磁盘上的文件在未做控制的情况下,往往可以被操作系统上的很多不相关进程随意读写,这些非预期的代码读写会造成额外的风险。
一种做法是去控制每一个文件的读写权限,如统一设置为 600,另一种做法是干脆只允许服务器上运行一个业务进程,实现操作系统级别隔离。
容器技术提供了一种良好的隔离进程方案:如在 Kubernetes 体系下,代码仓库存储在 PV 上,并只被挂载进代码仓库的应用容器内读写,而且基于容器的调度和弹性特性可以较好的支持高可用并避免资源浪费。
完整性和可用性
我们知道 Git 本身会通过哈希校验机制来确保仓库的完整性,但前提是仓库文件是完备的。如果仓库的文件丢失或者损坏,Git 的哈希校验也将无法工作。数据的完整性有很多种解决方案,最常见的冷备,半实时备份,实时备份,磁盘快照等方案都是为了确保文件在丢失或者损坏的时候可以找回,来确保仓库的完整性的。不过总的来说,备份往往是事后的恢复手段,无法实现即时的自愈,最终依据备份机制来进行数据修复往往会影响可用性。
虽然业界没有针对代码仓库的通用高可用方案,但数据库主从策略和 RAID 机制是两个可以参考的做法,这里来做下简要介绍。
数据库主从策略,一种做法是数据写入主库,从库自动增量同步数据。当主库发生故障时,从库自动替代。代码存储类似,可以把存储节点分为主节点和从节点。
RAID 机制是一种磁盘分片存储的冗余机制,有多种做法,如 RAID5,分片存储,并存储一份校验信息,当任意一块磁盘坏掉,可以通过校验信息来复原数据。
腾讯云 CODING DevOps 在这方面进行了深入研究,并结合了主从和 RAID 的思路,实现了针对代码仓库的高可用策略,可妥善保障仓库的完整性。
如图所示,对于 D 仓库来说,他的主仓库 D(m) 存放于第二个节点,他的从仓库 D(s) 存放于第一个节点(实质上还可以设定更多从仓库,这里为了图示方便,只显示了一个)。这样的设计让各个节点都可以不闲置计算资源,而且任意一个节点出现损坏都可以快速恢复。
检出安全
代码检出后才能使用,而检出也涉及传输机密性问题,这点与检入部分没有区别。而对于 Git 仓库来说,检出环节的仓库完整性会由 Git 的哈希校验机制保证,也不会有太大问题。检出环节的安全问题往往是因为不合适的权限策略和密钥管理导致代码泄露。
企业内部代码通常有如下四个场景:
- 检出开发
- 阅读评审
- 自动执行(CI,自动化测试等)
- 管理审计
检出开发权限
需要区分开发者能读写的权限范围,保护好关键资源和密钥,按如下原则:
- 按照业务、组件等进行分门别类的存放,仓库隔离
- 根据所处的部门和组织关系配置仓库的权限
- 为分支设定读写权限,只允许有权限的成员写入
- 使用文件锁定方式保护敏感文件不被误修改
- 统一传输协议,如只允许 HTTPS 或者 SSH
- 为个人密码,令牌,公钥等设定有限期
- 审计密码,令牌,公钥等的使用记录
- 为目录设定读写权限,只允许指定开发者读取或者写入某些目录
- 禁止强制推送策略,防止代码被回退
如图所示,设置仓库内的目录权限:
阅读评审权限
诉求是看源码和辅助信息,并做出自己的评审结果,不涉及写入代码,按如下原则:
- 区分读写和只读成员群体,禁用后者的写入权限
- 区分深入评审和轻量级评审,禁用后者的代码检出权限,只允许其 Web 页面查看源码
- 使用 CODEOWNERS 机制自动指定评审成员
如图所示,设置仓库的 CODEOWNERS:
自动执行权限
自动检出,检出行为背后不对应一个人,不涉及代码写回,按如下原则:
- 禁止成员把自己的密码,令牌,密钥用于自动执行
- 使用项目/仓库令牌,部署公钥机制确保令牌和密钥只对指定仓库有权限
- 为不同场景设置专用的令牌,不得混用,也不得用于其他用途
- 为令牌,公钥等设置有效期
- 为令牌,公钥等设置禁止写入权限
- 审计令牌,公钥等的使用记录
如图所示,设置令牌的权限和有效期:
管理审计权限
这种场景是非技术人员希望了解仓库统计信息,活跃情况,了解研发过程进度等,按如下原则:
- 给成员开放所管辖的仓库列表和仓库详情的 Web 页访问权限
- 禁止成员使用 HTTPS/SSH 协议把源码检出到本地
- 禁止成员在网页端下载源码包
如图所示,设置禁止仓库写入等权限
总结
代码资产管理是个体系化的工程,这个过程中的安全性不是某个单点可以完全保障的,需要从检入,存储,检出三个环节对全链条进行风险分析。很多企业在这些方面很重视,但聚焦错了方向,可能付出了很大努力,但实质上依然冒着代码资产的丢失和泄露的巨大风险。希望此文可以帮助企业正视代码资产安全,为代码资产管理者提供一个审视安全的基本框架。
让 CODING 为您的代码资产保驾护航