上一期文章把 FTP 服务的基础配置跑通了匿名用户能顺利连上、下载文件。但说实话那种配置只能算“能开机”真要放到生产环境或者多人协作场景里匿名访问几乎没法用。最典型的问题是只要想让某个人能上传文件就得给他开一个系统账号而系统账号一旦多起来密码策略、目录权限、shell 权限全都搅在一起风险也跟着上来。这篇文章针对 CentOS Stream 10重点讲怎么配置虚拟用户访问把 FTP 账号和系统账号完全拆开。这样既保住了登录安全问题又不用给每个人开系统级身份。所谓虚拟用户本质上是一套“只存在于 FTP 服务内部”的用户体系。访问 FTP 的人用 vuser 账号登录时vsftpd 会通过 PAM 模块去一个数据库文件里查用户名和密码而不是直接调操作系统的 /etc/passwd 和 /etc/shadow。认证通过后虚拟用户会被映射到系统里一个固定的本地账号比如 vftp然后所有文件操作都落在这个映射账号身上。这样做的好处很明显FTP 的密码写在自己维护的数据库里跟系统密码隔离开FTP 用户没有 shell、没有登录系统的资格每个虚拟用户还能单独指定主目录和写入权限互不干扰。整个配置链路涉及 vsftpd、PAM、Berkeley DB、firewalld、SELinux任何一个环节出错FTP 登录就会失败。下面我把每一步都拆开讲包括为什么要这么做、参数怎么选、踩过哪些坑。1. 虚拟用户方案为什么不直接用系统账号1.1 系统账号做 FTP 的三个痛点先说清楚为什么要有虚拟用户这个东西。很多人第一次搭 FTP 时第一反应是“给每个人 useradd 一个账号然后让他登录”。这个思路在只有三五个用户的小内网里确实能用但一旦用户数量上来问题就非常明显。第一个痛点是密码管理。系统账号的密码是跟着系统认证走的如果 FTP 密码要定期改你就要去改系统密码如果某个人离职了你要记得把他的系统账号禁用或删除否则这个可能已经泄露的密码仍然有效。第二个痛点是权限边界。系统账号一旦创建哪怕你把 shell 设成 /sbin/nologin它依然会出现在 /etc/passwd 里而且只要目录权限给得不小心就可能被用于登录其他服务。第三个痛点是目录隔离。每个系统账号默认对应 /home/xxx用户习惯五花八门统一管理目录很麻烦做 chroot 时也容易因为权限和属主问题报错。虚拟用户方案就是在 FTP 服务和操作系统之间加了一层“缓冲垫”。用户账号不存在于系统里只存在于 FTP 自己的数据库里目录权限则通过一个统一的映射账号来控制。这样上面的三个痛点全部被绕开了。1.2 虚拟用户的认证路径与安全边界虚拟用户登录时数据流向大概是这样的客户端发起 FTP 连接vsftpd 收到用户名密码后把认证请求交给 PAM 模块PAM 里配置的是 pam_userdb.so这个模块会去读取一个 Berkeley DB 格式的数据库文件在里面查用户名和密码查到了认证通过vsftpd 把这个虚拟用户当成配置里指定的 guest_username 来处理。之后所有文件读写、目录列表、上传下载都是以这个 guest_username 的系统账号身份去执行的。这里面有个关键理解点FTP 用户的身份和操作系统用户身份是两码事。虚拟用户列表只存在于数据库文件里系统里看不到这些账号而真正操作系统层面的文件权限全部落在唯一一个映射账号身上。所以你要做的就是两件事第一确保 pam_userdb 能正确读取数据库第二确保映射账号对虚拟用户的主目录有足够权限。安全边界也因此变得清晰了。就算某个虚拟用户的密码泄露了攻击者也只能进入 FTP 允许的目录范围不能登录系统不能访问其他用户的目录。数据库文件本身权限设成 600 就够再配合 chroot 锁定目录安全性比直接开系统账号高一个等级。2. 环境准备装好依赖软件与数据库工具2.1 安装 vsftpd 与数据库相关的工具CentOS Stream 10 的默认源里自带 vsftpd版本是 3.x。开始之前先把系统更新一下然后装软件包dnf update -y dnf install -y vsftpd db5-utils这里 db5-utils 是用来生成虚拟用户数据库的工具包里面提供了 db_load 命令。如果你不确定自己的系统里 db_load 属于哪个包可以先跑一下dnf provides */db_load看到哪个包提供这个路径就装哪个。老一点的系统里叫 db4-utilsCentOS Stream 9 以后多数是 db5-utils。装完以后检查一下rpm -q vsftpd db5-utils db_load -h只要 db_load 能正常打印参数说明环境就算齐了。2.2 创建虚拟用户账号的文本源文件虚拟用户数据库不是直接手写二进制文件的而是先写一个纯文本文件再用 db_load 转成数据库格式。这个文本文件的格式非常严格每两行一组第一行是用户名第二行是密码不能有空格、不能有空行、不能有多余的注释。mkdir -p /etc/vsftpd vi /etc/vsftpd/vuser.txt假设我要建三个用户分别是 alice、bob、carol文件内容如下alice Pssw0rd-2024 bob B0b!Secure carol Car0l!Ftp写完之后把文件的权限收紧一些因为里面是明文密码chmod 600 /etc/vsftpd/vuser.txt这一步很多人会忽略。文本文件只是过渡产物真正的认证数据在 .db 文件里但如果你把明文密码文件留在系统里权限又不设好就相当于把密码表直接送人了。生产环境我建议这个文件放在 /etc/vsftpd 下面跟最终数据库放同一个目录方便后面迁移和备份。3. 核心配置让 PAM 认账、让 vsftpd 放行3.1 用 db_load 生成 Berkeley DB 认证库文本文件准备好以后执行下面的命令把它转成 Berkeley DB 数据库db_load -T -t hash -f /etc/vsftpd/vuser.txt /etc/vsftpd/vuser.db参数的含义-T 表示输入文件是普通文本而不是已有的数据库-t hash 指定数据库类型为 hashpam_userdb 默认能识别-f 后面跟输入文件最后一个路径是输出文件。执行完以后检查一下ls -l /etc/vsftpd/vuser.db file /etc/vsftpd/vuser.db正常的话你会看到这个 db 文件已经生成。然后设置它的权限和属主chmod 600 /etc/vsftpd/vuser.db chown root:root /etc/vsftpd/vuser.db这里特别强调一下db 文件的属主必须是 root权限必须是 600否则 PAM 模块读取时会因为权限问题直接拒绝认证。另外每次你修改了 vuser.txt都要重新执行一次 db_load然后重启 vsftpd 才能生效。这个流程后面维护用户时还会反复用到。3.2 写 PAM 规则vsftpd 怎么认虚拟用户PAM 是 Linux 系统的认证框架vsftpd 通过它来对接不同的认证方式。默认情况下vsftpd 走的是系统账号认证也就是直接用 pam_unix.so 读 /etc/shadow。现在我们要改成只认自己数据库里的用户所以需要新建或修改 /etc/pam.d/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_userdb 会自动拼上。这里只保留两行就够了不要学某些教程把 pam_unix.so 也留下。如果同时存在两套认证规则虚拟用户认证会出问题还会导致系统账号和 FTP 账号混在一起认证违背我们拆分的初衷。写完保存后可以用一个简单的命令测试一下 PAM 模块能不能正确读取数据库pamtester vsftpd alice Pssw0rd-2024如果 pamtester 没安装先装一下dnf install -y pamtester能输出类似 “pam_authenticate: Success” 的信息说明 PAM 这块已经通了。后面如果再遇到 530 登录失败可以回到这一步来排查看是数据库问题还是 PAM 配置问题。3.3 vsftpd.conf 核心参数逐条解读PAM 配置好了还不行vsftpd 这边也要开启虚拟用户模式并关闭匿名和系统用户访问。打开主配置文件vi /etc/vsftpd/vsftpd.conf原文件里默认的匿名配置直接关掉改成下面这套核心配置。我按模块拆开讲每条参数背后都有用途。首先是基础行为控制anonymous_enableNO local_enableYES write_enableYES local_umask022 dirmessage_enableYES xferlog_enableYES xferlog_std_formatYES connect_from_port_20YESanonymous_enable 必须设为 NO否则匿名还能进来。local_enable 设为 YES因为虚拟用户最终会被映射成一个本地系统账号所以 vsftpd 会把虚拟用户当作 local user 来处理这个开关不能关。write_enable 必须开否则上传、删除、重命名这些写操作全部无效。然后是 chroot 和目录锁定chroot_local_userYES allow_writeable_chrootYESchroot_local_userYES 是最核心的安全参数它把所有本地用户包括映射后的虚拟用户都关在各自的主目录里不允许用户访问上一级目录。allow_writeable_chroot 是为了解决一个经典的报错如果用户的主目录本身是可写的且开启了 chrootvsftpd 会拒绝启动会话并报 500 OOPS。这个参数的作用就是允许用户主目录可写时依然能正常 chroot。它确实稍微降低了一点安全性但实际操作里比费劲去改目录权限要省事得多家用和小团队场景够用。如果追求极致安全可以让用户根目录只读、内部再放一个可写的子目录这样就不用这个参数了。然后是虚拟用户核心开关guest_enableYES guest_usernamevftp pam_service_namevsftpdguest_enableYES 表示开启虚拟用户支持guest_usernamevftp 表示所有虚拟用户都映射到系统里的 vftp 账号pam_service_namevsftpd 告诉 vsftpd 去读 /etc/pam.d/vsftpd 这个 PAM 配置。整个链路就是靠这三个参数串起来的。稍后我会讲怎么创建 vftp 账号。接着是被动模式端口范围pasv_enableYES pasv_min_port40000 pasv_max_port50000被动模式是 FTP 客户端默认的连接方式。主动模式下服务器去连客户端的随机端口在很多 NAT 网络里会失败被动模式改由客户端连服务器的端口区间所以必须把端口段显式声明出来并在防火墙里放行。这里我习惯用 40000-50000段够大也能满足并发连接。最后把监听方式固定一下listenYES listen_ipv6NO如果你的服务器启用了 IPv6也可以把 listen_ipv6 打开但双栈同时监听容易让 vsftpd 起不来所以通常二选一。IPv4 场景下就用 NO 就好。3.4 创建映射账号 vftp 并规划虚拟用户目录树现在回到系统层面创建 vftp 这个映射账号。它不需要登录权限主目录放在 /data/ftp 下面useradd -d /data/ftp -s /sbin/nologin vftp mkdir -p /data/ftp chown -R vftp:vftp /data/ftp chmod 755 /data/ftpvftp 账号是整个虚拟用户体系的“实际文件操作者”。所有虚拟用户登录进去之后在操作系统层面都被视为 vftp。也就是说虚拟用户能不能写某个目录取决于 vftp 用户对该目录有没有写权限。所以每个虚拟用户的主目录属主都要设成 vftp。规划目录树时我建议的结构是/data/ftp/ ├── alice/ ├── bob/ └── carol/创建方式mkdir -p /data/ftp/alice /data/ftp/bob /data/ftp/carol chown -R vftp:vftp /data/ftp/alice /data/ftp/bob /data/ftp/carol chmod 755 /data/ftp/alice /data/ftp/bob /data/ftp/carolchmod 755 的话虚拟用户能读、能进入目录但不能写。如果想让某个用户能上传就改成chmod 775 /data/ftp/alice或者给 755 加组写权限也行取决于你按属主还是按组区分权限。我习惯直接让目录属主是 vftp再用 775 放行写权限。这样在配置文件里不用设 anon_world_readable_only 之类的东西简单直接。接下来是每个虚拟用户的独立配置。vsftpd 提供了 user_config_dir 机制就是给每个虚拟用户单独放一个配置文件覆盖主配置里的参数。创建目录mkdir -p /etc/vsftpd/user_conf然后在 vsftpd.conf 里加上这一行user_config_dir/etc/vsftpd/user_conf接着给 alice 创建一个配置文件vi /etc/vsftpd/user_conf/alice内容local_root/data/ftp/alice write_enableYES anon_umask022local_root 指这个虚拟用户登录后看到的根目录write_enable 控制是否允许写。如果某个用户只想下载就把 write_enable 去掉vi /etc/vsftpd/user_conf/boblocal_root/data/ftp/bob write_enableNO这样就实现了细粒度的权限控制。主配置里的 write_enable 只是“总开关”开启后不代表每个虚拟用户都能写每个虚拟用户自己的配置文件里再控制一次才能做到一人一套策略。4. 防火墙与 SELinuxCentOS Stream 10 的两道坎4.1 firewalld 放行 FTP 流量CentOS Stream 10 默认启用 firewalld而且状态是启动的。如果只改完 vsftpd 配置就立刻测试客户端大概率会卡在“连接超时”或“读取目录列表失败”上原因就是防火墙没放行 21 端口和被动模式端口段。执行下面三条命令firewall-cmd --permanent --add-serviceftp firewall-cmd --permanent --add-port40000-50000/tcp firewall-cmd --reload第一条直接放行标准 FTP 服务端口 21第二条把被动模式端口段 40000-50000 全部放行第三条让配置立即生效。检查一下是否生效firewall-cmd --list-all看到 services 里有 ftp、ports 里有 40000-50000/tcp 就对了。这一步别省也别偷懒只放行 21 端口。很多 FTP 能登录却列出不了目录的问题最后查下来都是被动模式端口没放行。4.2 SELinux 策略调整CentOS Stream 10 的 SELinux 默认也是 Enforcing 状态。vsftpd 从系统的非标准目录比如 /data/ftp读取文件时会被 SELinux 策略拦截导致即使防火墙放行了登录后也看不到文件、上传也提示拒绝。最简单的做法是打开 ftpd_full_access 布尔值setsebool -P ftpd_full_access 1-P 参数表示持久化重启后依然生效。这个布尔值开了之后FTP 相关的文件访问限制基本就都放开了。如果不想开得这么宽也可以更精细地设置目录上下文semanage fcontext -a -t public_content_t /data/ftp(/.*)? restorecon -Rv /data/ftp这样只对 /data/ftp 下的文件放行 FTP 读取权限。两种方案我建议看场景内网测试直接开 ftpd_full_access省心生产环境如果对安全要求高用 semanage fcontext 的精细化方案再按需放行写权限。这里有个容易忽略的点如果你用的是 /data/ftp 这种自定义路径SELinux 默认不会给它打上正确的文件类型标签。哪怕账号权限都对了SELinux 还是会拦。改完策略后记得用getenforce确认当前状态再用ls -Z /data/ftp看一下文件上下文是否已经变成 public_content_t 或对应的 ftpd 类型。5. 验证测试从命令行到图形客户端5.1 本地自测与日志定位配置全部完成后先把 vsftpd 启动并设为开机自启systemctl enable --now vsftpd systemctl status vsftpd状态显示 active (running) 之后先在本地用命令行自测一下ftp localhost输入虚拟用户名 alice 和密码 Pssw0rd-2024如果能看到 230 Login successful 的提示再接一个 ls 命令能看到文件列表说明整个链路已经通了。注意本地用 localhost 测试时走的也是 PAM 认证跟远程客户端没有区别这一步能过就说明核心逻辑没问题。如果登录失败第一时间看日志而不是瞎改配置journalctl -u vsftpd -n 50 tail -n 50 /var/log/secureCentOS Stream 10 下 /var/log/secure 会记录 PAM 认证失败的详细信息比如数据库路径不对、权限错误、账号不存在等。绝大多数问题在这一步就能定位。5.2 常见客户端连接配置命令行能通之后再用图形客户端验证一遍因为图形客户端的连接方式和命令行有点差别尤其是被动模式配置。以 FileZilla 为例连接时输入服务器公网 IP、虚拟用户名、密码、端口 21。连接成功后重点检查两件事第一能否看到属于该用户的 local_root 目录内容第二目录列表是否正常刷新。如果连接成功但列表卡住多半是被动模式端口没有放行或者 PASV 配置有问题。可以在 FileZilla 的设置里把传输模式改成“被动”并确认客户端允许使用服务器返回的端口范围。从客户端上传一个小文件确认写入权限生效再下载一个文件确认读取权限正常。如果 alice 有写权限而 bob 只有读权限就在客户端分别用两个账号各测一遍确保权限隔离没有交叉。6. 问题排查实录我踩过的坑和解决办法6.1 530 Login incorrect这是最常见的报错几乎所有人第一次配虚拟用户都会撞上。登录时提示 530 Login incorrect字面意思就是认证没通过但原因可能有一大堆。我按概率排一下排查顺序PAM 配置不对。/etc/pam.d/vsftpd 里如果写了多行不同的认证规则或者 db 路径写错了都会导致失败。检查必须只有两行auth 和 account 各一行。db 文件不存在或权限不对。执行 ls -l /etc/vsftpd/vuser.db确认属主是 root、权限是 600。如果之前 root 重启时目录权限变了PAM 模块读不了数据库也会报 530。文本文件改完忘记重新 db_load。很多人改了 vuser.txt 直接重启 vsftpd结果数据库还是旧的新用户登不上。密码大小写或空格问题。文本文件里第二行如果有不可见字符或者末尾多了空格密码就跟输入的不一致。排查时用 pamtester 测试 PAM 模块是最快的它能直接告诉你认证是成功还是失败直接区分是数据库问题还是 vsftpd 配置问题。6.2 500 OOPS: vsftpd: refusing to run with writable root inside chroot()开启 chroot_local_userYES 后如果用户的主目录本身可以被该用户写入vsftpd 会直接拒绝启动会话客户端登录时看到的就是这一串 500 OOPS。这是安全策略有意为之防止用户被 chroot 之后还能在根目录里创建文件产生概念混淆。解决办法有两个。我推荐先加 allow_writeable_chrootYES因为这一行能直接解决问题而且对普通场景影响不大。如果对安全有洁癖就把用户的主目录改成不可写然后在主目录里建一个子目录用来放文件。比如 local_root/data/ftp/alice而 /data/ftp/alice 属主是 root、权限 755里面再建 /data/ftp/alice/files属主才是 vftp、权限 775。这样用户能写 files 子目录但根目录本身只读chroot 也能正常工作。6.3 目录列表刷新失败与被动模式客户端能登录也能执行上传之类的命令但列表经常转圈半天最后报错“无法取得目录列表”或“连接超时”。这个问题几乎全是被动模式端口没放行引起的。客户端在被动模式下会去连服务器的 40000-50000 端口区间如果 firewalld 只放行了 21 端口这个区间里的连接会被直接丢弃。排查命令firewall-cmd --list-all ss -lntp | grep 40000看到端口有监听但防火墙没有放行就执行前面的放行命令然后 reload。另外如果你在云服务器上记得还要在云平台的安全组里放行同样的端口段否则本机防火墙放行了也没用。6.4 上传报 550 Permission denied能登录、能看到文件列表但上传文件时提示 550 Permission denied这说明认证没问题是文件系统层面的权限不够。虚拟用户操作文件时是以 vftp 账号的身份去写的所以目标目录必须允许 vftp 写入。检查目录属主和权限ls -ld /data/ftp/alice如果属主不是 vftp或者权限是 755改成chown vftp:vftp /data/ftp/alice chmod 775 /data/ftp/alice别忘了还要检查 SELinux用ls -Z /data/ftp/alice看上下文。如果是 user_home_t 或 httpd_sys_content_t 这类标签在 Enforcing 模式下也会拦截写入。需要重新设置成 public_content_rw_t 并打开 ftpd_anon_write 布尔值semanage fcontext -a -t public_content_rw_t /data/ftp/alice(/.*)? restorecon -Rv /data/ftp/alice setsebool -P ftpd_anon_write 16.5 虚拟用户目录没生效有时候所有虚拟用户登录后都看到根目录下同一个地方或者 user_config_dir 里配置的 local_root 没起作用。这种情况先确认文件名和虚拟用户名是否完全一致大小写也必须对上。vsftpd 读取 user_config_dir 下的文件是按照登录用户名精确匹配的。alice 的配置放成 Alice 文件就不会被读取。另外确认 vsftpd.conf 里 user_config_dir 这行没有被注释而且目录路径存在。如果配置没问题重启 vsftpd 后再测试。改完配置文件一定要重启服务这是老生常谈但真的很容易忘记systemctl restart vsftpd7. 日常维护与批量管理心得7.1 新增虚拟用户的完整流程把整个流程理顺之后新增一个虚拟用户其实就是四步先创建目录再追加文本文件然后重新生成数据库最后写独立配置并重启服务。以新增用户 dave 为例完整命令是mkdir -p /data/ftp/dave chown vftp:vftp /data/ftp/dave chmod 775 /data/ftp/dave echo dave /etc/vsftpd/vuser.txt echo Dave2024! /etc/vsftpd/vuser.txt db_load -T -t hash -f /etc/vsftpd/vuser.txt /etc/vsftpd/vuser.db chmod 600 /etc/vsftpd/vuser.db echo local_root/data/ftp/dave /etc/vsftpd/user_conf/dave echo write_enableYES /etc/vsftpd/user_conf/dave systemctl restart vsftpd我建议把这套命令写成一个脚本参数化用户名和密码以后新增用户就一行命令搞定。脚本里务必包含 chmod 600 和最后的服务重启这两个步骤漏掉会带来非常隐蔽的故障。7.2 数据库备份与迁移技巧虚拟用户数据库只是一个 db 文件备份特别方便。你不需要单独备份 .db 文件只需要把 vuser.txt 这个文本源文件备一份就行。因为任何时候都可以用 db_load 重新生成 .db 文件而 .db 文件一旦损坏反而很难手工恢复。我个人的习惯是把 vuser.txt 纳入备份任务同时在服务器的另一个目录放一份副本。迁移到新服务器时只需要装好 vsftpd 和 db5-utils把 vuser.txt 拷过去重新生成 .db再同步 /data/ftp 目录结构和 vftp 账号即可。整个过程比迁移系统账号用户要干净太多。还有一个小技巧密码更新后不需要把用户目录删掉重建只要改 vuser.txt、重新 db_load、重启 vsftpd 三步就好用户所有文件都还在。这也是虚拟用户方案在运维上的重要优势。这套配置我在 CentOS Stream 10 上反复验证过从本地测试到跨网段访问都稳定。刚开始配虚拟用户时最容易急躁因为报错信息要么很笼统要么指向性不强。记住一条原则先查 PAM、再查数据库文件权限、最后查 SELinux 和防火墙按这个顺序排查绝大多数问题十分钟内就能解决。