从部署应用程序的那一刻起,开发人员和IT人员就要花费时间来将其封锁起来:配置ACL,轮换IP地址,使用像GRE隧道这样的笨拙解决方案。有一种更简单、更安全的方法可以保护您的应用程序和web服务器免受直接攻击:Cloudflare Tunnel。可以借助良心企业Cloudflare完全免费的Tunnel服务,快速安全地加密应用程序到任何类型基础设施的流量,让您能够隐藏你的web服务器IP地址,阻止直接攻击。Tunnel后台程序在源web服务器和Cloudflare最近的数据中心之间创建一条加密隧道,同时无需打开任何公共入站端口。使用防火墙锁定所有源服务器端口和协议后,HTTP/S端口上的任何请求都会被丢弃,包括容量耗尽DDoS攻击。数据泄露尝试被完全阻止,例如传输中数据窥探或暴力登录攻击。同时Tunnel支持gRPC
的流量转发,用来配置哪吒探针也没有问题。
配置Cloudflare Tunnel
Cloudflare Tunnel支持多种部署方式,并且平台的适配也很完善,具体项目信息可以从Github上查看。下面以Debian为例,简单介绍一下安装方法。
在Cloudflare Dashboard中新建一个隧道,之后你可以获得一串密钥,之后下载合适的deb
安装包
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
然后可以直接使用一键安装
sudo dpkg -i cloudflared.deb && sudo cloudflared service install eyJhI...
注意:这样通过官网service install一键安装的方式需要在/etc/systemd/system/cloudflared.service
中增加--protocol http2
参数
ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel run --protocol http2 --token eyJhI...
更推荐使用Docker安装,部署方式如下:
docker run -d \
--name cloudflared \
--restart always \
--network host \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate run --protocol http2 --token eyJhI...
配置隧道时如果你直接穿透web服务,你可以设置到目标端口,如http://127.0.0.1:12345
。假使需要采用Tunnel+Nginx这样的组合,你的服务必须使用https协议,设置Service为https://127.0.0.1:443
,同时需要打开No TLS Verify和HTTP2 connection Origin,并配置好Server Name和HTTP Host Header。
部署Nginx转发
这块比较简单,可以通过监控/proto.NezhaService
,区分不同流量,直接贴出我的配置供参考
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
proxy_ssl_server_name on;
server_tokens off;
ssl_certificate /opt/cert/web.crt;
ssl_certificate_key /opt/cert/web.key;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security max-age=15768000;
server_name status.canghai.org;
client_max_body_size 128M;
underscores_in_headers on;
location / {
proxy_pass http://127.0.0.1:5015;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
}
location ~ ^/(ws|terminal/.+)$ {
proxy_pass http://127.0.0.1:5015;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
}
location /proto.NezhaService {
grpc_read_timeout 300s;
grpc_send_timeout 300s;
grpc_socket_keepalive on;
grpc_pass grpc://grpcservers;
}
}
upstream grpcservers {
server localhost:5016;
keepalive 512;
}
部署哪吒探针
哪吒探针的部署官网很详细,你可以轻松的使用Docker部署
docker run -d \
--name nezha \
--restart always \
-v "/opt/nezha/dashboard/data:/dashboard/data" \
-v "/opt/nezha/dashboard/resource:/dashboard/resource" \
-p 5015:80 \
-p 5016:5555 \
ghcr.io/naiba/nezha-dashboard
探针前端自定义
- 使用自定义主题的话本地目录结构如下:
resource
├── static
│ └── custom
│ ├── main.css
│ └── mixin.js
└── template
└── theme-custom
├── footer.html
├── header.html
├── home.html
├── menu.html
├── network.html
├── service.html
└── viewpassword.html
- 其中
main.css
中修改内容如下:
/* 适配不同宽度窗口 */
@media only screen and (min-width:1200px) {
.ui.container {
width:95% !important;
font-size: 90% !important;
max-width: 1300px !important;
}
}
@media only screen and (min-width:767px) {
.status.cards {
display: grid !important;
justify-content: center !important;
grid-template-columns: repeat(auto-fill, 305px);
grid-gap: 15px;
}
.status.cards .card {
width: 305px !important;
margin: 0 !important;
}
}
/* 隐藏页脚 */
.footer {
display: none !important;
}
/* 修改字体 */
*:not(.icon) {
font-family: 'LXGW WenKai Screen' !important;
}
/* 修改字体大小 */
.ui.card>.content>.header:not(.ui),
.ui.cards>.card>.content>.header:not(.ui) {
line-height: 1em;
font-size: 1.2em !important;
}
.status.cards .three.wide.column {
font-weight: bold;
text-align: center;
width: 22% !important;
}
.status.cards .thirteen.wide.column {
font-weight: bold;
width: 78% !important;
padding-left: 0;
}
.status.cards .ui.content.popup {
min-width: calc(100%) !important;
line-height: 1.72rem !important;
font-size: 0.9rem !important;
border-radius: 5px !important;
border: 1px solid transparent !important;
}
- 可通过修改
home.html
来对主页部分显示信息进行自定义
<!-- 未开启SWAP分区显示 -->
<div v-if="server.Host.SwapTotal != 0" class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).class">
<div class="bar" :style="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).style">
<small>@#parseInt(server.State.SwapUsed/server.Host.SwapTotal*100)#@%</small>
</div>
</div>
</div>
<div v-else class="thirteen wide column">
未开启SWAP分区<br />
</div>
<script>
// 增加单位TB
getK2Gb(bs){
bs = bs / 1024 / 1024 / 1024 / 1024;
if (bs >= 1) {
return Math.ceil(bs.toFixed(2)) + 'TB';
} else {
bs = bs * 1024;
if (bs >= 1) {
return Math.ceil(bs.toFixed(2)) + 'GB';
} else {
bs = bs * 1024;
return Math.ceil(bs.toFixed(2)) + 'MB';
}
}
},
// 温度显示四舍五入
getTemperature(temperatureList, sensorList) {
const lowerCaseSensorList = sensorList.map(sensor => sensor.toLowerCase());
const filtered = temperatureList.filter(item => item.Temperature !== 0 && lowerCaseSensorList.includes(item.Name.toLowerCase()));
if (filtered.length > 0) {
return filtered.reduce((max, current) => {
return current.Temperature > max ? current.Temperature : max;
}, filtered[0].Temperature);
}
if (filtered.length > 0) {
const maxTemperature = filtered.reduce((max, current) => {
return current.Temperature > max ? current.Temperature : max;
}, filtered[0].Temperature);
return Math.round(maxTemperature);
}
const nonZeroTemps = temperatureList.filter(item => item.Temperature !== 0);
if (nonZeroTemps.length > 0) {
const maxTemperature = nonZeroTemps.reduce((max, current) => {
return current.Temperature > max ? current.Temperature : max;
}, nonZeroTemps[0].Temperature);
return Math.round(maxTemperature);
}
return 0;
}
</script>