上一篇我们在 CentOS Stream 10 上把 vsftpd 装好、让 FTP 服务能正常启停了。但你如果真拿系统里的真实账号去登录一次,多半会发现这条路不太好走:密码跟 SSH 登录是同一个,权限全靠系统目录去卡,用户想上传个文件你还得斟酌半天该不该给写权限,一个账号一旦泄露,等于把系统登录凭证也搭了进去。所以生产环境里我几乎不用"系统本地账号直登 FTP"这套,用的都是虚拟用户访问。
虚拟用户说白了,就是给 FTP 单独设计的账号体系。它只活在 vsftpd 的认证库里,跟 /etc/passwd 没有任何关系,密码再长再乱也不会牵连系统登录,多用户之间的目录隔离、权限控制也能做得更干净。这篇文章就接着上一篇往下走,把 CentOS Stream 10 + vsftpd 虚拟用户配置整个流程过一遍,包含账号规划、Berkeley DB 认证库、PAM 模块调用、SELinux 与防火墙放行,最后把我这几年来遇到的各种报错情况整理成一份排查速查表,照着敲基本上不会卡壳。
1. 为什么不用本地用户:三种认证模式的实际对比
1.1 匿名、本地用户、虚拟用户,到底差了些什么
FTP 认证模式翻来覆去就三种:匿名、本地用户、虚拟用户。很多新手上来默认用了本地用户方案,就是因为在 vsftpd.conf 里看到local_enable=YES以为这是唯一选项,实际上这是最容易埋雷的做法。
- 匿名模式(anonymous_enable=YES):不需要账号密码,适合公共资源共享下载。它的所有访问都映射到 ftp 或 anonymous 用户,权限结构最简单,基本不考虑多用户隔离。
- 本地用户模式(local_enable=YES):直接用系统账号登录,好处是账号管理跟系统统一,缺点是密码、家目录、登录权限全部耦合在一起。你想给 A 一个 FTP 账号,就等于给他一个能登录系统的真实凭证,哪怕把 shell 设成 nologin,心里的不踏实感还是消不掉。
- 虚拟用户模式(guest_enable=YES):账号密码存在独立的数据库里,跟系统用户完全脱钩。认证通过后,由 vsftpd 把本次会话统一映射到一个指定的系统用户身份上,权限力度再通过独立配置细化。
一句话概括:匿名是"开门迎客",本地用户是"拿家门钥匙迎客",虚拟用户是"给客人发单独的临时门卡"。
1.2 vsftpd 的 guest 机制到底是怎么工作的
虚拟用户这套方案,核心是 vsftpd 的 guest 机制,理解它之后排查问题会顺手很多。
- FTP 客户端发起登录,提交用户名密码。
- vsftpd 把认证请求抛给 PAM 模块,PAM 再调用 pam_userdb.so 去查指定的 Berkeley DB 数据库文件。
- 数据库里账号密码匹配成功,PAM 返回通过。
- vsftpd 将会话身份切换到 guest_username 指定的系统用户(比如 vftp)。
- 后续所有文件读写、目录遍历,都以 vftp 这个系统身份去执行。
这个设计最妙的地方在于:虚拟用户只存在于 FTP 认证体系里,就算密码被人看光了,别人也拿它登录不了系统,因为没有对应的系统账号。而且多个虚拟用户最后共享一个系统身份,不会出现 /etc/passwd 里账号膨胀的问题,真要给每个人单独目录和权限,再通过 user_config_dir 机制做差异化配置就行。
2. 动手前的准备:环境确认与基础安装
2.1 先确认系统状态,别急着装包
虽然标题写的是 CentOS Stream 10,但我仍然建议先看一眼系统版本和防火墙状态,尤其是手头有多台服务器的情况下:
cat /etc/redhat-release dnf repolist | head -20CentOS Stream 10 的仓库默认就带 vsftpd,正常执行安装就能拉到包。另外确认一下 SELinux 当前的模式,我见过不少新手直接把 SELinux 关掉来排除问题,这是最不推荐的做法,后面会专门讲怎么正确放行。
getenforce如果是 Enforcing,照常往下走,别关。
2.2 安装 vsftpd 和数据库工具
虚拟用户认证需要使用 Berkeley DB 格式的数据库文件,创建数据库依赖 db_load 命令,这个命令在 libdb-utils 包里。很多人只装了 vsftpd,结果卡在这一步:
dnf install -y vsftpd libdb-utils装完后顺手确认一下服务是否存在:
systemctl list-unit-files | grep vsftpd2.3 规划账号目录结构
动手之前先想清楚三件事:映射到哪个系统用户、虚拟用户的根目录放哪、独立配置目录放哪。我习惯这样规划:
- 映射用户:vftp,shell 设置为 /sbin/nologin,主目录 /data/ftp
- 虚拟用户示例:user01、user02
- 独立配置目录:/etc/vsftpd/vuser_conf
这一步先规划好,后面能少改好几次配置。规划映射用户时,目录不一定要放在 /var/ftp,放在独立的 /data/ftp 更清晰,也方便挂载独立磁盘或做存储扩容。
3. 搭建虚拟用户认证体系:从文本到 Berkeley DB
3.1 创建映射用的系统用户
虚拟用户最终要落到一个系统身份上,所以先建这个"幕后用户":
useradd -s /sbin/nologin -d /data/ftp vftp mkdir -p /data/ftp chown vftp:vftp /data/ftp-s /sbin/nologin这一项很关键,相当于给这个账号上了锁,防止它被用于 SSH 登录。如果你用的是 CentOS Stream 10 或类 Red Hat 系系统,注意检查一下 /sbin/nologin 是否存在于 /etc/shells,否则有些 PAM 模块会较真。
3.2 创建虚拟用户密码文件
在 /etc/vsftpd 目录下建一个纯文本文件,用来管理虚拟用户账号密码。格式非常简单,一行用户名、一行密码,交替排列,不要加任何多余符号:
vim /etc/vsftpd/vusers.txt内容示例:
user01 P@ssw0rd_2025 user02 P@ssw0rd_2026这里有两个经验之谈。第一,密码不要用弱口令,因为 FTP 流量是明文传输,密码在网络上是裸奔的,弱口令等于把门锁挂在大街上。第二,这个文本文件在生成数据库之后就不是必需品了,但建议保留备份,放在一个权限严格限制的位置,方便以后维护账号,毕竟改数据库文件没有改文本文件直观。
3.3 用 db_load 生成认证数据库
文本文件不能直接被 PAM 读取,需要用 db_load 转换成 Berkeley DB 格式:
db_load -T -t hash -f /etc/vsftpd/vusers.txt /etc/vsftpd/vusers.db chmod 600 /etc/vsftpd/vusers.db这里解释一下参数的含义,理解了以后出问题你会知道去哪找原因:
-T:告诉 db_load 输入是普通文本文件(text file)-t hash:指定数据库底层用 hash 结构存储,vsftpd 的 PAM 认证默认推荐这种结构-f:后面跟输入文本文件路径- 最后一个参数是输出的数据库文件路径
权限也值得较真。数据库文件里存的是密码,如果权限太宽松,别的系统用户就能读走,所以 600 是最低标准。文本源文件同理,建议也改成 600。
3.4 修改 PAM 配置文件
PAM 是 Linux 下的统一认证框架,vsftpd 通过 /etc/pam.d/vsftpd 来指定认证方式。修改之前先备份:
cp /etc/pam.d/vsftpd /etc/pam.d/vsftpd.bak vim /etc/pam.d/vsftpd推荐写入的最小配置:
auth required pam_userdb.so db=/etc/vsftpd/vusers account required pam_userdb.so db=/etc/vsftpd/vusers这里有一个特别容易踩的坑:db=参数指向数据库文件路径时,不能带 .db 后缀。也就是说文件是 /etc/vsftpd/vusers.db,这里要写 /etc/vsftpd/vusers,不带后缀。我带过的人里有好几个就是在这里多写了个 .db,结果 PAM 一直报错找不到文件。
PAM 配置里 auth 负责验证密码,account 负责检查账号是否允许访问。两行都指向同一个数据库,一个管"你是谁",一个管"你能不能进来"。
3.5 验证数据库是否正确生成
这一步很多人省略,但我觉得值得做。用 db_dump 或者 strings 快速看一眼数据库里内容是否完整:
db_dump -p /etc/vsftpd/vusers.db | head -20能看到 user01、user02 这样的关键字,基本就说明转换成功了。如果 db_dump 提示格式不对,重新检查 db_load 参数。
4. vsftpd.conf 核心配置:让虚拟用户真正生效
4.1 关键配置项逐一拆解
改完 PAM 后,真正的主角是 /etc/vsftpd/vsftpd.conf。大多数人配置虚拟用户失败,问题基本都出在配置项的排列组合上。下面这份是我实测可用的完整模板,直接粘贴后按你自己的规划调整路径即可:
anonymous_enable=NO local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES pam_service_name=vsftpd guest_enable=YES guest_username=vftp virtual_use_local_privs=YES user_config_dir=/etc/vsftpd/vuser_conf chroot_local_user=YES allow_writeable_chroot=YES pasv_enable=YES pasv_min_port=30000 pasv_max_port=30050逐个解释关键项:
guest_enable=YES是总开关,打开之后 vsftpd 才启用虚拟用户模式。guest_username=vftp指定所有虚拟用户登录后映射到的系统用户,必须和前面 useradd 创建的一致。
virtual_use_local_privs=YES是我特别想说的一项。默认情况下虚拟用户会被当作匿名用户同级别的权限来处理,也就是各种写权限要用 anon_upload_enable、anon_mkdir_write_enable 这些参数去控制,非常绕。设置成 YES 之后,虚拟用户就获得本地用户级别的权限,写权限只受 write_enable 和目录的实际属主权限影响,配置和维护都清爽很多。
chroot_local_user=YES把所有本地用户和虚拟用户都锁定在自己的主目录里,这样用户登录后只能在指定目录里活动,看不到服务器其他路径,这是多用户 FTP 场景下的安全底线。
allow_writeable_chroot=YES是为了一件事:vsftpd 从 2.3.5 之后的版本,如果用户主目录本身可写,配合 chroot 模式会直接报 500 OOPS refusing to run with writable root inside chroot。加这一项可以绕过限制,但生产环境更推荐的做法是主目录只读、写操作放到子目录里,这样 chroot 仍然有效。我个人的习惯是测试阶段直接加这项,正式上线前把目录结构调整好再移除它。
user_config_dir指定每个虚拟用户独立配置文件的存放目录,下面会细说。
pasv_min_port和pasv_max_port是被动模式端口范围,后面防火墙放行要用,别漏。
4.2 user_config_dir 机制:一人一配置
虚拟用户最实用的功能就是通过 user_config_dir 做差异化管理。vsftpd 会在指定的 /etc/vsftpd/vuser_conf 目录下查找"用户名同名文件",加载其中的配置项,覆盖主配置里的相应设置。
创建目录:
mkdir -p /etc/vsftpd/vuser_conf给 user01 单独设主目录和写权限:
vim /etc/vsftpd/vuser_conf/user01写入:
local_root=/data/ftp/user01 write_enable=YES local_umask=022这样 user01 登录后看到的根目录是 /data/ftp/user01,其他虚拟用户不受影响。目录一定要提前创建好,并设置正确的属主:
mkdir -p /data/ftp/user01 chown vftp:vftp /data/ftp/user014.3 为什么 anonymous_enable 必须关
虚拟用户模式下,如果 anonymous_enable 还开着,会同时开放匿名访问通道,页面会暴露公共目录,等于留了个后门。代码里把 anonymous_enable=NO 写死,不要手软。匿名功能这条线,真的需要的时候再单独开,和虚拟用户混在一起只会让排障变得痛苦。
5. 网络放行与客户端实测:SELinux、firewalld、MobaXterm
5.1 SELinux 的正确放行方式
CentOS Stream 10 默认 SELinux 处于 Enforcing 模式,这也是很多人在本地测试"一切正常"、一到新装服务器就"文件夹列不出来"的根本原因。vsftpd 相关的 SELinux 布尔值比较多,最省事的做法是直接放行 ftpd_full_access:
setsebool -P ftpd_full_access 1-P表示永久生效,重启后依然保留。如果不想开放得那么彻底,可以按需放开ftp_home_dir和ftpd_use_passive_mode,但我建议先用 ftpd_full_access 把流程跑通,之后再根据实际环境收紧。检查当前状态:
getsebool -a | grep ftpdSELinux 拦截时的典型现象是:认证能通过,ls 也能列目录,但上传文件时直接 553 Permission denied,查看/var/log/audit/audit.log里会有一堆 avc denied 记录。看到 avc 字样,十有八九就是 SELinux 拦路了。
5.2 firewalld 放行 FTP 与被动端口
CentOS Stream 10 默认用的防火墙是 firewalld。先放行 FTP 服务本身:
firewall-cmd --permanent --add-service=ftp firewall-cmd --reload同时把 vsftpd.conf 里配置的被动端口范围一起放行,否则客户端能连上但传输时会卡住:
firewall-cmd --permanent --add-port=30000-30050/tcp firewall-cmd --reload这里解释一下为什么必须放行被动端口:FTP 有主动和被动两种模式,现在的办公网络和 NAT 环境下基本都是被动模式。被动模式下,服务器会打开一个范围内的随机端口与客户端传数据,如果防火墙不放行,控制连接是通的,数据通道却握手不了,表现就是"登录成功但目录加载不出来"。
5.3 客户端连接:命令行、FileZilla 与 MobaXterm
服务端全部配好后,开始验证。最简单的测试方式是本机用 ftp 命令行连自己的 IP:
ftp 服务器IP输入 user01 和密码,能进入目录并执行 ls 就说明认证体系已经通了。
我日常更喜欢用 lftp,它的交互体验好得多:
dnf install -y lftp lftp user01@服务器IP进入 lftp 后执行:
ls put /etc/hosts上传成功之后再用 get 拉回来,确认读写都正常。
Windows 环境下用 FileZilla 是最直观的,主机填 IP,用户名填 user01,密码填虚拟用户密码,端口默认 21,连接类型选"仅使用普通 FTP",不要选 SFTP。
顺带回应一个最近总有人问的问题:MobaXterm 可以当 FTP 服务器吗?严格回答是不可以。MobaXterm 本身是一个增强型的 SSH 客户端,它内置的 SFTP/SCP 文件浏览功能确实可以让你像操作 FTP 一样上传下载文件,但它服务端依赖的是 SSH 服务;真正要让别人通过 21 端口用 FTP 协议来访问你的文件,服务端仍然需要部署 vsftpd 这类 FTP 服务器程序。MobaXterm 更适合当作管理工具箱来连接我们刚搭建好的 FTP 服务。
6. 常见问题排查实录与进阶玩法
6.1 报错速查表:解决九成以上虚拟用户问题
虚拟用户配置翻车的情况虽然多,但归根到底就是那么几个原因。我把踩过的坑整理成一份速查表,建议你截图存一份:
| 现象 | 大概率原因 | 解决方案 |
|---|---|---|
| 530 Login incorrect | PAM 配置或数据库路径不对 | 检查 pam_userdb.so 的 db 参数是否带 .db 后缀;重建数据库 |
| 500 OOPS: refusing to run with writable root inside chroot | chroot 锁定目录可写 | 在主配置加 allow_writeable_chroot=YES 或调整目录权限 |
| 553 Could not create file | 目录属主不对、SELinux 拦截 | 确认目录 chown vftp:vftp;检查 audit.log 里的 avc denied |
| 连上后目录列表为空 | SELinux 未放行 ftpd_full_access | setsebool -P ftpd_full_access 1 后重试 |
| 登录成功但传输卡死 | 防火墙未放行被动端口 | 放行 vsftpd.conf 里的 pasv_min_port / pasv_max_port |
| PAM 错误 / could not read PAM config | /etc/pam.d/vsftpd 被改坏 | 用备份恢复,再对照上面给出的配置重写 |
| 服务启动失败 | vsftpd.conf 语法错误 | 执行 vsftpd -olisten=YES 查看输出定位具体参数 |
| db_load 命令找不到 | libdb-utils 未安装 | dnf install -y libdb-utils |
6.2 "500 OOPS" 最常见的两类,展开说
第一类是 chroot 根目录可写,报错里一定有 refusing to run with writable root。按前面说的,加 allow_writeable_chroot=YES 能立刻解决,但这个做法会削弱 chroot 的隔离效果,因为可写的根目录给了用户更大操作空间。生产环境我建议:把用户家目录设为 755,只读,然后再在下面建 upload 子目录并设为可写,这样既满足上传需求,又保证了隔离。
第二类是 "500 OOPS: could not bind listening socket",多半是端口被占用,用ss -lntp | grep 21查一下谁占了 21 端口,一般就是已经有别的 FTP 进程在跑,重启服务前先确认没冲突。
6.3 进阶玩法一:限制带宽与最大连接数
虚拟用户方案跑通之后,可以做一些精细化的资源限制,防止某个用户把带宽占满。在主配置或用户独立配置文件中加:
max_clients=50 max_per_ip=5 local_max_rate=1024000local_max_rate单位是字节/秒,1024000 大约等于 1MB/s。按用户限制带宽时,把它写到 /etc/vsftpd/vuser_conf/user01 这样的独立配置文件里,就能实现"user01 限速 1MB,user02 不限速"的效果。
6.4 进阶玩法二:被动模式配合内网 NAT 映射
服务器如果处在内网,公网入口做了端口转发,FTP 的被动模式还需要指定对外 IP,否则客户端拿到的是内网 IP,连不上数据端口。在 vsftpd.conf 里加:
pasv_address=公网IP如果你的出口 IP 是动态的,配不上固定值,那就需要配合脚本定期更新这个参数,或者干脆考虑用 SFTP 替代。说句实话,遇到动态公网 IP 的场景,继续用 FTP 僵持下去是很痛苦的一件事,早点切 SFTP 更省心。
6.5 进阶玩法三:虚拟用户与目录权限模板
最好再给新人一个目录权限的通用模板。多用户 FTP 环境里我通常这样设计目录结构:
/data/ftp # vftp 用户主目录,权限 755,只读 /data/ftp/user01 # user01 的家目录,权限 755 /data/ftp/user01/upload # 上传目录,权限 775,属主 vftp /data/ftp/user01/download # 下载目录,权限 755,只读对应命令:
mkdir -p /data/ftp/user01/upload /data/ftp/user01/download chown -R vftp:vftp /data/ftp/user01 chmod 755 /data/ftp/user01 chmod 775 /data/ftp/user01/upload这样做的好处是:用户能往自己的 upload 目录写东西,但没办法改动根目录结构;管理员发布文件时,放到 download 目录就能让用户组只读下载。权限模型在 FTP 场景里越简单越稳,别玩那些花里胡哨的 ACL 组合,出了问题你很难跟同事解释清楚。
把虚拟用户这套跑通之后,再回头看本地账号方案,确实觉得回不去了。账号密码完全独立,权限能细化到一人一目录,要关停某个人的访问直接删数据库记录就行,不用去动系统用户。内网环境这样用完全没问题,但服务器有公网入口的话,得提醒你一句:FTP 是明文协议,密码和文件内容在网络上都是裸奔的,建议尽快把被动端口范围、登录失败策略和 fail2ban 一起配上,或者干脆切换到 SFTP。下一篇我打算写 vsftpd 的加固与日志审计,把限速、登录次数限制和 xferlog 日志分析展开聊聊。