[小团队自动化](二) Drone CI使用Vault做为凭据存储 —— 打造本身的CI/CD工做流

写在前边html

这一篇文章是基于 Gitea+Drone CI+Vault 打造属于本身的CI/CD工做流系列文章第二篇,让咱们一块儿来完成 dronevault 的搭配使用,这篇主要讲 vault 的部署和使用,以及怎么经过 drone 来使用 vaultnode

上一篇文章(一) Drone CI For Github —— 打造本身的CI/CD工做流咱们一块儿了解了,Drone 的部署和使用,一块儿感觉了 Drone的简单强大的功能带来的方便和快捷。在实际的应用中,咱们会有不少的敏感数据,Drone 自身的 Secret 是以仓库为单位进行管理,并且也没有严格独立的权限控制,实际上是不太方便的。mysql

带着这样的疑惑,我再次向@Dee luo老哥寻求帮助,老哥掏了掏肚子上的口袋,取出一本武林秘籍,曰:“小伙子,我这里有一个色情网站记录在上面,你看看”。我打开只看到 www.vaultproject.io 一行小字赫然记录在内,我火烧眉毛的打开,美滋滋的看了起来。nginx

此处省略一万个字...git

另外强烈推荐YUHAO的博客 私密信息管理利器 HashiCorp Vault系列文章github

好了,如今我已经知道该作什么了。web

说干就干,开始搞事。sql

了解Vault


Vault是一个管理Secrets并保护敏感数据的工具,来自HashiCorp,若是你对这个名字有点陌生,那么你必定知道Vagrantdocker

Vault是一种安全访问 Secret 的工具。Secret就是您要严格控制访问的任何内容,例如API密钥,密码或证书。Vault为任何机密提供统一的界面,同时提供严格的访问控制并记录详细的审计日志。数据库

现代系统须要访问大量Secret:数据库凭证,外部服务的API密钥,面向服务的体系结构通讯的凭证等。了解谁正在访问哪些秘密已经很是困难且特定于平台。若是没有自定义解决方案,几乎不可能添加密钥滚动,安全存储和详细的审计日志。这是Vault介入的地方。

Vault的主要功能包括:

  • 安全秘密存储:任意密钥/值秘密能够存储在Vault中。Vault会在将这些机密写入持久存储以前加密这些机密,所以获取对原始存储的访问权限不足以访问您的机密。Vault能够写入磁盘,Consul等。
  • 动态秘密:Vault能够按需为某些系统生成机密,例如AWS或SQL数据库。例如,当应用程序须要访问S3存储桶时,它会要求Vault提供凭据,Vault将根据须要生成具备有效权限的AWS密钥对。建立这些动态机密后,Vault也会在租约到期后自动撤消它们。
  • 数据加密:Vault能够加密和解密数据而无需存储数据。这容许安全团队定义加密参数,并容许开发人员将加密数据存储在SQL等位置,而无需设计本身的加密方法。
  • 租赁和续订:Vault中的全部机密都有与之相关的租约。在租约结束时,Vault将自动撤销该秘密。客户能够经过内置续订API续订租约。
  • 撤销:Vault内置了对秘密撤销的支持。保险柜不只能够撤销单个秘密,还能够撤销秘密树,例如特定用户读取的全部秘密,或特定类型的全部秘密。撤销有助于关键滚动以及在入侵状况下锁定系统。

上边都是废话,这里说一下我本身的体验。

首先我没有体验的很深,我如今只是体验了 key/value 结构和 database,而后看了一下 ACL Policies 管理权限。

  • 权限控制很严格,机制也很灵活和安全
  • UI美观,操做简单,不过UI功能也简单
  • CLI 操做容易理解,文档齐全。
  • 安全确实很放心,token定时刷新,能够经过token来获取数据库的配置,UI的激活须要输入key,容器重启也须要从新输入key来激活。

部署 vault


这里并非单独部署vault,而是参考上一篇文章来结合vault使用,在上一篇文章的docker-compose.yml 基础上,加入 vault 的容器,并为 vault 提供 web服务.

为了区分,如下代码块中,用 $ 开头表示宿主机命令,用 / # 开头表示容器中的命令

编写模板文件

...
 vault:
 image: vault:latest
 container_name: vault
 restart: always
 networks:
 - dronenet
 volumes:
 - ./vault/file:/vault/file
 - ./vault/config:/vault/config
 - ./vault/logs:/vault/logs
 cap_add:
 - IPC_LOCK
 environment:
 - VAULT_ADDR=http://127.0.0.1:8200
 command: vault server -config=/vault/config/local.json #这句很是重要,必定要替换原有的Dockerfile中的CMD,否则会自动初始化,生成的数据都在docker logs中,不说你确定找不到。dog.jpg,因此我选择手动初始化
...
复制代码

配置 vault

事先准备好一个数据库和对该数据库具备访问权限的数据库帐号

参考

  • database : vault
  • name : vault
  • password : vault123456

样例中的配置请事先阅读文档知晓含义

参考Vault Configuration Vault 的配置文件是HCL或者 json,这里我使用 json

HCL to json 请参考HCL

  1. 初始化 vault
$ mkdir -p vault/config
$ mkdir -p vault/file
$ mkdir -p vault/logs
$ vim vault/config/local.json
{
    "ui": true,
    "storage": {
        "mysql": {
            "address": "mysql:3306",
            "username": "vault",
            "password": "vault123456"
        }
    },
    "listener": {
        "tcp": {
            "address": "0.0.0.0:8200",
            "tls_disable": 1
        }
    },
    "backend": {
        "file": {
            "path": "/vault/file"
        }
    },
    "log_level": "Debug",       #调试阶段建议开启Debug
    "default_lease_ttl": "168h",
    "max_lease_ttl": "720h"
}
$ docker-compose up -d
$ docker-compose exec vault ash
/ # vault operator init
Unseal Key 1: cz/cSHqUep5IBQjtDWBgFnN+G02hLFh8s/19rPxKjxCe	#记下来
Unseal Key 2: o+BRjfy64sUKLTWKV0jV+JjvKZWd0R3ibBR7IUbCn8sB	#记下来
Unseal Key 3: xiJF+XI8gF1PWGMvOYhy0go16x2VgZdAVKw/xBIVGeo7	#记下来
Unseal Key 4: xd/H1hBdPGwm2qchkShgzGbVtWWHeeCv8S1RyYg34yKi	#记下来
Unseal Key 5: 4OOHfxxwuX7Hz40E/bHJLbkwLLWeZkWnz7/pmdtgm7mn	#记下来

Initial Root Token: s.RIeC53WBWizfl0OXVbDYuxbh   #记下来

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated master key. Without at least 3 key to
reconstruct the master key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.
复制代码

初始化动做为咱们生成了 5 个 Unseal key,此外还有默认的 Root Token。因此应该立刻把这些信息记录到安全的地方,由于之后你是没有办法再看到它们的。

  1. 编辑nginx,为vault提供web服务

编辑完记得重启nginx服务

server {
    listen       80;
    server_name vault.yiranzai.top;
    location / {
        proxy_pass http://vault:8200;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}
复制代码
  1. 访问 vault.yiranzai.top

Vault 对于数据保护是很是重视的。服务器启动后,并不可以立刻访问其数据,而必须通过一个解封(Unseal)的动做。

依次输入上边获得的五个Unseal Key中的三个来解封服务,而后使用 Root Token 登陆,登陆后如图,右侧会有教程,先跟着作一边熟悉熟悉。

  1. 建立一个Secrets Engines

这个东西,怎么说呢,不太好理解。字面意思就是 秘密引擎 ,是存储,生成或加密数据的一个组件,能够理解为一个数据集,也能够理解为一种数据渠道。只有 Secrets Engines 存在,数据才能被指定方式存储。

完整的支持和介绍请看这里

  • Generic
    • KV (这里只示范这种)
    • PKI Certificates
    • SSH
    • Transit
    • TOTP
  • Cloud
    • Active Directory
    • AliCloud
    • AWS
    • Azure
    • Google Cloud
    • Google Cloud KMS
  • Infra
    • Consul
    • Databases (支持多种驱动)
    • Nomad
    • RabbitMQ

  1. 建立一个 ACL Policy

建立一个 ACL Policy,配置为仅对刚刚建立的 Secrets Engine ——dronetest 有可读和列表权限。 ACL Policy 的配置格式为HCL,HCL 是 HashiCorp 创造的、专门用于配置文件的语言格式)

# Allow tokens to look up their own properties
path "auth/token/lookup-self" {
    capabilities = ["read"]
}
# Allow tokens to renew themselves
path "auth/token/renew-self" {
    capabilities = ["update"]
}
# Allow tokens to revoke themselves
path "auth/token/revoke-self" {
    capabilities = ["update"]
}
# Allow a token to look up its own capabilities on a path
path "sys/capabilities-self" {
    capabilities = ["update"]
}
path "dronetest" {
    capabilities = ["read", "list"]
}
path "dronetest/*" {
    capabilities = ["read", "list"]
}
复制代码

至此,咱们的 Vault 就配置好了,接下来让咱们建立 secret

建立 Secret

上面咱们简单配置了一下 Vault ,如今让咱们建立 Secret

首先咱们明白的是dronetest(Policy)dronetest(Secrets Engine) 只有可读和列表的权限。

  1. 登陆

回到 vault 容器中,使用 Root token 登陆

/ # vault login s.RIeC53WBWizfl0OXVbDYuxbh
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                s.RIeC53WBWizfl0OXVbDYuxbh
token_accessor       5JoYSxx4CqR10mmeG7HusChz
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]
复制代码
  1. 建立一个用于验证的 token

这个token应当只对 dronetest(Secrets Engine) 可读,因此咱们为他分配一个 dronetest(Policy)

/ # vault token create -policy=dronetest -no-default-policy
Key                  Value
---                  -----
token                s.mx4KQycrFAfiaHIuPnNLhFCy    #记下来很是重要
token_accessor       8rzot4pDJvfX0JTi2ImYLvg4
token_duration       168h
token_renewable      true
token_policies       ["dronetest"]
identity_policies    []
policies             ["dronetest"]
复制代码
  1. 写入 Secret

如图所示,这里咱们为 web 建立了两对KV

接下来尝试用命令建立

/ # vault kv put dronetest/test v=k
Key              Value
---              -----
created_time     2019-02-26T03:53:02.240676864Z
deletion_time    n/a
destroyed        false
version          1
/ # vault kv get dronetest/test
====== Metadata ======
Key              Value
---              -----
created_time     2019-02-26T03:53:02.240676864Z
deletion_time    n/a
destroyed        false
version          1

== Data ==
Key    Value
---    -----
v      k
/ # vault kv put dronetest/test a=b
Key              Value
---              -----
created_time     2019-02-26T03:53:31.073689586Z
deletion_time    n/a
destroyed        false
version          2
/ # vault kv get dronetest/test
====== Metadata ======
Key              Value
---              -----
created_time     2019-02-26T03:53:31.073689586Z
deletion_time    n/a
destroyed        false
version          2

== Data ==
Key    Value
---    -----
a      b
/ # curl \
> --header "X-Vault-Token: s.RIeC53WBWizfl0OXVbDYuxbh" \
> http://127.0.0.1:8200/v1/dronetest/data/test
{"request_id":"f5ce7d8f-cc60-ec79-20a5-5875c5e4362c","lease_id":"","renewable":false,"lease_duration":0,"data":{"data":{"a":"b"},"metadata":{"created_time":"2019-02-26T03:53:31.073689586Z","deletion_time":"","destroyed":false,"version":2}},"wrap_info":null,"warnings":null,"auth":null}
复制代码

能够看出后者覆盖了前者,注意 version 增长了1vault 为每一个 path 每次写入的数据都定义为了一个新的版本,这样就不难理解,这里为何是后者覆盖了前者。

Drone 中使用 Vault


以上咱们了解了 Vault 简单的部署和使用,如今咱们一块儿了解怎么在 Drone 中使用 Vault

编辑Docker-compose.yml

结合上一篇文章(一) Drone CI For Github —— 打造本身的CI/CD工做流,咱们将 Vault 加入进来

而后咱们还须要 drone-vault 这个插件来实现 DroneVault 之间的通讯中转

参考

version: "3.7"
services:
 nginx:
 image: nginx:alpine
 container_name: dronetest_nginx
 ports:
 - "80:80"
 restart: always
 networks:
 - dronenet
 mysql:
 image: mysql:5.7
 restart: always
 container_name: dronetest_mysql
 environment:
 - MYSQL_ROOT_PASSWORD=root_password
 - MYSQL_DATABASE=drone
 - MYSQL_USER=drone
 - MYSQL_PASSWORD=drone_password
 networks:
 - dronenet
 volumes:
 - /path/to/conf/my.cnf:/etc/mysql/my.cnf:rw
 - /path/to/data:/var/lib/mysql/:rw
 - /path/to/logs:/var/log/mysql/:rw
 vault:
 image: vault:latest
 container_name: vault
 restart: always
 networks:
 - dronenet
 volumes:
 - ./vault/file:/vault/file
 - ./vault/config:/vault/config
 - ./vault/logs:/vault/logs
 cap_add:
 - IPC_LOCK
 environment:
 - VAULT_ADDR=http://127.0.0.1:8200
 command: vault server -config=/vault/config/local.json #这句很是重要,必定要替换原有的Dockerfile中的CMD,否则会自动初始化,生成的数据都在docker logs中,不说你确定找不到。dog.jpg,因此我选择手动初始化
 drone-server:
 image: drone/drone:1.0.0-rc.5 #不要用latest,latest并不是稳定版本
 container_name: dronetest_server
 networks: 
 - dronenet
 volumes:
 - ${DRONE_DATA}:/var/lib/drone/:rw
 - /var/run/docker.sock:/var/run/docker.sock:rw
 restart: always
 environment:
 - DRONE_DEBUG=true
 - DRONE_DATABASE_DATASOURCE=drone:drone_password@tcp(dronetest_mysql:3306)/drone?parseTime=true   #mysql配置,要与上边mysql容器中的配置一致
 - DRONE_DATABASE_DRIVER=mysql
 - DRONE_GITHUB_SERVER=https://github.com
 - DRONE_GITHUB_CLIENT_ID=${Your-Github-Client-Id}  #Github Client ID
 - DRONE_GITHUB_CLIENT_SECRET=${Your-Github-Client-Secret} #Github Client Secret
 - DRONE_RUNNER_CAPACITY=2
 - DRONE_RPC_SECRET=YOU_KEY_ALQU2M0KdptXUdTPKcEw  #RPC秘钥
 - DRONE_SERVER_PROTO=http			#这个配置决定了你激活时仓库中的webhook地址的proto
 - DRONE_SERVER_HOST=dronetest.yiranzai.top
 - DRONE_USER_CREATE=username:yiranzai,admin:true  #管理员帐号,通常是你github用户名
 drone-vault:
 image: drone/vault
 container_name: dronetest_vault
 restart: always
 networks:
 - dronenet
 environment:
 - SECRET_KEY=7890bcce69bb685a9a424767fe9d1be1	 #和drone-agent通讯的加密
 - DEBUG=true									
 - VAULT_ADDR=http://vault:8200
 - VAULT_TOKEN_RENEWAL=84h
 - VAULT_TOKEN_TTL=168h
 - VAULT_TOKEN=s.mx4KQycrFAfiaHIuPnNLhFCy		#这里不要用root token,用上边生成的只读token
 drone-agent:
 image: drone/agent:1.0.0-rc.5
 container_name: dronetest_agent
 restart: always
 networks:
 - dronenet
 depends_on:
 - drone-server
 volumes:
 - /var/run/docker.sock:/var/run/docker.sock:rw
 environment:
 - DRONE_SECRET_SECRET=7890bcce69bb685a9a424767fe9d1be1
 - DRONE_SECRET_ENDPOINT=http://dronetest_vault:3000
 - DRONE_RPC_SERVER=http://dronetest_server
 - DRONE_RPC_SECRET=YOU_KEY_ALQU2M0KdptXUdTPKcEw
 - DRONE_DEBUG=true
 - DRONE_LOGS_DEBUG=true
 - DRONE_LOGS_PRETTY=true
 - DRONE_LOGS_NOCOLOR=false
networks:
 dronenet:
复制代码

改写仓库中的 .drone.yml

这里的 .drone.yml 与上一篇文章中的相似,没有太大改动。

参考

  1. dronetest 中的 web 建立一个新版本

私钥中的换行符须要替换成\n粘贴进来

{
    "port":"22",
    "name":"yiranzai",
    "host":"dronetest.yiranzai.top",
    "deploy_path":"/path/to/web",
    "rsa":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAu1uWokQH6tuExm1RcFTJj2F6b8TXUkAzpAywVFQuyIepQwVG\nfl+Y1iD8YrqB31ZDjJk3DbC6DYGsecJILpBKDJ6T1M4UPKF6m0DH+O4bb33rhBy/\nY/zeN2jOygKTfTWpxrs13ZLvsJzNsH7rs6P+K3c+2heAhrYlyzeXTO/VbqCAsjMp\nsDMRjCDEp7jxNeEOdGM/4gIxpatDkVSIbgCj1jkYJJ3C0ipS3KvI2A1Lic8Vjr4V\nxtRK82r4aa1qeWunUFJWaO1O/V11l63lixfrr12QDgRyAkC/GCVOso6+NRP+DfGj\nhGQ+XziBZRdaEkQwv4kI717l4ToFDvUj1G8K1QIDAQABAoIBADx0q/sIT0DYkpXb\nMPUw7W7361We6gBAXiPMTvot0pGeXeYSCiBpL/g40vwBvB47TzM8olcKDzqRAgKO\nn/IZXzNn2qLN4emT482p34b1O/AWtNFy7h4Le1SBernQULT1mQtIgt/rBMB2FzZ7\n//p4q0x3ZXKkRi+i/NTayi/pzW6DTxYreIek+FSIPPJ3Kc1tsooD8greiEYo3wef\nbzV17YIvEry3RRRKYcR/tjS/oWOKdG1YzxsiPVALrZtgHS3KcDCYRltQoALGfMjl\nL4iicfTe2Jlr36CvdH0yqFOsRPH0eh4dC1sFKBzHFqcccnNfnhOGgwCbj/zDEuX+\nDrUUnqECgYEA3NqvHSAg3ULF0Y+emRXDDLbLmBDsIX+7ygQeYcwC1EVVf7F2z+Tw\ntYTjTNu4HzXAHVQ3WcB4K0fXEAJuK0LtORpHPPxfMse6vURSOTubBqhp6+H33TTq\nJa0ph1vLvGp2G1sJHQ+QrwZw+SyJ2LtiSYlMTIf617DDYAc+iWoBc10CgYEA2XZ8\nfRy20YMb01znorEG7WQ/vErtasAJbN122M/e6a1FPMmDdiTuYvA4Q0xylDXBgwZ/\nXxBFc1fs6QI+Vks3J6lzAgJ+XbX3gtoPxTFRJu4yZjCmhxLOpr1zzR8eWWrJ3aA3\nIS2UmQhr8mGPPnQbc7OeLTUv+7ZokDElZmMpxekCgYB1ZPi4Lp/JfPjRz3mp3dt0\nIqZOCpC1rcAQPeg4a80FMGWmHprdHwCkPCLmc3SHIncgH+sMjjZSKzmyFNiivkyC\nkelUDYI813XnTS23pmtdOqAy3kmPD3V2eXkd0D6XxK3LEzTg8akin/XlPTt4rQIt\nvIGGHLHN/jOcE722JVboMQKBgQCFOyqaHHWFdyYdINZpvrvXxYum+ODsfitIH4co\n3nJcCGRbEbsRLx8+Tp6p3LR2SVj3xYVT4MwsFrp3J4C1re8R1ac4m/1/u3ShHqh6\nz/RAPb3zDGt6ZfNmBVk3WstlTR/e3QV+xkY8XASGw27XfJs1D37hI6z6Mo3tiC61\nxBdbwQKBgHMDXneApZOsByrm0fvrOdIeW247kkmO6jLrI6QI/mR70gjODKXApLbU\nAbyglSi/i6Ewp+Au3+2mMvHFGc8iRkh0pwEo+xMKqPUAZEnmCbo3mKSjnR/vE3Pa\nO1frNKNLFff6kMh0ufbO3YIixYHCNJO6j/k1GmkkECSTMMka1tig\n-----END RSA PRIVATE KEY-----"
}
复制代码
  1. 编辑 .drone.yml
---
kind: pipeline
name: drone

workspace:
 base: /app
 path: git/drone

steps:
 - name: build
 image: node:alpine
 volumes:
 - name: webroot
 path: /wwwroot
 commands:
 - /bin/sh bash.sh
 environment:
 host:
 from_secret: host
 port:
 from_secret: port
 abc: abctest
 - name: deploy
 image: appleboy/drone-scp
 when:
 status:
 - success
 settings:
 host:
 from_secret: host
 port:
 from_secret: port
 key:
 from_secret: rsa
 username:
 from_secret: username
 target:
 from_secret: deploy_path
 source: ./*

volumes:
 - name: webroot
 host:
 path: /opt
 - name: cache
 host:
 path: /tmp/cache

trigger:
 branch:
 - master
 event:
 - push
    
---
kind: secret

external_data:
 host:
 path: dronetest/data/web
 name: host
 username:
 path: dronetest/data/web
 name: name
 port:
 path: dronetest/data/web
 name: port
 deploy_path:
 path: dronetest/data/web
 name: deploy_path
 rsa:
 path: dronetest/data/web
 name: rsa
复制代码
  1. 提交修改
git add .;git commit -m 'init test 4';
git push origin master

复制代码
  1. 查看 DroneACTIVITY FEED

若是成功就会以下图所示

ACTIVITY FEED

  1. 去服务器检查一下

    看到 bash.sh.drone.yml 都被上传到这里(只是测试,不是真的让你这么干)

$ pwd
/home/www
$ ll -a
total 40
drwx------  4 www  www  4096 Feb 20 04:23 .
drwxr-xr-x. 4 root root 4096 Feb 20 03:55 ..
-rw-------  1 www  www    61 Feb 19 03:00 .bash_history
-rw-r--r--  1 www  www    18 Oct 30 13:07 .bash_logout
-rw-r--r--  1 www  www   193 Oct 30 13:07 .bash_profile
-rw-r--r--  1 www  www   231 Oct 30 13:07 .bashrc
-rw-r--r--  1 www  www    35 Feb 20 04:23 bash.sh
-rw-r--r--  1 www  www   812 Feb 20 04:23 .drone.yml
drwxr-xr-x  8 www  www  4096 Feb 20 04:23 .git
drwxr-xr-x  2 www  www  4096 Feb 19 02:40 .ssh
复制代码

总结和推荐


写到这里,就算是结束了。说实话,有了上一篇文章的的经验和踩坑,我已经对于 Drone 的文档至关失望了,没想到后边有更深的坑在等着我,当我了解到真相之后,气得差点砸键盘。

本次总结

  • Vault 的文档其实还算清晰,在实际操做之间,建议至少把KV相关的部分熟读,并且第一次激活以后的手册必定要照着实践一遍。(若是你的英文不错,你能够看 vault 官方提供的高级教程)
  • drone-vault 的官方样例中没有说明怎么与 vault 通讯,在我第一遍的操做中,我没有细想这个问题,跳过了,结果不用多说,天然是失败。后面发现问题一脸蒙蔽,这玩意他不说我怎么找呢?仍是@Dee luo老哥提醒我,我才想起来去看看源码,在源码中找到了相关参数。(强烈吐槽!!!仓库仍是只读的!!!)
  • drone-vault 是用GO语言开发的,drone-vault 使用的http 包,不支持没写协议的 url,例如 vault:8200 不能被识别,要写成 http(s)://vault:8200
  • vault 中的 Secret 存储路径是 {Engine Name}/data/{Secret Name},这一点要牢记

若是你对 vault 想有更深层次的了解,能够看下YUHAO的博客 私密信息管理利器 HashiCorp Vault系列文章

系列文章

好了,祝你们撸码愉快,沉迷于BUG不能自拔。

不要砸键盘!

相关文章
相关标签/搜索