集群流量的入与出,是安全管控里面最重要的一环,对于集群的所有流量,如果我们都能管控起来,那么很多的风险,都可以防患于未然。
比如,入口流量,在内测期间,需要做到隔离外网,或者仅放通内测客户方 CIDR,减小风险,并给自己预留更多的时间来进行安全优化。
比如,出口流量,需要做到隔离内网,如果涉及到多租户的业务模式,还需要 namespace 甚至 pod 层级的网络隔离。
看到这里,大家第一反应可能就是,不是有安全组吗,配置一下安全组不就可以了吗?
首先,安全组只能提供一个大范围的出入限制,但是更精细的 namespace 或者 pod 层级的网络隔离,安全组是没有办法做到的。
其次,一般安全组的出口流量,不会做限制,这也就导致了如果 pod 被入侵,很容易被利用去访问第三方的恶意网站和挖矿。
再者,如果 pod 被入侵以后,没有精细的流量管控,也可以轻易对集群内部的服务发起进攻,比如数据库 mysql redis 等等,也容易造成重大损失。
基于以上这些风险情况,我基于 istio 进行了深入的思考和探索,并分享其微服务网络安全体系,希望能对大家有帮助。
在流量管控体系中,我采取了 istio 作为核心,去管控集群的入和出。
使用 istio 的原因主要是为了跨集群的服务网格搭建,详情在这里 Istio 跨集群网络通信的落地实践 。
既然用了,那么基于 sidecar 去做文章,去管控所有 pod 的流量就顺理成章。
统一入口的好处就是,集群里面只有 istio-ingress 是 LoadBalancer 状态,其他的服务全部统一都设置成 ClusterIP。
每个服务只需要编写对应的 Virtual Service,ingress 会自动根据 Virtual Service 来找到服务,并转发流量。
现在举例一个场景:
我在 test namespace 下部署了一个 nginx 的服务,只部署成 ClusterIP,如何把这个服务对外访问呢?
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: test-ingress-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
/nginx/
的路由,重写成 /
,并转发到 nginx.test.svc.cluster.local:80
。apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: test-nginx
spec:
hosts:
- "*"
gateways:
- ingress-gateway
http:
- match:
- uri:
prefix: "/nginx/"
rewrite:
uri: "/"
route:
- destination:
port:
number: 80
host: nginx.test.svc.cluster.local
这样就实现 nginx 本身是 ClusterIP,却能够通过 istio-ingress 的入口进来访问,确保了入口的统一。
统一出口的好处就是,能够对出站的流量进行管控,以防访问第三方危险网站,或者挖矿。
istio-egress 提供了 Register Only
的能力,即出口的流量,必须现在 isito 集群内部注册了服务,才能够访问,否则无法访问。
这时候可能大家会有个疑惑,如果有业务需求访问非集群内的服务怎么办呢?
但是注意了,istio-egress 规则是服务要在集群内注册,而不是服务一定要在集群内。
所以这里配合服务发现和注册系统即可。
举一个最简单的例子,比如我要访问一个云数据库mongodb
。
因为 istio-egress 开启了 Register Only
,所以我们没办法直接访问的,需要先把 mongo 这个服务注册到集群内。
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: mongo
spec:
hosts:
- my-mongo.tcp.svc
addresses:
- $MONGODB_IP/32
ports:
- number: $MONGODB_PORT
name: tcp
protocol: TCP
location: MESH_EXTERNAL
resolution: STATIC
endpoints:
- address: $MONGODB_IP
此时再访问 mongo,即可调通,此时流量在集群中的关系如下图。
但是这个出口,是 pod 直接对外访问的出口,并不是从 istio-egress 出来的,如果我们需要让流量从 isito-egress 出来,还需要这样做。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: egressgateway-for-mongo
spec:
host: istio-egressgateway.istio-system.svc.cluster.local
subsets:
- name: mongo
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: direct-mongo-through-egress-gateway
spec:
hosts:
- my-mongo.tcp.svc
gateways:
- mesh
- istio-egressgateway
tcp:
- match:
- gateways:
- mesh
destinationSubnets:
- $MONGODB_IP/32
port: $MONGODB_PORT
route:
- destination:
host: istio-egressgateway.istio-system.svc.cluster.local
subset: mongo
port:
number: $EGRESS_GATEWAY_MONGODB_PORT
- match:
- gateways:
- istio-egressgateway
port: $EGRESS_GATEWAY_MONGODB_PORT
route:
- destination:
host: my-mongo.tcp.svc
port:
number: $MONGODB_PORT
weight: 100
这次流量在集群中的关系如下。
这样我们就实现了,对外访问,同统一由 istio-egress 去访问。
看到这里的标题,大家肯定有个疑惑,前面不是已经统一了出入口吗,为什么这里还需要强制统一出入口?
因为这里其实还有个风险隐患。
istio 之所以能统一所有 pod 的流量出入,主要是基于 sidecar,所以,如果有个违法的 pod 启动不带 sidecar,它就完全可以无视 isito 的规则肆意访问。
istio 本身是不能够保证 istio-ingress 和 istio-egress 是集群唯一的出入口,这就需要我们做一些强制手段,确保集群出入口只有这两个,这样就算 pod 启动绕开了 sidecar,由于集群本身堵死了其他出口,它们也无从发起攻击,进一步降低风险。
这个强制,就需要用到 Network Policy 插件。
Network Policy 提供了 namespace 甚至 pod 层级力度的网络隔离,对于所有的 namespace,我们可以限制出入口只有 istio-ingress 和 isito-egress,实现强制统一出入口。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ingress-networkpolicy
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
istio: system
podSelector:
matchLabels:
app: istio-ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-networkpolicy
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kube-system: "true"
ports:
- protocol: UDP
port: 53
- to:
- namespaceSelector:
matchLabels:
istio: system
podSelector:
matchLabels:
app: istio-egress
经过这一层的隔离,大大加强了集群网络的安全。
上面提到的 Network Policy,对 pod 的隔离的力度是没办法颗粒化的,比如,要么就同个 namespace 下的 pod 都可以相互访问,要么就是同个 namespace 下的 pod 都不可以相互访问。
但是很多时候我们业务场景是这样的,A 服务是前端,B 服务是后端,C 服务是数据库,我希望 A 能访问 B,B 能访问 C,但是 A 不能访问 C。
这就需要对 pod 有更精细的流量权限控制。
apiVersion: "security.istio.io/v1beta1"
kind: AuthorizationPolicy
metadata:
name: mongodb-policy
namespace: default
spec:
selector:
matchLabels:
app: mongodb
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/B"]
to:
- operation:
ports: ["27017"]
这样就实现了只允许 B 服务访问数据库,而其他服务无法访问。
TLS 加密也是 istio 提供的一个强大能力,主要是预防中间人的攻击,比如入侵了某个注册好的 pod,伪装自己人发起攻击,所以开启双向 TLS 认证,也可以降低这种风险。
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
name: "default"
spec:
mtls:
mode: STRICT
开启 STRICT 模式之后,只允许服务间双向 TLS 通信。
看到这里,相信大家对这个安全架构有了一定的了解,所以我这里可以抛出最后总的架构图,加深大家的印象。
说完了安全,再来说一下性能。很多时候,我们需要结合业务场景,来对安全架构做一些取舍,如果只谈安全不谈性能,无异于耍流氓。
下面我们来比较一下五种情况的出口流量场景:
针对以上场景,在 istio 第三方测评有如下性能测试数据:
可以看到,前三种场景性能上差距不大,而后两种场景,也就是加了双向认证和域名通配代理之后,带来了明显的性能下降。
随着组件和限制的增加,安全性也在增加,但其实成本也在增加。
所以这其实只是一个模板,根据业务的需求及成本去做加减法,优化适配成适合自己业务的架构,才是最好的。