最近安全圈里讨论得最凶的一件事,就是自托管的Dify实例被批量扫描:控制台裸奔、API端点无鉴权、后台被人塞了一堆来路不明的Bot。Dify现在是大模型应用开发里很顺手的一站式平台,不少人拿它搭知识库、做企业内部AI助理,但正因为部署门槛低,很多实例的默认配置直接暴露在公网,成了别人眼里的肉鸡。这标题既然挂着“紧急预警”,我直接把最近实测过的一套修复方案整理成文:先自查暴露面,再做三步关键修复,最后处理善后痕迹。无论你是个人开发者还是小团队运维,只要手上有Dify实例,都建议按顺序过一遍。
1. 漏洞背景:Dify未授权访问的本质与影响
1.1 什么是未授权访问,为什么对Dify伤害特别大
未授权访问,字面意思就是攻击者不需要提供任何身份凭证,就能直接访问系统里的敏感功能或数据。对Dify这类应用来说,问题就非常严重了。一个典型的Dify实例里,有工作流编排、知识库文件、Prompt模板、API Key、对话日志、用户账号体系,这些东西只要任何一个端点没做鉴权,就等于把整套AI应用的核心资产摊在公网上。
有人可能觉得,我数据集里都是些公开文档,聊天记录也没什么机密,被访问一下能怎样。真不是这样。攻击者拿到Dify后台之后,可以做四件非常实际的事情:第一,遍历你配置过的所有大模型供应商Key,用你的配额跑自己的任务,账单算在你头上;第二,直接读取知识库里的全部原始文档,哪怕你本意是内部资料,对一个知识库型应用来说这等于数据裸奔;第三,篡改或复制你的工作流和Prompt,把你的应用整体搬走,或者反向投毒;第四,通过你已经配置好的模型能力做恶意输出、钓鱼文案批量生成,最终追溯源头还是你的IP和域名。
更麻烦的是,Dify的很多API路径是高度可猜测的。控制台相关接口一般在/console/api下,应用服务的对话接口是/v1/chat-messages,而老版本里还有一批未做严格校验的接口。攻击者根本不需要多高深的技术,拿扫描工具全段探测,只要发现访问响应不是401而是200,直接就进来了。这就是标题里说“频发”的原因,不是某个漏洞多复杂,而是暴露的实例太多、利用门槛太低。
1.2 为什么自托管Dify实例特别容易中招
很多自托管用户部署Dify用的是官方提供的docker compose方案,这个方案默认情况下会把所有端口绑定到0.0.0.0,然后云服务器安全组又习惯性放行所有入站流量。两个默认值叠加在一起,服务就直接暴露在公网了。官方文档确实写了要配置反向代理和TLS,但很多教程拿着compose文件一把梭,根本没人提收口的事,环境装好了就立刻开始建应用,谁能想到第一步就埋了雷。
另一个隐患是默认密钥。本地部署的.env文件里会生成一个SECRET_KEY,如果你是从旧版本一路升级上来,或者从别人分享的配置模板里复制过来的,这个Key很可能长时间没换过。它用来签名Cookie、生成内部Token、加密一些敏感数据,一旦泄露,攻击者就可以伪造会话。配合未鉴权的管理接口,等于连爆破都省了。
还有一个大家忽视的点:自托管版没有平台方帮你做安全兜底。Dify云服务由官方统一维护,但自托管实例的防火墙、鉴权、访问控制、版本更新全部落在使用者自己头上。很多小团队或个人的习惯是容器起来之后就不管了,版本常年不升级,安全补丁迟迟不跟进,等到被扫描到的时候,别人已经用旧版本的已知问题进了后台。
2. 修复前必做的三个自查动作
在动手改配置之前,建议先花十分钟搞清楚两件事:你的实例到底有没有暴露在公网,以及被暴露的现状到了什么程度。不做自查就直接改配置,很容易出现“改完觉得安全了,其实入口还开着”的情况。
2.1 自查一:确认实例是否已经暴露在公网
先看监听状态。在部署Dify的服务器上执行:
ss -tlnp | grep -E "80|443|3000|5001"如果看到类似0.0.0.0:80或0.0.0.0:3000的监听,说明服务在等待所有网卡的连接。接下来用另一台不在同一内网的机器,或者直接用手机流量访问http://你的服务器IP,如果页面能正常加载出Dify的登录页或控制台,那就已经暴露了。
还有一种情况是安全组层面挡了,但本机防火墙没开。很多云服务器默认安全组是全放行的,Ubuntu的ufw和CentOS的firewalld默认又是关闭状态,这时候即使本机没监听端口,安全组也把流量放进来了。建议顺手检查一下:
sudo ufw status sudo firewall-cmd --list-all如果这两个命令显示firewall是inactive或没有规则,就该意识到当前实例的访问控制几乎是空白的。再配合一个从外部视角的检测,直接在本地电脑上访问服务器IP的80和443端口,这是最直观的验证方式。
2.2 自查二:摸清当前版本和敏感配置现状
确认自己的Dify版本和配置状态,需要进入部署目录。如果是docker compose部署,先找到docker-compose.yaml所在的目录,然后执行:
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"这样能看到api、web、worker各容器的镜像标签,标签里的版本号就是当前运行版本。接下来看.env文件里的关键项:
grep -E "SECRET_KEY|GUEST|INIT_PASSWORD|API____KEY" .env重点关注三处:一是SECRET_KEY是不是一个高强度随机值,如果看起来像your-secret-key这种占位符,基本等于没有;二是是否存在游客模式相关的开关,如果开着,说明没登录的人也能访问部分功能;三是有没有配置过初始管理员密码的强校验选项。这些信息决定你第三步要改哪些东西。
2.3 自查三:检查是否有异常账号、应用和API Key
如果实例已经暴露了一段时间,光看配置还不够,要查一下数据库里有没有被塞进来的脏数据。Dify默认使用PostgreSQL,执行:
docker compose exec db psql -U postgres -d dify先看用户表:
SELECT id, email, status, created_at FROM users ORDER BY created_at DESC LIMIT 50;重点看有没有近期批量注册的陌生邮箱,尤其那些域名看起来像临时邮箱的。接着看应用列表和API Key列表:
SELECT id, name, created_at FROM apps ORDER BY created_at DESC LIMIT 100; SELECT id, type, last_used_at FROM api_tokens ORDER BY last_used_at DESC NULLS LAST LIMIT 100;如果发现有自己从未创建过的应用,或者API Key的使用时间在自己不知情的时间点,那说明已经被访问过了。这一步的自查结果直接决定第三步是只做配置修复,还是需要做一次全量清理。
3. 三步完成关键修复
市面上很多安全文章喜欢罗列一堆参数,看起来严谨,实际操作时反而不知道该先做哪件事。我按止损优先级把Dify的未授权访问修复拆成三步:第一步关入口,第二加固身,第三步清善后。顺序不能乱,因为入口不关,后面改什么配置都可能被再次绕过。
3.1 第一步:把服务从公网剥离开来,只通过网关访问
这一步的核心思路是让Dify的原有端口不再直接暴露给外部网络,所有外部流量都经过一层由你控制的代理网关。容器本身不知道外面谁是好人坏人,但Nginx或Caddy可以在更靠外的位置做访问控制。
先修改docker-compose.yaml里的端口映射。打开文件后找到web服务的ports段落,默认大概长这样:
services: web: ports: - "80:80"把它改成只绑定到本机回环地址:
services: web: ports: - "127.0.0.1:80:80"如果服务器上已经装了Nginx,直接让它监听公网端口并把请求转发到http://127.0.0.1:80。保存后重建容器:
docker compose up -d这时候再从外网访问原来的IP,应该已经连不上Dify了。但光这样还不够,因为云安全组如果放行了80,流量还是会到Nginx。所以继续在Nginx配置里加上访问控制,我用一个基本的server块示例:
server { listen 443 ssl http2; server_name dify.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.htpasswd; location / { proxy_pass http://127.0.0.1:80; 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_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }auth_basic就是HTTP基本认证,相当于给整站加了一层“前置门锁”,密码用htpasswd工具生成:
sudo apt install apache2-utils -y htpasswd -c /etc/nginx/.htpasswd difyadmin如果你希望更精细一些,还可以用IP白名单替代或叠加Basic Auth。我自己常用的做法是:办公网段直接放行,其余网段要求Basic Auth,既方便内部使用,又挡住了公网扫描器。
提示:改完端口映射和Nginx配置之后,一定要从外部网络实际访问一次,确认原来的端口已经无法到达。很多人在这一步只改了配置没重启容器,等于白做。
3.2 第二步:加固Dify自身配置,强制轮换密钥并升级版本
网关层是外部防护,Dify自身的配置是内部防线。第二步的核心是把所有该换的密钥全部换掉,把默认弱配置全部纠正,同时把版本升级到当前已修复问题的最新版。
先更新.env里的SECRET_KEY,生成一个新的高强度随机值:
python3 -c "import secrets; print(secrets.token_hex(32))"把输出填入.env:
SECRET_KEY=随机生成的那一串接下来检查并关闭游客模式。不同小版本里这个开关的名字略有差异,常见的是GUEST_MODE或ENABLE_GUEST相关键,设置为false即可。如果.env里找不到,就去官方文档里查当前版本对应的变量名。同时检查初始化密码相关的配置,如果之前用的是默认初始化密码,现在必须强制修改一次。
改完配置后重建容器让配置生效:
docker compose up -d然后带着新的配置重新初始化管理员账号。如果原本的管理员密码已经被别人动过,这一步尤其重要。执行后打开控制台,进入账号管理,把所有不认识的账号全部停用或删除,给保留的账号重新设置强密码,并开启二步验证。
API Key也要全部重置。进入每个应用的“访问API”页面,把旧的Key全部删除,生成新的Key,然后更新你所有业务系统里的密钥。这一步很麻烦,但必须做,因为旧Key可能已经被扫描器抓走并用于调用你的模型额度。
最后升级Dify版本。在部署目录下执行:
git pull docker compose pull docker compose up -d如果你在compose文件里做过自定义修改,git pull可能会提示冲突,建议升级前先备份整个部署目录。版本升级之所以重要,是因为很多未授权访问问题已经在后续版本里修复了,单靠外部网关挡不了所有未知接口,版本本身必须有对应的安全补丁。
3.3 第三步:清理痕迹,确认没有遗留后门
配置修好了不代表事情结束了。如果之前已经被人进过后台,数据库里很可能留着他们创建的账号、应用、API Key,甚至可能有人改了Prompt模板做钓鱼用。第三步是用审计的方式把脏东西捞出来清干净。
先看容器日志,找出最近的访问来源:
docker compose logs api --since 24h | grep -E "console/api|v1/chat-messages" | awk '{print $NF}' | sort | uniq -c | sort -nr | head -20如果看到大量来自陌生IP的/console/api访问,结合前面数据库里的异常记录,基本可以判断被访问过。把这些IP记下来,在防火墙里永久封禁:
sudo ufw deny from 1.2.3.4数据库里清理异常用户和关联数据。只删除users表里的记录通常会因为外键约束失败,需要先清理子表数据。经验操作是:把可疑用户查出来后,记录其ID,依次删除messages、conversations、api_tokens、workflow中与该用户关联的数据,最后再删users记录。为安全起见,动手前先做一次全量备份:
docker compose exec db pg_dump -U postgres dify > dify_backup_$(date +%F).sql清理完数据库后,检查一下容器里有没有被挂载的异常目录或新增的定时任务。攻击者有时会利用未授权接口写入计划任务,在宿主机上执行crontab -l看看有没有可疑条目,再检查一下/tmp和/var/spool/cron下有没有奇怪的文件。
最后把日志和监控接起来,别让下一次暴露再次裸奔。最朴素的做法是把Nginx的access log丢到ELK/Loki里,或者在服务器上写一个定时脚本,每十分钟扫描一次/console/api和/v1路径的访问来源,有陌生IP就告警。下面是几个关键配置的检查清单,直接对着看就行:
| 检查项 | 合规状态 | 处理方式 |
|---|---|---|
| 80/3000端口监听地址 | 必须只监听127.0.0.1 | 修改docker-compose.yaml的ports |
| 外部访问是否经过反代 | 必须是Nginx/Caddy网关 | 配置代理并启用TLS |
| 是否启用Basic Auth或白名单 | 必须至少启用一项 | 配置访问控制 |
| SECRET_KEY强度 | 必须是随机高熵值 | 重新生成并更新.env |
| 游客模式 | 必须关闭 | 设置对应环境变量 |
| 管理员账号 | 仅保留自己人 | 清理异常用户并改密码 |
| API Key | 全部重新生成 | 逐个应用更新 |
| Dify版本 | 更新到最新版 | git pull && compose pull |
| 日志监控 | 能看到访问来源 | 接入日志系统或定时脚本 |
4. 常见问题与排查技巧实录
4.1 后台已经有人建了未知应用,为什么删不干净
这个我实际遇到过。表面上是删了一个恶意应用,但过几天又出现新应用,原因基本是两个:要么管理员账号密码没有改,攻击者还在通过正常登录方式进来;要么你遗留了旧的API Key,攻击者通过API直接创建应用。所以处理顺序一定要正确:先重置所有账号和密钥,再清理脏数据,最后再封掉来源IP。如果顺序反了,这边删那边建,永远清不完。
另一个常见原因是删除应用时没有清理关联知识库。Dify的应用和知识库之间有引用关系,直接删应用可能留下孤立数据,建议进入知识库页面把可疑内容一并删除。删除前先备份数据库,避免误删自己正常的数据。
4.2 修复后原有API Key全部失效,业务报错怎么办
这基本是预期内的结果,因为旧Key已经不可信。业务侧会有一段时间的适配期,需要批量替换所有客户端环境变量里的Dify API Key。如果应用数量多,建议先做一份映射表:应用名、旧Key、新Key、调用方负责人,然后按业务优先级逐个替换。替换完成后重点测试一个消息流接口和一个文件上传接口,确认新Key权限正常。
4.3 反代之后出现400、413或WebSocket断连
这个问题经常出在Nginx配置缺失上。400通常是proxy_set_header Host没设置,Dify收到的请求头不带域名导致校验失败;413是上传知识库文件时client_max_body_size太小,默认1M肯定不够,建议设置到100M以上;WebSocket断连则是缺少Upgrade相关请求头。把第一步配置里那两行proxy_set_header Upgrade和Connection "upgrade"加上,一般就能解决。
4.4 公网封了,但内网其他容器会不会横向访问
有这个担心是对的。容器之间在同一个Docker网络里互访是不需要经过宿主防火墙的。如果你的服务器上还跑了其他业务容器,而这些容器被攻破,攻击者很可能会从内网直接访问Dify的端口。更稳妥的做法是把Dify的关键服务放入独立的Docker网络,只在网关容器上开放对外映射。或者更进一步,在内网侧也加一层访问控制,例如通过云平台的安全组限制只有指定跳板机才能访问该服务器。
4.5 升级之后原有应用数据还在吗
在。Dify的数据都存在PostgreSQL和向量数据库里,升级容器不会主动清空数据。但我见过有人在升级时直接用docker compose down -v把卷一起删了,这会把数据全部清掉。正确的升级过程是docker compose pull加docker compose up -d,绝对不要带-v参数。升级前备份数据库是我反复强调的事,尤其是你已经发现自己被扫过的情况下,备份意味着你随时有后悔药。
5. 后补的一些经验
这几步做完之后,我的习惯是把每次修复过程整理成简短的运维记录:什么时候发现暴露、改了哪些配置、换了哪些Key、封了哪些IP。记录看起来不起眼,但下次再出问题时会省很多时间。Dify这类平台以后可能还会出现新的安全问题,但只要你把网关层、应用层、版本层这三道防线都立住,绝大多数批量扫描到你这儿就会直接跳过。
最后再分享一个我自己的小技巧:修复完成后不要急着把旧日志清掉,留着至少一个月。因为有些攻击者会在你修复前就抓走过知识库内容,等到数据泄露事件真正暴露出来时,旧日志能帮你判断泄露时间窗口和数据范围。如果你有条件,尽量在网关层记录完整的请求体和响应体大小,这样后续做取证时有据可查。安全这件事,从来都是“谁暴露谁负责”,动手越早,损失越小。