# 高级部署

## 前提

请确认你有过单机部署的经验，不建议第一次就搞这样分布架构

建议有一定Docker部署及操作经验者阅读此文档

在进行以下步骤之前，你需要先安装好ctfd-whale插件

## 目的

分离靶机与ctfd网站服务器，CTFd通过tls api远程调用docker

## 架构

两台vps

- 一台作为安装CTFd的网站服务器，称为 `web` ，需要公网IP
- 一台作为给选手下发容器的服务器，称为 `target` ，此文档用到的服务器是有公网IP的，但如果没有，可也在 `web` 服务器用 `frps` 做转发

本部署方式的架构如图所示

![架构](imgs/arch.png)

---

## 配置Docker的安全API

参考来源：[Docker官方文档](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket)


### target服务器配置

建议切换到 `root` 用户操作

### 克隆本仓库

```bash
$ git clone https://github.com/frankli0324/ctfd-whale
```

### 开启docker swarm

```bash
$ docker swarm init
$ docker node update --label-add "name=linux-target-1" $(docker node ls -q)
```

把 `name` 记住了，后面会用到

创建文件夹
```bash
$ mkdir /etc/docker/certs && cd /etc/docker/certs
```

设置口令，需要输入2次
```bash
$ openssl genrsa -aes256 -out ca-key.pem 4096
```

用OpenSSL创建CA, 服务器, 客户端的keys
```bash
$ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
```

生成server证书，如果你的靶机服务器没有公网IP，内网IP理论上也是可以的，只要web服务器能访问到
```bash
$ openssl genrsa -out server-key.pem 4096
$ openssl req -subj "/CN=<target_ip>" -sha256 -new -key server-key.pem -out server.csr
```

配置白名单
```bash
$ echo subjectAltName = IP:0.0.0.0,IP:127.0.0.1 >> extfile.cnf
```

将Docker守护程序密钥的扩展使用属性设置为仅用于服务器身份验证
```bash
$ echo extendedKeyUsage = serverAuth >> extfile.cnf
```

生成签名证书，此处需要输入你之前设置的口令
```bash
$ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -extfile extfile.cnf
```

生成客户端(web服务器)访问用的 `key.pem`
```bash
$ openssl genrsa -out key.pem 4096
```

生成 `client.csr` ，此处IP与之前生成server证书的IP相同
```bash
$ openssl req -subj "/CN=<target_ip>" -new -key key.pem -out client.csr
```

创建扩展配置文件，把密钥设置为客户端身份验证用
```bash
$ echo extendedKeyUsage = clientAuth > extfile-client.cnf
```

生成 `cert.pem`
```bash
$ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out cert.pem -extfile extfile-client.cnf
```

删掉配置文件和两个证书的签名请求，不再需要
```bash
$ rm -v client.csr server.csr extfile.cnf extfile-client.cnf
```

为了防止私钥文件被更改以及被其他用户查看，修改其权限为所有者只读
```bash
$ chmod -v 0400 ca-key.pem key.pem server-key.pem
```

为了防止公钥文件被更改，修改其权限为只读
```bash
$ chmod -v 0444 ca.pem server-cert.pem cert.pem
```

打包公钥
```bash
$ tar cf certs.tar *.pem
```

修改Docker配置，使Docker守护程序可以接受来自提供CA信任的证书的客户端的连接

拷贝安装包单元文件到 `/etc` ，这样就不会因为docker升级而被覆盖
```bash
$ cp /lib/systemd/system/docker.service /etc/systemd/system/docker.service
```

将第 `13` 行
```
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
```
改为如下形式
```
ExecStart=/usr/bin/dockerd --tlsverify \
--tlscacert=/etc/docker/certs/ca.pem \
--tlscert=/etc/docker/certs/server-cert.pem \
--tlskey=/etc/docker/certs/server-key.pem \
-H tcp://0.0.0.0:2376 \
-H unix:///var/run/docker.sock
```

重新加载daemon并重启docker
```bash
$ systemctl daemon-reload
$ systemctl restart docker
```

**注意保存好生成的密钥，任何持有密钥的用户都可以拥有target服务器的root权限**

---

### Web服务器配置

在`root`用户下配置

```bash
$ cd CTFd
$ mkdir docker-certs
```

先把刚才打包好的公钥 `certs.tar` 复制到这台服务器上

然后解压
```bash
$ tar xf certs.tar
```

打开 `CTFd` 项目的 `docker-compose.yml` ，在`CTFd` 服务的 `volumes` 下加一条
```
./docker-certs:/etc/docker/certs:ro
```

顺便把 `frp` 有关的**所有**配置项删掉，比如`frp_network`之类


然后执行 `docker-compose up -d`

打开`CTFd-whale`的配置网页，按照如下配置docker

![whale-config1](imgs/whale-config1.png)

注意事项

- `API URL` 一定要写成 `https://<target_ip>:<port>` 的形式
- `Swarm Nodes` 写初始化 `docker swarm` 时添加的 `lable name`
- `SSL CA Certificates` 等三个路径都是CTFd容器里的地址，不要和物理机的地址搞混了，如果你按照上一个步骤更改好了 `CTFd` 的 `docker-compose.yml` ，这里的地址照着填就好

对于单容器的题目，`Auto Connect Network` 中的网络地址为`<folder_name>_<network_name>`，如果没有改动，则默认为 `whale-target_frp_containers`

![whale-config2](imgs/whale-config2.png)

*多容器题目配置 未测试*

---

## FRP配置

### 添加泛解析域名，用于HTTP模式访问

可以是这样
```
*.example.com
*.sub.example.com (以此为例)
```

### 在target服务器上配置

进入 `whale-target` 文件夹
```bash
$ cd ctfd-whale/whale-target
```

修改 `frp` 配置文件
```bash
$ cp frp/frps.ini.example frp/frps.ini
$ cp frp/frpc.ini.example frp/frpc.ini
```

打开 `frp/frps.ini`

- 修改 `token` 字段， 此token用于frpc与frps通信的验证
- 此处因为frps和frpc在同一台服务器中，不改也行
- 如果你的target服务器处于内网中，可以将 `frps` 放在 `web` 服务器中，这时token就可以长一些，比如[生成一个随机UUID](https://www.uuidgenerator.net/)
- 注意 `vhost_http_port` 与 [docker-compose.yml](/whale-target/docker-compose.yml) 里 `frps` 映射的端口相同
- `subdomain_host` 是你做泛解析之后的域名，如果泛解析记录为`*.sub.example.com`, 则填入`sub.example.com`



#### 打开 `frp/frpc.ini`

- 修改 `token` 字段与 `frps.ini` 里的相同

- 修改 `admin_user` 与 `admin_pwd`字段， 用于 `frpc` 的 basic auth

---

### 在WEB服务器上配置

打开whale的设置页面，按照如下配置参数

![frp配置页面](imgs/whale-config3.png)

网页中，

- `API URL` 需要按照 `http://user:password@ip:port` 的形式来设置
- `Http Domain Suffix` 需要与 `frps.ini` 中的 `subdomain_host` 保持一致
- `HTTP Port` 与 `frps.ini` 的 `vhost_http_port` 保持一致
- `Direct Minimum Port` 与 `Direct Maximum Port` 与 `whale-target/docker-compose.yml` 中的段口范围保持一致
- 当 API 设置成功后，whale 会自动获取`frpc.ini`的内容作为模板

---

至此，分离部署的whale应该就能用了，可以找个题目来测试一下，不过注意docker_dynamic类型的题目似乎不可以被删除，请注意不要让其他管理员把测试题公开

你可以用 
```bash
$ docker-compose logs 
```
来查看日志并调试，Ctrl-C退出
