使用 Azure 防火墙保护 Azure Kubernetes 服务 (AKS) 群集

本文介绍如何使用 Azure 防火墙 保护出站和入站流量,进而保护 Azure Kubernetes 服务 (AKS) 群集。

背景

Azure Kubernetes 服务 (AKS) 提供 Azure 上的托管 Kubernetes 群集。 有关详细信息,请参阅 Azure Kubernetes 服务

尽管 AKS 是完全托管的解决方案,但它未提供内置解决方案来保护群集与外部网络之间的入口和出口流量。 Azure 防火墙为此提供了解决方案。

AKS 群集部署在虚拟网络上。 此网络可以是托管的(由 AKS 创建),或者是自定义的(由用户预先配置)。 在这两种情况下,群集都对该虚拟网络外部的服务具有出站依赖项(该服务没有入站依赖项)。 为了便于管理和操作,AKS 群集中的节点需要访问特定的端口和完全限定的域名 (FQDN) - 这些域名描述了这些出站依赖项。 这对于各种函数都是必需的,包括但不限于与 Kubernetes API 服务器通信的节点。 它们会下载并安装核心 Kubernetes 群集组件和节点安全更新,或者从 Microsoft 容器注册表 (MCR) 等位置拉取基础系统容器映像。 这些出站依赖项几乎完全是使用 FQDN 定义的,不附带任何静态地址。 缺少静态地址意味着无法使用网络安全组锁定来自 AKS 群集的出站流量。 因此,在默认情况下,AKS 群集具有不受限制的出站(出口)Internet 访问权限。 此级别的网络访问权限允许运行的节点和服务根据需要访问外部资源。

但是,在生产环境中,应保护与 Kubernetes 群集的通信,以防止数据外泄和其他漏洞。 必须根据安全规则监视和控制所有传入和传出网络流量。 若要执行此操作,需要限制出口流量,但必须保持有限数量的端口和地址可供访问,这样才能维护正常运行的群集维护任务并满足前面提到的出站依赖项。

最简单的解决方案是使用可基于域名控制出站流量的防火墙设备。 防火墙通常会在受信任的网络和不受信任的网络(例如 Internet)之间建立屏障。 例如,Azure 防火墙可根据目标的 FQDN 限制出站 HTTP 和 HTTPS 流量,从而提供精细的出口流量控制,但同时又可提供对包含 AKS 群集出站依赖项的 FQDN 的访问(这是 NSG 无法做到的)。 同样,可以在部署到共享外围网络的 Azure 防火墙上启用基于威胁情报的筛选器,来控制入口流量并提高安全性。 此筛选功能可以发出警报,并拒绝传入和传出已知恶意 IP 地址和域的流量。

请参阅下列来自 Abhinav Sriram 的视频,快速简要了解这在示例环境中的实际运用:

可从下载中心下载包含 bash 脚本文件和 yaml 文件的 zip 文件,用来自动配置视频中使用的示例环境。 它配置了 Azure 防火墙来保护入口和出口流量。 以下指南详细介绍了脚本的每个步骤,以便你可设置自定义配置。

下图显示了视频中脚本和指南配置的示例环境:

显示设有 Azure 防火墙来筛选入口出口流量的 AKS 群集的示意图。

该脚本和以下指南之间存在一项区别。 该脚本使用托管标识,而指南使用服务主体。 这显示了创建标识来管理和创建群集资源的两种不同的方法。

使用 Azure 防火墙限制出口流量

通过环境变量设置配置

定义创建资源时要使用的一组环境变量。

PREFIX="aks-egress"
RG="${PREFIX}-rg"
LOC="chinaeast"
PLUGIN=azure
AKSNAME="${PREFIX}"
VNET_NAME="${PREFIX}-vnet"
AKSSUBNET_NAME="aks-subnet"
# DO NOT CHANGE FWSUBNET_NAME - This is currently a requirement for Azure Firewall.
FWSUBNET_NAME="AzureFirewallSubnet"
FWNAME="${PREFIX}-fw"
FWPUBLICIP_NAME="${PREFIX}-fwpublicip"
FWIPCONFIG_NAME="${PREFIX}-fwconfig"
FWROUTE_TABLE_NAME="${PREFIX}-fwrt"
FWROUTE_NAME="${PREFIX}-fwrn"
FWROUTE_NAME_INTERNET="${PREFIX}-fwinternet"

创建包含多个子网的虚拟网络

创建包含两个单独子网的虚拟网络,其中一个子网用于群集,一个子网用于防火墙。 还可以选择为内部服务入口创建一个。

空网络拓扑

创建一个资源组来存放所有资源。

# Create Resource Group

az group create --name $RG --location $LOC

创建具有两个子网的虚拟网络来托管 AKS 群集和 Azure 防火墙。 每个子网都有自己的子网。 让我们从 AKS 网络开始。

# Dedicated virtual network with AKS subnet

az network vnet create \
    --resource-group $RG \
    --name $VNET_NAME \
    --location $LOC \
    --address-prefixes 10.42.0.0/16 \
    --subnet-name $AKSSUBNET_NAME \
    --subnet-prefix 10.42.1.0/24

# Dedicated subnet for Azure Firewall (Firewall name cannot be changed)

az network vnet subnet create \
    --resource-group $RG \
    --vnet-name $VNET_NAME \
    --name $FWSUBNET_NAME \
    --address-prefix 10.42.2.0/24

使用 UDR 来创建和设置 Azure 防火墙

必须配置 Azure 防火墙入站和出站规则。 防火墙的主要用途是使组织能够针对传入和传出 AKS 群集的流量配置精细的规则。

防火墙和 UDR

重要

如果群集或应用程序创建众多定向到相同目标或目标子集的出站连接,则可能需要更多的防火墙前端 IP 来避免用尽每个前端 IP 的端口。 有关如何创建具有多个 IP 的 Azure 防火墙的详细信息,请参阅此处

创建将用作 Azure 防火墙前端地址的标准 SKU 公共 IP 资源。

az network public-ip create -g $RG -n $FWPUBLICIP_NAME -l $LOC --sku "Standard"

注册预览版 CLI 扩展以创建 Azure 防火墙。

# Install Azure Firewall preview CLI extension

az extension add --name azure-firewall

# Deploy Azure Firewall

az network firewall create -g $RG -n $FWNAME -l $LOC --enable-dns-proxy true

现在,可将前面创建的 IP 地址分配到防火墙前端。

注意

设置 Azure 防火墙的公共 IP 地址可能需要几分钟时间。 若要对网络规则使用 FQDN,需要启用 DNS 代理。如果启用,防火墙将侦听端口 53,并将 DNS 请求转发到前面指定的 DNS 服务器。 这将允许防火墙自动转换该 FQDN。

# Configure Firewall IP Config

az network firewall ip-config create -g $RG -f $FWNAME -n $FWIPCONFIG_NAME --public-ip-address $FWPUBLICIP_NAME --vnet-name $VNET_NAME

当前面的命令成功时,保存防火墙前端 IP 地址以便稍后进行配置。

# Capture Firewall IP Address for Later Use

FWPUBLIC_IP=$(az network public-ip show -g $RG -n $FWPUBLICIP_NAME --query "ipAddress" -o tsv)
FWPRIVATE_IP=$(az network firewall show -g $RG -n $FWNAME --query "ipConfigurations[0].privateIPAddress" -o tsv)

注意

如果通过授权 IP 地址范围安全访问 AKS API 服务器,需要将防火墙公共 IP 添加到授权的 IP 范围。

创建包含 Azure 防火墙跃点的 UDR

Azure 自动在 Azure 子网、虚拟网络与本地网络之间路由流量。 若要更改 Azure 的任何默认路由,可以创建一个路由表。

创建一个要与给定子网关联的空路由表。 该路由表将下一跃点定义为前面创建的 Azure 防火墙。 每个子网可以有一个与之关联的路由表,也可以没有。

# Create UDR and add a route for Azure Firewall

az network route-table create -g $RG -l $LOC --name $FWROUTE_TABLE_NAME
az network route-table route create -g $RG --name $FWROUTE_NAME --route-table-name $FWROUTE_TABLE_NAME --address-prefix 0.0.0.0/0 --next-hop-type VirtualAppliance --next-hop-ip-address $FWPRIVATE_IP
az network route-table route create -g $RG --name $FWROUTE_NAME_INTERNET --route-table-name $FWROUTE_TABLE_NAME --address-prefix $FWPUBLIC_IP/32 --next-hop-type Internet

请参阅虚拟网络路由表文档,了解如何替代 Azure 的默认系统路由或者在子网的路由表中添加更多路由。

添加防火墙规则

注意

对于需要与 API 服务器通信的 kube 系统或 gatekeeper 系统命名空间外部的应用程序,除了为 fqdn-tag AzureKubernetesService 添加应用程序规则之外,还需要一条额外的网络规则来允许与 API 服务器 IP 的端口 443 进行 TCP 通信。

可以使用以下三个网络规则来配置防火墙。 可能需要根据部署调整这些规则。 第一个规则允许通过 TCP 访问端口 9000。 第二个规则允许通过 UDP 访问端口 1194 和 123。 这两个规则仅允许目标为当前所用 Azure 区域 CIDR 的流量,本例中为中国东部。

最后,我们将通过 UDP 向 Internet 时间服务器 FQDN 添加第三个网络规则打开端口 123(例如:ntp.ubuntu.com)。 将 FQDN 添加为网络规则是 Azure 防火墙的一项特定功能,使用自己的选项时需要对其进行调整。

设置网络规则后,还将使用 AzureKubernetesService 添加应用程序规则,以涵盖可通过 TCP 端口 443 和端口 80 访问的必需 FQDN。 此外,可能需要根据部署配置更多网络和应用程序规则。 有关详细信息,请参阅适用于 Azure Kubernetes 服务 (AKS) 群集的出站网络和 FQDN 规则

添加 FW 网络规则

az network firewall network-rule create -g $RG -f $FWNAME --collection-name 'aksfwnr' -n 'apiudp' --protocols 'UDP' --source-addresses '*' --destination-addresses "AzureCloud.$LOC" --destination-ports 1194 --action allow --priority 100
az network firewall network-rule create -g $RG -f $FWNAME --collection-name 'aksfwnr' -n 'apitcp' --protocols 'TCP' --source-addresses '*' --destination-addresses "AzureCloud.$LOC" --destination-ports 9000
az network firewall network-rule create -g $RG -f $FWNAME --collection-name 'aksfwnr' -n 'time' --protocols 'UDP' --source-addresses '*' --destination-fqdns 'ntp.ubuntu.com' --destination-ports 123

添加 FW 应用程序规则

az network firewall application-rule create -g $RG -f $FWNAME --collection-name 'aksfwar' -n 'fqdn' --source-addresses '*' --protocols 'http=80' 'https=443' --fqdn-tags "AzureKubernetesService" --action allow --priority 100

请参阅 Azure 防火墙文档来详细了解 Azure 防火墙服务。

将路由表关联到 AKS

若要将群集与防火墙相关联,群集子网的专用子网必须引用前面创建的路由表。 可以通过向包含群集和防火墙的虚拟网络发出更新群集子网路由表的命令来执行关联。

# Associate route table with next hop to Firewall to the AKS subnet

az network vnet subnet update -g $RG --vnet-name $VNET_NAME --name $AKSSUBNET_NAME --route-table $FWROUTE_TABLE_NAME

将出站类型为 UDR 的 AKS 部署到现有网络

现在,可将 AKS 群集部署到现有的虚拟网络。 还要使用出站类型userDefinedRouting,此功能确保通过防火墙强制执行任何出站流量,并且不存在其他传出路径(默认情况下,可以使用负载均衡器出站类型)。

aks-deploy

要部署到的目标子网是使用环境变量 ($SUBNETID) 定义的。 在前面的步骤中,我们未定义 $SUBNETID 变量。 若要设置子网 ID 的值,可使用以下命令:

SUBNETID=$(az network vnet subnet show -g $RG --vnet-name $VNET_NAME --name $AKSSUBNET_NAME --query id -o tsv)

定义出站类型以使用子网中已存在的 UDR。 使用此配置,AKS 可跳过负载均衡器的设置和 IP 预配。

重要

有关出站类型 UDR(包括限制)的详细信息,请参阅流出量出站类型 UDR

提示

可以将其他功能添加到群集部署,例如专用群集或更改 OS SKU

可以添加 API 服务器已授权 IP 范围 AKS 功能,以便限制 API 服务器仅访问防火墙的公共终结点。 已授权 IP 范围功能在图中表示为可选。 启用已授权 IP 范围功能来限制 API 服务器访问权限时,开发人员工具必须使用防火墙虚拟网络中的 Jumpbox,或者必须将所有开发人员终结点添加到已授权 IP 范围。

az aks create -g $RG -n $AKSNAME -l $LOC \
  --node-count 3 \
  --network-plugin $PLUGIN \
  --outbound-type userDefinedRouting \
  --vnet-subnet-id $SUBNETID \
  --api-server-authorized-ip-ranges $FWPUBLIC_IP

注意

若要通过 kubenet 网络插件创建和使用自己的 VNet 和路由表,需要使用用户分配的托管标识。 对于系统分配的托管标识,我们在创建群集之前无法获取标识 ID,这会导致角色分配生效延迟。

通过 azure 网络插件创建和使用自己的 VNet 和路由表,支持系统分配的和用户分配的托管标识。

使开发人员能够访问 API 服务器

如果在上一步中为群集使用了已授权 IP 范围,则必须将开发人员工具 IP 地址添加到 AKS 群集的已批准 IP 范围列表,以便从该处访问 API 服务器。 另一种做法是在防火墙虚拟网络中的单独子网内,使用所需的工具配置 Jumpbox。

使用以下命令将另一个 IP 地址添加到已批准范围

# Retrieve your IP address
CURRENT_IP=$(dig @resolver1.opendns.com ANY myip.opendns.com +short)

# Add to AKS approved list
az aks update -g $RG -n $AKSNAME --api-server-authorized-ip-ranges $CURRENT_IP/32

使用 az aks get-credentials 命令配置 kubectl,使其连接到新创建的 Kubernetes 群集。

az aks get-credentials -g $RG -n $AKSNAME

使用 Azure 防火墙限制入口流量

现在可以开始公开服务并将应用程序部署到此群集。 此示例将公开公共服务,但也可以选择通过内部负载均衡器公开内部服务。

公共服务 DNAT

通过将以下 yaml 复制到名为 example.yaml 的文件来部署 Azure 投票应用程序。

# voting-storage-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: voting-storage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: voting-storage
  template:
    metadata:
      labels:
        app: voting-storage
    spec:
      containers:
      - name: voting-storage
        image: mcr.microsoft.com/azuredocs/voting/storage:2.0
        args: ["--ignore-db-dir=lost+found"]
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 250m
            memory: 256Mi
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_ROOT_PASSWORD
        - name: MYSQL_USER
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_USER
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_PASSWORD
        - name: MYSQL_DATABASE
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_DATABASE
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim
---
# voting-storage-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: voting-storage-secret
type: Opaque
data:
  MYSQL_USER: ZGJ1c2Vy
  MYSQL_PASSWORD: UGFzc3dvcmQxMg==
  MYSQL_DATABASE: YXp1cmV2b3Rl
  MYSQL_ROOT_PASSWORD: UGFzc3dvcmQxMg==
---
# voting-storage-pv-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
# voting-storage-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: voting-storage
  labels:
    app: voting-storage
spec:
  ports:
  - port: 3306
    name: mysql
  selector:
    app: voting-storage
---
# voting-app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: voting-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: voting-app
  template:
    metadata:
      labels:
        app: voting-app
    spec:
      containers:
      - name: voting-app
        image: mcr.microsoft.com/azuredocs/voting/app:2.0
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
          name: http
        env:
        - name: MYSQL_HOST
          value: "voting-storage"
        - name: MYSQL_USER
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_USER
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_PASSWORD
        - name: MYSQL_DATABASE
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_DATABASE
        - name: ANALYTICS_HOST
          value: "voting-analytics"
---
# voting-app-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: voting-app
  labels:
    app: voting-app
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
    name: http
  selector:
    app: voting-app
---
# voting-analytics-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: voting-analytics
spec:
  replicas: 1
  selector:
    matchLabels:
      app: voting-analytics
      version: "2.0"
  template:
    metadata:
      labels:
        app: voting-analytics
        version: "2.0"
    spec:
      containers:
      - name: voting-analytics
        image: mcr.microsoft.com/azuredocs/voting/analytics:2.0
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
          name: http
        env:
        - name: MYSQL_HOST
          value: "voting-storage"
        - name: MYSQL_USER
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_USER
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_PASSWORD
        - name: MYSQL_DATABASE
          valueFrom:
            secretKeyRef:
              name: voting-storage-secret
              key: MYSQL_DATABASE
---
# voting-analytics-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: voting-analytics
  labels:
    app: voting-analytics
spec:
  ports:
  - port: 8080
    name: http
  selector:
    app: voting-analytics

运行以下命令来部署服务:

kubectl apply -f example.yaml

将 DNAT 规则添加到 Azure 防火墙

重要

使用 Azure 防火墙限制出口流量并创建用户定义的路由 (UDR) 来强制所有出口流量时,请确保在防火墙中创建适当的 DNAT 规则,以正确允许入口流量。 结合使用 Azure 防火墙和 UDR 时,会因为路由不对称而中断入口设置。 (如果 AKS 子网具有指向防火墙专用 IP 地址的默认路由,但你使用的是公共负载均衡器 - 类型为 LoadBalancer 的入口或 Kubernetes 服务,则会出现此问题)。 在这种情况下,将通过负载均衡器的公共 IP 地址接收传入的负载均衡器流量,但返回路径将通过防火墙的专用 IP 地址。 由于防火墙是有状态的,并且无法识别已建立的会话,因此会丢弃返回的数据包。 若要了解如何将 Azure 防火墙与入口或服务负载均衡器集成,请参阅将 Azure 防火墙与 Azure 标准负载均衡器集成

若要配置入站连接,必须将一个 DNAT 规则写入到 Azure 防火墙。 为了测试与群集的连接,为防火墙前端公共 IP 地址定义了规则,以便路由到内部服务公开的内部 IP。

可以自定义目标地址,因为它是防火墙上要访问的端口。 转换的地址必须是内部负载均衡器的 IP 地址。 转换的端口必须是 Kubernetes 服务的已公开端口。

需要指定分配给由 Kubernetes 服务创建的负载均衡器的内部 IP 地址。 运行以下命令来检索该地址:

kubectl get services

所需的 IP 地址会在“EXTERNAL-IP”列中列出,如下所示。

NAME               TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)                AGE
kubernetes         ClusterIP      10.41.0.1       <none>            443/TCP                10h
store-front        LoadBalancer   10.41.185.82    203.0.113.254     80:32718/TCP           9m
order-service      ClusterIP      10.0.104.144    <none>            3000/TCP               11s
product-service    ClusterIP      10.0.237.60     <none>            3002/TCP               10s
rabbitmq           ClusterIP      10.0.161.128    <none>            5672/TCP,15672/TCP     11s

运行以下内容来获取服务 IP:

SERVICE_IP=$(kubectl get svc store-front -o jsonpath='{.status.loadBalancer.ingress[*].ip}')

运行以下内容来添加 NAT 规则:

az network firewall nat-rule create --collection-name exampleset --destination-addresses $FWPUBLIC_IP --destination-ports 80 --firewall-name $FWNAME --name inboundrule --protocols Any --resource-group $RG --source-addresses '*' --translated-port 80 --action Dnat --priority 100 --translated-address $SERVICE_IP

验证连接

在浏览器中导航到 Azure 防火墙前端 IP 地址来验证连接。

应看到 AKS 商店应用。 此示例中,防火墙公共 IP 为 203.0.113.32

在此页上,可以查看产品,将其添加到购物车,然后下订单。

清理资源

若要清理 Azure 资源,请删除 AKS 资源组。

az group delete -g $RG

后续步骤