CentOS Stream 10 上搭好虚拟用户 FTP,是很多人踩坑后的共识:这活儿看着不大,但牵扯到 Berkeley DB 用户库、PAM 认证、vsftpd 目录映射三块东西,哪一环接不上都登录不进去。上一篇讲完了基础 FTP 服务和匿名访问,这篇把虚拟用户这部分一次说透。
虚拟用户解决的是个很实际的痛点——你不想为每个 FTP 账号都创建系统用户,因为系统账户越多,被爆破、被撞库的风险就越大。虚拟用户的思路是:FTP 账号只存在于专门的数据库文件里,系统层面只用一个“映射账户”兜底。CentOS Stream 10 相比老版本,把 FTP 这块的配置迁移到了 /etc/vsftpd 下的独立配置文件,PAM 的模块路径也变了,直接照抄旧博客的写法大概率会翻车。这篇文章假设你已经能在 CentOS Stream 10 上正常安装并启动 vsftpd,接着把虚拟用户从无到有完整跑通。
1. 虚拟用户机制的核心思路
1.1 说到底,虚拟用户到底“虚”在哪
FTP 的账号体系其实就三种:匿名、本地用户、虚拟用户。匿名大家都会配,安全性约等于零,只适合放公开下载资源;本地用户用的是系统 /etc/passwd 里的真实账号,管理方便但暴露面太大,而且本地账户权限往往和系统职能绑死,你很难为了一个临时下载需求去创建一个专用账户。
虚拟用户恰恰卡在两者中间:账号和密码存放在独立的数据库文件里,vsftpd 通过 PAM 模块去查询这个数据库,校验通过后,再把登录身份“映射”到系统上的一个固定账户上。对系统来说,所有 FTP 虚拟用户最终都表现为同一个系统账户,而实际能访问的内容、权限、目录,全看 vsftpd 自己的配置。这样做的好处很明显:
- 没有系统账户,就不会被 SSH 等其他服务误用,少一条渗透路径。
- 账号和密码全在库里管,批量创建、禁用、改密都是改一行记录的事。
- 想给不同虚拟用户不同目录和权限,一个 user_config_dir 目录就搞定,不用再造多个系统用户。
- FTP 账号即使被爆破,影响面最坏也就是 FTP 目录里的文件,而不会波及其他系统服务。
实现路径也很清晰:vsftpd 收到登录请求 → 调用 PAM 配置的认证规则 → PAM 的 userdb 模块去读 Berkeley DB 文件 → 验证账号密码 → 通过后把权限切换到 guest_username 指定的系统账户。
这套机制里最容易被忽略的是“数据库文件本身”这个环节。Berkeley DB 格式和纯文本不一样,它不能直接文本编辑,必须通过 db_load 工具把格式化的文本导入进去。很多人倒在这一步,以为直接写个 users.txt 就能被 PAM 读取,结果怎么都是 530 Login incorrect。
1.2 为什么新系统值得花力气配置虚拟用户
日常运维里,最常见的需求就是给几个人开个临时下载上传通道,然后过两个月又得批量回收账号。如果全部做成系统用户,回收账号时要同时处理 /etc/passwd、/etc/shadow、home 目录三处,哪个忘了都是隐患。用虚拟用户,删掉数据库里一行重新生成 db 文件就完事。
CentOS Stream 10 的安全策略更严了,默认的 SELinux 对 FTP 家目录写入的拦截比旧版本更细致,这也让不少人直接放弃了 vsftpd 转投 SFTP。但其实只要掌握了 setsebool 和文件上下文这两把钥匙,虚拟用户方案在 CentOS Stream 10 上完全能稳定运行,性能上比 SFTP 好不少,尤其在大量小文件传输的场景下。
如果你有 20 个以上的 FTP 账号需求,强烈建议优先考虑虚拟用户方案而不是批量创建系统用户。账号数量越多,虚拟用户的维护优势越明显。
2. CentOS Stream 10 环境准备与软件包安装
2.1 安装 vsftpd 与 Berkeley DB 工具链
CentOS Stream 10 的默认仓库里已经包含了我们需要的全部软件包,不需要额外添加任何第三方源。先检查系统里是否已经装了 vsftpd:
rpm -qa | grep vsftpd如果输出为空,直接安装。注意 CentOS Stream 10 使用 dnf 作为包管理器,和 CentOS 7 时代的 yum 命令相比,依赖解析速度和错误提示都更友好些:
dnf install -y vsftpd db4-utils这里要留意 db4-utils 这个包。在老版本系统(比如 CentOS 6/7)中数据库工具包叫 db4-utils,在 CentOS Stream 10 中该包对应的是 libdb-utils。你可以在安装前先确认一下仓库里的实际包名:
dnf search libdb | grep utils如果没有找到 db4-utils 而是看到 libdb-utils,就执行:
dnf install -y vsftpd libdb-utilslibdb-utils 提供了 db_load 和 db_dump 这两个命令,它们是生成和管理虚拟用户数据库的核心工具,少了它们整条链路就断了。
安装完成后分别验证一下两个关键程序是否就位:
which db_load which vsftpd2.2 SELinux 与防火墙的预处理
CentOS Stream 10 默认启用了 SELinux enforcing 模式,这一点和 CentOS 7/8 一致,但新版本的策略集更严格。vsftpd 涉及的核心布尔值主要包括 ftpd_full_access、ftp_home_dir、allow_ftpd_use_cifs、allow_ftpd_use_nfs,其中影响最大的是前两个。你可以先查看当前状态:
getsebool -a | grep ftp常见的输出可能是 ftpd_full_access 为 off 而 ftp_home_dir 为 off。为了虚拟用户能正常读写家目录,建议先设置 ftp_home_dir 为 on,而不要把 ftpd_full_access 完全打开,以免过度放宽策略导致安全审计出问题:
setsebool -P ftp_home_dir 1如果后续排查发现 vsftpd 依然无法写文件,再考虑逐步放开其他相关布尔值。这里特别强调 -P 参数,它让修改在重启后保持生效,否则 SELinux 状态会在重启后回滚到默认值,到时候又是六小时排查。
防火墙方面,如果你启用了 firewalld,需要放行 FTP 服务端口。标准做法是:
firewall-cmd --permanent --add-service=ftp firewall-cmd --reload被动模式所需的高位端口如果使用了非默认区间,需要额外放行对应端口段,否则客户端在 PASV 模式下会卡在“正在连接数据连接”这一步。关于端口配置,后面实战部分会具体展开。
2.3 vsftpd 配置文件结构的变化
CentOS Stream 10 中 vsftpd 的主配置文件路径与旧版一致,仍然是 /etc/vsftpd/vsftpd.conf。需要注意的关键变化是,新版对配置项做了更严格的合法性检查,包括:
- 未知配置项直接报错并终止启动,不再像老版本那样宽容跳过。
- 部分旧配置项已经改名或废弃,比如 anon_other_flags 这类不太规范的写法已被移除。
- 配置权限不支持 777 和 group 可写,防止恶意提权。
这让很多从旧版本“复制粘贴”配置的做法直接失效。我在 CentOS Stream 10 上测试时,发现只要 conf 文件里存在一个不再支持的旧参数,服务就无法启动。因此配置虚拟用户时,建议逐步添加配置并用 vsftpd -s 手动验证语法,不要一次性堆一大段网上抄来的配置。
3. 创建虚拟用户数据库文件
3.1 规划虚拟用户名单和密码
在动手之前,先规划一下虚拟用户清单。我用一个实际项目里的例子说明:假设要开三个 FTP 账号,分别是 alice(运营部下载)、bob(运营部上传)、carol(临时项目共享),这是后续配置目录映射和权限隔离的基础。
虚拟用户库的文本格式非常简单:奇数行是用户名,偶数行是密码,逐行交替。比如:
alice Al1ce_PassWD! bob Bob_PassWD! carol Carol_PassWD!建议密码强度尽可能高于纯字母数字强度,因为 PAM userdb 模块对暴力枚举的防护能力弱于系统 PAM(没有 lockout 机制)。一旦这个库文件泄露,所有 FTP 账号都会被直接读走。更稳妥的做法是在服务端用独立密码生成器生成随机密码,再分发给对应用户,而不是让用户自己随便指定简单密码。
3.2 用 db_load 生成 Berkeley DB 文件
把上面的内容保存为 /etc/vsftpd/vusers.txt,然后执行导入。这里要仔细看命令参数,漏了 -T 很容易出问题:
cd /etc/vsftpd db_load -T -t hash -f vusers.txt vusers.db参数含义:
- -T 表示从文本文件导入,允许应用直接访问数据库内容。
- -t hash 指定数据库类型为哈希表,这是 PAM userdb 模块的默认预期类型。
- -f 指定输入文本文件路径。
- 最后一个参数是输出数据库文件名。
执行完成后,确认一下 db 文件确实生成了:
ls -l /etc/vsftpd/vusers.db这里有一个很多人忽略的坑:CentOS Stream 10 的 SELinux 对 /etc/vsftpd 目录下的自定义文件有额外的上下文要求。生成的 vusers.db 文件上下文可能是 var_t 而非 etc_t,导致 vsftpd 启动时无法读取。验证方式:
ls -Z /etc/vsftpd/vusers.db如果上下文不是 system_u:object_r:etc_t:s0,就需要手动修复:
restorecon -v /etc/vsftpd/vusers.db或者直接用 chcon 指定正确上下文。我在实际部署中遇到过一次这个问题,表现是服务正常启动、配置全部正确,但虚拟用户始终认证失败,折腾了一大圈才发现是数据库文件被 SELinux 拦了。排错优先级一定要把 SELinux 排在最前面。
3.3 权限收紧与备份策略
db 文件生成后,要立刻收紧权限。vsftpd 进程通常以 root 身份启动,但处理数据连接时会降权,因此数据库文件至少要能允许 vsftpd 读取。比较稳妥的权限是 600(仅 root 可读写):
chmod 600 /etc/vsftpd/vusers.db chmod 600 /etc/vsftpd/vusers.txt文本格式的 vusers.txt 在导入完成后可以保留也可以删除。从安全角度建议删除,因为明文密码文件留着就是靶子。但是要保留一个备份副本,存放在 /root 下或者加密存储里,方便后续批量修改用户时重新生成 db。
改完数据库内容后,更新 db 文件的方法是:编辑文本文件 → 重新执行 db_load 命令覆盖 → 重启 vsftpd 服务。
重要提示:每次修改虚拟用户库后,必须同时确认 SELinux 上下文没有丢失(restorecon 可能因为文件重建而需要重新执行),这是新手最容易反复踩的地方。
4. PAM 认证配置详解
4.1 PAM 文件路径与配置格式
vsftpd 在 CentOS Stream 10 上的 PAM 服务名仍然是 vsftpd,对应配置文件位于 /etc/pam.d/vsftpd。让我们看一下当前文件的默认内容:
cat /etc/pam.d/vsftpd默认配置指向的是系统账户认证,即通过 pam_unix.so 来验证本地用户。要让 vsftpd 使用虚拟用户库,必须改为指向 pam_userdb.so:
#%PAM-1.0 auth required pam_userdb.so db=/etc/vsftpd/vusers account required pam_userdb.so db=/etc/vsftpd/vusers这里的 db= 参数指向数据库文件的路径,注意不能带 .db 扩展名。pam_userdb.so 会自动在指定路径后面补上 .db 后缀。如果你写成 db=/etc/vsftpd/vusers.db,它会去查找 /etc/vsftpd/vusers.db.db,结果必然失败,这是常见的低级错误,但错误日志里不会明着告诉你多拼了个扩展名,非常耗时间。
对应地,virtual user 的 PAM 配置里同时需要 auth 和 account 两部分。前者负责账号密码校验,后者负责账户状态检查和资源限制。某些网上教程只保留了 auth,导致 vsftpd 启动后登录直接报错 500 OOPS,这就是缺少 account 行引发的。
4.2 为什么 PAM userdb 能读但登录依旧失败
PAM userdb 模块的工作机制是把用户名和密码交给 Berkeley DB 去匹配。vsftpd 从网络收到登录请求后,会先读取 /etc/pam.d/vsftpd 中定义的规则链,依次执行 auth 和 account 规则。userdb 模块在匹配时会将输入的用户名和密码以明文形式与数据库中的记录比对,因此数据库里的密码就是纯文本存储的,不具备加密安全性。
这里顺带解释一下为什么数据库文件权限必须严格控制到 600:因为 vusers.db 内部保存的就是明文密码,一旦被其他系统用户读取,相当于所有 FTP 账号密码直接裸露。你可以在文本文件 V 目录集中维护账号,也可以后期再加一层加密,但实用性有限。对于大多数内部使用场景,严格文件权限加上防火墙限制已经足够。
4.3 PAM 配置的备份和恢复
配置前建议备份原始 PAM 文件:
cp /etc/pam.d/vsftpd /etc/pam.d/vsftpd.bak如果后续需要切换回本地用户认证(例如要用系统账户做 FTP 登录),只需恢复备份文件并重启 vsftpd 即可。这个备份在 CentOS Stream 10 升级期间尤其有用,因为部分升级会把 PAM 文件覆盖为默认值。
我踩过的一个坑是:CentOS Stream 10 的某些安全更新会重置 /etc/pam.d/vsftpd 到默认内容,虚拟用户认证因此失效。排查时如果确认数据库文件没问题但登录仍失败,不妨看一眼 PAM 文件是否被改回 pam_unix.so 了。
5. vsftpd 主配置与虚拟用户目录映射
5.1 面向虚拟用户的核心配置项
编辑 /etc/vsftpd/vsftpd.conf,确保包含以下关键配置:
anonymous_enable=NO local_enable=YES write_enable=YES guest_enable=YES guest_username=ftpuser virtual_use_local_privs=YES pam_service_name=vsftpd user_config_dir=/etc/vsftpd/user_conf chroot_local_user=YES allow_writeable_chroot=YES逐项说明,这些配置缺一不可:
- anonymous_enable=NO:关闭匿名访问,虚拟用户方案不需要匿名通道。
- local_enable=YES:允许本地用户登录。虚拟用户最终映射到本地用户,必须启用这个选项,否则连接时直接报 530 Login incorrect。
- guest_enable=YES:开启虚拟用户支持,这是 vsftpd 识别虚拟用户的核心开关。
- guest_username=ftpuser:所有虚拟用户映射到的系统账户。这里要求系统中已存在 ftpuser 用户,且拥有合理的权限。
- virtual_use_local_privs=YES:让虚拟用户具备与本地用户相同的权限级别。如果不启用,虚拟用户被锁定在“仅匿名权限”的框架内,上传即报错 550 Permission denied。
- user_config_dir:指定每个虚拟用户独立配置文件的目录。没有这个目录时,所有虚拟用户共享主配置的根目录;有了它,就可以针对 alice、bob、carol 定制各自的 local_root、anon_world_readable_only 等参数。
- chroot_local_user=YES:把所有虚拟用户锁定在自己的家目录里,避免跨目录访问。
- allow_writeable_chroot=YES:允许用户家目录是 group 可写的(或用户自身可写)同时还配合 chroot。否则会出现 500 OOPS: vsftpd: refusing to run with writable root inside chroot() 的经典报错。
5.2 创建映射系统账户并规划目录结构
创建一个没有 shell、不能登录的本地用户作为映射账户:
useradd -d /data/ftp -s /sbin/nologin ftpuser这里 -d 参数指定了虚拟用户默认的根目录,我设置为 /data/ftp,因为 FTP 数据不应该放在根分区,避免系统盘空间被打满。实际的目录结构建议这样组织:
/data/ftp/ ├── alice/ # alice 用户根目录 ├── bob/ # bob 用户根目录 └── carol/ # carol 用户根目录创建目录并调整权限:
mkdir -p /data/ftp/{alice,bob,carol} chown -R ftpuser:ftpuser /data/ftp chmod 755 /data/ftp chmod 755 /data/ftp/*/ chmod 2770 /data/ftp/bob chmod 2770 /data/ftp/carol- /data/ftp 本身用 755,保证用户可以进入。
- alice 目录用 755 表示只读,配合主配置的 write_enable 和目录权限决定她只能下载。
- bob 和 carol 用 2770,强制组内新文件的属组关系,FTP 用户创建的目录或文件即便换用户名,访问也不乱套。
这里补充一个细节:chroot_local_user=YES 之后,每个虚拟用户的根目录就是其 local_root 指向的目录,即便你给了 ftpuser 全盘可读,用户也没法跳出自己的目录,因为 chroot 在文件系统级别做了隔离。数据安全性主要靠这个保证。
5.3 按用户细粒度配置目录与权限
创建 /etc/vsftpd/user_conf 目录,并为每个用户创建独立的配置文件:
mkdir /etc/vsftpd/user_confalice 用户配置(只读下载):
local_root=/data/ftp/alice anon_world_readable_only=NO write_enable=NObob 用户配置(可读可写):
local_root=/data/ftp/bob anon_world_readable_only=NO write_enable=YES anon_upload_enable=YES anon_mkdir_write_enable=YES anon_other_write_enable=YEScarol 用户配置(临时项目,可写):
local_root=/data/ftp/carol anon_world_readable_only=NO write_enable=YES anon_upload_enable=YES anon_mkdir_write_enable=YES anon_other_write_enable=YES为什么虚拟用户配置里要用 anon_ 开头的选项而不是 local_ 开头的?这正是 virtual_use_local_privs 和 guest 机制带来的灵活性。当 virtual_use_local_privs=NO 时,vsftpd 把所有虚拟用户当成匿名用户处理,权限取 anon_* 系列配置;当它为 YES 时,则取 local_* 权限。实际测试中两种模式都可以用,但 anon_* 系列在 user_config_dir 里配置比 local_* 更精确控制得细,这也是官方推荐的做法。
5.4 被动模式与网络参数
FTP 主动模式在云环境和大多数办公网络里基本不可用,因为客户端 NAT 和防火墙会阻断服务器的回连。虚拟用户方案需要配置为被动模式:
pasv_enable=YES pasv_min_port=30000 pasv_max_port=31000 pasv_address=192.0.2.10参数说明:
- pasv_min_port/pasv_max_port:指定被动模式数据连接使用的端口范围,务必与防火墙放行范围一致。
- pasv_address:如果服务器在 NAT 环境或者有公网映射,这里要填写客户端可见的 IP 地址。填错会导致客户端卡在 LIST 命令后无法连接数据端口。
- pasv_promiscuous=YES:仅在客户端使用主动模式、跨 NAT 等特殊场景下才需要开启,常规不要开,这会影响安全性。
对应防火墙放行:
firewall-cmd --permanent --add-port=30000-31000/tcp firewall-cmd --reload被动模式的端口规划建议避开 1024 以下低端口和常见的 8080、8888 等端口,选择 30000 开始的连续段既能减少冲突,也方便安全组规则管理。
6. 启动服务与常见报错排查实录
6.1 服务启停与启动验证
配置完成后,先检查语法再启动服务:
vsftpd -s /etc/vsftpd/vsftpd.conf该命令只会做语法检查,不输出错误即代表配置文件没有语法问题。然后启动并设置开机自启:
systemctl enable --now vsftpd systemctl status vsftpd如果服务启动失败,优先查看日志:
journalctl -u vsftpd -n 50 tail -f /var/log/messages | grep vsftpd6.2 高频报错与处理操作实录
用一段时间下来,虚拟用户方案最容易翻车的地方集中在下面几张表里,建议收藏备用。
| 报错现象 | 直接原因 | 处理方法 |
|---|---|---|
| 500 OOPS: vsftpd: not found: file specified in pam_service_name | PAM 服务文件不存在或 pam_service_name 拼写错误 | 检查 /etc/pam.d/vsftpd 是否存在,确认配置里写的名字一致 |
| 530 Login incorrect | 数据库文件不存在、PAM 文件被重置、db 路径写错 | 按顺序检查 db 文件存在性、db_load 是否有报错、PAM 配置是否仍指向 userdb 模块 |
| 500 OOPS: vsftpd: refusing to run with writable root inside chroot() | chroot 目录对用户可写,且未开启 allow_writeable_chroot | 在 vsftpd.conf 中设置 allow_writeable_chroot=YES,或者把目录权限改小 |
| 550 Permission denied | 写入权限未开启,或者 SELinux 拦截写操作 | 确认 write_enable=YES、anon_upload_enable 等配置项;查看 setsebool ftp_home_dir 和文件上下文 |
| Connection timed out (空连接) | 被动模式端口未放行或 pasv_address 配置错误 | 检查 firewalld 端口范围,核对 pasv_address 是否为客户端可达的 IP |
| 226 Transfer complete 后卡住 | 数据通道是主动模式但防火墙拦截 | 检查客户端 FTP 是否设置被动模式,服务端检查 pasv_promiscuous 是否误开 |
6.3 一个典型的一小时排错实录
下面是我在 CentOS Stream 10 上配置虚拟用户时遇到过的真实情况,完整复盘一遍,能帮你省去大量弯路。
故障现象:虚拟用户 alice 登录时报 530 Login incorrect,日志只显示 PAM authentication failed,没有更多细节。
检查路径:
- 确认 vusers.db 存在:ls -l /etc/vsftpd/vusers.db,文件有。
- 手动验证数据库内容:db_dump -p vusers.db,输出正常,alice 记录在里面。
- 查看 PAM 文件:/etc/pam.d/vsftpd,内容是 userdb 双行,没错。
- 到这里按老经验已经走到死胡同,直到我看了一眼 SELinux 上下文:
ls -Z /etc/vsftpd/vusers.db输出显示 unconfined_u:object_r:var_t:s0,而不是 etc_t。restorecon 之后重启 vsftpd,登录成功。
这个问题的坑点在于:vusers.txt 在导入生成 db 文件之前,我曾修改过 /etc/vsftpd 目录的权限,导致文件上下文被重置。CentOS Stream 10 的 SELinux policy 会阻止 vsftpd 读取上下文不匹配的文件,但错误日志里只表现为认证失败,不会提一句 SELinux。所以排错时永远把 SELinux 上下文列为第一嫌疑对象。
还有一个比较隐蔽的问题:我最初在 /etc/vsftpd 目录下创建 user_conf 时,忘了设置目录的 SELinux 上下文。后来补充:
semanage fcontext -a -t etc_t '/etc/vsftpd/user_conf(/.*)?' restorecon -Rv /etc/vsftpd如果你没有安装 semanage,可以用 yum install policycoreutils-python-utils 补装。
6.4 客户端连接验证与权限最终确认
服务端配置完成后,用实际客户端走一遍全流程。最简单的测试方法是使用命令行 FTP 客户端:
ftp <服务器IP>输入用户名 alice,密码验证无误,进入后查看:
ftp> pwd ftp> ls ftp> put test.txt如果 put 报 550,检查一下 alice 的 user_conf 是否给写了 allow 项以及目录权限是否为可写。
如果想知道文件实际落到了哪个目录、属主是什么,在服务器上查看:
ls -la /data/ftp/alice虚拟用户上传的文件属主应该是 ftpuser,如果属主不是 ftpuser 而是某个其他 UID,说明 useradd 创建映射账户时家目录指定出了问题,需要回头重新对齐目录结构。
用 Python 快速模拟多重账号登录的一种方式是写个简单脚本循环调用 ftplib,能显著提升验收效率:
import ftplib server = '192.0.2.10' users = {'alice': 'Al1ce_PassWD!', 'bob': 'Bob_PassWD!'} for name, passwd in users.items(): ftp = ftplib.FTP() ftp.connect(server, 21, timeout=5) ftp.login(name, passwd) print(f'{name}: OK, cwd -> {ftp.pwd()}') ftp.quit()脚本输出正确,说明虚拟用户认证、目录映射全部打通。如果某个用户输出报错,直接检查对应的 user_conf 和目录权限即可。
7. 把虚拟用户方案做扎实的几条实操心得
配置这块在文档里是线性流程,但实际生产中会遇到一些文档不写的边界情况,分享几条亲身经验:
第一,虚拟用户库的密码建议每季度轮换一次。由于 PAM userdb 是按明文存储,密码一旦泄露就完全失控。完全不建议为了“方便”在库文件里用统一密码或弱密码,即使内部网络也一样。要给每个用户独立随机密码,并记录发放时间,到期提醒更换。
第二,chroot 模式开启后,虚拟用户访问不了 local_root 以外的任何路径。这意味着如果你需要某个用户访问公共目录(比如同时读多个项目目录),不能只给一个 local_root 就完事,而是要设置 local_root 指向一个共享父目录,再在父目录里用子目录区分。虚拟用户是按目录隔离,不是按文件权限隔离。
第三,启用虚拟用户方案后,vsftpd 会同时允许本地用户登录。如果你不想本地用户通过 FTP 访问,需要显式禁用,否则只要有密码就能走 FTP。要彻底关闭本地用户,可以在 vsftpd.conf 里加入 local_enable=NO,但也意味着虚拟用户映射会失效,因为映射本质上踩的是 local 机制。这个矛盾在实际中通过 PAM 和 guest_username 组合来解决,具体做法是保留 local_enable=YES,但为所有非 ftpuser 的系统账号设置独立的 PAM 认证限制,或者使用 tcp_wrappers 控制来源 IP。
第四,用户配置文件的命名与虚拟用户名必须完全一致,包括大小写。vsftpd 不会做任何容错性匹配,bob 写了 Bob 就是读不到。
第五,日志是这类问题排查的关键。永远保持 /var/log/xferlog 和 /var/log/messages 的日志打开状态,不要为了省几十 MB 磁盘而关闭。出错时先看日志,再去看配置,能砍掉一大半的无效排查。
最后再分享一个小技巧:在 user_conf 里给虚拟用户配置 umask 时,不要沿用系统默认的 022,FTP 场景建议设成 002 或 0007 结合组权限使用。这样上传的文件天然带正确的组可写位,后续批处理程序再去处理这些文件时,不会因为权限不够而失败。
虚拟用户这套方案在 CentOS Stream 10 上跑稳之后,日常管理基本就是改一下 vusers.txt、重新 db_load、重启服务这三步,新用户开通不会超过一分钟。上点规模以后,可以把用户库的生成逻辑封装成一个 Shell 脚本,批量导入导出,维护成本还能再压一截。这种 FTP 账号生命周期管理虽然看着不起眼,但对整个运维体系来说,少一扇系统账户的门,就少一分被横向渗透的风险。