ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CentOS Stream 10 vsftpd虚拟用户配置:PAM认证与目录权限隔离

CentOS Stream 10 vsftpd虚拟用户配置:PAM认证与目录权限隔离

上一期文章把 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-utils,CentOS 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 P@ssw0rd-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 指定数据库类型为 hash,pam_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 P@ssw0rd-2024

如果 pamtester 没安装,先装一下:

dnf install -y pamtester

能输出类似 “pam_authenticate: Success” 的信息,说明 PAM 这块已经通了。后面如果再遇到 530 登录失败,可以回到这一步来排查,看是数据库问题还是 PAM 配置问题。

3.3 vsftpd.conf 核心参数逐条解读

PAM 配置好了还不行,vsftpd 这边也要开启虚拟用户模式,并关闭匿名和系统用户访问。打开主配置文件:

vi /etc/vsftpd/vsftpd.conf

原文件里默认的匿名配置直接关掉,改成下面这套核心配置。我按模块拆开讲,每条参数背后都有用途。

首先是基础行为控制:

anonymous_enable=NO local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES xferlog_std_format=YES connect_from_port_20=YES

anonymous_enable 必须设为 NO,否则匿名还能进来。local_enable 设为 YES,因为虚拟用户最终会被映射成一个本地系统账号,所以 vsftpd 会把虚拟用户当作 local user 来处理,这个开关不能关。write_enable 必须开,否则上传、删除、重命名这些写操作全部无效。

然后是 chroot 和目录锁定:

chroot_local_user=YES allow_writeable_chroot=YES

chroot_local_user=YES 是最核心的安全参数,它把所有本地用户(包括映射后的虚拟用户)都关在各自的主目录里,不允许用户访问上一级目录。allow_writeable_chroot 是为了解决一个经典的报错:如果用户的主目录本身是可写的,且开启了 chroot,vsftpd 会拒绝启动会话并报 500 OOPS。这个参数的作用就是允许用户主目录可写时依然能正常 chroot。它确实稍微降低了一点安全性,但实际操作里比费劲去改目录权限要省事得多,家用和小团队场景够用。如果追求极致安全,可以让用户根目录只读、内部再放一个可写的子目录,这样就不用这个参数了。

然后是虚拟用户核心开关:

guest_enable=YES guest_username=vftp pam_service_name=vsftpd

guest_enable=YES 表示开启虚拟用户支持;guest_username=vftp 表示所有虚拟用户都映射到系统里的 vftp 账号;pam_service_name=vsftpd 告诉 vsftpd 去读 /etc/pam.d/vsftpd 这个 PAM 配置。整个链路就是靠这三个参数串起来的。稍后我会讲怎么创建 vftp 账号。

接着是被动模式端口范围:

pasv_enable=YES pasv_min_port=40000 pasv_max_port=50000

被动模式是 FTP 客户端默认的连接方式。主动模式下服务器去连客户端的随机端口,在很多 NAT 网络里会失败;被动模式改由客户端连服务器的端口区间,所以必须把端口段显式声明出来,并在防火墙里放行。这里我习惯用 40000-50000,段够大,也能满足并发连接。

最后把监听方式固定一下:

listen=YES listen_ipv6=NO

如果你的服务器启用了 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/ftp

vftp 账号是整个虚拟用户体系的“实际文件操作者”。所有虚拟用户登录进去之后,在操作系统层面都被视为 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/carol

chmod 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_enable=YES anon_umask=022

local_root 指这个虚拟用户登录后看到的根目录,write_enable 控制是否允许写。如果某个用户只想下载,就把 write_enable 去掉:

vi /etc/vsftpd/user_conf/bob
local_root=/data/ftp/bob write_enable=NO

这样就实现了细粒度的权限控制。主配置里的 write_enable 只是“总开关”,开启后不代表每个虚拟用户都能写;每个虚拟用户自己的配置文件里再控制一次,才能做到一人一套策略。

4. 防火墙与 SELinux:CentOS Stream 10 的两道坎

4.1 firewalld 放行 FTP 流量

CentOS Stream 10 默认启用 firewalld,而且状态是启动的。如果只改完 vsftpd 配置就立刻测试,客户端大概率会卡在“连接超时”或“读取目录列表失败”上,原因就是防火墙没放行 21 端口和被动模式端口段。

执行下面三条命令:

firewall-cmd --permanent --add-service=ftp firewall-cmd --permanent --add-port=40000-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 和密码 P@ssw0rd-2024,如果能看到 230 Login successful 的提示,再接一个 ls 命令,能看到文件列表,说明整个链路已经通了。注意本地用 localhost 测试时,走的也是 PAM 认证,跟远程客户端没有区别,这一步能过就说明核心逻辑没问题。

如果登录失败,第一时间看日志而不是瞎改配置:

journalctl -u vsftpd -n 50 tail -n 50 /var/log/secure

CentOS 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_user=YES 后,如果用户的主目录本身可以被该用户写入,vsftpd 会直接拒绝启动会话,客户端登录时看到的就是这一串 500 OOPS。这是安全策略有意为之,防止用户被 chroot 之后还能在根目录里创建文件,产生概念混淆。

解决办法有两个。我推荐先加 allow_writeable_chroot=YES,因为这一行能直接解决问题,而且对普通场景影响不大。如果对安全有洁癖,就把用户的主目录改成不可写,然后在主目录里建一个子目录用来放文件。比如 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 1

6.5 虚拟用户目录没生效

有时候所有虚拟用户登录后都看到根目录下同一个地方,或者 user_config_dir 里配置的 local_root 没起作用。这种情况先确认文件名和虚拟用户名是否完全一致,大小写也必须对上。vsftpd 读取 user_config_dir 下的文件,是按照登录用户名精确匹配的。alice 的配置放成 Alice 文件,就不会被读取。

另外,确认 vsftpd.conf 里 user_config_dir 这行没有被注释,而且目录路径存在。如果配置没问题,重启 vsftpd 后再测试。改完配置文件一定要重启服务,这是老生常谈但真的很容易忘记:

systemctl restart vsftpd

7. 日常维护与批量管理心得

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 'Dave@2024!' >> /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_enable=YES' >> /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 和防火墙,按这个顺序排查,绝大多数问题十分钟内就能解决。

返回列表