专题信息
专题:Serverless安全研究
往期回顾:
《Serverless安全研究 — Serverless概述》
《Serverless安全研究 — Serverless安全风险》
《Serverless安全研究 — Serverless安全防护》
一.引言
本篇为Serverless安全研究系列的完结篇,通过本系列的前三篇,我们对什么是Serverless,Serverless存在的安全风险及如何防护进行了较为全面的介绍。公有云Serverless借助AWS、Microsoft、Google等庞大的安全研发团队使其安全性得到保障,除此之外,私有化的Serverless安全也备受关注,通常来说,私有化Serverless部署方式兼容Kubernetes,目前市场关注度较高的有OpenFaaS、Knative、Kubeless、Openwhisk、Fission等。本文笔者以OpenFaaS平台作为研究对象,主要对其内置的安全机制进行了分析解读,除此之外,还通过实验进一步验证了Kubernetes安全机制对OpenFaaS函数的深度防护;希望本文可以引发各位读者更多的思考。
二.OpenFaaS简介
OpenFaaS作为一款开源的云原生Kubernetes Serverless平台,在2017年1月发布了第一个Release版本,其使用容器作为Serverless函数的承载体。值得一提的是,OpenFaaS在函数模板上做出的努力,其支持多种语言模板包括.NET、Dockerfile、Go、Java、NodeJS、PHP、Python、Ruby等,创建函数时,OpenFaaS为其自动生成相应的语言模板,其中包含依赖库文件、镜像Dockerfile、函数部署的yaml文件等,这样开发者只需关注于函数的逻辑实现。
图1我们可以看出,OpenFaaS的基础架构体系可分为三层,每一层负责不同的部分:
基础架构层:OpenFaaS部署在Kubernetes上,以Docker运行Serverless函数,函数镜像存储至镜像仓库中;
应用层:通过OpenFaaS网关,外部用户可对Serverless函数进行CRUD和调用操作,内置的Prometheus可对函数调用行为进行监控,NATS则提供函数的异步调用;
GitOps层:OpenFaaS Cloud建立在OpenFaaS的基础上,可通过Github或自行托管的Gitlab进行DevOps交付;
除上述介绍外,OpenFaaS的正常运行还需依赖以下组件:
faas-provider: OpenFaaS提供商,可为Serverless函数提供CRUD API及调用能力
Watchdog: 负责为OpenFaaS启动和监控函数的组件;
三.OpenFaaS安全功能分析
OpenFaaS提供的安全能力可覆盖认证、数据安全、安全配置三个方面。
3.1 认证防护
OpenFaaS的认证主要包含API网关认证和函数认证两个部分。
3.1.1 API网关认证
基本认证方式
基本认证方式使用了Kubernetes的secret机制,需要注意的是,我们需在部署OpenFaaS之前首先创建secrets资源,具体操作如下:
kubectl -n openfaas create secret generic basic-auth \
--from-literal=basic-auth-user=admin \
--from-literal=basic-auth-password=admin
部署OpenFaaS时,该secrets会被挂载至API网关的pod内部,如下所示:
nsfocus@openfaas-master:~$ kubectl exec -it gateway-df9df88df-bxlbz sh -n openfaas
/home/app $ cd /var/secrets/
/var/secrets $ cat basic-auth-password
G09ZqGc9dfprr3t87281jV5bi
/var/secrets $ cat basic-auth-user
admin
通过faas-cli登录之后,API网关Pod内部会做校验,校验成功即可访问API 网关:
root@openfaas-master:~# cat ~/secret/secret-api-key.txt | faas-cli login -u admin --password-stdin
credentials saved for admin http://192.168.19.197:31112
上述登录过程中,笔者未对API网关进行加密,为保证数据安全,生产环境中建议配置HTTPS证书。
认证插件
OpenFaaS支持用户自行构建认证插件,实现方式可通过在网关模块的yaml文件中指定auth_proxy_url、auth_pass_body环境变量实现,如下图所示:
OIDC和OAuth2认证
OpenFaaS商业版本提供OIDC和OAuth2认证机制,用户只需将认证提供商信息配置至OpenFaaS部署的yaml文件中即可。部署完成后,我们可以通过UI以及OpenFaaS命令行工具对认证服务进行访问,更为详细的配置页面可参考OpenFaaS官方文档[1]。
3.1.2 函数认证
针对函数认证,OpenFaaS考虑到开发者通常部署函数用于响应外部Webhooks,由于这些Webhooks中许多都不支持OAuth或基本的认证策略,例如Github,因此OpenFaaS转为使用HMAC认证方式,关于HMAC的相应实践可以参考官方提供的workshop[2],此处由于篇幅不再做赘述。
3.2 数据安全防护
OpenFaaS数据安全防护包括密钥管理及函数/API网关的TLS加密。
3.2.1 密钥管理
关于密钥防护,OpenFaaS底层使用Kubernetes的Secret机制进行密钥存储,为避免用户直接操作Kubernetes资源,OpenFaaS进行了相应封装,如下所示:
provider:
name: openfaas
functions:
protectedapi:
lang: dockerfile
skip_build: true
image: functions/api-key-protected:latest
secrets:
- secret-api-key#可指定多个secret
3.2.2 API网关TLS加密
关于TLS加密,OpenFaaS为API网关提供了TLS证书,用户需要在OpenFaaS安装包中进行相应配置,此外还需要添加IngressController资源和证书管理器,其中,IngressController用于接入外部的LoadBalance,证书管理器 用于对TLS证书进行生命周期管理,更详细的配置可以参考官方文档[4]
3.2.3 函数TLS加密
与API网关TLS加密基本类似,唯一不同为OpenFaaS不使用Ingress Controller资源,而使用其正在孵化的IngressOperator项目,IngressOperator实则提供了一种自定义资源(CRD)FunctionIngress, 用于提供函数的TLS,下例为“nodeinfo“函数配置了TLS加密:
apiVersion: openfaas.com/v1alpha2
kind: FunctionIngress
metadata:
name: nodeinfo-tls
namespace: openfaas
spec:
domain: "nodeinfo-tls.myfaas.club"
function: "nodeinfo"
ingressType: "nginx"
tls:
enabled: true
issuerRef:
name: "letsencrypt-staging"
kind: "Issuer"
3.3 安全配置
笔者对OpenFaaS提供的安全配置进行了汇总,主要包括如下几个部分:
1.函数文件系统只读 开发者可在函数部署文件中配置“readonly_root_filesystem: true“实现,默认为false;
2.函数超时时长 开发者可在函数部署文件中配置环境变量实现,下例展示了如何进行配置:
provider:
……
functions:
……
environment:
read_timeout: 20s #允许函数通过HTTP读取请求的时间为20s
exec_timeout: 20s #允许函数被终止之前最多运行20s
write_timeout: 20s#允许函数通过HTTP写入响应的时间为20s
3.函数弹性扩展 开发者可从“每秒请求数”和“CPU/内存利用率”两方面实现函数的弹性扩展,详细的配置可以参考官方文档[5]。
4.非特权模式部署容器及non-root用户运行容器
OpenFaaS函数默认以非特权模式进行部署,且以non-root用户运行,详细信息我们可通过查看函数Dockerfile,下面列举了Dockerfile部分内容:
#Add non root user
RUN addgroup -S app && adduser app -S -G app
四.针对OpenFaaS函数的进一步防护
通过以上OpenFaaS安全功能介绍,我们可以大致看出,除了认证、安全配置、数据安全之外,OpenFaaS在访问控制、函数运行时防护方面缺乏相应的安全能力,官方文档也未提供相关信息。
OpenFaaS部署在Kubernetes之上,其部署的函数也是作为一个Pod运行在集群中,那么理论上讲,Kubernetes的安全机制也应该可以复用在OpenFaaS函数上,笔者从此思路出发,分别从Kubernetes的RBAC、Pod安全策略(Pod Security Policy)、网络策略(NetworkPolicy)三方面对OpenFaaS函数进行了相关实验。
4.1 使用RBAC实现函数访问控制
实验目的
通过在默认的函数部署命名空间“openfaas-fn”中部署RBAC策略,,进而验证RBAC能否实现对OpenFaaS函数的访问控制。
实验过程 1.部署test-bot函数
root@openfass-master:~# faas-cli up -f test-bot.yml
[0] > Building test-bot.
... ...
[0] Worker done.
Deployed. 202 Accepted.
URL: http://192.168.19.211:31112/function/test-bot.openfaas-fn
2.查看部署函数默认使用的ServiceAccount
root@openfass-master:~# kubectl get pods test-bot-5c9cdd699c-x64g5 -n openfaas-fn -o yaml
apiVersion: v1
......
spec:
containers:
......
serviceAccount: default ##默认ServiceAccount
serviceAccountName: default ##默认ServiceAccount
......
可以看出该函数使用的为Kubernetes默认为其创建的ServiceAccount,下一步需要建立一个RBAC策略,并将策略中的ServiceAccount绑定至test-bot中。
3.创建并部署RBAC策略
RBAC策略文件内容如下所示:
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: openfaas
component: faas-test-rbac
name: faas-test-rbac
namespace: "openfaas-fn"
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
labels:
app: openfaas
component: faas-test-rbac
name: faas-test-rbac
namespace: "openfaas-fn"
rules:
- apiGroups:
- ""
resources:
- secrets
- pods
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
labels:
app: openfaas
component: faas-test-rbac
name: faas-test-rbac
namespace: "openfaas-fn"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: faas-test-rbac
subjects:
- kind: ServiceAccount
该策略具体实现为:
(1) “openfaas-fn"命名空间下创建一个名为faas-test-rbac的ServiceAccount;
(2)“openfaas-fn"命名空间下创建一个名为faas-test-rbac 的Role,该Role具有对secrets及pods资源的“get”、“list”、“watch”权限;
(3)“openfaas-fn"命名空间下创建一个名为faas-test-rbac的RoleBinding,赋予faas-test-rbac ServiceAccount实体faas-test-rbac Role的权限; 部署策略后通过kubectl的“access-matrix “插件可以看出策略生效了,如下图所示:
4.将RBAC策略中的ServiceAccount绑定至test-bot Pod
由于Kubernetes禁止通过kubectl edit直接修改Pod中的ServiceAccount和ServiceAccountName,因此我们可以通过打补丁的方式替换掉test-bot中默认的ServiceAccount,需要注意的是由于Kubernetes的Pod资源由ReplicaSet控制器下发的,而ReplicaSet又是由Deployment资源下发,因此我们需要从Deployment资源中打补丁,具体操作如下:
kubectl patch -n openfaas-fn deploy test-bot -p '{"spec":{"template":{"spec":{"serviceAccountName":"faas-test-rbac"}}}}' ##通过打补丁的形式
再次查看test-pod的yaml文件我们可以发现default ServiceAccount已替换为RBAC策略中的faas-test-rbac ServiceAccount。
至此,RBAC策略已经添加完毕,若函数容器中安装了curl命令,可通过如下命令验证Pod能否访问指定命名空间的Sercret。
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt --header "Authorization:Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT/api/v1/namespaces/$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)/secret
实验结论
通过上述实验可看出,OpenFaaS函数支持Kubernetes的RBAC策略,不过无法通过faas-cli直接创建RBAC策略,而需使用原生的kubectl创建,并将ServiceAccount以打补丁的形式绑定至函数中。
4.2 使用Pod安全策略实现函数细粒度权限控制
实验目的
通过在Kubernetes集群中部署Pod安全策略,进而观察函数创建过程是否满足条件,若不满足则调整策略,最终验证Pod安全策略能否对函数进行细粒度权限控制。
实验过程
1.为Kubernetes的kube-apiserver配置添加Pod安全策略项
root@openfass-master:~# vi /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
... ...
spec:
... ...
- --enable-admission-plugins=NodeRestriction,PodSecurityPolicy ##添加PodSecurityPolicy项
... ...
2.部署一个相对严谨的Pod安全策略,策略部分内容如下所示:
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
... ...
spec:
privileged: false
# 禁止特权提升至root权限
allowPrivilegeEscalation: false
# 这与非root及不允许权限升级是多余的。
# 但我们可以提供它的防御深度。
requiredDropCapabilities:
- ALL
# 允许的核心挂载卷类型
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
# 假设集群管理员设置的persistentVolumes可以安全使用。
- 'persistentVolumeClaim'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
# 要求容器在没有root权限的情况下运行。
rule: 'MustRunAsNonRoot'
seLinux:
# 该策略假设节点使用的是AppArmor而不是SELinux。
rule: 'RunAsAny'
supplementalGroups:
rule: 'MustRunAs'
ranges:
# 禁止添加root组。
- min: 1
max: 65535
fsGroup:
rule: 'MustRunAs'
ranges:
# 禁止添加root组。
- min: 1
max: 65535
#要求容器必须以只读方式挂载根文件系统来运行,(即不允许存在可写入层)
readOnlyRootFilesystem: false
3.授权Pod安全策略
可通过RBAC授权Pod安全策略至“openfaas-fn“命名空间。
(1)创建Role
root@openfass-master:~# kubectl -n openfaas-fn create role psp:restricted \
> --verb=use \
> --resource=podsecuritypolicy \
> --resource-name=restricted
role.rbac.authorization.k8s.io/psp:restricted created
该角色定义一个名为psp:restricted的Role并给予openfaas-fn命名空间中名为restricted的Pod安全策略的权限。
(2)创建RoleBinding
root@openfass-master:~# kubectl -n openfaas-fn create rolebinding default:psp:restricted \
> --role=psp:restricted \
> --serviceaccount=openfaas-fn:default
rolebinding.rbac.authorization.k8s.io/default:psp:restricted created
该角色绑定义一个名为default:psp:restricted的rolebinding并将Pod安全策略psp:restricted 绑定至openfaas-fn命名空间下的默认default账户
4.部署OpenFaaS函数,验证Pod安全策略
与4.1中部署过程相同,此处不再赘述。
部署完成后,通过kubectl查看此函数的部署状态,发现未部署成功,如下所示:
root@openfass-master:~# kubectl get pod -n openfaas-fn -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
callfromanother-6db9fd5f69-8z7nb 0/1 CreateContainerConfigError 0 53s 10.244.0.83 openfass-master <none> <none>
错误信息为“CreateContainerConfigError“,通过kubectl get event进一步查看,输出如下信息:
52m Warning Failed pod/callfromanother-6db9fd5f69-8z7nb Error: container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root
至此,结合之前下发的Pod安全策略,我们可以看出函数容器虽然使用非root用户app运行,但Pod安全策略无法验证app为非root用户,因此OpenFaaS应用启动失败。
此时,我们可将Pod安全策略的“runAsUser“规则适当放松一些,如下图所示:
再次部署Pod安全策略后,通过删除该Pod可以重新启动一个Pod,不久可以看到Pod正常部署了:
root@openfass-master:~# kubectl delete pod callfromanother-6db9fd5f69-8z7nb -n openfaas-fn
pod "callfromanother-6db9fd5f69-8z7nb" deleted
root@openfass-master:~# kubectl get pods -n openfaas-fn
NAME READY STATUS RESTARTS AGE
callfromanother-6db9fd5f69-8z7nb 1/1 Running 0 4s
实验结论
通过上述实验可以看出,Pod安全策略可对OpenFaaS函数进行细粒度的安全控制。
4.3 使用网络策略实现函数隔离
实验目的
本实验通过对函数添加网络策略,进而验证函数能否在网络层面被有效隔离。
实验前提
Kubernetes的网络策略需要指定的CNI插件,笔者使用的Cillum插件。
实验过程
1.部署函数
笔者部署了三个函数,用于后续实验 如下所示:
root@openfaas-master-cillum:~/serverless-function/network_policy# faas-cli list
Function Invocations Replicas
callfromanother 28 1
nodeinfo 4 1
sentimentanalysis 45 1
2.添加网络策略并验证
本实验场景为在openfaas-fn命名空间下,仅指定某函数可对另一函数进行访问 下发策略内容如下:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: access-sentimentanalysis
namespace: openfaas-fn
spec:
podSelector:
matchLabels:
faas_function: sentimentanalysis
ingress:
- from:
- podSelector:
matchLabels:
faas_function: callfromanother
该策略实现为:openfaas-fn命名空间下仅允许label字段为“faas_function=callfromanother” 的Pod访问label字段为“faas_function=sentimentanalysis”的Pod。
此处以sentimentanalysis、callfromanother 、nodeinfo函数举例,在未添加策略之前通过nodeinfo函数(nodeinfo Pod的lebel字段为faas_function=nodeinfo)访问sentimentanalysis函数,如下所示:
root@openfaas-master-cillum:~/serverless-function/network_policy# kubectl exec -it nodeinfo-5544dcd45d-4nkms sh -n openfaas-fn
~$wget --post-data="xxxx" --spider --timeout=1 10.99.21.80:8080 ###10.99.21.80:8080 为sentimentanalysis的clusterIP访问方式
Connecting to 10.99.21.80:8080 (10.99.21.80:8080)
可以看到访问成功
添加策略
root@openfaas-master-cillum:~/serverless-function/network_policy# kubectl apply -f openfaas-policy-1.yaml
networkpolicy.networking.k8s.io/access-sentimentanalysis created
再次尝试在nodeinfo函数中访问sentimentanalysis函数,如下所示:
root@openfaas-master-cillum:~/serverless-function/network_policy# kubectl exec -it nodeinfo-5544dcd45d-4nkms sh -n openfaas-fn
~ $ wget --post-data="xxxx" --spider --timeout=1 10.99.21.80:8080
Connecting to 10.99.21.80:8080 (10.99.21.80:8080)
wget: download timed out
可以看到访问超时,拒绝访问。
再次尝试在网络策略中允许的label为“faas_function=callfromanother”的Pod中访问sentimentanalysis函数,如下所示:
root@openfaas-master-cillum:~/serverless-function/network_policy# kubectl exec -it callfromanother-c5689794c-c5hj6 sh -n openfaas-fn
~ $ wget --post-data="xxx" --spider --timeout=1 10.99.21.80:8080
Connecting to 10.99.21.80:8080 (10.99.21.80:8080)
remote file exists
访问成功,策略生效
实验结论
通过上述实验可以看出,网络策略可实现OpenFaaS函数在网络层面的隔离。
五.总结
本文对OpenFaaS的安全机制进行了分析解读,其中不难看出OpenFaaS提供的安全能力是有限的,依托私有化Serverless平台兼容Kubernetes的特点,我们可以考虑使用Kubernetes安全机制对Serverless函数进行深度防护,从而使开源Serverless平台受到更为全面的防护。
六.参考文献
- https://docs.openfaas.com/reference/authentication/#oidc-and-oauth2-for-the-openfaas-api
- https://github.com/openfaas/workshop/blob/master/lab11.md
- https://www.jetstack.io/
- https://docs.openfaas.com/reference/ssl/kubernetes-with-cert-manager/#10-tls-for-the-gateway
- https://docs.openfaas.com/architecture/autoscaling/#scaling-by-requests-per-second
「真诚赞赏,手留余香」
真诚赞赏,手留余香