接着上一篇说。CentOS Stream 10 上跑 FTP 服务器,基础安装大家都能搞定,但真正到了生产环境、多人共享这一层,基本都会转向「虚拟用户访问」。
上一篇里我们已经把 vsftpd 装好、匿名和本地用户也调通。这回来硬核的:虚拟用户。说白了一句话——虚拟用户不是系统账号,只存在于 FTP 的认证库里,登录后统一映射到某个宿主系统用户上。账号可以随便开、目录可以随便隔离、权限可以随便切,但系统层面不会新增任何可登录用户,安全面一下就收窄了。
这篇我从原理讲到坑,把 CentOS Stream 10 上最容易翻车的 SELinux、PAM、目录权限全部拆开说清楚,给一份直接能抄的配置。
1. 虚拟用户模式到底解决了什么问题
1.1 本地用户模式的三道坎
用本地系统账号当 FTP 账号,一开始还行,账号一多问题就来了。
第一道坎是账号膨胀。五个同事要用 FTP,你就在系统里建五个用户,将来离职还得去删账号、处理文件归属,运维成本全堆在自己身上。十个人、二十个人呢?系统里乌泱泱一片,看着都头疼。
第二道坎是安全暴露。本地用户默认是能通过 SSH 登录的,就算你设/sbin/nologin,账号信息还是躺在/etc/passwd里。FTP 密码和系统密码一旦混用,密码泄露面就不是 FTP 一个点,而是整个服务器登录入口。
第三道坎是权限隔离麻烦。每个系统用户的 home 目录、umask、chroot 范围都要单独调,默认情况下配置稍有疏忽,用户就能在目录树里窜来窜去。
虚拟用户就是冲着这三道坎来的——账号独立于系统,认证交给 PAM,权限统一映射,目录用user_config_dir按用户名单独配置。
1.2 虚拟用户认证链路拆解
vsftpd 收到客户端连接请求后,用户名和密码会交给 PAM 模块pam_userdb.so处理。这个模块去读一个 Berkeley DB 格式的用户数据库,里面存的就是虚拟用户的用户名和密码。
认证通过后,vsftpd 把当前会话身份切换成guest_username指定的本地用户(比如vftp),后续所有文件读写都以这个本地用户的权限执行。
也就是说,vsftpd 主进程 root 启动,认证完成之后降权,数据连接进程以vftp身份跑。虚拟用户本身没有独立的 UID/GID,这就是「虚拟」二字的来源——系统列表里查无此人,但文件系统层面有人替它干活。
1.3 一个食堂门口的类比
把 FTP 服务想象成单位食堂,系统用户是正式员工,虚拟用户是访客。访客要进食堂,得先在门卫那儿查访客名单(PAM 查数据库),确认有名字、密码对上了,才能放行。进门之后统一换上食堂工作人员的工服(vftp身份),该端菜端菜、该擦桌擦桌,权限边界非常清晰。
这样食堂里永远不会因为访客变多而真的多出几百个编制,管理起来轻松得多。
2. 动手前把方案先定清楚
2.1 确认环境状态
先确认 vsftpd 已经装好,上一篇如果没做完,这里补上:
rpm -q vsftpd systemctl status vsftpd --no-pager没有输出包名或者服务没装,直接装:
dnf install -y vsftpd systemctl enable --now vsftpdCentOS Stream 10 用的还是传统 Systemd 管理方式,服务名就叫vsftpd,跟 CentOS 7/8 时代一样,没有太大变化。要注意的是 CentOS Stream 10 的 SELinux 默认 enforcing,FTP 这种服务尤其容易被 SELinux 卡,后面专门讲。
2.2 目录与账号规划
配置之前先列一张规划表,别装到一半才想目录结构。
| 项目 | 规划值 | 说明 |
|---|---|---|
| 宿主系统用户 | vftp | 虚拟用户登录后统一映射到此用户 |
| 虚拟用户库 | /etc/vsftpd/vuser.db | Berkeley DB 格式,可读性差,安全 |
| 虚拟用户文本源 | /etc/vsftpd/vuser.txt | db_load 转换前的明文源文件 |
| FTP 数据根目录 | /data/ftp | 独立数据目录,方便备份迁移 |
| 虚拟用户 user1 | ftpuser1 | 根目录/data/ftp/user1 |
| 虚拟用户 user2 | ftpuser2 | 根目录/data/ftp/user2 |
这里有两个设计点值得说。
一是数据目录放在/data/ftp而不是宿主用户vftp的家目录,因为数据目录独立出来后,后续要扩容挂新磁盘、做快照备份都方便,不会因为改用户家目录把数据路径搞乱。
二是宿主用户直接指定/sbin/nologin,这样即使宿主账号密码泄露,也无法通过 SSH 登录系统,把安全风险压到最低。
2.3 虚拟用户方案的四件套结构
整个方案就是四件套:一个宿主系统用户、一个 Berkeley DB 数据库、一份 PAM 认证配置、一组 vsftpd 主配置参数。四者缺一不可,任何一个环节配错,现象都是「登录验证失败」,排查起来特别容易绕圈子。
先在脑子里过一遍链路:客户端发账号密码 → vsftpd 收到 → 调 PAM 配置 → pam_userdb 读数据库 → 比对通过 → 进程身份切换为vftp→ 进入指定目录。哪个环节断了,报错都藏在日志里,后面排错章节会逐条对。
3. 一步一步把虚拟用户配起来
3.1 创建系统宿主用户
useradd -d /data/ftp -s /sbin/nologin vftp-d指定家目录为/data/ftp,-s /sbin/nologin禁止登录。这里不要手动去建/data/ftp,后面统一建目录和赋权,避免权限错乱。
家目录创建好之后顺便确认一下:
ls -ld /data/ftp3.2 生成虚拟用户数据库
虚拟用户账号存储在文本文件里,每两行一组:奇数行用户名,偶数行密码。
vi /etc/vsftpd/vuser.txt内容格式如下:
ftpuser1 123456 ftpuser2 abcdef然后调用db_load转换成 Berkeley DB 格式:
db_load -T -t hash -f /etc/vsftpd/vuser.txt /etc/vsftpd/vuser.db chmod 600 /etc/vsftpd/vuser.db /etc/vsftpd/vuser.txt这里解释两个关键点。
第一,vsftpd 的 PAM 模块pam_userdb.so读的是 Berkeley DB 二进制文件,不是文本文件。所以必须先转换,否则 PAM 会报「找不到数据库」或者读到空内容。
第二,-T表示把文本文件当成数据库源文件来读,-t hash指定哈希算法,-f后面跟源文件。这条命令在 CentOS Stream 10 上属于libdb-utils包,一般系统自带,如果没有就装:
dnf install -y libdb-utils以后要加虚拟用户,不用改 PAM 配置,只需要在vuser.txt里追加两行、重新执行db_load、systemctl restart vsftpd即可。这个流程要记牢,这是虚拟用户管理与系统用户管理最大的操作差异。
提示:
vuser.txt里是明文密码,权限务必600,且不要放在 Web 服务能访问到的路径下。安全要求更高的场景,可以把源文件放到/root/下,只把vuser.db留在/etc/vsftpd/。
3.3 编写 PAM 认证配置
vsftpd 默认使用/etc/pam.d/vsftpd作为 PAM 配置,因为主配置里没有显式指定pam_service_name时,默认值就是vsftpd。
编辑这个文件:
vi /etc/pam.d/vsftpd添加两行:
auth required pam_userdb.so db=/etc/vsftpd/vuser account required pam_userdb.so db=/etc/vsftpd/vuser注意db=后面只写到数据库前缀路径,不写.db后缀。这是新手最常踩的坑,写了后缀 PAM 就去读/etc/vsftpd/vuser.db.db,永远找不到文件。
文件里原本可能残留系统自带的一些 PAM 行,比如pam_shells.so、pam_listfile.so之类。用虚拟用户方案时,这些行可以保留,因为虚拟用户映射到的是vftp,shell 是/sbin/nologin,如果用pam_shells.so校验 shell,反而会挡住虚拟用户登录,建议直接注释掉。最稳的做法是让/etc/pam.d/vsftpd只保留pam_userdb.so相关行:
auth required pam_userdb.so db=/etc/vsftpd/vuser account required pam_userdb.so db=/etc/vsftpd/vuser这里不写session相关行,虚拟用户会话不涉及系统会话管理,避免多余校验导致 Unknow Error。
3.4 修改 vsftpd.conf 核心配置
核心配置文件在/etc/vsftpd/vsftpd.conf。先备份,再改:
cp /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd.conf.bak vi /etc/vsftpd/vsftpd.conf最小可用的虚拟用户配置如下:
anonymous_enable=NO local_enable=YES guest_enable=YES guest_username=vftp pam_service_name=vsftpd chroot_local_user=YES allow_writeable_chroot=YES local_umask=022 user_config_dir=/etc/vsftpd/vusers_dir write_enable=YES dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_file=/var/log/xferlog逐个说。
anonymous_enable=NO,关闭匿名访问,虚拟用户方案默认不需要匿名入口。
local_enable=YES,很多人卡在这里。虚拟用户不是系统用户,为什么还要开local_enable?因为虚拟用户认证成功后,进程身份是vftp,vsftpd 内部把它当作「本地用户」来执行目录权限、umask、chroot 等逻辑。不开这个,虚拟用户登录会被拒绝。
guest_enable=YES,开启虚拟用户模式。guest_username=vftp,指定虚拟用户登录后映射到的系统用户。
pam_service_name=vsftpd,指定 vsftpd 使用的 PAM 服务名,对应/etc/pam.d/vsftpd。如果不想动默认的 PAM 文件,可以改成别的名字,比如pam_service_name=vsftpd_virtual,前提是创建/etc/pam.d/vsftpd_virtual。
chroot_local_user=YES,把所有本地用户(包括虚拟用户映射后的身份)都锁在自己的根目录里。这能防止用户登录后跳到别的目录,是虚拟用户方案里必须开的安全项。
allow_writeable_chroot=YES,允许 chroot 后的根目录可写。如果不加这条,当虚拟用户根目录属性是777或属主是vftp时,vsftpd 会报500 OOPS: vsftpd: refusing to run with writable root inside chroot()直接拒绝连接。下一篇排错会细讲,这里先放上,实测 CentOS Stream 10 上用虚拟用户方案,基本都得开。
user_config_dir=/etc/vsftpd/vusers_dir,这个是虚拟用户方案的精髓。目录里每个虚拟用户对应一个同名配置文件,可以单独覆盖主配置里的参数,实现「一个用户一个权限」。下一章展开讲。
write_enable=YES,允许写入,包括上传、删除、改名。生产环境如果 FTP 只用作下载,可以关掉,按需开。
3.5 设置目录权限与 SELinux 放行
先建目录:
mkdir -p /data/ftp/user1 /data/ftp/user2 chown -R vftp:vftp /data/ftp虚拟用户登录后的所有文件操作都发生在vftp身份下,所以/data/ftp以及里面所有子目录都必须让vftp有写权限。把整个数据目录递归授权给vftp是最省事的做法。
chmod 755 /data/ftp chmod 770 /data/ftp/user1 /data/ftp/user2接下来是 CentOS Stream 10 上必然遇到的 SELinux 问题。默认 enforcing 状态下,FTP 服务读/data/ftp下的文件会被 AVC 拒绝,日志里能看到一堆denied,但 vsftpd 本身不会主动报错,表现就是「登录成功,列目录为空或者上传失败」。
setsebool -P ftpd_full_access on-P表示持久化,重启后依然生效。查看当前状态确认:
getsebool -a | grep ftp如果不想全量放开ftpd_full_access,也可以只开ftp_home_dir,但虚拟用户的数据目录在/data/ftp而不是用户家目录,只开ftp_home_dir不一定覆盖得到,实践中还是ftpd_full_access最省心。
注意:
ftpd_full_access相当于一次性放行 FTP 相关的读、写、端口绑定权限,允许它访问非 home 目录。你能接受的安全边界是「FTP 服务本身只服务于这些虚拟用户」,那就用这个布尔值。如果机器上还跑着其他高敏感服务,建议更精细地设置 context。
4. 启动服务并用客户端完整验证
4.1 启动服务与防火墙放行
配置改完,重启服务并设置开机自启:
systemctl restart vsftpd systemctl enable vsftpdfirewalld 默认没放行 FTP 端口,需要单独添加:
firewall-cmd --permanent --add-service=ftp firewall-cmd --reload firewall-cmd --list-services如果用了被动模式且指定了端口范围,还需要放行对应的 TCP 端口段,比如:
firewall-cmd --permanent --add-port=10000-10100/tcp firewall-cmd --reload这里多说一句:FTP 主动模式下的数据连接是服务端主动连客户端的高端口,NAT 环境通常走不通;被动模式下客户端主动连服务端的高端口,对 NAT 友好。所以实际部署时几乎都是被动模式,端口范围要提前规划好并放行。
4.2 命令行客户端实测流程
重启完先用系统自带的命令行客户端做一次完整验证,别急着上图形工具。
ftp ftpuser1@服务器IP输入密码后:
230 Login successful. Remote system type is UNIX. Using binary mode to transfer files.pwd看看当前路径,上传一个测试文件:
put /etc/hostname ./hostname.txt ls get hostname.txt能上传能下载,说明读写权限通了。再用ftpuser2登录一次,确认两个账号都能独立登录、互不干扰。这一步过了,虚拟用户基础功能就算打底完成。
4.3 图形客户端与两个常见误区
命令行测试通过后,通常会用 FileZilla、WinSCP 之类图形工具验证。FileZilla 连接时协议选FTP - File Transfer Protocol,加密方式选「只用普通 FTP」(因为我们没配 TLS,选「要求显式 FTP over TLS」会直接连接失败)。
有两个认识误区可以顺便纠正。
一是「MobaXterm 里能不能架 FTP 服务器」——MobaXterm 是一个连接管理工具,它的 FTP/SFTP 功能是客户端性质,用来连别人的服务器,不是服务器软件。它不能让你的 Windows 电脑变成一个等待别人连接的 FTP 服务器。想搭 FTP 服务端,老老实实在 Linux 服务器上装 vsftpd。
二是「Ubuntu 部署 FTP 和 CentOS Stream 10 有什么区别」——核心原理和配置完全一样,PAM 模块都是pam_userdb.so,数据库生成都是db_load,只是包名不同。CentOS 装libdb-utils,Ubuntu 装db-util;配置文件都在/etc/vsftpd/——这一点上两个发行版习惯保持一致,换系统时无需重新学习思路。
4.4 验证隔离效果
多用户场景下,验证隔离是重点。用ftpuser1登录后尝试:
cd /data/ftp/user2如果chroot_local_user=YES生效且根目录锁在/data/ftp/user1,这条命令应该直接报550 Failed to change directory。
这就是权限隔离最直观的验证。虚拟用户方案的价值在这一刻才真正体现出来——两个系统用户ftpuser1和ftpuser2根本没在/etc/passwd里出现过,但彼此的目录谁也不能越界。
5. 常见报错排查与避坑实录
5.1 530 Login incorrect——九成是数据库或 PAM 的问题
现象最典型:账号密码明明对着数据库写的,但登录总是530 Login incorrect。
排查四步走。
第一步看日志:
journalctl -u vsftpd --no-pager -n 50或查安全日志:
tail -50 /var/log/secure日志里如果出现pam_userdb(vsftpd:auth): user_unknown,说明 PAM 根本没在数据库里找到这个用户。检查vuser.db是否是最新转换的,增删账号后有没有重跑db_load。
第二步检查db=路径是不是带了.db后缀。带了就删掉,只写到/etc/vsftpd/vuser。
第三步确认数据库文件权限。vuser.db如果权限是644,PAM 进程可能读不了(取决于 vsftpd 的降权机制),执行:
chmod 600 /etc/vsftpd/vuser.db第四步确认/etc/pam.d/vsftpd里没有残留的旧认证规则把它拦住了。虚拟用户方案下只保留pam_userdb.so两行最干净。
5.2 500 OOPS: vsftpd: refusing to run with writable root inside chroot()
登录成功但马上断开,日志里是这句。原因是 chroot 后的根目录本身可写。vsftpd 出于安全考虑,不允许用户在一个「可写的根目录」里待着——这是防越狱机制,防止 chroot 的用户在根目录里写文件后利用漏洞逃出 chroot。
两种解法:
allow_writeable_chroot=YES在主配置里加上这条,允许可写根目录存在。另一种是把根目录权限改成755,让vftp不可写根目录,再给子目录单独赋写权限。
推荐第一种,简单直接,配合子目录隔离场景更灵活。
5.3 登录成功但列目录为空或上传失败——SELinux 在背后拦截
最坑的一种:日志不报错、客户端能登录、pwd正常,但ls看不到任何文件,或者上传文件大小永远是 0 字节。
先确认是不是 SELinux:
getenforce如果是Enforcing,再看:
grep ftpd /var/log/audit/audit.log | tail -20有denied字样基本就是 SELinux 拦截了。直接放开:
setsebool -P ftpd_full_access onCentOS Stream 10 默认强制 SELinux,这是新人最容易忽视的坑。本地测试环境嫌烦可以setenforce 0临时关闭验证,但生产环境千万别关,正确做法是按上面开放对应布尔值。
5.4 能下载不能上传——权限链没打通
下载走读权限,上传走写权限,能下不能上说明读没问题、写被卡。
从上到下检查:
ls -ld /data/ftp /data/ftp/user1确认vftp对目录有w权限。再检查主配置write_enable=YES有没有写。
还有一个细节:local_umask=022决定上传文件的默认权限。如果希望上传的文件对组和其他用户开放读权限、只保留属主写权限,022合适;如果希望更私密,077也行。上传后文件的属主是vftp,而不是虚拟用户本身,因为虚拟用户没有真实 UID,所有文件都记在vftp名下。
5.5 500 OOPS: bad IP address——被动模式端口没有放行
这个报错常见于客户端能连接但数据通道建立失败。检查pasv_min_port、pasv_max_port指定的端口范围在 firewalld 里有没有放行,以及客户端所在网络能不能访问这些端口。
提示:FTP 排错有一条铁律——客户端报错只看一半,另一半永远在服务端日志里。
journalctl -u vsftpd和/var/log/secure是两条最核心的日志入口,改配置之后务必重启服务再测,别热加载赌运气。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 530 Login incorrect | PAM 读不到数据库 / 密码错误 | 重跑 db_load,检查 db= 路径与权限 |
| 500 OOPS refusing writable root | chroot 根目录可写 | 加 allow_writeable_chroot=YES |
| 列目录为空 / 上传 0 字节 | SELinux 拦截 | setsebool -P ftpd_full_access on |
| 能下不能上传 | 目录权限 / write_enable | 检查 /data/ftp 属主与权限 |
| 500 OOPS bad IP address | 被动端口不可达 | 放行 pasv 端口段 |
6. 进阶玩法:虚拟用户按账号隔离与多发行版扩展
6.1 每个虚拟用户独立根目录与独立权限
前面配置里预留的user_config_dir=/etc/vsftpd/vusers_dir就是干这个的。
先创建目录:
mkdir -p /etc/vsftpd/vusers_dir然后按用户名建文件,比如ftpuser1:
vi /etc/vsftpd/vusers_dir/ftpuser1写入:
local_root=/data/ftp/user1 write_enable=YES anon_world_readable_only=NO再建ftpuser2,根目录指到/data/ftp/user2:
local_root=/data/ftp/user2 write_enable=NO这样ftpuser1上传下载自由,ftpuser2只能看不能传。主配置文件里全局的write_enable=YES会被用户级配置文件里的write_enable=NO覆盖,实现按需控制。
local_root指定每个虚拟用户的根目录,效果等同于把每个账号锁进自己的数据目录。注意local_root的目录必须属于vftp,否则写权限又会出问题。
6.2 虚拟用户与匿名访问共存的取舍
默认方案里anonymous_enable=NO已经关死匿名。有些场景需要「公开下载 + 登录上传」双通道,可以开匿名:
anonymous_enable=YES anon_root=/data/ftp/public但匿名用户走的是ftp匿名用户身份,虚拟用户走的是vftp身份,两者文件权限互不相通,目录要分开规划。匿名区只给只读权限,登录区按虚拟用户管理,避免匿名用户在上传区搞乱。
6.3 压缩一下跨发行版的要点
CentOS Stream 10 上的这套配置,思路可以直接搬到 Ubuntu 或者其他 Debian 系发行版上。
| 项目 | CentOS Stream 10 | Ubuntu / Debian |
|---|---|---|
| vsftpd 安装 | dnf install -y vsftpd | apt install -y vsftpd |
| db 转换工具 | libdb-utils | db-util |
| PAM 模块 | pam_userdb.so | pam_userdb.so(一致) |
| 主配置路径 | /etc/vsftpd/vsftpd.conf | /etc/vsftpd.conf |
| 日志入口 | journalctl -u vsftpd | journalctl -u vsftpd |
差异主要是配置文件名和路径,其余的核心机制——数据库、PAM、guest_username映射——完全一致。Ubuntu 上没有 SELinux 这道坎,会省不少事,但也要注意 AppArmor 是否拦了/data/ftp的访问,理念是相通的。
6.4 最后提醒一个运维细节
每次往vuser.txt里追加账号后,必须重新执行:
db_load -T -t hash -f /etc/vsftpd/vuser.txt /etc/vsftpd/vuser.db systemctl restart vsftpd漏了第一步,新增账号永远登录不上。漏了第二步,数据库虽然更新了,但旧配置还在缓存里。这两步绑在一起做,形成肌肉记忆就不会踩坑。
我个人的建议是把这条命令串写成一个简单的脚本,比如/usr/local/bin/vuser_sync.sh,每次改完vuser.txt直接跑一下,省心很多,也减少手误的概率。