1. 项目背景:为什么RustDesk API服务器会成为安全短板
1.1 RustDesk自托管部署的组件拓扑
先说清楚一件事:RustDesk本身是一个开源远程桌面工具,自托管部署时通常包含hbbs(ID/中继服务端)、hbbr(中继服务器)、以及一个API服务器(也叫web api / rustdesk-api-server,一般用Go或Node写的)。很多人第一次部署的时候只关注了hbbs/hbbr能不能连通,端口通不通,却把API服务器当成一个“附属品”随便扔在公网里。
但恰恰是这个API服务器,承载着设备列表、用户登录、token签发、设备分组、日志下发等一系列重要功能。更关键的是,在RustDesk的官方客户端配合自定义API服务器时,客户端的登录验证、设备注册、甚至远程会话的授权逻辑,都会经过这个API。也就是说,API服务器是整条链路里的"闸门"。
如果这个闸门本身不设防,那远端设备等于把凭据和入口都暴露给了攻击者。我见过不少团队,部署了RustDesk后所有东西都能连通,但API服务器直接裸奔——HTTP明文、无认证头校验、数据库口令硬编码在配置里。这种问题一旦被扫描到,整个远程控制体系就形同虚设。
1.2 API服务器面临的主要攻击面
把API服务器暴露在公网,等于给所有人递了一张访问卡。常见的攻击路径有这么几条:
- 未授权接口探测:很多rustdesk-api项目默认提供
/api/login、/api/users这类接口,如果没有做访问控制,攻击者可以直接调用接口遍历用户列表。 - 弱口令撞库:API服务器如果使用数据库存储用户令牌或密码哈希,弱口令一旦存在,配合批量扫描工具很快就会被撞开。
- 中间人窃听:不使用HTTPS时,客户端和API之间传输的凭据、token,在公共网络里如同裸奔。
- 资源耗尽攻击:API服务器默认没有限流时,攻击者可以用脚本并发打接口,直接把服务器拖垮,影响正常远程会话。
- 日志泄露:很多默认配置会把请求体、token甚至密码明文打到日志里,日志一旦被拿到,等于直接交出控制权。
这些攻击面如果不处理,RustDesk再安全也无济于事。所以这篇博文我打算从部署前置、访问链路、API自身、数据存储、运维监控几个层面,把一套完整的加固方案拆开讲清楚。
2. 部署前置:基础环境与访问链路的安全准备
2.1 服务器选型与系统级加固
在配置RustDesk API之前,服务器底子先要干净。我建议从一开始就不要使用官方一键脚本直接跑在root用户下,而是创建一个单独的系统账号。以Linux环境为例:
sudo useradd -r -s /usr/sbin/nologin rustdesk sudo mkdir -p /opt/rustdesk-api sudo chown -R rustdesk:rustdesk /opt/rustdesk-apinologin可以保证这个账号无法直接登录shell,即使API服务器被利用getshell,也无法轻易切换交互环境。系统层面还要做几件事:
- 关掉不需要的服务端口,至少保证本机只监听需要的端口,不要开着SSH后还把API绑定在
0.0.0.0:80上。 - 开启防火墙,只放行必要端口。默认情况下API服务器监听
21148这类自定义端口,配合反向代理时只需要放行80/443。 - 配置fail2ban或者类似工具,针对SSH入口做暴力破解防护,不要把服务器的安全都寄托在密码强度上。
- 定期升级系统补丁,尤其Linux内核版本过旧、存在已知提权漏洞时,一定要及时更新。
我个人的习惯是:部署之前先做一次基础镜像,用最小的发行版(比如Debian精简安装),能不装的软件一律不装。API服务器只依赖运行时和必要库,其他的一概不要。攻击面小了,排查问题时也简单。
2.2 域名、DNS与TLS证书规划
HTTPS不是可选项,是这个方案里第一优先级。不要用自签证书或者HTTP裸连,否则token和密码分分钟被截获。域名规划上我建议单独用一个子域,比如rd-api.example.com,不要和其他业务混在一起。
证书申请方面,用Let's Encrypt的免费证书完全够用,配合certbot自动续期,省心不费钱:
sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d rd-api.example.com申请完之后注意一点:TLS配置不要用默认值,至少启用TLS 1.2以上,禁用旧协议。Nginx里可以这么配置:
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384'; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; ssl_prefer_server_ciphers on;这样配置之后,即使客户端比较老,也能保证加密强度不至于被一把梭。另外不要把证书私钥的权限放开给普通用户,Nginx运行用户只要能读取公钥和私钥文件即可,不要用644权限直接暴露给所有人。
2.3 反向代理层的基础防护
RustDesk API服务器一般不建议直接对外监听,最好在API前面挂Nginx或者Caddy。反向代理的作用不只是转发请求,它可以做以下几层防护:
- HTTP头过滤:只允许特定的HTTP方法(GET、POST、PUT、DELETE),禁止其他方法。
- 请求体大小限制:防止上传超大payload拖垮API。
- 超时控制:避免慢连接占用连接池。
- 访问日志脱敏:不让token和密码出现在日志里。
Nginx关键配置参考:
server { listen 443 ssl http2; server_name rd-api.example.com; client_max_body_size 10m; proxy_connect_timeout 5s; proxy_read_timeout 30s; # 阻止非预期方法 if ($request_method !~ ^(GET|POST|PUT|DELETE)$) { return 405; } location / { proxy_pass http://127.0.0.1:21148; 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; # 去掉不必要的header proxy_hide_header X-Powered-By; } }这只是基础,更细的访问控制放到API层去做,nginx负责网络边界,API负责业务鉴权,两者配合才不会出现单点失效。
3. API服务自身的权限模型与认证强化
3.1 API请求链路中的身份鉴别
很多开源版RustDesk API设计得比较简单,用户登录成功后签一个token,后续请求在请求头里带上Authorization: Bearer <token>。这个模型本身没问题,问题在于很多部署没有检查token的有效性、过期时间、以及token绑定的客户端信息。
我在实际加固时,给API加了一层中间件,所有需要鉴权的接口统一走中间件逻辑:
- 解析请求头里的Bearer token
- 验证token签名(用HMAC-SHA256或者RSA)
- 校验token的过期时间,以及是否被服务端主动吊销
- 检查token关联的客户端ID与当前请求的客户端ID是否一致
- 将解析出的用户信息注入到请求上下文
这样一来,即使某个token被泄露,也无法在过期后继续使用,也无法关联到其他客户端设备。
在实现层面,如果开始就用的是成熟的Go或者Node框架,加中间件不难。关键是别把token校验逻辑散落在每个接口里,那样很容易漏判。统一入口处理,后续再添加新的鉴权规则也很方便。
3.2 令牌生命周期管理与密钥轮换
token不能永久有效。我见过有人配置完之后,token有效期设成一年甚至不设置过期时间。这在远程控制场景里非常危险,因为一旦设备丢失,或者员工离职,token如果还活着,就等于把内部系统的钥匙留在了外面。
建议至少做到:
- 短期令牌(如2小时)用于客户端会话,到期自动刷新
- 长期令牌(如7天)用于设备注册,不可手动刷新
- 服务器端保存一份token黑名单,吊销时直接加入黑名单
轮换HMAC密钥也不能偷懒。如果API服务器的签名密钥写死在配置文件里,且多年不变,一旦泄露,所有历史token都能被伪造。我一般把密钥放在环境变量或者独立的密钥文件中,并且用脚本定期轮换,当前密钥、旧密钥同时支持一段时间,避免所有客户端强制重新登录。
你可以参考这个伪代码逻辑:
def generate_token(user_id, expires_in): payload = { "user_id": user_id, "exp": int(time.time()) + expires_in, "iat": int(time.time()), "jti": uuid.uuid4().hex } token = jwt.encode(payload, current_secret, algorithm="HS256") return token def verify_token(token): try: payload = jwt.decode(token, current_secret, algorithms=["HS256"]) if payload.get("jti") in revoked_tokens: raise InvalidToken return payload except jwt.ExpiredSignatureError: # 可使用旧密钥尝试解析,如仍未过期的token可放行 payload = jwt.decode(token, previous_secret, algorithms=["HS256"]) return payload注意jti字段要生成并记录,便于吊销单个token,而不用全部失效。
3.3 基于IP的访问控制与内部信任边界
如果你的RustDesk API并不是必须要提供给外网设备访问,那最稳的方案就是直接把API服务器放在内网,只允许公司办公网段或者专线网段访问。这比任何应用层防火墙都好使。
比如在Nginx层加白名单:
location / { allow 192.168.10.0/24; allow 100.64.0.0/10; deny all; proxy_pass http://127.0.0.1:21148; }如果确实有外网访问需求,比如在外出差的员工需要远程内网电脑,那就不能只按来源IP锁死,得通过只能使用HTTPS+token。对于这类场景,可以在API前面再加一层防火墙策略,限制来源地域、ASN范围,甚至只允许公司出口公网IP。
实际操作时,我倾向于“多重叠加”:对管理接口(比如用户增删改、密钥轮换、票据查询)一律内网白名单访问;对客户端调用接口,正常走公网HTTPS+token,但也要在反向代理层记录来源IP,一旦发现异常来源,立刻封禁。
3.4 接口动作级权限拆分
这里要说一个很多人都忽略的点:RustDesk API一般不止一种角色,比如管理员和普通用户。如果所有角色都能调用管理接口,权限设计形同虚设。我建议至少在数据库用户表加一个role字段,在中间件里做角色校验:
管理员:可以管理用户、查看全局设备、轮换密钥 普通用户:只能查看自己的设备列表、生成自己的会话凭证 访客/受限用户:只能被远程,不能主动发起会话接口级别做权限控制后,还要保证前端拿到的功能列表也是按角色动态返回的。不要只靠“隐藏按钮”来限制,接口本身必须响应403。后端模型和前端展示是两码事。
4. 数据存储与后台日志安全加固
4.1 数据库访问隔离与加密存储
API服务器在这个方案里最常见的存储关联是SQLite或者PostgreSQL。如果是SQLite,尽量让它只绑定在本地回环地址,不要让数据库监听公网端口。如果是PostgreSQL,则通过密码认证、SSL连接、独立账号三管齐下。
无论哪种数据库,都必须做到以下几点:
- 数据库账号和API服务器账号分开,不要用postgres超级用户
- 数据库连接串中的密码不要明文写在配置文件里,使用环境变量或者密钥管理服务
- 敏感字段加密存储,尤其是用户密码哈希、令牌签名密钥、设备序号
在代码层面,密码哈希不要用MD5/SHA1,至少用bcrypt或者argon2:
from passlib.hash import bcrypt def hash_password(pwd): return bcrypt.hash(pwd) def verify_password(pwd, stored_hash): return bcrypt.verify(pwd, stored_hash)注意bcrypt有72字节的密码长度上限,如果密码过长会被截断。实际选择中可以用argon2,更安全一些,但计算开销稍微高一点。对于小规模部署,bcrypt足够了。
数据库传输层也要加密。PostgreSQL的pg_hba.conf里配置hostssl只允许SSL连接,防止在局域网内被嗅探。
4.2 配置信息脱敏与环境变量管理
配置文件往往是泄露的源头。很多人把API连接串、数据库密码、JWT密钥、甚至管理后台账号密码一起写在config.toml或.env文件里,然后整个仓库提交到Git仓库。这个习惯一定要改。
建议的方案是:
- 配置文件里只保留环境变量引用,比如
${DB_PASSWORD} - 启动API服务时通过 systemd 的环境文件加载,或者通过 Docker Secret 注入
- 敏感信息由专人管理,权限设置为600(仅属主可读写)
- 禁止把配置文件提交到Git仓库,使用
.gitignore排除
systemd 服务示例:
[Service] EnvironmentFile=/etc/rustdesk/api.env ExecStart=/usr/bin/rustdesk-api User=rustdesk Group=rustdesk Restart=always RestartSec=10 NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadOnlyPaths=/其中ProtectSystem=strict和ProtectHome=true可以防止API服务器进程随意修改系统文件,主目录也访问不到。NoNewPrivileges=true则避免进程获得额外的特权。这些systemd的隔离参数虽然不能完全阻止攻击,但能显著提高被利用后的提权难度。
4.3 审计日志与告警配置
日志是做安全分析的关键。API服务器的访问日志至少需要记录以下字段:
- 时间戳(精确到毫秒)
- 请求方法、路径、状态码
- 来源IP和User-Agent
- 用户ID(如果已鉴权)
- 请求耗时
但是要注意,不要把路径中的query参数、header里的Authorization信息直接打印出来。Nginx默认的访问日志会记录完整请求头,需要设置过滤或自定义日志格式。我采用过一种做法:在Nginx层记通用访问日志,在API层记录业务日志,业务日志里面对token做截断,只显示前几位后几位。
配置日志轮转也很重要,避免日志文件无限膨胀。用logrotate按天轮转,保留30天即可。
告警方面,最简单的做法是监控API的非200状态码比例,特别是401、403、500突然增多时,用shell脚本或者Prometheus报警。一通电话提醒,比事后翻日志强十倍。
5. 运维实践中的避坑记录与排查建议
5.1 常见安全误配置
我接触过不少RustDesk部署,很多团队栽在同一个地方:只开了443端口,却把API绑定在公网IP上,以为有HTTPS就安全。实际上HTTP头里带着客户端的真实IP、token,一旦客户端的反代没有处理好,还是会有泄露风险。
整理一份常见误配置清单,可以对照自检:
| 配置项 | 错误示例 | 正确操作 |
|---|---|---|
| API监听地址 | 0.0.0.0:21148 | 127.0.0.1:21148 |
| TLS协议 | TLSv1 TLSv1.1 | TLSv1.2 TLSv1.3 |
| 用户密码 | 明文存储 / MD5 | bcrypt/argon2 |
| Token过期 | 永久 / 一年 | 2-24小时 |
| 数据库端口监听 | 公网可达 | 仅本机/内网 |
| 日志敏感字段 | 记录token | 截断脱敏 |
有一次我给某公司排查问题,发现他们API都跑了一年了,数据库里用户密码还是明文。我问他们怎么做的登录认证,说直接比对数据库字符串。这种操作只能说运气好还没被攻破,但防线完全经不起考验。
5.2 高并发场景下的安全性能平衡
加安全措施肯定会带来性能损耗,比如HTTPS握手、token校验、bcrypt密码哈希。但损耗不至于让服务崩溃,关键在于合理设计。
我在压测时发现,如果每个请求都做一次数据库查询验证token,QPS上到500左右,PostgreSQL的CPU就飙高了。优化方法是:
- token的校验结果用内存缓存(1分钟过期),降低数据库压力
- 密码哈希计算只在登录接口做,其他接口不重复计算
- Nginx和API之间开启HTTP keep-alive,减少握手开销
如果并发量更大,可以在API前面加一层Redis缓存,做短期令牌的校验记录,防止同一token频繁跑数据库。但这会增加部署复杂度,小规模场景没必要。
安全与性能之间,有一条底线不能妥协:认证环节不能跳过。缓存的只能是“校验结果”,不能直接缓存“用户已认证”的结论,因为token可以被吊销。
5.3 应急响应与吊销机制
不管配置多完善,一旦发现token泄露或者API服务器被扫描到异常登录,必须有应急响应流程。我建议提前准备一套脚本:
- 一键吊销指定用户的所有token
- 一键封禁来源IP地址(火烧眉毛时直接iptables)
- 一键切换JWT签名密钥,让所有历史token失效
- 强制所有用户重新登录
举个例子,吊销脚本可以简化为:
#!/bin/bash USER_ID=$1 curl -X POST https://rd-api.example.com/admin/revoke_user_tokens \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d "user_id=$USER_ID"配合管理后台的页面,运维人员可以在手机上一分钟内完成应急处理。千万别等到攻击发生后才开始现写shell脚本,那样太迟了。
5.4 我踩过的几个坑与心得
最后分享几个实际运维中踩过的坑,希望后面的朋友不要重蹈覆辙:
第一个坑:非root用户跑不起来。有些API版本会写日志到当前目录,如果目录属于root且权限为755,非root用户启动时就报Permission denied。解决办法是提前创建好/var/log/rustdesk-api目录,属主设为rustdesk用户,或者直接用systemd的LogsDirectory=rustdesk-api自动管理。
第二个坑:反向代理后客户端获取到的IP都是127.0.0.1。如果你在API代码里用r.RemoteAddr做IP限制,会发现所有IP都变成了localhost。为了让API能拿到真实IP,Nginx里必须设置X-Real-IP和X-Forwarded-For,API里也要选择正确的方式获取客户端IP。建议在API代码里统一封装一个获取真实IP的工具函数,优先检查X-Real-IP头部。
第三个坑:token过期时间设置太短,导致用户频繁掉线。我在测试的时候把token设成了15分钟,结果远程会话超过15分钟就断开了,体验极差。最后把短期token设成4小时,同时允许刷新机制,既保证安全又不影响正常使用。所以过期时间要结合业务场景权衡,不是越短越好。
第四个坑:证书自动续期失败导致服务突然中断。Let's Encrypt的证书三个月有效,如果certbot定时任务没有配置好,过期之后HTTPS握手直接失败,RustDesk客户端就会报连接异常。要定时检查certbot的续期日志,并监控证书到期时间,不要等到报警了才处理。
根据我的经验,RustDesk API服务器的安全配置并不需要非常复杂的商业安全产品,更多是基本功是否扎实。把上文提到的几个层面逐一落实,普通规模的团队用起来已经很放心了。后续如果业务规模变大、设备数超过几百台,还可以考虑把API服务器拆成独立集群,配合更细粒度的审计平台,但那是后话了。