系统安全和系统保护设计
要保证数据安全和系统稳定可用,我们应当全方位地对系统进行保护,这里主要分为两个层面。
一是系统的安全方面,这主要是面向非法入侵、非法请求的。我们需要阻止和屏蔽不信任的请求源访问,我们要保证数据的安全可靠,不被人窃取。
二是系统的健壮性方面,面向合法、信任的请求源,为了保证系统的可用性,我们需要在请求并发高于系统设计容量时,拦截和丢弃超载的请求(有损服务),以避免因为请求过大发生雪崩效应,导致整个系统都不可用。
第一部分:系统安全设计
对于系统的安全防范,需要从全方位、多角度做工作,以确保整个业务链路、整个体系范围都能保证安全。以下就从多个角度来说明如何进行系统安全设计。
网络安全
传输加密
我们一般开放 Web 访问,走 HTTP 协议。要对外公开接口或页面时,面向外网应当以 HTTPS 的形式公开。
网络隔离
通过网络互不连通来防止入侵,这是最严格也是最暴力的安全解决方案。在我们的生产实践中,主要体现在如下几个方面:
- 不公开服务的机器,不分配外网 IP
- 办公网络和生产网络相互不可以直接通信
网关
在我们需要对外公开 HTTP/HTTPS 接口时,需要使用网关收敛对外公开的接触面。设计独立的接入层专门负责网络请求的接入,不得将非接入层的机器公开出去。
容器
我们的应用目前主要基于容器部署。在启动容器时,我们应当为每个不同的项目创建和指定不同的网络,从而隔离不同的项目服务。
认证安全
对于使用应用的实体,无论是人还是系统程序,都应当做到对每个请求都应当能找到对应的责任实体。因此,面向人我们需要做到严格的登录鉴权,面向应用我们应当做到严格的接口签名和数据校验。我们通常将这一层称之为 ACL(Access Control Layer)。
登录鉴权
目前能够落地实践的方案,有如下几个值得推荐:
群体 |
外网 |
内网 |
员工 |
接入 OA 登录 |
接入 OA 登录 |
外部用户 |
接入微信、企业微信、QQ 登录 |
不允许外部用户直接登录/访问内网(*) |
TIP: 不允许外部用户直接登录内网,必需在外网部署登录验证服务,验证通过之后,才能由外网的服务通过应用网关发起对 OA 服务的请求。所有外部请求均只落到外网区域,不允许通过反向代理将请求直接转发到应用网关继而将未经过登录验证的请求路由到 OA 服务器上。
如果要自建登录验证模块,那么需要注意如下几个方面:
- 登录页面/登录接口应当有防刷机制(比如验证码),防止机器暴力破解。
- 应当具备随机登录因子(比如 token、短信验证码),降低密码泄露的后果影响。
- 多次登录尝试失败之后,应当锁定账户或 IP,不再允许登录。
- 采用增强的密码管理策略,如拒绝弱密码、密码最小长度要求、字符集要求、定期更改密码要求等等。
登录态管理
我们的服务大多需要集群部署,因此需要是无状态的。虽然智能网关和大多数反向代理一样都支持有状态服务,但为了管理方便和更高的可扩展性,我们的最佳实践是保证开发部署的所有业务服务都是无状态的。而登录态本身是有状态的,因此登录态本身需要从业务服务中剥离出来,以避免导致有状态服务的出现。此时我们通常会将登录凭据保存到 Redis 中。因此,我们需要要求:
- 登录态应当独立存储
- Redis 配置密码,并对客户端进行 IP 限制
另一方面,业务侧从 ACL 拿到登录凭据之后,需要注意如下两个方面:
- 不得将登录凭据(如 access_token、refresh_token)等泄露到客户端(包括 cookie、url 跳转等),更不得将员工真实 ID 泄露到客户端。
- 推荐的方式是业务侧生成临时的、指代当前登录身份的 hash 值,以该 hash 值为凭据与客户端交互。
- 对当前用户身份,不得信任传入的外部信息。需要身份信息之后,必须经过验证后从 ACL 获取。
应用安全
请求签名
我们应当始终对外部请求持谨慎态度,包括请求的来源,请求的数据,都要进行谨慎的处理。只有通过接口签名验证的请求,才信任为合法的请求。
对请求的接口签名设计,可参考学习 QQ、微信的相关接口。也可直接接入应用网关,使用应用网关的签名机制。我们也可以设计自己的接口签名算法,但需要重点保证用来签名的关键信息(如 app_secret)不得通过请求本身传递,也要告知和约束用户保证用来签名的关键信息不得泄露。
TIP: 对请求签名除了保证能区分合法和非法请求,也能通过签名追述每一次请求具体的来源应用。
数据加密
对于敏感数据,不得明文传输和明文存储。
对外方面,不得明文传输。我们可以采用 map hash 的方式做映射,如 ACL 返回的身份凭据,我们不应当直接写入客户端 Cookie,而是应当生成临时 hash,以映射的关系与客户端关联;也可以对数据本身进行加密传输,如启用 HTTPS,或者将关键信息通过加密之后再传输。
对内方面,不得明文存储。我们可以加密后再存储,如密码;也可以直接脱敏处理,如日志。
容器
在我们打包容器时,应当使用安全可靠的基础镜像进行打包。
运维安全
理想状况下我们不应当登录服务器进行任何操作,但实际生产却不是这样的。由于运维工具功能的不完善和滞后性,我们往往不得不直接登录到服务器上去直接执行命令,进行日志维护、服务进程维护、服务器维护等操作。由于已经直接触达服务器本身,因此这也是整个系统安全最高危的部分。
运行账户
为减少由于我们的服务被攻击之后造成的损失,减小影响面。在运行应用时,不得以 root 权限运行,而应当另外创建专有用户,并授予最小权限。这样即使应用本身被攻击成功,黑客也需要有一个提权的过程,提高了攻击的门槛。
运维账户和运维审计
对于运维操作,账户本身应当得到严密的管控,严密管控 root 账号。每一个运维人员都应该采用实名登录,并且在每一次登录、操作、登出都进行完善的记录以备审计。
对于高危操作,需要提升运维人员的意识,避免发生由于粗心大意造成的严重后果。
高危命令
谨慎执行高危命令,在必要时使用alias
指令重命名高危命令。
参考:
数据安全
数据的安全是重中之重,以上所有的措施其根本也都是为了保证数据的安全。同时,对于数据本身,我们还有一些可以做的事情。
容器隔离
容器化部署时,我们往往会挂载外部目录到容器中。此时我们应当确保:
- 尽可能使用稳定、可靠的云存储,避免单机故障
- 同一个挂载目录不应当跨容器共享,而应当由单个容器独占
租户隔离
随着 SaaS 化系统越来越多,我们也采用了更多的多租户设计。在系统交互、数据读写方面,务必强要求区分租户。不得因为租户信息被泄露,导致跨租户的其他数据被泄露,必须严控影响面。
离线票据
跨系统交互时,我们可能会涉及到一些敏感票据,比如 app_key/app_secret、pub_key/private_key 等。其中往往 app_key、pub_key 是公开的,允许通过请求传递,而 app_secret、private_key 是需要严格保密的,不得直接通过请求传递。对于这些需要严格保密的敏感票据,应当通过线下、脱离于当前系统的方式进行传递,如打印并邮寄,如单独的、人工的邮件等等。
日志脱敏
日志的敏感性是很容易被忽略的。可能我们花了很大的力气做了系统安全加固,缺忘记了日志里往往也存在大量的敏感信息,导致信息通过日志被泄露了。那么在开发时,我们需要时刻注意日志内容,不应当有敏感信息。在日志传输和存储时,也应当对日志内容进行脱敏。
数据备份
和日志一样,数据的备份文件也是容易被忽视的地方,我们应当花足够大的精力来保证备份文件不被窃取。
政策合规
对于我们的业务来讲,政策合规性一般不存在问题。但从设计上,也应当留有多个方面的合规审计空间。
操作审计
记录用户的操作对象和操作历史。
权限审计
对权限的分配和变更,做严格的控制和记录,避免授予的权限不必要的扩大。同时对于已经失效的用户,应当即使回收权限。
数据审计
对关键数据的读、写,都应当有权限和审批流程进行控制。实际的示例比如 HRC 和 TOF 中,对员工信息的读取,是需要进行权限申请和审批的。
敏感词审计
应当接入或建立敏感词库,对于 UGC 内容,务必确保拦截和过滤敏感词,以避免不合规的内容展示到系统中,导致政策和舆论风险。
第二部分:系统保护设计
对系统的保护,主要是要使得系统具有更高的健壮性(鲁棒性),要求系统在输入错误、磁盘故障、网络过载或有意攻击情况下,能否不死机、不崩溃。这其中包含的内容较为广泛,可做的事情也很多,此处只简略说两个部分。
防御性设计
除了代码及的防御性编程,我们还应当有接口层面的保护。这方面主要致力于解决在网络过载情况下的防护,不至于因为客户端密集高并发调用时,拖垮整个系统,影响接入平台的其他应用。
目前常规的做法是基于微服务架构设计,接入 api 网关来做接口保护。包括“腾讯里约”、“ASF”在内,以及市面上的大多数成熟 api 网关都有相关的功能:
- IP 白名单
- 消费者鉴权
- 消费限额
- 快速拒绝
- 服务 QPS 预警和限制
- 服务熔断
- 防重复
- 接口缓存
TIP: 成熟的 Api 网关不仅具有上述功能,一般还具有自定义路由、健康检查、高可用接入、协议适配等其他功能。
有了以上功能,我们的服务在需要提供给外部系统访问时就可以通过 api 网关来公开,使得所有进入的请求都流经 api 网关。实际操作上,我们对自身业务系统的容量有了合理评估之后,可以在 api 网关上设置合适的阈值。
通过配置 IP 白名单、消费者鉴权,可以将非法的请求拒之门外;
通过配置消费限额、快速拒绝,可以将过量的、非预期的请求拒之门外,同时避免(通过了消费鉴权的请求)消耗无谓的系统资源;
通过配置服务 QPS 预警和限制,可以在服务能力即将达到临界时,向运维人员发送告警提醒,在实际达到临界时,拒绝更多请求压垮服务;
通过配置服务熔断,可以在我们的服务达到容量极限无法支撑时,保护系统不再受到更多请求流量的冲击;
通过配置防重复和接口缓存,可以在一定程度上减少涌向服务的请求数,避免过多的资源消耗。
以上,不论是来自外部系统的正常还是非正常高并发请求时,过载的网络请求都不会直接涌至我们的服务,而是被 api 网关给拦截或拒绝了回去,从而实现对我们自身业务系统和平台的保护,不至于由于网络过载而导致请求雪崩,系统不可用。
防御性编程
跟上文“应用安全-请求签名”一节中一样,我们除了对接口请求应当持不信任的态度之外,我们也应该对内部 api、method 的调用方持不信任的态度。此时就需要防御性设计和防御性编程的思想,以此来保证我们的程序能够适应更广泛的输入错误,不至于在意外输入时而崩溃。
总结
以上简略地描述了做好系统安全设计和系统保护设计需要做的工作,具体没有展开。在实际工作中由于各个项目的安全级别、网络环境等不用,也可能导致实际能够落地的部分难以面面俱到,此时需要仔细分析我们系统的薄弱和风险环节,有针对性地采取措施,详细深入地做好对应的防护。