基于AWS的密钥管理系统
互联网服务中有各种密钥使用场景,比如数据加解密、保障传输的安全完整性、验证合法性等。在项目初期密钥一般散落在代码,配置文件或服务器上。当对服务的安全性要求越来越高时,通常会引入密钥管理系统,把所有服务的密钥统一管理起来,并对已泄漏或者有泄漏风险的密钥进行轮转,泄漏风险一般指密钥的明文依旧出现在某些文档的历史记录中,比如wiki,google doc, git commit history等。并且对某些敏感的密钥设置定期自动rotate。
所以,一个密钥管理系统的基本功能要包括密钥的获取/创建/存储/轮转/访问控制/审计/第一把钥匙管理等功能。业界比较出名的密钥管理系统是Hashicorp Vault,不过使用Vault需要购买license,并需要自己搭建和维护,服务接入Vault也需要一定的代码改动并且Vault官方提供的SDK支持的语言并不多。如果公司还没到一定规模或者因为安全需求要快速把密钥集中管理起来,可以基于公有云的Secret Manager服务或者KMS服务构建一个密钥管理系统,快捷的把散落的密钥管理起来。
本文介绍的密钥管理系统基于AWS的Secret Manager实现,并利用AWS EC2 Instance Metadata服务,AWS Systems Manager(SSM)服务和AWS Security Token Serivce(STS)服务做第一把钥匙的的分发,外加一个自建的web服务实现密钥的组织管理(创建/删除/逻辑结构)和细粒度的访问控制。
本文不涉及服务的高性能,高可用,网络或者基础设施的安全性等方面,只介绍密钥管理系统的实现逻辑。
密钥的获取

准备阶段:第一把钥匙的生成。密钥管理系统的使命就是把应用服务所有的密钥都集中管理起来,所以应用服务访问密钥管理系统API的Credentials也不能配置在应用服务中,一般采用动态从应用服务部署的服务器上获取。从服务器上如何能保证获取到密钥?根据不同的服务器环境,大致可用分成三类:
1. AWS EC2。给EC2绑定相应的IAM role之后,就能通过EC2的instance metadata服务(169.254.169.254)获取到AWS的STS,因为服务器绑定的Role挂了访问AWS Secret Manager或AWS KMS的policy,所以这个STS可以获取WS Secret Manager和AWS KMS的资源。
2. 自建数据中心的VM或者其他云服务提供商的VM。这类VM不支持直接绑AWS IAM Role,可以给非EC2机器安装一个SSM agent,注册到AWS SSM同时绑AWS IAM Role,SSM会分配一个instanceId给非EC2并通过SSM agent在这台机器的/root/.aws/文件夹下生成一个credentials文件,内容是AWS的STS。
3. k8s容器环境,包括AWS EKS和自建的k8s环境。容器环境不支持绑role,不过可以在pod上配置service account然后在service account上绑相应的role。pod启动的时候根据其配置的service account生成一个token,便可以用aws sts:assumerolewithwebidentity API获取STS。
第一步:应用服务从本机获取AWS STS。AWS EC2访问EC2的instance metadata服务(169.254.169.254);非EC2的VM访问本机的/root/.aws/credentials文件;k8s环境调用sts:assumerolewithwebidentity。
第二步:应用服务通过密钥管理系统的agent获取密钥。agent运行在VM上只支持本机访问, agent从web端获取密钥的metadata然后访问AWS Secret Manager或KMS直接获取secrets。采用agent的主要好处是实现方便,避免不同的语言的应用服务都实现一遍同样的逻辑。为什么不通过密钥管理的web服务直接获取secrets呢,即把4&5并成一步。主要是出于安全方面的考虑,如果所有服务都通过密钥管理系统的web服务获取secrets,这个web服务如果有安全漏洞(比如heap dump)的话,可能会导致所有应用服务的密钥都有丢失的风险。基于安全性的考虑,应用服务直接找AWS获取密钥中间不经过任何其他的系统是最安全的。
第三步:获取密钥的元数据信息。通过path获取path下的所有存储在AWS Secret Manager上的secret alias,通过secretId获取KMS secret的ciphertext等。
第四步:密钥管理系统web端验证STS是否合法。通过AWS的sts:get-caller-identity验证STS,合法则返回metadata信息。
第五步:根据元数据信息到AWS获取相应的密钥。
密钥的管理
创建
密钥的创建可以分为人工创建和自动创建两种。人工创建的场景一般是已知密钥的明文,比如一个第三方服务提供的生成jwt的密钥,工程师直接通过密钥管理系统web服务的控制台把密钥存到AWS Secret Manager中。自动创建的密钥一般是应用服务自己用或者内部服务之间使用,工程师并不需要知道密钥内容,只要输入密钥长度、使用年限、加密算法和使用场景(存储加密,传输加密,密钥协商,数字签名等)密钥管理系统会按照安全团队制定的标准生成密钥。
存储
用于数据加解密的密钥,比如IM聊天信息的落盘加密,通常采用AES-GCM-256进行加密,这类数据通常要保存若干年,因为解密需要,AES密钥长时间不能销毁,但平均每半年就需要轮转一个新密钥对新数据进行加密。这种场景下,通常采用AWS KMS,基于同一个Customer Master Key(CMK)每半年生成一个data key。data key的plain text用于AES Key,data key的cipher text与密文存到一起,密钥管理系统存储应用服务的某个功能使用了AWS KMS哪个CMK等不敏感的信息。
其他类型的密钥直接把明文存到AWS Secret Manager中。AWS Secret Manager内部也是通过KMS CMK对存入的密钥进行加密存储并且存到更加安全的硬件上。AWS Secret Manager的secret是Json格式的,需要一起轮转的密钥存到一个secret中,比如DB的username和password。AWS Secret Manager的secret也支持存二进制数组,可用于存储文件类型的密钥,比如cert文件。
组织
不管是AWS KMS还是AWS Secret Manager都不提供复杂的密钥的组织管理功能比如namespace,层级关系,覆盖关系等。但是这方面对业务和运维是非常重要的,我们在实践中采用了树状结构来对密钥进行组织。

namespace对环境进行隔离,application在应用层面进行隔离,application之下的子节点支持应用服务自己添加,根据应用实际部署的拓扑可以无限扩展,一般是按照地区=>集群=>可用区=>数据中心的顺序,从大区域到小区域进行层层划分。每一个节点下面都能挂0到多个密钥,子树通用的密钥建立在父节点,某个环境单独使用的密钥建立在叶子节点; 同时遵循子节点密钥覆盖父节点同名密钥的原则,保证共用于单独使用共存的能力。
树状结构能很好的完成应用服务自身的密钥组织,但还有一种需求是密钥是多个应用服务共享的,比如某个密钥A服务用来加密,B服务用来解密。对于这种跨服务的密钥,可以采取引用的形式,A服务创建一个密钥并share给B服务,B服务reference服务的密钥,建立了引用关系之后,A服务对密钥的任何改动,对B服务也同时生效。这种引用关系的建立存储在密钥管理系统的web服务端,在密钥发生轮转时,方便通知所有的引用方。
访问控制
访问控制包括获取密钥的IP限制和控制台基于角色的访问控制。
IP限制一个是通过IP白名单或者安全组只允许明确的某些IP可以访问密钥管理系统的web服务,另一个是在AWS IAM的policy condition上明确只有某些IP能够使用相关的STS访问AWS Secret Manager和KMS服务。
控制台的访问控制主要解决哪些人能够访问哪些应用服务的密钥,并具有哪些操作权限,控制台的访问控制基于角色,这个角色并不是密钥管理系统自己的角色而是AWS IAM Role, 直接采用AWS Role一来可以避免密钥管理系统自身具有访问AWS服务的能力,保证了密钥管理系统在权限上的安全性;二来可以复用IT团队给员工分配的AWS的账号的访问权限,减轻了密钥管理系统运维的工作量并且保证了一致性。
审计
密钥管理系统从API调用到控制台访问的任何操作,都要记录用户(or 应用服务)名称, 动作, 时间戳,IP地址。并进行审计检查。
密钥的轮转
基于安全性的要求,每个密钥都要设定一个过期时间,在过期之前完成密钥的轮转。密钥轮转功能要满足下面几个基本需求:
- 尽量在一个较短的时间窗口内完成
- 保证应用服务的稳定性不受影响
- 有监控大盘展示密钥轮转的进度和最终结果
- 支持人工干预:控制节奏,终止和回退。
密钥轮转的一个常见场景是发现密钥泄漏后进行轮转:启用新密钥并销毁老密钥。运维工程师会给泄漏的密钥添加一个新版本,然后等待应用服务的每个instance获取新密钥并替换掉内存中的老密钥,当确认每个instance都不再使用老密钥后,便把老密钥销毁掉。这个过程主要耗时的环节是等待每个应用服务的instance都获取到新密钥,触发应用服务获取新密钥主要通过轮询(polling)确认自己的密钥集合的hash值是否有变化,如果有变化则触发获取新密钥。轮询有两种实现方式:一种是短轮询,轮询请求立刻返回等待一段时间后再发请求过来;一种是长轮询,密钥管理系统hold住这个请求1~3分钟,如果在hold的时间内密钥有改动则立即返回,超过最大hold时间返回后立即发起下一个长轮询。短轮询的好处是实现简单而且通用,并不是所有的web服务端都支持长轮询,比如Hashicorp Vault。长轮询的好处是可以立即触发应用服务来获取新密钥,但是密钥系统要保持较多的http请求,长轮询可以参考nacos的实现。
对于跨应用密钥的轮转,因为涉及到多个应用服务的联动,轮转过程更复杂一些,对这类密钥首先要支持阶段性标签pending=>current=>previous=>revoked:
- pending:该版本尚处于等待阶段,密钥的share方可以使用但是reference方不能使用
- current: 该版本即为当前版本,share方和reference方都可使用
- previous: 该版本是一个老版本,share方依旧可以使用,但reference方不能使用
- revoked: 该版本是一个已经销毁的版本,share方和refereence方都不能使用,只能通过密钥管理系统的控制台查看到,用于回退。
有了这些标签就可以按照流程进行密钥轮转。在轮转之前密钥只有一个current版本,给密钥添加一个pending状态的新版本,等所有share方的instance都获取到pending版本,这样share方就支持了新老两个版本secrets的使用。然后再把pending改成current(老版本的current会同时改成previous),再等待share方和reference方都获取到新的current版本,当确认双方都使用current版本后,再把previous标签的密钥设置成revoked完成轮转。
监控大盘是判定应用服务是否已经全部换上新secret的依据,监控大盘的数据来源于应用服务调用轮询API带的参数,包括应用名称,path,path版本号,instanceId,AppProcessId。path是应用服务要获取的密钥的路径:namespace/application/region/cluster。 path版本号是密钥服务在第一次获取密钥时返回的一个时间戳,当path上的密钥有改动时,path版本号会变成密钥改动的时间戳。当轮询返回path版本号发现时间戳变化之后,会触发获取新密钥。AppProcessId是应用服务进程启动时随机生成的字符串,作为应用服务进程的Id,这样可以区分同一台机器上部署多个相同服务的情况(主要是k8s环境)。

浙公网安备 33010602011771号