
1. 为什么本地模型服务也要做安全加固前几天我在自己的开发机上部署了一套基于 nanobot 的 Qwen3-4B 推理服务本来想着反正是测试环境能跑通就行结果第二天看系统日志就发现有人在扫端口而且目标正好是模型服务默认监听的 8000 端口。那一刻我才意识到一个很现实的问题本地部署不等于安全部署模型服务一旦裸奔在外网上就等同于把你的算力、数据和 API 无条件敞开给别人。这篇文章就是把你拉回现实的那只手。我会完整走一遍 nanobot Qwen3-4B 的部署加固流程重点解决三个问题防火墙怎么放行、SELinux 怎么不误伤模型服务、以及怎么用最小权限让服务进程没有多余能力可滥用。适合刚接触本地模型部署的读者也适合那些已经跑起来但不知道怎么收尾的人。虽然看起来是安全教程但核心思路其实是让服务跑得更可控、更稳、更不容易被当跳板。说白了安全加固不是给大公司做合规用的而是每个跑过模型服务的人都应该有的基本操作。下面我按三个阶段来讲先把服务跑起来拿一个基线然后加网络层防护再做系统层隔离最后补掉那些藏在细节里的坑。2. 服务搭建与基线运行2.1 用 nanobot 拉起 Qwen3-4B 服务nanobot 是一个很轻的模型服务管理工具它不会像某些重型框架那样把依赖装一地而是把模型加载、推理接口、进程生命周期这些都收拢到一个命令里。我这边拿到的版本是一个单二进制文件直接放在/usr/local/bin/nanobot就能用配置通过命令行参数传入日志输出到标准输出方便被 systemd 收集。第一次运行时我是这样启动的/usr/local/bin/nanobot --model /models/qwen3-4b-instruct-q4_k_m.gguf --host 0.0.0.0 --port 8000这段命令的意思是加载qwen3-4b-instruct-q4_k_m.gguf这个量化后的模型文件监听所有网络接口的 8000 端口提供 OpenAI 风格的兼容推理接口。不同版本的参数名略有差异如果你手里的版本参数不一样先跑一下nanobot --help看一眼再操作这个习惯能省掉很多莫名其妙的报错。我选 q4_k_m 量化版本的原因是Qwen3-4B 原始权重在 FP16 下大约要占 8GB 显存而 q4_k_m 量化后权重降到 4GB 左右推理时的激活值和 KV cache 再占 2-3GB一张 8GB 显存的普通显卡就能舒服地跑起来。如果你只有 CPU 环境也建议先用这个量化级别内存占用同样可控。2.2 模型文件应该放在哪里很多部署教程会让你把模型随便丢在/root/models或者~/downloads下面这其实埋了两个雷一是 root 目录下的文件普通用户读不到二是系统强制访问控制也就是 SELinux会对奇怪位置的程序文件给出一堆看不懂的拒绝日志。所以从最初就遵守一个约定的目录结构后面会省很多事。我个人用的是/var/lib/nanobot/下面再分两个子目录/var/lib/nanobot/models/ # 模型文件 /var/lib/nanobot/run/ # 进程运行时状态模型文件从仓库下载后放到/var/lib/nanobot/models/下面。文件名我建议写清楚型号和量化格式比如qwen3-4b-instruct-q4_k_m.gguf不要只叫model.bin。不然你过两个月回来看这个目录根本不知道里面是什么。2.3 不能一上来就用 root 跑服务直接在 root 下启动模型服务从能把服务跑通的角度看确实最快但从加固角度讲这是最差的起点。root 进程一旦被攻破攻击者就直接拿到整台机器。哪怕你的服务只是放在内网环境我也强烈建议从一开始就规划好专用账户。不过先不急着建账户这里你只要记住一个原则后面所有加固动作都是围绕让这个服务以一个受限的身份运行来展开的。如果你现在已经用 root 跑起来了也没关系咱们在最小权限那一节把它挪到一个独立用户下面去。基线跑通之后先确认两件事第一curl http://127.0.0.1:8000/v1/models能看到模型信息第二ss -tlnp | grep 8000能看到监听地址确实是0.0.0.0:8000。这两条命令是后面所有安全验证的锚点后面每做一步加固都要回来测一遍确认服务没有被锁死。3. 第一道防线按需收口的防火墙策略3.1 先搞清楚你到底需要谁访问防火墙配置之前先问自己一个问题这个 Qwen3-4B 服务是要给谁用的答案决定放行策略。如果你的服务只跑在本机由本机的其他程序通过127.0.0.1:8000调用那么防火墙策略最干净——什么都不用放行因为回环接口默认就是允许的。如果想让同一内网网段的开发者机器访问那就在防火墙里放行指定网段。如果想暴露到公网我不建议直接在模型服务端口上开公网更稳妥的做法是前面套一层反向代理后面有需要的话再单独聊。我自己的测试环境用的是第二种方案允许办公网段的机器直接访问其余外部请求一律丢弃。这个方案的好处是配置简单而且即使模型服务本身有漏洞外部扫描器也无法触达。3.2 firewalld 最小放行配置以常见的 firewalld 为例操作思路是默认区域保持拦阻只给来源网段开特定端口。# 查看当前默认区域 firewall-cmd --get-default-zone # 查看默认区域已有的规则 firewall-cmd --zonepublic --list-all # 添加一条富规则仅允许 192.0.2.0/24 网段访问 8000 端口 firewall-cmd --permanent --zonepublic --add-rich-rulerule familyipv4 source address192.0.2.0/24 port port8000 protocoltcp accept # 重载防火墙 firewall-cmd --reload # 确认规则已生效 firewall-cmd --zonepublic --list-all这里有一个我踩过两次的坑--add-rich-rule后面如果没带--permanent规则会立刻生效但防火墙进程一重启就消失。测试的时候可以用临时规则验证生产环境一定要带--permanent再 reload否则某次重启之后服务就神秘失联了。如果你希望更严格一点还可以把默认区域从 public 改成 drop这样所有未匹配的入站包直接丢弃而不是返回拒绝扫描器会以为这个 IP 根本不存在减少暴露面。改之前务必确认你远程登录管理机器的端口已经被放行否则下一步就是搬着显示器去机房救机器了。3.3 验证防火墙到底挡没挡住配置不是写完了就完事验证才算数。我从内网另一台机器上执行# 从被允许的网段测试 nc -vz 192.0.2.10 8000 # 从其他网段测试 nc -vz 203.0.113.8 8000被允许的网段返回succeeded其他网段返回timed out或者no route to host这就说明防火墙规则生效了。如果两个网段都能通八成是规则没匹配上或者服务监听的是0.0.0.0且默认区域本身就放行了所有端口。再检查一下firewall-cmd --list-all-zones看看哪个 zone 处于 active 状态有时候服务连上了错误区域的网卡规则就白写了。另外不要忘记检查服务本身监听的地址。如果 nanobot 启动参数里写的是--host 0.0.0.0那么防火墙即使只放行了 8000 端口也意味着所有网卡上这个端口都能被网段规则命中。更稳妥的做法是让服务只监听回环地址127.0.0.1:8000然后由局域网内其他机器通过 SSH 隧道或者反向代理转发的方式访问。这一步后面最小权限部分还会再提。4. 第二道防线让 SELinux 帮你挡住路径级风险4.1 先看看 SELinux 当前的状态很多人在 Linux 上部署模型服务时SELinux 是直接被忽略或者干脆setenforce 0关掉的。关掉确实能让服务立刻正常但等于把系统最值钱的一道防线亲手拆了。SELinux 的作用是细粒度的访问控制即使进程被攻破它也无法随意读取/etc/shadow、无法绑定高权限端口、无法在你的用户目录里乱翻。动手之前先检查机器当前状态getenforce输出如果是Enforcing说明强制模式已经开启如果是Permissive说明只是记录日志不拦截如果是Disabled说明内核启动时就没启用需要改/etc/selinux/config并重启机器。我的建议是加固过程中把模式先设成Permissive让系统把所有应该拦截的行为都记到审计日志里等规则全部补齐之后再切回Enforcing。别一上来就强制否则服务跑不起来你会花一整天跟 AVC 日志搏斗。也别一上来就关掉那跟没要这层防护没区别。4.2 给模型目录和应用打文件上下文标签SELinux 管文件的方式是标签而不是路径。你放在/var/lib/nanobot/models/下的模型文件默认标签很可能是var_tnanobot 进程在某个受限域里运行时能不能读这个文件要看策略里有没有对应规则。先用ls -Z看看当前标签ls -Z /var/lib/nanobot/models/qwen3-4b-instruct-q4_k_m.gguf我这边看到的是var_t进程访问时被拒绝了。解决办法不是chmod 777而是给目录设置一个让服务进程可以读的类型标签。我这里选了一个在多媒体服务器场景里很常用的做法把它标记为httpd_sys_content_t因为这个类型本身就表示Web 服务要从这里读静态内容nanobot 进程通过allow规则就能访问这类标签的文件semanage fcontext -a -t httpd_sys_content_t /var/lib/nanobot/models(/.*)? restorecon -Rv /var/lib/nanobot/models改完再ls -Z确认标签变成了httpd_sys_content_t。口令是semanage fcontext改的是持久化记录restorecon是把记录应用到实际文件。只跑restorecon不写semanage下次文件更新时标签会被重置这个细节极易踩坑。4.3 给非标准端口加 SELinux 端口标签SELinux 对端口也有管理。nanobot 监听 8000 端口这个端口并不在所有默认策略的http 端口列表里所以即使防火墙放行了SELinux 依然可能拦截 bind 操作。验证办法是先看审计日志里有没有端口相关的拒绝记录ausearch -m avc -ts recent | grep 8000如果有类似commnanobot的socket或name_bind拒绝记录就需要把端口加入 http_port_t 类型的端口集合前提是你的 nanobot 进程域被策略允许绑定这个类型的端口semanage port -l | grep http_port_t semanage port -a -t http_port_t -p tcp 8000这里有个需要说明的点SELinux 端口标签和进程域必须配合。如果进程本身跑在一个受限域里且没有绑定 http_port_t 的 allow 规则单独加端口标签是不够的。所以加完端口还是拒绝的话继续走下面的 audit2allow 流程。4.4 用 audit2allow 处理剩余拒绝但不能无脑 allow剩下那些零散的拒绝最实用的工具就是audit2allow。它会审计日志里的 AVC 拒绝记录生成一条最小化的 SELinux 策略模块# 查看最近被拒绝的日志 ausearch -m avc -ts recent # 把最近的拒绝生成策略模块 ausearch -m avc -ts recent | audit2allow -M nanobot_local生成的nanobot_local.pp文件用semodule -i nanobot_local.pp安装。装完再跑一次服务观察是否还有新拒绝。但我必须提醒一句audit2allow是把日志里的拒绝转成放行规则它不是安全专家。如果你发现拒绝日志里出现execstack、ptrace、writes to /etc这类敏感操作一定要先搞清楚程序为什么要这么做再决定要不要放行。我见过有人把一整份包含几十条规则的模块直接装上去事后才发现有一条规则允许了进程写/etc/passwd这种加固比不加还危险。安全加固的目的不是让所有日志变绿而是让服务只拿到它真正需要的那一点点权限。5. 第三道防线最小权限让进程身无长物5.1 创建专用账户并收敛目录权限前面说过不要用 root 跑服务现在正式来操作。创建系统账户表示类型上不允许登录 shell目录划分到/var/lib/nanobot下useradd -r -s /usr/sbin/nologin -d /var/lib/nanobot nanobot mkdir -p /var/lib/nanobot/models /var/lib/nanobot/run chown -R nanobot:nanobot /var/lib/nanobot chmod 750 /var/lib/nanobot注意-r表示这是一个系统账户不会出现在登录界面上-s /usr/sbin/nologin表示任何想要su nanobot的尝试都会被拒绝目录权限750意味着同组用户可以进入但其他人不可以。这里如果你把权限改成777那最小权限的第一步就白做了。5.2 用 systemd 服务单元固化完整 sandbox手工启动 nanobot 只适合调试。真正的部署应该交给 systemd好处是开机自启、崩溃自动重启、还能叠加一系列沙箱参数。我用的服务单元文件长这样[Unit] DescriptionNanobot Qwen3-4B inference service Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Usernanobot Groupnanobot ExecStart/usr/local/bin/nanobot --model /var/lib/nanobot/models/qwen3-4b-instruct-q4_k_m.gguf --host 127.0.0.1 --port 8000 WorkingDirectory/var/lib/nanobot NoNewPrivilegesyes ProtectSystemstrict ProtectHomeyes PrivateTmpyes ProtectKernelTunablesyes ProtectKernelModulesyes ProtectControlGroupsyes RestrictSUIDSGIDyes RestrictRealtimeyes CapabilityBoundingSet MemoryMax8G TasksMax64 LimitNOFILE1024 [Install] WantedBymulti-user.target把文件放到/etc/systemd/system/nanobot.service然后systemctl daemon-reload systemctl enable --now nanobot.service systemctl status nanobot.service这里有几个参数我得单独解释因为它们不是摆设ProtectSystemstrict整个文件系统对服务变成只读服务只能在ReadWritePaths里指定的路径写入。如果你没在 ExecStart 之外加任何可写路径但服务需要写临时文件它就会失败。ProtectHomeyes服务无法访问/home、/root下的任何内容防止它读你的私人文件。CapabilityBoundingSet清空所有 Linux capabilities。这是个容易被忽略的点——如果你的服务在普通用户下运行它本来就不该有 root 能力显式清空后即使程序代码里某个地方试图提升权限系统也不会给它任何机会。NoNewPrivilegesyes禁止进程通过execve获取新的权限这个参数对很多提权漏洞有很好的兜底作用。MemoryMax8G、TasksMax64、LimitNOFILE1024限制内存、线程树规模、文件描述符数量。Qwen3-4B 在 8G 内存限制下不会溢出到把整机拖死进程被攻破后没有能力开启几百个线程继续横向扩散。有一个容易踩的坑是我每次都要强调的ProtectSystemstrict加上之后/var/lib/nanobot/run目录对进程来说是只读的如果程序需要在运行目录回写 state 文件就会启动失败。解决办法是在服务单元里增加一行ReadWritePaths/var/lib/nanobot/run改完要再跑systemctl daemon-reload再 restart。不要小看这步我在实验环境里就是少了这一行导致服务一直statusexit-code排查了半天才反应过来。5.3 验证最小权限是否真的生效单元文件写好后用一个 systemd 自带的安全审计命令来评估当前的暴露面systemd-analyze security nanobot.service输出会给出一个从 0 到 10 的暴露分数值越低越安全。默认服务通常是 9 以上我这个单元文件配置完评分能压到 2 左右。这个命令把抽象的安全细节变成直观的数字用来对比加固前后效果特别有说服力。再手动确认几件事进程是不是真的跑在nanobot用户下监听地址是不是真的只在回环ps -o user,group,cmd -C nanobot ss -tlnp | grep 8000第一行应该看到nanobot而不是root第二行应该看到监听地址是127.0.0.1:8000而不是0.0.0.0:8000。只有这样外部网段即使端口探测到了也无法直接连进来要访问模型服务只能先 SSH 到这台机器再做端口转发。6. 加固效果验证与排障实录6.1 分层验证清单按下面这个清单逐条过每一层都有明确的预期结果检查项命令预期结果防火墙规则firewall-cmd --zonepublic --list-all只看到 ssh 端口和 8000 端口的来源限制服务监听ss -tlnp | grep 8000监听地址是 127.0.0.1:8000SELinux 状态getenforceEnforcingSELinux 拒绝ausearch -m avc -ts recent没有新增拒绝记录运行用户ps -o user,cmd -C nanobot用户是 nanobot资源限制systemctl show nanobot.service -p MemoryMax -p TasksMax输出 8G / 64安全评分systemd-analyze security nanobot.serviceExposure 等级为 0-2验证之前先确认服务还能正常响应curl http://127.0.0.1:8000/v1/models。安全加固做得再漂亮模型接口不能用了也不合格。6.2 常见问题速查现象大概率原因正确处理外部访问超时防火墙规则没带--permanent或没 reload重加规则并firewall-cmd --reload服务启动即退出ProtectSystemstrict导致运行目录不可写加ReadWritePaths/var/lib/nanobot/run日志里有 Permission deniedSELinux 标签不对或目录权限不对看ls -Z和ausearch -m avc内存一直涨最终被杀MemoryMax设置低于模型推理峰值观察journalctl里 OOM 记录后调大外部端口探测是 closed默认 zone 设置的 drop 生效这是期望行为固件更新后 SELinux 拒绝变多自定义模块与策略版本冲突semodule -l查看必要时重建模块6.3 一个真实排障思路的复盘有一次我改完防火墙规则后内网其他机器突然连不上服务了。我先以为规则写错了结果firewall-cmd --list-all一看8000 端口规则还在。又排查半天发现是默认 region 变了——我当时给另一张网卡改了默认 zone导致服务所在的网卡落入了drop区域。这种问题用firewall-cmd --list-all-zones一眼就能看出来但如果你只知道用默认 zone 去排查很容易绕弯路。后来我又遇到过一次模型加载很慢当时怀疑是 SELinux 在反复检查文件看了ausearch又没有拒绝记录最后发现是磁盘 IO 被另一个高负载进程占满了。很多看似安全相关的问题实际上是性能问题。排障的通用顺序我总结为三句话先看journalctl -u nanobot.service拿服务自己的日志再看ausearch -m avc -ts recent拿系统拦截记录最后用ss和firewall-cmd确认网络链路正常。按这个顺序走90% 的问题都能定位到具体环节。回到我开头提到的那个被端口扫描的场景。做完这三层加固之后我再用外部机器扫这个端口得到的响应是timeout服务如同隐身。而我 SSH 进服务器本机通过回环地址正常调用 Qwen3-4B 推理接口响应速度和之前没有差别。这种你该访问的人不受影响不该访问的人进不来的状态才是模型服务应该有的样子。我个人在这套流程里反复体会最深的一点是安全加固不是某一个命令的事而是把网络、系统、进程三层习惯都养好。防火墙挡的是门SELinux 管的是房间钥匙最小权限是让进到房间里的人发现自己什么都拿不到。这三件事做完哪怕以后模型本身出现新的漏洞你也有足够的时间去响应而不是一晚上就被拖进矿池或者变成代理跳板。最后再送你一个小技巧每一次改了防火墙或者 SELinux 策略之后都在终端里留一条带日期的记录比如把当前配置导出保存一下。别小看这个习惯等过两个月你再回来看这台机器能一口说出当时为什么这样配的人只有当时的你自己。