Consul网络和安全操作

简介

这个章节上接上一份文档,包含了Consul集群的安全和网络设置,这里假设用户已经完成了上一章节中数据中心的部署。

Gossip加密

Consul内部针对两套不同的子系统使用不同的通信加密方式:Gossip加密TLSTLS用于保护代理间的RPC调用,代理间的Gossip通信是通过UDP完成的,使用对称密钥保护安全,这个章节我们仅讨论Gossip加密

Gossip加密密钥

要启用Gossip加密,需要在启动Consul服务时添加加密密钥,用户可以在配置文件中增加encrypt参数,密钥大小为16字节,使用Base64编码,用户可以使用Consul命令行生成密钥:

$ consul keygen
cg8StVXbQJ0gPvMd9o7yrg==

在新集群上生效

如果用户想在新集群上启用Gossip加密,可以将密钥参数添加到配置文件,并在启动时通过-config-dir参数指定文件:

{
  "data_dir": "/opt/consul",
  "log_level": "INFO",
  "node_name": "bulldog",
  "server": true,
  "encrypt": "JY34uTPZyfUE+6tinMYEVw=="
}
$ consul agent -config-dir=/etc/consul.d/
==> Starting Consul agent...
==> Starting Consul agent RPC...
==> Consul agent running!
         Node name: 'Armons-MacBook-Air.local'
        Datacenter: 'dc1'
            Server: false (bootstrap: false)
       Client Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)
      Cluster Addr: 10.1.10.12 (LAN: 8301, WAN: 8302)
    Gossip encrypt: true, RPC-TLS: false, TLS-Incoming: false
...

如果正确配置了加密,输出中将包含Gossip encrypt: true,这里需要注意的是,集群中的所有节点必须使用一份共同的加密密钥,使用WAN连接的多个数据中心之间也需要保持统一。

在现有集群上生效

Gossip加密也可以在现有集群上启用,需要额外几个步骤:

  • 使用consul keygen生成密钥:
$ consul keygen
JY34uTPZyfUE+6tinMYEVw==
  • 设置加密密钥,将配置文件中的crypto_verify_incomingcrypto_verify_outgoing参数设置为false,然后滚动更新Consul集群,执行完此步骤后,代理可以解密Gossip加密数据,但此时发送的数据还未加密。这里的滚动更新可以通过依次重新启动Consul节点来完成,consul reload或者kill -HUP <process_id>不足以改变Gossip配置。
{
  "data_dir": "/opt/consul",
  "log_level": "INFO",
  "node_name": "bulldog",
  "server": true,
  "encrypt": "JY34uTPZyfUE+6tinMYEVw==",
  "encrypt_verify_incoming": false,
  "encrypt_verify_outgoing": false
}
  • encrypt_verify_outgoing设置更新为true,然后滚动更新Consul集群,现在代理将发送加密的Gossip数据,并允许接收未加密的数据:
{
  "data_dir": "/opt/consul",
  "log_level": "INFO",
  "node_name": "bulldog",
  "server": true,
  "encrypt": "JY34uTPZyfUE+6tinMYEVw==",
  "encrypt_verify_incoming": false,
  "encrypt_verify_outgoing": true
}
  • 最后将crypto_verify_incoming更新为true滚动更新Consul集群,在验证完上一步全部生效之后,才能执行此步:
{
  "data_dir": "/opt/consul",
  "log_level": "INFO",
  "node_name": "bulldog",
  "server": true,
  "encrypt": "JY34uTPZyfUE+6tinMYEVw==",
  "encrypt_verify_incoming": true,
  "encrypt_verify_outgoing": true
}

现在所有代理都将严格执行加密Gossip协议,此处需要注意的是encrypt_verify_incomingencrypt_verify_outgoing这两个参数默认都是true

使用TLS加密保护Agent端通信安全

在生产环境中使用TLS加密来保护数据中心安全是非常必要的步骤,TLS配置也是我们安全模型的先决条件。正确配置TLS是一个非常复杂的过程,尤其考虑到部署的范围会非常大。本章将会介绍可用于生产的RPC通信一致性通信(consensus communication)的TLS配置,同时用户也需要保障HTTP请求的安全。
Consul支持使用TLS对服务端和客户端进行鉴权,启用TLS需要所有服务端都具有单个证书颁发机构签名的证书,同时客户端也应该具有相同认证的证书。

先决条件

本章节中介绍的证书生成及分发步骤仅适用于新的Consul集群,在启动Consul服务之前,应完成所有的配置工作;如果需要在已有的数据中心上启用TLS加密,需要将此章节内容结合其他操作,这个会在后续再做介绍。

分发客户端证书

分发客户端证书有两种方法,手动加密自动加密,Consul在1.5.2版本中引入了自动加密方式,减轻了运维人员手动生成和分发客户端正式的步骤。这个方法使用Connect CA命名生成客户端证书,然后Consul会将证书自动分发给所有客户端,这对于有许多客户端的大型数据中心非常有效。
如果需要使用第三方CA或需要对证书管理进行更细粒度的控制,建议使用手动加密方式。有关将OpenSSL作为第三方CA的示例,将在后续再做介绍。

初始化内置证书

为Consul配置TLS的第一步就是生成证书,为了防止未经授权的数据中心访问,Consul要求所有证书均由同一证书颁发机构签名。这里应该是一个私有证书,而不是公共证书,因为使用该证书签名的凭证都能与本数据中心进行通信。
有多种工具可以管理证书,比如Value中的PKI,这里为了简化操作,我们使用Consul内置的TLS助手来创建证书。整个数据中心仅需创建一个证书,创建证书的节点应保持稳定,最好不要是Consul代理或者云服务器,创建指令如下:

$ consul tls ca create
==> Saved consul-agent-ca.pem 
==> Saved consul-agent-ca-key.pem

CA证书:consul-agent-ca.pem包含验证Consul证书所需的公共密钥,必须分发给运行Consul代理的各个节点
CA密钥:consul-agent-ca-key.pem将用于签署访问Consul节点的证书,必须保持私有,拥有此密钥,任何人都可以受信访问Consul服务器,并可以生成新的证书,获得所有Consul数据(包括ACL令牌)的访问权限。

创建服务端证书

使用下述命令创建服务端证书

$ consul tls cert create -server
==> WARNING: Server Certificates grants authority to become a
    server and access all state in the cluster including root keys
    and all ACL tokens. Do not distribute them to production hosts
    that are not server nodes. Store them as securely as CA keys.
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
==> Saved dc1-server-consul-0.pem
==> Saved dc1-server-consul-0-key.pem

在同一个节点重复这个操作,直到每台服务器都有一个单独的证书。该命令可以反复调用,它会自动增加证书和密钥序列。
为了对Consul服务端进行身份验证,服务端会提供一种特殊的证书,证书的名称中包含server.dc1.consul。如果用户启用verify_server_hostname,那么只有能提供此证书的代理才能作为服务端。假设没有设置verify_server_hostname = true,攻击者可能会破坏Consul客户端代理,并将其作为服务端重新启动,然后就能访问数据中心的所有数据,这就是服务端证书很特殊的原因,并且仅服务端需要配置它们。

分发服务端证书

生成服务端证书后,用户需要将它们分发到Consul服务器,并修改配置文件,指定证书的目录地址。
用户需要将下述文件复制到Consul服务端机器:

  • consul-agent-ca.pem:CA公共证书
  • dc1-server-consul-0.pem:dc1数据中心Consul服务器节点公共证书
  • dc1-server-consul-0-key.pem:dc1数据中心Consul服务器节点私钥

重复上述过程,直到所有服务端机器都具有这三个文件。

自动加密方式

服务端配置

在上述章节中,用户创建了服务端证书并成功分发了它们,要使用自动加密来分发客户端证书,用户需要启用auto_encrypt功能,使用以下选项配置服务器:

{
  "verify_incoming": true,
  "verify_outgoing": true,
  "verify_server_hostname": true,
  "ca_file": "consul-agent-ca.pem",
  "cert_file": "dc1-server-consul-0.pem",
  "key_file": "dc1-server-consul-0-key.pem",
  "auto_encrypt": {
    "allow_tls": true
  }
}

注意,除了verify_xxx设置之外,还需要启用allow_tls配置,verify_xxx设置确保服务端和客户端之间的所有通信都进行了鉴权,启用auto_encrypt会默认启用verify_outgoing

客户端配置

客户端配置如下所示:

{
  "verify_incoming": false,
  "verify_outgoing": true,
  "verify_server_hostname": true,
  "ca_file": "consul-agent-ca.pem",
  "auto_encrypt": {
    "tls": true
  }
}

注意,此方法将证书存储在内存中,并没有持久化保存。

手动加密方式

服务端配置

使用以下选项配置服务端:

{
  "verify_incoming": true,
  "verify_outgoing": true,
  "verify_server_hostname": true,
  "ca_file": "consul-agent-ca.pem",
  "cert_file": "dc1-server-consul-0.pem",
  "key_file": "dc1-server-consul-0-key.pem"
}

通过verify_outgoingverify_server_hostname配置项,TLS可用于服务端鉴权,也可以选择使用verify_incoming对客户端进行鉴权。

设置客户端

要对客户端通信进行加密,需要将CA证书、客户端证书及其私钥分发给每个客户端,下面描述具体步骤。

生成客户端证书

在Consul集群节点上,使用consul tls cert create -client命令生成客户端证书:

$ consul tls cert create -client
==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
==> Saved dc1-client-consul-0.pem
==> Saved dc1-client-consul-0-key.pem

客户端证书应该也要由CA签名,但是它没有服务端证书的特征,这意味着如果启用了verify_server_hostname配置,它们不能作为服务端启动,创建完服务端证书之后,即可以分发到Consul集群每个客户端中。

客户端配置

配置客户端代理:

{
  "verify_incoming": true,
  "verify_outgoing": true,
  "verify_server_hostname": true,
  "ca_file": "consul-agent-ca.pem",
  "cert_file": "dc1-client-consul-0.pem",
  "key_file": "dc1-client-consul-0-key.pem"
}

启动Consul

现在,用户已经配置好了服务端和客户端,可以开启Consul服务了

# systemctl start consul

使用ACL增强Consul安全性

Consul使用访问控制列表(ACLs)来保护UIAPICLI服务通信代理通信,当用户希望保护集群安全时,应该优先配置ACL。ACL的核心理念是将规则分组为策略,然后将一个或多个策略与令牌相关联。

初始化ACL系统

用户需要通过两个步骤初始化ACL系统:

  1. 启用ACLs
  2. 创建引导令牌
在代理中启用ACLs

在代理的配置文件中添加ACL参数,然后重新启动Consul服务可以启用ACL,为了正确启用ACL配置,用户需要将相同的参数应用于数据中心的每个服务端和客户端。如果希望减少Consul客户端的重启次数,可以在添加令牌时一同启用ACL。

## agent.hcl
acl = {
  enabled = true
  default_policy = "deny"
  enable_token_persistence = true
}

注意:这里需要注意的是令牌持久化在1.4.3版本中才引入,用户在使用HTTP API时无法持久化令牌。

在此示例中,默认策略配置为deny,这意味着处于白名单模式,这里还启用了持久化令牌(token persistence),这样令牌会保留在磁盘上,并在代理重启时重新加载。

注意:如果要在现有数据中心上引导ACL,首先配置 default_policy = allow 在代理上启用ACL。默认策略配置为allow将启用ACL,打开所有权限,从而在创建并使用令牌后,集群也可以正常工作,这有助于减少停机时间。

创建引导令牌

使用acl bootstrap命令创建引导令牌:

consul acl bootstrap

输出内容包含了令牌的重要信息:global-managementSecretID

注意:默认情况下,Consul赋予引导令牌 global-management 策略,这个令牌拥有特权,不受任何限制。在紧急情况下,拥有一个不受限的令牌时非常重要的,但是只应少量管理员拥有此令牌。SecretID是一个UUID,在使用Consul命令行或HTTP API时用于标识令牌。

CONSUL_HTTP_TOKEN环境变量设置为引导令牌:

export CONSUL_HTTP_TOKEN=<your_token_here>

接下来的所有示例,都以服务器consul-server-one为例。

将代理中应用令牌

在代理中应用令牌分为三个步骤:

  1. 创建代理策略;
  2. 使用新创建的策略创建令牌;
  3. 将令牌添加到代理。
创建代理策略

我们建议将代理策略配置为具有写权限的配置,包括在目录中自注册、更新节点级别的健康检查、以及对代理中的配置文件具有写访问权限等。

# consul-server-one-policy.hcl
node "consul-server-one" {
  policy = "write"
}

创建代理策略时,需要先查看节点规则,只有可以使用Consul命令行对其进行初始化。(同样也可以使用HTTP API)

$ consul acl policy create \
  -name consul-server-one \
  -rules @consul-server-one-policy.hcl

命令行输出将包含策略信息,对Consul数据中心中的所有服务端和客户端都重复此过程,每个代理都有对应自己节点名称相应的策略。

创建代理Token

创建完代理策略之后,就要为所有代理创建单独的令牌,执行consul acl token create命令:

$ consul acl token create -description "consul-server-one agent token" -policy-name consul-server-one

这条命令返回令牌信息令牌信息包括描述策略信息,对每个代理都重复此过程,运维人员需要将这些令牌保存在安全的地方,这里我们推荐使用Vault

代理添加Token

最后,使用set-agent-token子命令配置令牌:

consul acl set-agent-token agent "<agent token here>"

用户需要在每个代理中都执行此操作,此外还需要将CONSUL_HTTP_TOKEN环境变量设置为引导令牌或使用API配置。
至此,每个具有令牌的代理都可以再次向Consul读写信息,但只能用于与节点相关的操作,对服务的操作尚不能运行。

在服务中添加令牌

服务令牌的创建和应用过程类似于代理:

  1. 创建一个策略;
  2. 使用该策略创建令牌;
  3. 将令牌添加到服务。

下面是一个需要配置令牌的服务示例,这个服务定义在某个客户端服务器的配置目录中:

{
  "service": {
    "name": "dashboard",
    "port": 9002,
    "check": {
      "id": "dashboard-check",
      "http": "http://localhost:9002/health",
      "method": "GET",
      "interval": "1s",
      "timeout": "1s"
    }
  }
}

首先,创建一个仅向dashboard服务授予写权限的策略:

# dashboard-policy.hcl
service "dashboard" {
  policy = "write"
}

使用策略定义来初始化策略:

consul acl policy create -name "dashboard-service" -rules @dashboard-policy.hcl

接下来,使用该策略创建一个token:

consul acl token create -description "Token for Dashboard Service" \
  -policy-name dashboard-service

与之前一样,该命令会返回有关令牌的信息,需要将它们妥善保管,最后将令牌添加到服务定义中:

{
  "service": {
    "name": "dashboard",
    "port": 9002,
    "token": "57c5d69a-5f19-469b-0543-12a487eecc66",
    "check": {
      "id": "dashboard-check",
      "http": "http://localhost:9002/health",
      "method": "GET",
      "interval": "1s",
      "timeout": "1s"
    }
  }
}

如果服务正在运行中,需要重新启动它生效。与代理令牌不同,没有HTTP API接口可以在服务中设置令牌,如果该服务之前是通过配置文件注册的,还必须在配置文件中设置令牌;如果之前是使用HTTP API注册服务的,用户可以在header中使用X-Consul-Token传递令牌。

Consul KV令牌

Consul KV创建令牌的步骤与前几类令牌相似,与它们不同的是,Consul KV有许多不同的使用场景:

  • 服务可能需要访问KV集中的配置数据;
  • 用户存储Session分布式锁信息;
  • 运维人员需要权限更新配置值;

KV集的规则有四个政策级别:denywritereadlist
根据使用场景不同,令牌的使用方式也会有所差异。对于服务,用户需要将令牌添加到HTTP客户端;对于运维人员,需要在执行命令行或API时添加令牌。

递归读取
key_prefix "redis/" {
  policy = "read"
}

上述示例中,用户允许读取任何前缀是redis/的键,如果用户发出指令consul kv get -recurse redis/ -token=<your token>,可以获得一列redis/为前缀的键的列表。
配置这种类型的策略有利于运维人员递归读取KV集中存储的配置参数,同样配置写权限可以更新前缀为redis/的所有键。

某一键值的写权限
key "dashboard-app" {
  policy = "write"
}

上述示例中,用户赋予dashboard-app键读写权限,允许使用getdeleteput操作。

某一键值的读权限
key "counting-app" {
  policy = "read"
}

上述示例中,用户赋予counting-app键读权限,允许使用get操作。

Consul UI令牌

在建立ACL系统后,用户在界面访问也受到了限制,如果在代理上未设置默认令牌,则将匿名令牌授予UI访问权限,这样所有的操作都会被拒绝,包括查看节点和服务。
运维人员可以创建并分发token给用户,重新启用UI功能,用户可以在 ACL页面添加获取的token:

access-controls.png

在操作完之后,用户可以在页面看到添加的令牌:
tokens.png

浏览器会存储用户添加的令牌,运维人员可以分配不同权限的令牌给不同的用户。
下述是一个策略的示例,该策略允许用户对ServicesNodesKey/ValueIntentions界面进行访问,同时还需要配置acl = read才能查看策略和令牌,否则无法访问界面的ACL部分:

# operator-ui.hcl
service_prefix "" {
  policy = "read"
}
key_prefix "" {
  policy = "read"
}
node_prefix "" {
  policy = "read"
}

管理ACL策略

本章节内容主要面向运维人员,关注创建和管理ACL令牌,其中包括一些建议,如何找到完成操作所需的最低权限。 在整篇指南中,我们将提供一些示例,用户可以根据自己的环境进行调整,阅读完本章节后,用户将对如何有效管理ACL策略和令牌有更好的了解。
我们希望运维人员在生产环境中自动执行策略和令牌生成过程。 此外,如果用户使用了容器编排器(container orchestrator),虽然本指南中的概念仍然适用,但整个过程还是将有所不同。 如果用户使用的是官方的Consul-Kubernetes Helm图表来部署Consul,请使用相关的鉴权方法文档(authentication method documentation)

先决条件

在本指南中,我们提供的是一些抽象并高级的建议,并不会按部就班地用命令描述令牌生成过程,这里假定所有的代理上都设置了default_policydeny

安全性和可用性

本章节中的示例描述了,如何创建可用于完成同一任务的多个策略。 例如,使用完全匹配资源(exact match resourc)规则是最安全的,它授予完成任务所需的最少权限,通常,创建具有最小权限的策略和令牌将导致更多的策略定义。为了简化过程,前缀资源(prefix resources)规则可以应用于零到多个对象,这样创建令牌的过程会相对简单,但是权限的作用域半径会扩大。

发现所需的权限

在引导完ACL系统并使用令牌配置Consul代理后,用户还需要创建令牌来完成数据中心内的所有其他任务,包括注册服务等。
在发现令牌所需的最低权限之前,先要了解令牌的基本组成部分。规则(rule)对应一份特定的权限,是令牌的基本单位,不同的规则组合在一起形成策略(policy)。规则有两个主要部分:资源(resource)策略配置(policy disposition),资源是规则适用的对象,策略配置表示权限。下面的示例适用于任何名为web的服务,策略配置为读(read)权限:

service "web" {
    policy = "read"
}

为了发现特定操作所需的最低权限,我们给出三条建议:
首先,关注环境中需要保护的数据,确保敏感数据具有特定且受限制的策略,由于可以组合使用策略来创建令牌,因此通常会为敏感数据创建更多策略。敏感数据可以是键值存储(key-value store)中的特定应用程序或一组值。
其次,参考Consul文档中的规则(rule)页面和API页面,以了解任何给定操作所需的权限。
规则文档介绍了所有规则资源,对于启用了ACL的任何运行数据中心,以下四种资源类型都是至关重要的:

规则 描述
acl acl规则授予acl权限,包括创建、更新或查看令牌和策略
node、node_prefix node规则授予节点层注册的权限,包括向数据中心和目录添加代理
service、service_prefix service规则授予服务层注册的权限,包括向目录中添加服务
operator 授予数据中心操作的权限,包括与Raft进行交互

API页面上,每个端点都有一个对应包含所需ACL的表。如下图所示,节点健康检查端点需要对应节点nodeservice权限。

api-endpoint.png

最后,在生产中使用令牌之前,应测试它是否具有正确的权限,如果使用的令牌缺少权限,那么Consul将发出错误响应。

$ consul operator raft list-peers
Error getting peers: Failed to retrieve raft configuration:
Unexpected response code: 403 (rpc error making call: Permission denied)

根据数据的操作和类型,用户会看到404403

操作示例:排列显示Consul代理

要查看数据中心中的所有Consul代理,可以使用consul members命令行指令:

$ consul members
Node          Address              Status  Type    Build  Protocol  DC    Segment
server.one    18.220.91.175:8302   alive   server  1.6.0  2         dc1   <all>
client.one    18.223.155.113:8302  alive   client  1.6.0  2         dc1   <all>
client.two    18.220.91.175:8302   alive   client  1.6.0  2         dc1   <all>
client.three  18.223.155.113:8302  alive   client  1.6.0  2         dc1   <all>

为了使此命令可以成功执行,用户至少需要为数据中心的每个代理配置读权限。根据用户对应的威胁模型(threat model),可以配置以下任一策略。
如果用户的生产环境是动态的,并且规模很大,则可能需要考虑创建一种适用于所有代理策略,以减少运营团队的工作量:

agent_prefix "" {
  policy = "read"
  }

注意,""表示此规则应用于数据中心中的所有代理,针对上面的示例,此规则将适用于server.oneclient.oneclient.twoclient.three
如果用户的生产环境是静态的,并需要最大程度保证安全性,则需要为每个代理都创建一个单独的规则。每次将新代理添加到数据中心时,都需要创建一个新令牌。

agent "server.one" {
  policy = "read"
  }
agent "client.one" {
  policy = "read"
  }
agent "client.two" {
  policy = "read"
  }
agent "client.three" {
  policy = "read"
  }

安全访问控制:Operator-Only Access

最安全的访问控制策略是将使用acl="write"策略的令牌仅授予一到几个受信的运维人员,拥有acl="write策略令牌的人操作几乎不受限,因为他们可以使用其他资源和策略生成令牌,操作人员负责创建所有其他策略和令牌,对数据中心进行权限管控,我们称这种实现方式为operator-only实现,此实现类型最复杂,也最安全。
对于此控制策略,运维人员负责管理以下策略和令牌:

  • 服务(service)连接(connect proxy)注册
  • 意向(intention)管理
  • 代理(agent)管理
  • API命令行UI访问

Operator-Only Access 示例

在下面的示例中,运维人员保留服务令牌管理的责任,但将启用连接(Connect-enabled)的服务之间的访问控制委托给安全团队。
首先,运维人员会为安全团队单独创建一个对intention管理权限的令牌,开发人员将获得单独的令牌,他们能够注册自己的应用程序,而无需管理intention。安全团队令牌中策略对应的服务都必须包含intention配置策略。

service "wordpress" {
    policy = "read"
    intentions = "write"
  }

这使安全团队可以创建一个intention,使wordpress服务可以打开与上游mysql服务的新连接:

$ consul intention create wordpress mysql

default_policy配置为deny时,负责管理intention的安全团队需要可以创建intention,因为默认从ACL配置继承的权限为deny all
在实现中,开发人员负责为服务请求令牌,对于启用了连接的服务,运维人员需要创建一个策略,为该服务和代理提供权限,并为所有服务和节点提供权限,以发现其他上游依赖项。

service "mysql" {
  policy = "write"
}
service "mysql-sidecar-proxy" {
  policy = "write"
}
service_prefix "" {
    policy = "read"
}
node_prefix "" {
    policy = "read"
}

有了令牌,开发人员即可以使用register命令在Consul中注册服务。

$ consul services register mysql

上面的示例说明了如何在安全范围内创建策略。第一个示例,使用完全匹配资源规则是最安全的,它仅授予完成任务所需的最少权限,通常,创建最小权限的策略和令牌需要定义很多策略,但是这有助于保证最安全的环境。相反,前缀规则可以应用于0到多个对象。运维人员需要权限操作的复杂性与系统的安全性,这不仅适用于代理,也适用于其他所有规则。

操作数据中心所需的权限

下表是常见操作所需的最低权限,创建特定策略时,用户可以使用完全匹配规则

命令行指令 所需权限
consul reload agent_prefix "": policy = "write"
consul monitor agent_prefix "": policy = "read"
consul leave agent_prefix "": policy = "write"
consul members node_prefix "": policy = "read"
consul acl acl = "write"
consul catalog services service_prefix "": policy = "read"
consul catalog nodes node_prefix "": policy = "read"
consul services register service_prefix "": policy = "write"
consul services register (Connect proxy) service_prefix "": policy = "write", node_prefix "": policy = "read"
consul connect intention service_prefix "": intention = "write"
consul kv get key_prefix "": policy = "read"
consul kv put key_prefix "": policy = "write"

排除ACL系统故障

Consul提供了一套可靠的API,可用于检查数据中心的运行状况。 在本章节中,用户将了解几个Consul命令行指令,可用于排除令牌和策略问题的故障。 此外,用户还将了解在紧急情况下,如何对ACL系统进行重置。

先决条件

本章节中的内容假定现有的数据中心运行的Consul 1.4或者更高版本,这里还需要确保可以正常执行Consul命令行指令,可以是Consul代理,或者是远程控制数据中心的本地二进制文件。
本章节中的所有命令都需要一个有效的令牌,为方便起见,我们建议使用引导令牌,因为它没有权限限制。

Consul Members指令

在使用令牌配置代理时,用户可以使用consul members命令来检查它们是否具有加入数据中心所需的权限。
如果一个代理(服务端或客户端)没有在列表中出现,那么该代理上的ACL没有正确配置,或者令牌没有正确的权限,列表中仅显示有权限再目录中注册的代理。

$ consul members
Node      Address          Status    Type    Build  Protocol  DC   Segment
server-1  172.17.0.2:8301  alive     server  1.4.4  2         dc1  <all>
server-2  172.17.0.3:8301  alive     server  1.4.4  2         dc1  <all>
server-3  172.17.0.4:8301  alive     server  1.4.4  2         dc1  <all>

对列出的各个节点使用consul acl命令可帮助解决令牌权限问题。

Consul Catalog指令

consul catalog nodes -detailed指令会显示节点的详细信息,其中包括标记地址(TaggedAddresse),如果任一代理的标记地址为空,则该代理的ACL配置不正确。用户可以通过查看所有服务器上的Consul日志来进行调试,如果正确启用了ACL,则可以关注代理的令牌。

$ consul catalog nodes -detailed
Node      ID             Address        DC  TaggedAddresses
server-1  a82c7db3-fdc3  192.168.1.191  kc  lan=192.168.1.191, wan=192.168.1.190
server-2  a82c7db3-fdc3  192.168.1.192  kc  lan=192.168.1.192, wan=192.168.1.190
server-3  a82c7db3-fdc3  192.168.1.193  kc  lan=192.168.1.193, wan=192.168.1.190

对列出的各个节点使用consul acl命令可帮助解决令牌权限问题。

Consul ACL指令

一旦确认问题不是由于配置错误引起的,就可以使用以下命令来帮助解决令牌或策略问题。

Consul ACL Policy List 指令

consul acl policy list命令将输出所有可用策略。 用户首次设置令牌时,应使用执行此命令来确保策略列表及其规则符合预期。 在下面的示例中有两个策略; Consul创建的全局管理(global-management)策略,用户创建的名为server-one-policy的策略。

$ consul acl policy list
global-management:
   ID:           00000000-0000-0000-0000-000000000001
   Description:  Builtin Policy that grants unlimited access
   Datacenters:
server-one-policy:
   ID:           0bcee22c-6602-9dd6-b147-964958069426
   Description:  policy for server one
   Datacenters:

用户可以使用consul acl policy read -id <policy_id>指令来关注单个策略,在下面的示例中,server-one-policy策略有节点consul-server-one的写权限。

$ consul acl policy read -id 0bcee22c-6602-9dd6-b147-964958069426
ID:           0bcee22c-6602-9dd6-b147-964958069426
Name:         server-one-policy
Description:  policy for server one
Datacenters:
Rules:
node "consul-server-one" {
  policy = "write"
}
Consul ACL Token List指令

consul acl token list命令将列出所有令牌,应确保此列表仅包含正在使用的令牌,这对确保数据中心的安全性非常重要,应该经常检查它。由于令牌不会过期,因此运维人员应当删除未使用的令牌。
在下面的示例中,有三个令牌:第一个令牌是Consul在引导过程中创建的,通常称为引导令牌;第二个令牌是用户生成的,并且是通过server-one-policy策略创建的;第三个令牌也由Consul创建,但它没有任何权限。

$ consul acl token list
AccessorID:       cf827c04-fb7d-ea75-da64-84e1dd2d5dfe
Description:      Master Token
Local:            false
Create Time:      2019-05-20 11:08:27.253096 -0500 CDT
Legacy:           false
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

AccessorID:       5d3c3a03-e627-a749-444c-2984101190c0
Description:      token for server one
Local:            false
Create Time:      2019-10-17 11:46:27.106158 -0500 CDT
Legacy:           false
Policies:
   0bcee22c-6602-9dd6-b147-964958069426 - server-one-policy

AccessorID:       00000000-0000-0000-0000-000000000002
Description:      Anonymous Token
Local:            false
Create Time:      2019-05-20 11:08:27.253959 -0500 CDT
Legacy:           false

consul acl token read -id <token_id>命令用来提供指定令牌的信息,应确保令牌的权限是符合预期的,在下面的示例中,返回信息与consul acl token list命令相似:

$ consul acl token read -id 5d3c3a03-e627-a749-444c-2984101190c0
AccessorID:       5d3c3a03-e627-a749-444c-2984101190c0
SecretID:         547a969c-5dff-f9a8-6b84-fb1d23f9a5cb
Description:      token for server one
Local:            false
Create Time:      2019-10-17 11:46:27.106158 -0500 CDT
Policies:
   0bcee22c-6602-9dd6-b147-964958069426 - server-one-policy

当使用consul catalog指令或consul members指令返回意外结果时,先使用consul acl token read指令。

重置ACL系统

如果遇到无法解决的问题,或者引导令牌放错了位置,用户可以通过更新索引来重置ACL系统。 首先,通过crul任何节点上的/v1/status/leader端点来找到领导者,ACL重置操作必须在领导者机器上运行。

$ curl 172.17.0.1:8500/v1/status/leader
"172.17.0.3:8300"%

在此示例中,领导者的IP地址为172.17.0.3,我们在该服务器执行如下命令,重新运行bootstrap命令获取索引号:

$ consul acl bootstrap
Failed ACL bootstrapping: Unexpected response code: 403 (Permission denied: ACL bootstrap no longer allowed (reset index: 13))

将重置索引写入重置文件,此处重置索引为13

$ echo 13 >> <data-directory>/acl-bootstrap-reset

重置ACL系统后,用户可以重新创建引导令牌。

DNS缓存

DNS是Consul的主要接口之一,使用DNS可以将Consul集成到现有基础架构之中,而无需进行任何高度集成(high-touch integration)
默认情况下,Consul返回DNS结果,TTL值为0,这样可以防止任何缓存。这样做的好处是,每次查询DNS都会重新计算,因此可以得到最及时的信息;但是,这增加了每次查询的延迟,并可能耗尽群集的查询吞吐量。Consul提供了许多可调参数,用来自定义DNS查询的处理方式。
在本章节中,我们将讨论一些参数,用于调整非及时读取(stale reads)负响应缓存(negative response caching)TTL值,所有DNS配置都必须在代理的配置文件集合中设置。

非及时读取(stale reads)

非及时读取可用于减少延迟并增加DNS查询的吞吐,用于控制DNS查询非及时读取的设置为:

  • dns_config.allow_statle:设置为true,激活非及时读取;
  • dns_config.max_statle:过时数据的最大时间。

通过这两个设置,用户可以启用或禁止非及时读取,下面我们将讨论两者的优缺点。

允许非及时读取

Consul0.7.1开始,allow_stale参数默认情况下处于启用状态,max_stale默认阈值为大致10年,使用这种配置,即使Consul集群长期没有领导者,也可以继续提供DNS查询服务。

{
  "dns_config": {
    "allow_stale": true,
    "max_stale": "87600h"
  }
}

注意,上述的示例是默认配置,用户无需显式配置它。
启用非及时读取机制时,所有Consul服务都可以提供查询服务,只是非领导节点的返回值可能是过期数据。如果对数据的及时性有足够容忍度,用户就可以水平扩展伸缩性,所有Consul服务器都能处理请求,增加Consul集群的服务器数量即能提高吞吐量。

防止非及时读取

如果用户想要读取及时数据,或者限制数据的过期时间,可以将allow_stale设置为false,或者将max_stale阈值调低。当把allow_stale设置为false时,所有的读取服务都由单个领导者节点提供,读取服务的结果将保持高度一致,但是会受到单个节点吞吐量的限制。

{
  "dns_config": {
    "allow_stale": false
  }
}

负响应缓存(negative response caching)

一些DNS客户端会缓存负响应(negative responses)结果,即有些服务本身是存在,但是没有健康检查端点,Consul返回了not found样式,这样会导致服务长时间不可用,类似宕机状态。

配置SOA

Consul1.3.0及更高的版本中,用户可以调整SOA响应配置来修改分析器的负TTL缓存,具体的配置为soa.min_ttl属性:

{
  "dns_config": {
    "soa": {
      "min_ttl": 60
    }
  }
}

一个常见的示例是Windows将负响应缓存时间默认设为15分钟,DNS转发器(DNS forwarders)也会缓存负响应,效果相同。为了避免上述问题的发生,用户需要检查客户端与Consul通路上任何操作系统和DNS转发器的默认缓存值,并适当修改、通常情况下,简单的做法就是在服务恢复可用前,先关闭负响应缓存配置。

TTL值

用户可以设置TTL值将DNS结果缓存在Consul下游,较高的TTL值会减少查询Consul服务的次数,并加速客户端查询速度,但结果会越来越陈旧。默认情况下,TTL设置为0,不会有任何缓存。

{
  "dns_config": {
    "service_ttl": {
      "*": "0s"
    },
    "node_ttl": "0s"
  }
}
启用缓存

为了启用查询节点的缓存(例如foo.node.consul),我们可以设置dns_config.node_ttl,当设置为10s时,所有节点提供10秒TTL缓存时间。
用户还可以以更细的粒度指定服务的TTL时间。默认情况下,用户可以使用通配符TTL,为所有服务设置TTL;前缀匹配的优先级低于严格匹配,例如my-service-x优先级高于my-service- *。执行通配符匹配时,会优先匹配最长路径,my-service- *优先级高于my-***为默认的匹配值,如果找不到任何匹配项,那么TTL值为0。
TTL配置大致如下所示:

{
  "dns_config": {
    "service_ttl": {
      "*": "5s",
      "web": "30s",
      "db*": "10s",
      "db-master": "3s"
    }
  }
}
预查询

预查询(prepared queries)提供了对TTL的额外控制,它们可以与查询语句一起定义TTL,在更新查询定义时可以及时更改。如果预查询未配置TTL,则会使用TTL特定配置,如果未配置任何TTL,则TTL默认值为0。

转发DNS

默认情况下,系统通过53端口提供DNS服务,大多数操作系统都需要提升权限。我们可以使用DNS服务器或端口重定向将查询请求转发到非特权端口上运行的Consul,而不是使用root账户运行Consul。
在本章节中,我们将从以下几个方面演示转发:

  • BIND
  • dnsmasq
  • Unbound
  • systemd-resolved
  • iptables
  • maxOS

配置转发后,我们将演示如何测试配置,最后我们还将提供一些故障指南。

BIND 设置

注意,在此示例中,BIND和Consul运行在同一台计算机上,首先用户必须禁用DNSSEC,这样Consul和BIND可以互相通信,配置示例如下:

options {
  listen-on port 53 { 127.0.0.1; };
  listen-on-v6 port 53 { ::1; };
  directory       "/var/named";
  dump-file       "/var/named/data/cache_dump.db";
  statistics-file "/var/named/data/named_stats.txt";
  memstatistics-file "/var/named/data/named_mem_stats.txt";
  allow-query     { localhost; };
  recursion yes;

  dnssec-enable no;
  dnssec-validation no;

  /* Path to ISC DLV key */
  bindkeys-file "/etc/named.iscdlv.key";

  managed-keys-directory "/var/named/dynamic";
};

include "/etc/named/consul.conf";
区域(Zone)文件

然后在consul.conf文件中为Consul管理记录(managed records)配置一个区域(zone)

zone "consul" IN {
  type forward;
  forward only;
  forwarders { 127.0.0.1 port 8600; };
};

这里我们假设Consul使用默认配置运行,并在8600端口提供DNS服务。

Dnsmasq 设置

Dnsmasq通常通过dnsmasq.conf/etc/dnsmasq.d目录中的一系列配置文件来配置:

# Enable forward lookup of the 'consul' domain:
server=/consul/127.0.0.1#8600

# Uncomment and modify as appropriate to enable reverse DNS lookups for
# common netblocks found in RFC 1918, 5735, and 6598:
#rev-server=0.0.0.0/8,127.0.0.1#8600
#rev-server=10.0.0.0/8,127.0.0.1#8600
#rev-server=100.64.0.0/10,127.0.0.1#8600
#rev-server=127.0.0.1/8,127.0.0.1#8600
#rev-server=169.254.0.0/16,127.0.0.1#8600
#rev-server=172.16.0.0/12,127.0.0.1#8600
#rev-server=192.168.0.0/16,127.0.0.1#8600
#rev-server=224.0.0.0/4,127.0.0.1#8600
#rev-server=240.0.0.0/4,127.0.0.1#8600

创建完配置后,重新启动dnsmasq服务,dnsmasq中还有一些重要配置:

# Accept DNS queries only from hosts whose address is on a local subnet.
#local-service

# Don't poll /etc/resolv.conf for changes.
#no-poll

# Don't read /etc/resolv.conf. Get upstream servers only from the command
# line or the dnsmasq configuration file (see the "server" directive below).
#no-resolv

# Specify IP address(es) of other DNS servers for queries not handled
# directly by consul. There is normally one 'server' entry set for every
# 'nameserver' parameter found in '/etc/resolv.conf'. See dnsmasq(8)'s
# 'server' configuration option for details.
#server=1.2.3.4
#server=208.67.222.222
#server=8.8.8.8

# Set the size of dnsmasq's cache. The default is 150 names. Setting the
# cache size to zero disables caching.
#cache-size=65536

Unbound 设置

我们通常通过unbound.conf/etc/unbound/unbound.conf.d目录中一系列文件来配置Unbound

#Allow insecure queries to local resolvers
server:
  do-not-query-localhost: no
  domain-insecure: "consul"

#Add consul as a stub-zone
stub-zone:
  name: "consul"
  stub-addr: 127.0.0.1@8600

用户必须在/etc/unbound/unbound.conf文件底部添加以下内容,才能包含新配置:

include: "/etc/unbound/unbound.conf.d/*.conf"

systemd-resolved 设置

systemd-resolved通常使用/etc/systemd/resolved.conf配置,在resolved.conf文件中添加以下内容:

DNS=127.0.0.1
Domains=~consul

这个配置的主要限制是DNS字段不能包含端口,要使此功能生效,必须将Consul配置为监听53端口,而不是8600端口,或者可以使用iptables命令将53端口映射到8600,指令如下:

[root@localhost ~]# iptables -t nat -A OUTPUT -d localhost -p udp -m udp --dport 53 -j REDIRECT --to-ports 8600
[root@localhost ~]# iptables -t nat -A OUTPUT -d localhost -p tcp -m tcp --dport 53 -j REDIRECT --to-ports 8600

绑定53端口需要以特权用户身份运行,如果用户使用Consul docker镜像,则需要在环境变量中增加CONSUL_ALLOW_PRIVILEGED_PORTS=yes,允许Consul使用端口。

iptables设置

设置iptables时,iptables规则必须与Consul实例设置在同一主机上,中继机(relay hosts)不应位于同一主机上,否则重定向会拦截流量。
在支持配置iptables的Linux系统上,传入的请求和对本地主机的请求可以使用iptables转发到同一台主机上的其他端口,而无需其他辅助服务。由于Consul默认情况下仅解析.consul TLD,因此,如果希望iptables还能解析其他域名,如何使用recursors选项尤为重要。recursors不应包含local host,否则重定向只会拦截该请求。
iptables方法适用于以下场景,在系统中已经有外部的DNS服务当做recursor使用或者将现有的DNS服务当做查询端点,将consul域名下的请求转发到Consul服务器。在这两种情况下,用户需要在不增加Consul主机开销的情况下,查询Consul服务。

[root@localhost ~]# iptables -t nat -A PREROUTING -p udp -m udp --dport 53 -j REDIRECT --to-ports 8600
[root@localhost ~]# iptables -t nat -A PREROUTING -p tcp -m tcp --dport 53 -j REDIRECT --to-ports 8600
[root@localhost ~]# iptables -t nat -A OUTPUT -d localhost -p udp -m udp --dport 53 -j REDIRECT --to-ports 8600
[root@localhost ~]# iptables -t nat -A OUTPUT -d localhost -p tcp -m tcp --dport 53 -j REDIRECT --to-ports 8600

macOS 设置

在macOS系统上,用户可以使用macOS系统解析器将所有.consul请求指向Consul。用户只需在/etc/resolver/中添加一个解析器条目指向Consul。该功能的文档可通过man5 resolver命令获得。创建一个新文件/etc/resolver/consul(用户需要使用sudo /root权限),并将下述内容写入文件:

nameserver 127.0.0.1
port 8600

这告诉macOS解析程序所有的.consul TLD请求都转发到127.0.0.18600端口。

测试

首先对Consul执行DNS查询,确保记录存在:

[root@localhost ~]# dig @localhost -p 8600 primary.redis.service.dc-1.consul. A

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.32.amzn1 <<>> @localhost primary.redis.service.dc-1.consul. A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11536
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;primary.redis.service.dc-1.consul. IN A

;; ANSWER SECTION:
primary.redis.service.dc-1.consul. 0 IN A 172.31.3.234

;; Query time: 4 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Wed Apr  9 17:36:12 2014
;; MSG SIZE  rcvd: 76

然后对BIND实例执行相同的查询,确保获得相同的结果:

[root@localhost ~]# dig @localhost -p 53 primary.redis.service.dc-1.consul. A

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.32.amzn1 <<>> @localhost primary.redis.service.dc-1.consul. A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11536
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;primary.redis.service.dc-1.consul. IN A

;; ANSWER SECTION:
primary.redis.service.dc-1.consul. 0 IN A 172.31.3.234

;; Query time: 4 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Wed Apr  9 17:36:12 2014
;; MSG SIZE  rcvd: 76

如果需要的话,使用相同的方法验证反向DNS(reverse DNS)

[root@localhost ~]# dig @127.0.0.1 -p 8600 133.139.16.172.in-addr.arpa. PTR

; <<>> DiG 9.10.3-P3 <<>> @127.0.0.1 -p 8600 133.139.16.172.in-addr.arpa. PTR
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3713
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;133.139.16.172.in-addr.arpa.   IN  PTR

;; ANSWER SECTION:
133.139.16.172.in-addr.arpa. 0  IN  PTR consul1.node.dc1.consul.

;; Query time: 3 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Sun Jan 31 04:25:39 UTC 2016
;; MSG SIZE  rcvd: 109
[root@localhost ~]# dig @127.0.0.1 +short -x 172.16.139.133
consul1.node.dc1.consul.

故障排除

如果用户没从DNS服务器(如BINDDnsmasq)处获得答案,那么也可以从Consul处获得答案,但最好的选择还是打开DNS服务器的查询日志了解详情,对于BIND

[root@localhost ~]# rndc querylog
[root@localhost ~]# tail -f /var/log/messages

日志可能会显示如下错误:

error (no valid RRSIG) resolving
error (no valid DS) resolving

这表示没有正确禁用DNSSEC.
如果看到有关网络连接的错误,可以验证运行BINDConsul的服务器之间是否有防火墙路由问题。
对于Dnsmasq,需要日志查询配置项和USR1信号。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容