2026-08-21 17:10:22
本文仅记录Armada的部署过程,有关具体的技术细节(如Nostr、Concord),这里只简单介绍一下,如果你对Armada背后的这些技术感兴趣,可以Google或者AI探索一下。
Nostr是一个简单、开放的去中心化网络协议,旨在创建一个抗审查的全球化社交网络。而Concord是基于Nostr网络构建的开源通信协议,用于私密、加密的群聊和社区。
Armada主要分为:前端 + 后端(Nostr中继)+ 语音服务(Livekit)+ Blossom服务(媒体存储服务)
关于自托管Armada,其实可以根据自己的需要来选择是否要托管上述所有的服务,如果你接受将数据保存到别人的服务器上,那么可以只托管一个前端。我们这里做事做全套,本文会把所有的服务都搭建起来。如果你选择和本文一样自建全部服务,服务器的内存最好有8GB,主要是Docker镜像在构建过程中需要较多内存以及运行时的OpenSearch需要很多内存。当然你也可以选择更轻量的开源中继服务实现,这里就不多说了。
准备工作,一个域名添加如下解析记录:
| 名称 | 类型 | 内容 | 注释 |
|---|---|---|---|
| relay | A | 8.9.6.4 | #后端(中继)服务 |
| blossom-armada | A | 8.9.6.4 | #媒体存储服务 |
| av | A | 8.9.6.4 | #音视频通话服务 |
| armada | A | 8.9.6.4 | #前端服务 |
安装Docker等需要用到的软件包:
apt update
apt install curl nginx python3-certbot-nginx git
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
因Armada前端代码仓库托管在Nostr网络上,其地址以nostr://开头,Git本身无法识别这些地址,我们需要安装ngit,这是一个Git插件:
wget https://github.com/DanConwayDev/ngit-cli/releases/download/v2.6.3/ngit-v2.6.3-x86_64-unknown-linux-gnu.2.17.tar.gz
tar -xzvf ngit-v2.6.3-x86_64-unknown-linux-gnu.2.17.tar.gz -C /usr/local/bin/
验证安装:
ngit --version
git-remote-nostr --version
安装一下nak这个CLI工具,后续会用到:
wget https://github.com/fiatjaf/nak/releases/download/v0.20.6/nak-v0.20.6-linux-amd64
chmod +x nak-v0.20.6-linux-amd64
mv nak-v0.20.6-linux-amd64 /usr/local/bin/nak
验证安装:
nak --version
Nostr虽然是去中心化的,但是你的数据最终会保存到中继服务器和Blossom服务器,中继服务器负责传递和存储文本等结构化事件数据,而Blossom服务器专门负责大文件(如图片、视频、音频)的存储与分发。我们这里先把最关键且最核心的中继服务搭起来。其实Github上有非常多的开源Nostr中继服务实现,你并不一定要使用和我一样的中继服务实现,只是为了与Armada完美结合,这里我使用Ditto Relay,因为它支持NIP-50 / NIP-42。
cd /opt
git clone https://gitlab.com/soapbox-pub/ditto-relay.git
cd ditto-relay/
复制一份配置文件:
cp .env.example .env
编辑配置文件:
nano .env
这里只列出特别重要以及我修改过的内容:
PORT="13131"
RELAY_URL="wss://relay.example.com/"
NOSTR_NSEC="nsec..."
IP_HEADER="X-Real-IP"
1.修改RELAY_URL为你自己的域名。
2.NOSTR_NSEC填写中继的私钥,私钥可以使用之前安装的nak工具生成:
nak key generate
生成出来的内容还需要进行转换,因为Ditto Relay只支持nsec...这种格式,而nak默认生成出来的是十六进制私钥,例如你生成的值是:
fca5f6b1f172d57dbd89f3f6bb21aac93d828256082665bfa470301e1c20e549
转换为nsec:
nak encode nsec fca5f6b1f172d57dbd89f3f6bb21aac93d828256082665bfa470301e1c20e549
最终的值就是:
nsec1ljjldv03wt2hm0vf70mtkgd2ey7c9qjkpqnxt0aywqcpu8pqu4ystz0zpt
[可选]这里额外记录一下如何通过私钥获取对应的公钥,首先使用你的十六进制私钥获取:
nak key public fca5f6b1f172d57dbd89f3f6bb21aac93d828256082665bfa470301e1c20e549
对应的公钥就是:
8671a5f886f16deeda11ddd998c7fe61cdf9fc2589d5e954caa0852f9931a5bc
将公钥转换为npub...的格式:
nak encode npub 8671a5f886f16deeda11ddd998c7fe61cdf9fc2589d5e954caa0852f9931a5bc
对应的npub公钥就是:
npub1sec6t7yx79k7aks3mhve33l7v8xlnlp93827j4x25zzjlxf35k7qve74ml
3.我们使用NGINX反向代理Ditto Relay,所以IP_HEADER请务必配置为X-Real-IP或者X-Forwarded-For。
启动Ditto Relay,请注意官方没有发布预构建的镜像,目前是在本地构建镜像再启动:
docker compose up -d
Ditto Relay反向代理配置:
nano /etc/nginx/sites-available/ditto-relay
写入如下内容,请注意WebSocket是必须要启用的,Nostr的中继完全依靠WebSocket通信:
server {
listen 80;
server_name relay.example.com;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:13131;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
启用站点:
ln -s /etc/nginx/sites-available/ditto-relay /etc/nginx/sites-enabled/ditto-relay
签发证书:
certbot --nginx
访问你的中继域名,应该可以看到如图所示的页面:
查看日志:
docker compose logs -f
如有类似输出,则说明中继服务一切正常:
relay-1 | {"level":"info","msg":"index_created","index":"nostr-events"}
relay-1 | {"level":"info","msg":"opensearch_connected","node":"http://opensearch:9200"}
relay-1 | {"level":"info","msg":"indexer_worker_started"}
relay-1 | {"level":"info","msg":"protocol_pool_started","workers":1}
relay-1 | {"level":"info","msg":"protocol_worker_started"}
relay-1 | {"level":"info","msg":"started","port":13131,"log_level":"info","protocol_workers":1}
relay-1 | {"level":"info","msg":"trends_scheduled","interval_ms":900000}
relay-1 | {"level":"info","msg":"bg_worker_started"}
现在我们来部署Blossom服务,与中继服务一样,Github上也有相当多的开源Blossom服务实现,你并不一定要使用和我一样的实现,这里我选择hzrd149开源的Blossom服务实现,因为该存储库的作者hzrd149正是Blossom协议规范的提出者与核心设计者。克隆blossom-server存储库到本地:
cd /opt
git clone https://github.com/hzrd149/blossom-server.git
cd blossom-server/
复制一份配置文件:
cp config.example.yml config.yml
编辑配置文件:
nano config.yml
这里只列出我修改过的内容:
port: 3005
host: 0.0.0.0
publicDomain: "blossom-armada.example.com"
storage:
rules:
- type: "*"
expiration: 100 year
dashboard:
enabled: true
username: admin
password: "89641937"
lookupRelays:
- wss://relay.example.com
1.默认的3000端口改为3005,因我的服务器3000端口被别的程序占用了。
2.publicDomain改为你自己的域名。
3.默认情况下blossom-server不永久保存用户上传的文件,我这里改为永久保存,配置成100 year是因为该程序目前有BUG,按正常情况下来说将rules这里配置成[]就可以,但实测这会导致用户无法上传任何类型的文件。
4.务必修改管理员密码,并将中继地址lookupRelays改为你自己的。
编辑一下compose文件:
nano docker-compose.yml
将端口映射改为仅监听在本地:
services:
blossom:
build: .
ports:
- "127.0.0.1:3005:3005"
启动blossom-server,该项目同样没有提供预构建的镜像,首次启动会先在本地构建:
docker compose up -d --build
blossom-server反向代理配置:
nano /etc/nginx/sites-available/blossom-server
写入如下内容:
server {
listen 80;
server_name blossom-armada.example.com;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:3005;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
启用站点:
ln -s /etc/nginx/sites-available/blossom-server /etc/nginx/sites-enabled/blossom-server
签发证书:
certbot --nginx
现在我们来部署前端,前端就是一个纯静态的网站,仓库内提供了一个Dockerfile构建脚本,该脚本会构建网站并使用NGINX在80端口上提供服务。让我们先克隆仓库:
cd /opt
git clone nostr://soapbox.pub/relay.ngit.dev/armada
cd armada
配置是在构建时作为Docker构建参数嵌入的,新建一个compose文件:
nano docker-compose.yml
写入如下内容:
services:
kirara-chat:
image: moyu-chat:latest
container_name: moyu-chat
restart: always
build:
context: . # 以当前Git根目录作为构建上下文
args:
VITE_APP_NAME: "MoYu Chat"
VITE_APP_RELAYS: "wss://relay.example.com"
VITE_SEARCH_RELAYS: "wss://relay.example.com"
VITE_APP_BLOSSOM_SERVERS: "https://blossom-armada.example.com"
VITE_CONCORD_AV_SERVERS: "https://av.example.com"
ports:
- "127.0.0.1:8085:80"
1.完整的构建时参数(环境变量)见:Configuration (build-time env)
2.构建时的参数,如果修改了这些内容,必须重新构建,否则新配置是不会生效的。
3.该镜像使用NGINX在80端口提供服务,但我们使用反向代理,所以端口映射这里配置的端口为8085。
构建并启动前端:
docker compose up -d --build
前端反向代理配置:
nano /etc/nginx/sites-available/armada
写入如下内容:
server {
listen 80;
server_name armada.example.com;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:8085;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
启用站点:
ln -s /etc/nginx/sites-available/armada /etc/nginx/sites-enabled/armada
签发证书:
certbot --nginx
现在我们来部署语音服务器,这也是最麻烦的一步了,因为该项目的默认设置没有考虑到将所有服务部署在同一台服务器内的场景,这导致我们需要修改一些配置才能使其与之前部署的这些服务共存。如果你愿意单独将语音服务部署在另外一台服务器内,那没什么好说的,官方的配置直接就能跑起来,但我还是更倾向于将一个完整的项目部署在同一台服务器内。
克隆语音服务的存储库:
git clone nostr://[email protected]/relay.ngit.dev/armada-av
cd armada-av/deploy/single
复制一份配置文件:
cp .env.example .env
编辑.env:
nano .env
需要修改的内容如下:
AV_DOMAIN=av.example.com
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
1.AV_DOMAIN请与之前在构建前端时的配置VITE_CONCORD_AV_SERVERS保持一致
2.LIVEKIT_API_KEY / LIVEKIT_API_SECRET使用如下命令生成:
docker run --rm livekit/livekit-server generate-keys
编辑compose文件:
nano docker-compose.yml
将Caddy服务注释或者删除掉,以防止Caddy抢占主机NGINX的80 / 443端口,另外一定要将livekit的镜像TAG从v1.8改为latest,v1.8已经无法与Armada的前端一起正常工作了,这是我踩过的一个坑,可能是作者忘了修改:
# One box: the broker, one LiveKit node, and TLS. `cp .env.example .env`, edit,
# then `docker compose up -d`.
#
# All three services share the host network namespace: media is latency-
# sensitive UDP at thousands of packets per second, and Docker's userland
# proxy adds per-packet cost, so this is the configuration LiveKit itself
# recommends.
services:
livekit:
image: livekit/livekit-server:latest
network_mode: host
restart: unless-stopped
command: --config /etc/livekit.yaml
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
broker:
build:
context: ../..
network_mode: host
restart: unless-stopped
depends_on:
- livekit
environment:
PORT: "8086"
# Loopback only. All three services share the host's network namespace,
# so without this the broker answers on every interface — and with
# TRUST_PROXY on, a caller reaching it directly could forge
# X-Forwarded-For and rotate its rate-limit key at will. Caddy is the
# only way in.
HOST: "127.0.0.1"
# What clients sign their grants against — the public name, not the
# container's. A mismatch here rejects every request (CORD-07 §2 `u` tag).
PUBLIC_ORIGIN: "https://${AV_DOMAIN}"
# What clients are told to connect to. A BARE origin: the LiveKit SDK
# appends /rtc itself, and a url that already carries it breaks signaling.
LIVEKIT_URL: "wss://${AV_DOMAIN}"
# Health probes take the loopback shortcut rather than going out and back
# through TLS.
LIVEKIT_API_URL: "http://127.0.0.1:7880"
LIVEKIT_API_KEY: "${LIVEKIT_API_KEY}"
LIVEKIT_API_SECRET: "${LIVEKIT_API_SECRET}"
# Caddy sets X-Forwarded-For, and it is the only thing in front of us. The
# broker reads the client IP from the rightmost entry (the peer Caddy saw),
# so a client cannot spoof its rate-limit key by stuffing the header.
TRUST_PROXY: "1"
TOKEN_TTL_SECONDS: "${TOKEN_TTL_SECONDS:-21600}"
RATE_LIMIT_PER_MINUTE: "${RATE_LIMIT_PER_MINUTE:-60}"
# caddy:
# image: caddy:2-alpine
# network_mode: host
# restart: unless-stopped
# volumes:
# - ./Caddyfile:/etc/caddy/Caddyfile:ro
# - caddy_data:/data
# - caddy_config:/config
# environment:
# AV_DOMAIN: "${AV_DOMAIN}"
#volumes:
# caddy_data:
# caddy_config:
启动:
docker compose up -d --build
该项目必须使用反向代理,现在改为用主机的NGINX反向代理:
nano /etc/nginx/sites-available/armada-av
写入如下内容:
server {
listen 80;
server_name av.example.com;
client_max_body_size 0;
location /rtc {
proxy_pass http://127.0.0.1:7880;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location ~ ^/\.well-known/concord/av {
proxy_pass http://127.0.0.1:8086;
}
location / {
default_type text/plain;
return 200 "armada-av";
}
}
启用站点:
ln -s /etc/nginx/sites-available/armada-av /etc/nginx/sites-enabled/armada-av
签发证书:
certbot --nginx
到这里整个项目就全部部署完成了,该项目目前还在大力开发中(大量使用AI)基本每天发布一个新版本,如果没有重大的破坏性更改,那么要更新的话基本上就是拉新代码、重新构建:
git pull
docker compose up -d --build
接下来简单说一下如何使用Armada,因为Nostr中继的特殊性,你自建了中继,并不代表你的数据只存在于你自建的中继内,如果你没有预先设置好,你的数据可能会发往多个公共的中继。当然这是Nostr设计之初的意图:去中心化。且Armada本身就是端到端加密的,即便你把数据写到其它的中继了,中继服务器也看不到明文。
别说中继了,就是你通过Armada上传到blossom-server的大部分图片、视频、音频等文件都是加密的,相当于blossom-server服务器管理员知道你上传了一张图片文件,但看不到你的图片是什么内容。所以安全这块不用担心。
但我想的是完全掌控自己的数据,我不需要其它中继参与进来,我想在Nostr上建一个封闭的圈子,所有的数据只存在于我自己的中继服务器和blossom-server服务器内。所以下面的配置可能不适用于每个人。
首次访问Armada会显示下图内容,点加入:
如果你没有账户就点击创建账户按钮,你也可以使用nak再生成一个私钥,这里所谓的账户其实就是一串nsec格式的私钥:
生成私钥:
妥善保管你的私钥,这是你账户唯一的登录凭证:
登录后,找到社区中继,把这些中继全部删除,然后添加自己的:
或者在创建群组的时候,这里只使用自己的中继,那么这个群组就只存在于你自己的中继上:
还有一个用于DM(私聊)的中继,这里默认有一个relay.armada.buzz(Armada官方的中继)删掉:
请注意的上面配置仅生效于你自己的账号,别人使用你自建的Armada客户端还是遵循Armada默认的配置,啥意思呢?比如另外一个人用你的Armada客户端给你发消息,Armada默认的DM配置中有一个relay.armada.buzz,这是Armada官方运营的中继,他没删除这个中继的话,他发给你的消息就会存储到relay.armada.buzz这个中继。目前这个设置没办法全局生效,这算是一个小问题吧。啥时候官方能够支持在构建镜像的时候指定这个配置就好了。
只使用自己的中继,如何确保别人能够找到我?给我发消息?配置一下NIP-65就行了,把自己的中继加上去,点保存:
这会把你当前使用的中继列表发送给purplepag.es,一个存储用户NIP-65列表的公共“黄页”中继,类似于通讯录。别人(别的客户端)就可以通过查询purplepag.es知道你的专属中继地址,从而正确地读取你的动态并向你发送消息。除了purplepag.es外,Armada默认还会将列表发给user.kindpag.es和relay.nos.social。这个行为可以在构建镜像的时候使用VITE_NIP65_DISCOVERY_RELAYS指定要发送的中继,但我觉得没有必要去改动。
除了中继,还有媒体服务,确保使用的是自己的:
音视频通话我也试了一下,群组里面可以正常用,私聊不行,应该是BUG,之前用自建的服务器是点了双方都没反应,现在是点了之后对方没反应,过一会儿提示no answer就自动挂断了。。。不过有点神奇的是,大概一个月以前私聊可以用官方的服务器,现在连官方的服务器都不行了。。。
这应该是目前Nostr生态环境中比较成熟的聊天项目了,除了上述的私聊音视频通话有点问题外,没发现明显的BUG,E2EE特别牛逼,群聊甚至发送的图片都是加密的。硬要说缺点的话就是使用起来有点门槛,得先了解什么是Nostr,尤其是各种NIP。这让我想起了某人的一句话:Nostr去中心化的意思就是有一堆中心化的中继,以至于你不知道哪个是中心。。。
2026-08-17 15:37:17
前几天通过一个偶然的方式了解到这款BT客户端,试用了几天,我只想说这个下载速度和上传速度是真的牛批,公网BT轻轻松松跑满2Gbps,还支持流式传输,意味着可以边下边播,用处嘛你懂的~
作者发版比较慢,即便仓库累计了很多更新,也可能很长一段时间不发Release,所以我当时用的Docker镜像标签是main。巧合的是就在两天前作者发布了9.0正式版,要知道上一个版本发布还是半年前,我刚得知这项目还用没两天就发布新版本了,也是有点缘分= =。rqbit自带一个Web UI,有点糙但是够用。你要想把这个rqbit拿来跑PT的话也不是不行,就是得自己改UA和Peer ID,撸代码这种事交给AI去做行了,其实rqbit本身也基本实现了自定义UA这个功能,但还差临门一脚,我用AI给加上了。还差自定义Peer ID这个功能没做,我也不打算弄了,想来想去PT我还是用qBittorrent,这个rqbit就单纯拿来看片= =
安装Docker:
apt update
apt install curl nginx python3-certbot-nginx
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
创建目录和compose文件:
mkdir -p /opt/rqbit && cd /opt/rqbit && nano docker-compose.yml
写入如下内容:
services:
rqbit:
image: ikatson/rqbit:latest
network_mode: host
environment:
- RQBIT_HTTP_API_LISTEN_ADDR=0.0.0.0:3030
- RQBIT_LISTEN_PORT=4240
- RQBIT_HTTP_BASIC_AUTH_USERPASS=imlala:89641937
volumes:
- db:/home/rqbit/db
- cache:/home/rqbit/cache
- ./download:/home/rqbit/downloads
volumes:
db:
cache:
1.建议使用主机网络,达到最佳的网络传输性能。
2.如果你打算使用反向代理Web UI,可以把3030端口监听在本地。
3.全部可配置的环境变量见:rqbit.conf
启动:
docker compose up -d
[可选]配置反向代理:
nano /etc/nginx/sites-available/rqbit
写入如下内容:
server {
listen 80;
server_name rqbit.example.com;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:3030;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
启用站点:
ln -s /etc/nginx/sites-available/rqbit /etc/nginx/sites-enabled/rqbit
签发证书:
certbot --nginx
流式传输视频有两种,一种是复制播放列表URL:
可以在VLC等视频播放器中打开:
另外一种是直接在浏览器播放,这种应该只支持少量格式,比如MP4这种:
之前作者没发布新版本的时候,我遇到了这两个BUG:#600 / #552 为此我还特地手动编译了个仓库里面的main分支版本,但现在看来已经没有记录的必要了,新版本已经修复了这些BUG,而且手动部署很麻烦,不如Docker一根。
2026-08-14 14:09:54
CrowdSec常规的白名单不能用于启用了CRS的AppSec WAF,如果是被CRS规则拦截的,则最好使用插件的方式来排除。创建插件目录,例如grafana-exclusions:
mkdir -p /var/lib/crowdsec/data/crs-plugins/grafana-exclusions
创建*-before.conf文件,例如grafana-before.conf:
nano /var/lib/crowdsec/data/crs-plugins/grafana-exclusions/grafana-before.conf
文件名必须以-before.conf结尾,这样包含的规则才会在CRS主规则集执行之前生效(才能提前排除规则或更改参数)写入SecRule规则:
SecRule REQUEST_FILENAME "@beginsWith /apis/dashboard.grafana.app/" \
"id:9900001,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveById=911100,\
ctl:ruleRemoveById=932115,\
ctl:ruleRemoveById=942100"
SecRule REQUEST_FILENAME "@beginsWith /api/ds/query" \
"id:9900002,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveById=942100"
SecRule REQUEST_FILENAME "@beginsWith /api/datasources" \
"id:9900003,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveById=911100,\
ctl:ruleRemoveById=931100"
1.排除规则自身的ID最好从9900001开始,避免与其它的规则ID冲突。
2.排除规则不会写没关系,只要能找到误报的规则ID和详情,后面的事交给AI就行了,AI会帮你写排除规则。
误报的规则ID,可以通过这个命令查询:
cscli alerts list
找到ID后,再用这个命令查看详情:
cscli alerts inspect 24705
会输出类似下面这样的内容:
################################################################################################
- ID : 24705
- Date : 2026-08-14T04:26:51Z
- Machine : 33229323947d41878f3730daae8f8f340mkPL0rvRql9cJd3
- Simulation : false
- Remediation : false
- Kind : waf
- Reason : anomaly score block: anomaly: 8,
- Events Count : 5
- Scope:Value : Ip:130.12.180.77
- Country : US
- AS : Railnet LLC
- Begin : 2026-08-14T04:26:50Z
- End : 2026-08-14T04:26:50Z
- UUID : edf7d1f7-3d6f-41c1-99e8-b55a64aea6ef
- Context :
╭───────────────┬──────────────────────────────────────────────────────────────╮
│ Key │ Value │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ ja4h │ ge11nn010000_b8bcd45ac095_000000000000_000000000000 │
│ matched_zones │ REQUEST_HEADERS.Host │
│ matched_zones │ REQUEST_BASENAME │
│ matched_zones │ TX.extension │
│ method │ GET │
│ msg │ Host header is a numeric IP address │
│ msg │ URL file extension is restricted by policy │
│ name │ native_rule:920350 │
│ name │ native_rule:920440 │
│ target_uri │ /config.php.bak │
│ user_agent │ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 │
│ │ (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 │
╰───────────────┴──────────────────────────────────────────────────────────────╯
触发的规则ID是920350和920440,触发的URI是/config.php.bak,你直接把这些内容全部复制给AI,让AI写排除规则就行了,当然我这里只是演示,这并不是一个真正的误报。
建议每次添加或者修改规则后,先检查一遍有无错误:
crowdsec -t
没问题再重载配置:
systemctl reload crowdsec
2026-08-14 11:35:00
前几天龟壳发邮件说让降配到2C12G,不然就删鸡了。。我登上去找半天才找到降配的页面:
把这里改成2和12:
保存之后小鸡会重新引导,然后不出意外就要出意外了,因为我是私自安装的Debian系统,所以又遇到了之前GRUB引导丢失的问题了:龟壳ARM Debian11 UEFI GRUB启动项丢失修复,如果按照我之前这篇文章的步骤操作,只能暂时修复,如果龟壳哪天又维护,估计引导又丢了。
我这回仔细研究了一下,发现是龟壳维护后以及某些场景下会重置小鸡的NVRAM(UEFI的非易失性存储),Debian在默认情况下又不会将GRUB安装到UEFI的fallback路径,这就导致找不到引导最终进入EFI shell。解决办法其实很简单,重新安装GRUB的时候带上--removable参数即可:
grub-install --target=arm64-efi --efi-directory=/boot/efi --bootloader-id=debian --removable
这会在/boot/efi/EFI/BOOT/目录下生成BOOTAA64.EFI文件:
/EFI/BOOT/BOOTAA64.EFI是UEFI规范中的默认路径,这样即便NVRAM被重置,系统也能自动识别并开机。
还有一个需要注意的地方:Debian自身的系统更新,如果Debian更新了grub-efi软件包,系统可能会重新运行默认的grub-install,从而只更新/EFI/debian而忘记更新/EFI/BOOT,最好运行以下命令重新配置grub-efi:
dpkg-reconfigure grub-efi-arm64
前面弹出配置内核参数的界面都保持默认直接回车就好了,在下面这两个界面选择Yes:
配置完成后,以后无论是龟壳维护,还是你自己运行apt full-upgrade升级系统内核与GRUB,系统都会自动保证/EFI/BOOT里的兜底文件是最新的。
另外现在创建控制台连接也不用那么麻烦了,可以直接用龟壳的Cloud Shell:
如果小鸡引导丢了,此时在Cloud Shell敲一个回车就能看到EFI shell了,然后输入exit还是quit我忘了= =,即可看到小鸡的BIOS界面:
然后按照之前文章里面的步骤从Boot维护管理里面的Boot From File先把机子启动再重新安装GRUB即可。
2026-08-10 17:17:02
其实我一直有在关注Stalwart Mail这个项目,早在2年多以前我就写过一篇部署的文章,当时的Stalwart Mail还没有Web UI,所有操作都只能通过CLI完成。我还有一台服务器一直在使用Stalwart Mail,只不过这台服务器运行的版本比较旧了:
0.14还是升级过的,如果我没记错的话,我应该是从0.11升级上来的。时过境迁,这个项目也从最初的几百个star变成了如今的破万star,我觉得我有资格谈一谈这个程序目前面临的一些问题。
用了这么多年这个邮件服务器,让我觉得最操蛋的地方就是每次大版本更新简直就是灾难,隔个版本就来一大堆破坏性的更改,每次更新都得跟着官方release里面的步骤小心翼翼一步步来,没有哪一次大版本更新是能够轻松完成的。在我的印象里,像这种打包成一个二进制文件或者用一个docker容器就能跑起来的程序,更新不就是pull个新的image然后up就完事了么,但是Stalwart Mail很任性,你要想这么简单的升个级那等着你的绝对是boom,这也就是为什么我之前这台服务器一直运行的是0.14版本,我已经不打算把它升级到0.15然后再从0.15升级到目前最新的0.16了。
现在好就好在作者意识到这个问题了,且之前有说过0.16是个很关键的版本,在0.16后应该不会再引入大规模的破坏性更改,且在今年晚些时候会发布具有里程碑意义的1.0版本:《立即升级到0.16,或者等待1.0版本》
然后就是这个项目的定位我觉得有点问题,作者似乎在开发这块有点割裂,想让Stalwart Mail成为一个新手小白都能快速上手的All-in-One邮件服务器,但同时又想兼顾具备专业知识和有能力的人去折腾,这就导致了两个问题:
1.官方文档写的摸棱两可,不清不楚,明明给个示例配置就完事儿的事情,非要在那里车轱辘话说一大堆,到头来新手看了半天文档还是不知道怎么操作,老鸟又没有看的必要。。我认为一篇优秀的文档应该是理论与实践并行,缺一不可,如果只能2选1,那我宁愿选择实践。
2.Web UI对每个功能的可配置性我觉得有点过于细致了,作者似乎想把有关邮件服务器内所有能配置的东西都给你在Web UI上列出来,让你可以看到每一个细节,修改每一个参数。这看上去很美好,但是新手一看到这个Web UI头皮都是麻的,完全看不懂这些设置项有什么用,该怎么配置。别说新手了,就算是具备一定专业知识的人可能都不能完全了解这些设置的作用,再加上文档极其抽象,第一次接触到这个Web UI的人就只会感觉到复杂与繁琐。
当然这些只是我的猜测,作者真实的开发意愿我无从得知,我也无意干预作者对项目开发的路线,我只是想说如果能在这之间找到一个平衡点那当然是最好的,如果找不到平衡点那就专攻一个点去做就行了。
好就好在现在的0.16版本似乎在朝着这个“平衡”的方向发展,在用户首次登录的时候会有一个“配置向导”,只要你按照这个向导去完成配置,那么剩下的那些“让人看不懂”的配置基本就可以不用管了,你的邮件服务器也能正常工作。对于喜欢折腾或者有更多需求的人可以在向导完成后去修改那些更高级的配置。
除了上述我说的这些问题外,还有一个我有点担心的问题是,作者能否保持初心不去恰烂钱,目前来看似乎有点这方面的趋势,小声bb:Masked Emails功能。但愿作者后续不会把关键且核心的功能加到企业版本付费才能使用。
让我们正式开始部署,本文尽量用大白话把一些容易出问题的地方给讲清楚,同时为方便理解,本文不对敏感内容脱敏。准备工作,一个域名(本文示例:ohsb.cc)接入到Cloudflare并做好如下DNS解析:
| 名称 | 类型 | 内容 |
|---|---|---|
| A | 152.53.89.75 | |
| bulwark | A | 152.53.89.75 |
我们不在这里配置MX、SPF、DKIM、DMARC等DNS记录,这些记录稍后统一交给Stalwart自动管理,由Stalwart自行添加。除了这些正向DNS记录外,你的服务器最好支持设置PTR/rDNS,这里我以Netcup的VPS为例,值与正向DNS中的A记录匹配:
验证PTR/rDNS是否生效:
dig -x 152.53.89.75 +short
安装NGINX、CertBot、Docker:
apt update
apt install curl nginx python3-certbot-nginx
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
创建目录和compose文件:
mkdir -p /opt/stalwart-mail && cd /opt/stalwart-mail && nano docker-compose.yml
写入如下内容:
services:
stalwart:
image: stalwartlabs/stalwart:v0.16
container_name: stalwart
restart: unless-stopped
ports:
- "8443:443"
- "8080:8080"
- "25:25"
# - "587:587"
- "465:465"
# - "143:143"
- "993:993"
# - "110:110"
# - "995:995"
# - "4190:4190"
volumes:
- ./stalwart-etc:/etc/stalwart
- ./stalwart-data:/var/lib/stalwart
bulwark:
image: ghcr.io/bulwarkmail/webmail:latest
container_name: bulwark
restart: unless-stopped
environment:
BULWARK_TELEMETRY: off
ports:
- "3008:3000"
depends_on:
- stalwart
volumes:
- bulwark-settings:/app/data/settings
- bulwark-config:/app/data/admin
- bulwark-state:/app/data/admin-state
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://127.0.0.1:3000/api/health",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes:
bulwark-settings:
bulwark-config:
bulwark-state:
1.由于stalwart容器使用的是bind-mounts,且该镜像以非特权用户(UID2000)身份运行,所以主机目录的所有者必须是UID2000,我们就自行创建目录并修改所有者:
mkdir stalwart-data stalwart-etc && chown 2000:2000 stalwart-data/ stalwart-etc/
2.根据官方的安全实践,禁用了587 / 143 / 110 / 995 / 4190这些非必要且可能有安全问题的端口,如果你有需要可以取消相应的注释。
3.考虑到stalwart后续的大版本更新可能会比较麻烦,官方也不建议将镜像的tag改成latest,所以这里固定使用v0.16,避免导致后续升级时的混乱,例如当前使用的latest指向的是0.16,但这期间官方连发了2个大版本,最新的latest已经指向0.18了,那现在本地的这个0.16去拉最新的0.18升级肯定是要出问题的。
4.官方文档关于NGINX反向代理的示例配置是使用TCP直通,既NGINX不终止TLS,直接将TLS会话原封不动地传递给stalwart。我不打算使用这种方式,我觉得这种方式有点脱裤子放屁的意思,谁没事会在一台服务器上面运行两个甚至多个邮件服务器?完全没有必要通过NGINX去转发25 / 465 / 993的流量嘛,且一旦NGINX使用stream模块监听443端口后,443端口会被stream模块独占,那么http模块就不能再监听443端口了,如果你的服务器上还运行着其它网站,那这些网站都将无法访问。除了这些以外,你还得去配置Proxy Protocol,不然stalwart拿不到客户端真实IP一大堆的功能都会出问题。这简直是一个亏损最大化的反代方式。。。如果要我用这种方式,那我不如单独拿一台服务器出来只跑stalwart得了。。。
考虑再三,我决定使用一个折中的方案,既我们只使用NGINX反向代理stalwart的HTTP端口(8080),其它的邮件服务端口,如25 / 465 / 993这些全部由stalwart自身处理。这种方案唯一的缺点是你需要维护两套TLS证书,既NGINX需要一套证书,stalwart自身也需要一套证书,但好在证书申请和配置都比较简单,NGINX有certbot,stalwart则可以通过ACME DNS 01全自动完成。这种方案也不会导致stalwart的功能有异常或者缺失,因为stalwart 8080端口提供的服务(OAuth、OIDC、JMAP、Autoconfig等)与stalwart 443端口提供的服务完全一致。
启动:
docker compose up -d
查看临时的管理员账号和密码:
docker logs stalwart 2>&1 | grep -A8 'bootstrap mode'
注意这个管理员账号和密码仅用于引导程序运行设置向导,等向导完成后会重新配置一个永久的管理员帐户:
配置NGINX反向代理,先配置stalwart的反代:
nano /etc/nginx/sites-available/stalwart
写入如下内容:
server {
listen 80;
server_name mail.ohsb.cc autoconfig.ohsb.cc autodiscover.ohsb.cc ua-auto-config.ohsb.cc mta-sts.ohsb.cc;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 支持 JMAP Push (WebSocket)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
1.注意server_name不仅仅要有mail.ohsb.cc,自动配置、自动发现以及mta-sts这些功能也需要配置单独的主机名。
2.JMAP依赖WebSocket,务必启用。
启用站点:
ln -s /etc/nginx/sites-available/stalwart /etc/nginx/sites-enabled/stalwart
继续配置bulwark webmail的反代:
nano /etc/nginx/sites-available/bulwark
写入如下内容:
server {
listen 80;
server_name bulwark.ohsb.cc;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:3008;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
启用站点:
ln -s /etc/nginx/sites-available/bulwark /etc/nginx/sites-enabled/bulwark
签发证书:
certbot --nginx
访问https://mail.ohsb.cc/admin打开Web UI,如果你发现访问https://mail.ohsb.cc/admin报错502,那么暂时先使用http://152.53.89.75/admin,这大概率是因为stalwart没有获取到客户端的真实IP,将所有传入的请求都识别为Docker桥接网络的网关IP,然后因为有公网扫描器执行的一些探测扫描被stalwart识别到了,stalwart就把Docker桥接网络的网关IP给ban了导致的,这个问题稍后等安装向导完成后再来解决。
使用临时管理员账号登录开始安装向导,配置主机名和邮箱域名:
配置存储,直接用默认的RocksDB就好了,单机部署最佳选择:
账户目录,直接用默认的内部目录即可:
日志目标,这里默认的是文件形式,且保存路径在/var/log/stalwart:
由于我们之前没有挂载这个目录,建议将这里的日志输出模式改为console,后续可以通过如下命令来查看日志:
docker compose logs
如果硬要以文件形式来保存日志,那么应该在compose文件内增加如下内容:
volumes:
- ./stalwart-etc:/etc/stalwart
- ./stalwart-data:/var/lib/stalwart
- ./stalwart-log:/var/log/stalwart
并主动创建目录及修改所有者:
mkdir stalwart-log && chown 2000:2000 stalwart-log
然后就到了非常关键的一步了:自动DNS管理。将Cloudflare的API Token输入到这里,即可让stalwart自动为你创建DNS记录,例如MX、SPF、DKIM、DMARC等。注意MX记录默认是没有为你创建的,需要后续手动配置,我稍后会详细说明:
向导完成后,会重新打印一个管理员账号和密码,这就是永久的管理员账号了,请妥善保管:
重启容器以使新配置生效:
docker compose down
docker compose up -d
再次登录到Web UI,让我们继续完善设置。向导虽然为我们配置了大多数设置,但还有以下步骤缺一不可,请务必全部按照操作完成。
1.找到HTTP Server页面,启用Obtain remote IP from Forwarded header,这将确保stalwart能够获取到客户端的真实IP:
2.找到HTTP Security页面,启用Permissive CORS policy,这将解决bulwark webmail无法正常登录,报错CORS禁止的问题:
3.如果你之前访问https://mail.ohsb.cc/admin报错502,那么请找到Blocked IP addresses页面,不出意外的话,这里会列出你的Docker桥接网络的网关IP:
把这个IP从封禁列表删除,然后找到Actions页面,点击Blocked IPs list按钮重载配置,使其生效:
之前已经启用了Obtain remote IP from Forwarded header,所以stalwart不会再将所有通过NGINX代理的IP都识别为Docker桥接网络的网关IP,这个问题也就解决了,且自动封禁的功能也将正常工作。
4.我们使用stalwart自动管理DNS记录,所以找到Domains页面,点击你的域名进入详情页:
找到DNS Management内的Record Types,可以看到这里默认没有MX记录:
勾选MX记录,保存设置:
找到Actions,重载服务器配置使其生效,请注意,大多数配置修改后都需要这一步才能使新的配置生效:
最后我们还需要找到Tasks页面,创建一个新的任务:
Task type选择Perform DNS management for a domain,Record Types选择MX records,Due设置为你当前时间的后1分钟:
5.在你的个人账户设置页面,创建一个名为Archive的文件夹,以适配bulwark webmail及其它邮件客户端的归档功能:
到这里stalwart就配置好了,接下来配置bulwark webmail,在你首次访问bulwark webmail的时候会让你输入一个安装token:
使用如下命令查看bulwark容器日志以获取安装token:
docker compose logs -f bulwark
bulwark webmail是一个基于JMAP协议的客户端,这里需要配置你的JMAP server URL:
测试一下邮件发送,能发保底10分:
能收:
文章篇幅有限,更多高级功能如PGP加密,反垃圾邮件配置,这些内容另外单独用一篇文章说明,未完待续。。。
2026-08-04 10:53:44
Wordfence CLI是一款开源、高性能的安全扫描器,使用Python编写,能够快速扫描文件系统,检测PHP及其他恶意软件和WordPress漏洞。CLI支持并行运行、定时执行,可以通过管道接收输入,也可以将输出通过管道传递给其他命令。
官方提供了多种安装方式,包括pip包、deb包、二进制文件等等,我这里选择二进制文件安装,因为Debian现在不允许直接用pip全局安装pip包了,你要装一个包还得先建个venv,特别麻烦。deb软件包在Debian 13有依赖问题,这坑我已经踩过了:
apt install ./wordfence.deb
Note, selecting 'wordfence' instead of './wordfence.deb'
Solving dependencies... Error!
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:
Unsatisfied dependencies:
wordfence : Depends: libpcre3 but it is not installable
Error: Unable to correct problems, you have held broken packages.
Error: The following information from --solver 3.0 may provide additional context:
Unable to satisfy dependencies. Reached two conflicting decisions:
1. wordfence:amd64=5.0.5rc1 is selected for install
2. wordfence:amd64 Depends libpcre3
but none of the choices are installable:
[no choices]
所以二进制是最舒服的,下载解压就能用了:
wget https://github.com/wordfence/wordfence-cli/releases/download/v5.0.4/wordfence_amd64.tar.gz
tar -xvf wordfence_amd64.tar.gz
试试看能不能运行,首次运行应该会提示让你注册一个免费的许可证,以及生成默认的配置文件:
./wordfence version
同时看一下扫描引擎的支持情况,现在PCRE和Vectorscan应该都显示的是No:
Wordfence CLI 5.0.4
PCRE Supported: No
Vectorscan Supported: No
我找了半天硬是找不到Debian 13的这个PCRE3的依赖包名叫啥,应该和之前安装deb包的那个依赖问题一样,索性我干脆放弃PCRE了,直接使用Vectorscan。
实际上也是Vectorscan更好用,Vectorscan的扫描速度比PCRE快几十倍,所以支不支持PCRE已经不重要了。在Debian 13安装这个包即可:
apt install libvectorscan5
我还有一台Debian 11的机器也需要装,发现Debian 11根本没有这个libvectorscan5,取而代之可以安装libhyperscan5,因为Vectorscan是Hyperscan的一个分支,并保持着兼容的API:
apt install libhyperscan5
Wordfence CLI目前同时支持这两种技术,但如果Vectorscan的API与Hyperscan的API出现差异,这种情况可能会随时间而改变,到时候就看具体情况怎么处理了。再次检查一下扫描引擎的支持情况,如果正常应该显示:
Wordfence CLI 5.0.4
PCRE Supported: No
Vectorscan Supported: Yes - Version: 5.4.2 2024-12-30 (API Version: 5.4.2)
默认情况下,CLI将使用PCRE进行扫描。要将CLI配置为使用Vectorscan,可以使用以下命令行参数:
./wordfence malware-scan --match-engine=vectorscan /var/www/wordpress
但每次都加上--match-engine=vectorscan使用起来不太方便,可以编辑配置文件:
nano ~/.config/wordfence/wordfence-cli.ini
在[MALWARE_SCAN]下面写入:
[MALWARE_SCAN]
match_engine=vectorscan
开始扫描:
./wordfence malware-scan --output-format csv --output-path scan_report.csv /var/www/wordpress
默认情况下Wordfence CLI不会扫描全部文件,例如图片之类的文件会跳过,如果你想扫描全部文件请使用:
./wordfence malware-scan --include-all-files --output-format csv --output-path scan_report.csv /var/www/wordpress
扫描结果会保存至scan_report.csv:
cat scan_report.csv
测试了一下,可以扫到后门:
/tmp/wf_test/test_webshell.php,11121,Backdoor:PHP/short.assert.11121,Short RCE,
如果没有检测到任何恶意程序,则scan_report.csv内容为空,你看到文件内没有内容应该感到高兴而不应该认为是Wordfence CLI没有正常工作。也可能是人家的后门太牛逼,Wordfence CLI扫不出来。
Wordfence CLI虽然是专为WordPress打造的,但请注意Wordfence CLI也可以扫描其它网站程序的PHP后门和恶意程序,并不是说你的网站程序不是WordPress就不能用Wordfence CLI。只是Wordfence CLI针对WordPress有更多的功能,比如扫描CVE漏洞,自动修复被篡改的文件等。
Wordfence CLI其实是一款收费软件,只是官方同时提供了免费版本,免费版本与收费版本的区别在于免费版本的数据库比收费版本慢了30天。如果你使用Wordfence CLI扫描后还不太放心,可以再试试YARA。
YARA是一款旨在(但不限于)帮助恶意软件研究人员识别和分类恶意软件样本的工具。YARA被誉为“恶意软件研究人员的瑞士军刀”,由VirusTotal的安全团队开发和维护。这么说吧,在网络安全行业里,几乎所有主流的杀毒软件和安全大厂(如卡巴斯基、赛门铁克等等)都在广泛使用YARA。
安装YARA:
apt install yara
YARA只是一个检测引擎,规则还需要自己写或者用别人现成的,Github上有很多,这里我使用signature-base。我们把规则下载放到目录内:
mkdir yara-rule && cd yara-rule/
wget https://raw.githubusercontent.com/Neo23x0/signature-base/master/yara/gen_webshells.yar
然后就可以使用这个规则来扫描了:
yara -r yara-rule/gen_webshells.yar /tmp/wf_test/
如果目录内有多个规则,可以用通配符:
yara -r yara-rule/*.yar /tmp/wf_test/
测试了一下,也可以检测到:
EXT_WEBSHELL_PHP_Generic /tmp/wf_test//test_webshell.php
WEBSHELL_PHP_Base64_Encoded_Payloads /tmp/wf_test//test_webshell.php
WEBSHELL_PHP_Gzinflated /tmp/wf_test//test_webshell.php
WEBSHELL_PHP_OBFUSC_3 /tmp/wf_test//test_webshell.php
WEBSHELL_PHP_Dynamic_Big /tmp/wf_test//test_webshell.php
补充点内容,如果你的站点使用WordPress,可以使用WP-CLI校验Core文件的官方哈希值:
sudo -u www-data wp core verify-checksums --path=/var/www/wordpress
一旦发现某个文件的指纹对不上就说明:文件被黑客修改、注入了恶意代码。请注意WP-CLI默认只拿英文版本做比对,如果你的WordPress是简中版本,可能会有个别文件误报,为了让它精准比对简中版本,请加上locale参数:
sudo -u www-data wp core verify-checksums --path=/var/www/wordpress --locale=zh_CN
WP-CLI还可以校验主题、插件的哈希值,但是仅支持WordPress官方市场安装的主题、插件。考虑到大部分站点都会从其它地方安装主题和插件,所以校验的价值就不太高了。
南无阿弥陀佛,佛祖保佑,希望自己永远也不会用到这些工具!!