前言
DocumentRoot是 Apache HTTP Server 里最基础的一条指令,它指定「HTTP 请求映射到文件系统的哪个目录」。看起来只是改个路径,但真正在生产里改过它的人都知道:改完之后最常见的结局是访问任何文件都返回 403 Forbidden,而不是期望中的页面。原因不在DocumentRoot本身,而在与它配套的那几样东西——Directory 容器指令、文件系统权限、SELinux 标签,以及 2.2 到 2.4 的授权语法变更。
本文要讲的正是这些「改一处、连带四处」的东西。先说明符号约定:Apache 的配置里有若干容器指令,在配置文件中以尖括号包住名字和参数的形式书写,例如Directory、Location、VirtualHost。下文在正文里一律称它们为「Directory 容器」「VirtualHost 容器」,具体写法只在代码片段中出现。
本文示例基于 Apache HTTP Server 2.4(RHEL 8/9、Rocky Linux 9 上是httpd,Ubuntu 20.04/22.04 上是apache2),两个发行版在服务名、运行用户、配置目录结构上都有差异,会分别标注。若仍在用 2.2,请注意授权语法完全不同,第三节专门讲这件事。
一、DocumentRoot 到底做了什么,没做什么
DocumentRoot只做一件事:定义 URL 到文件系统路径的映射起点。请求/images/a.png加上DocumentRoot "/var/www/html",映射结果就是/var/www/html/images/a.png。
它不做下面这些事,而这些恰恰是 403 的来源:
| 事情 | 由谁负责 | 与 DocumentRoot 的关系 |
|---|---|---|
| 允许访问该目录 | Directory容器里的Require指令 | 必须单独配置,且路径要写对 |
| 目录项列表 | Options Indexes/-Indexes | 与 DocumentRoot 无关,但常被一起改 |
| 文件系统读权限 | 目录属主、权限位 | Apache 以apache(RHEL)/www-data(Ubuntu)身份读文件 |
| SELinux 标签 | httpd_sys_content_t | RHEL 家族特有,非标准路径必查 |
| 跟随软链接 | Options FollowSymLinks | 不开则软链目标不可访问 |
| 虚拟主机归属 | VirtualHost容器的ServerName/ServerAlias | 多个 vhost 时请求落在哪个 root 上 |
先记住这个结论:改DocumentRoot至少要同步改三处——Directory容器里的路径、目标目录的文件系统权限、以及 RHEL 家族上的 SELinux 上下文。只改一处,得到的必然是 403。
1.1 两个发行版的默认值与配置文件位置
| 项目 | RHEL / Rocky / AlmaLinux | Ubuntu / Debian |
|---|---|---|
| 包名 / 服务名 | httpd | apache2 |
| 运行用户/组 | apache/apache | www-data/www-data |
| 主配置 | /etc/httpd/conf/httpd.conf | /etc/apache2/apache2.conf |
| 站点配置 | /etc/httpd/conf.d/*.conf(直接生效) | sites-available/加sites-enabled/软链 |
| 默认 DocumentRoot | /var/www/html | /var/www/html |
| 模块 / 站点管理 | 配置文件里LoadModule | a2enmod/a2ensite |
| 配置检查 | apachectl configtest或httpd -t | apachectl configtest或apache2ctl configtest |
注意/var/www/html这个默认值:Ubuntu 在 14.04 之前默认是/var/www,老教程照抄会得到 404。用apachectl -S可以直接看到每个虚拟主机实际生效的DocumentRoot:
# 打印虚拟主机映射与各自的关键配置 sudo apachectl -S # RHEL 家族也可以直接看最终配置里的指令 sudo httpd -t -D DUMP_RUN_CFG 2>&1 | head -40apachectl -S的输出会列出每个 vhost 的端口、名称、对应配置文件和行号,排查「请求落到哪个 vhost」时它是第一选择。
二、改 DocumentRoot 的正确流程
下面以「把站点挪到/data/www」为例走一遍完整流程,两个发行版的差异用注释标出。
2.1 站点配置
以 RHEL 家族为例,写在/etc/httpd/conf.d/mysite.conf:
# 适用:RHEL 8/9、Rocky Linux 9、AlmaLinux 9 上的 httpd 2.4 <VirtualHost *:80> ServerName www.example.com ServerAlias example.com DocumentRoot "/data/www" <Directory "/data/www"> # 与 DocumentRoot 路径必须完全一致 Options -Indexes +FollowSymLinks AllowOverride None Require all granted </Directory> ErrorLog /var/log/httpd/mysite-error.log CustomLog /var/log/httpd/mysite-access.log combined </VirtualHost>Ubuntu / Debian 上的差异只有三处:站点文件写在/etc/apache2/sites-available/mysite.conf而不是conf.d/;日志路径用${APACHE_LOG_DIR}(在envvars里定义,值通常是/var/log/apache2)替代绝对路径;写完必须启用并停用默认站点,否则它仍会作为默认 vhost 生效。
sudo a2ensite mysite sudo a2dissite 000-default sudo apache2ctl configtest sudo systemctl reload apache22.2 文件系统权限
Apache 以apache(RHEL)或www-data(Ubuntu)身份读取文件,除了文件本身可读,每一级父目录都必须有执行(x)权限,否则路径无法穿透。
# 目录属主与权限(推荐:root 拥有,world 可读可进入) sudo chown -R root:root /data/www sudo find /data/www -type d -exec chmod 755 {} \; sudo find /data/www -type f -exec chmod 644 {} \; # 关键:逐级检查父目录是否有 x 权限 namei -l /data/www/index.htmlnamei -l会逐层列出路径上每个组件的权限,一眼就能看出是哪一级断了,比一层层ls -ld快得多。
2.3 SELinux(仅 RHEL 家族)
这一步是「权限全对但还是 403」的头号原因。SELinux 默认只允许 httpd 读取带httpd_sys_content_t标签的文件,而新建的/data目录标签是default_t。
# 确认 SELinux 状态、当前标签、以及最近的拒绝记录 getenforce ls -Zd /data/www /var/www/html sudo ausearch -m AVC,USER_AVC -ts recent | tail -30 # RHEL 8/9 需要 policycoreutils-python-utils 提供 semanage sudo dnf install -y policycoreutils-python-utils # 添加持久化的文件上下文规则并应用 # (只 chcon 会在 restorecon 或重新打标签后丢失) sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" sudo restorecon -Rv /data/www # 需要让 Apache 往该目录写(如上传目录)时改用 rw 标签 sudo semanage fcontext -a -t httpd_sys_rw_content_t "/data/www/uploads(/.*)?" sudo restorecon -Rv /data/www/uploads排障时可以临时用sudo setenforce 0验证「是不是 SELinux 导致的」,但验证完必须sudo setenforce 1改回来,绝不能把 enforcing 关掉当成修复方案。
2.4 应用与验证
改完先sudo apachectl configtest(期望Syntax OK),再sudo systemctl reload httpd(或reload apache2),最后用curl -sI http://127.0.0.1/与apachectl -S确认请求走进了预期的 vhost。
三、2.2 与 2.4 的授权语法不能混用
这是老教程挖得最深的坑。Apache 2.4 把访问控制从mod_access_compat的Order/Allow/Deny改成了mod_authz_core的Require系列,两套语法不能混着写。
| 需求 | Apache 2.2 写法(旧) | Apache 2.4 写法(新) |
|---|---|---|
| 允许 / 拒绝所有人 | Order allow,deny+Allow from all/Deny from all | Require all granted/Require all denied |
| 只允许某网段 | Allow from 10.0.0.0/8 | Require ip 10.0.0.0/8 |
| 只允许某主机 | Allow from 192.168.1.10 | Require host 192.168.1.10 |
| 组合条件 | 无法直接表达 | 用RequireAll/RequireAny容器包多条Require |
在 2.4 上使用旧语法的典型报错是启动失败并提示找不到Order指令(大意是Invalid command 'Order')。RHEL 8/9 与 Ubuntu 20.04/22.04 的默认配置都没有加载mod_access_compat,所以旧语法直接就是启动错误,不会静默失效——报错比不报错好查。
多条件组合的写法在 2.4 里是这样:
<Directory /data/www/private> # 与:两个条件必须同时满足 <RequireAll> Require ip 10.0.0.0/8 Require valid-user </RequireAll> </Directory>Require指令支持三种基本形式:Require all granted、Require all denied、Require ip 网段、Require host 主机名、Require valid-user、Require user 用户名。带not前缀可以取反,例如Require not ip 203.0.113.0/24。这些都在mod_authz_core里,具体可用形式请以官方文档该模块章节为准。
3.1 Directory 与 Location 选哪个
这两个容器最容易混,因为它们看起来都是「给某个路径定规则」,但匹配的对象完全不同:
| 对比项 | Directory容器 | Location容器 |
|---|---|---|
| 匹配对象 | 文件系统路径 | URL 路径(不含主机名) |
| 参数示例 | /var/www/html | /images |
| 通配符 | 支持*、?、[a-z],可用~走正则 | 支持前缀、~走正则 |
| 能否跟随软链接 | 是,作用在解析后的真实目录上 | 否,只看 URL |
| 典型用途 | 访问控制、Options、AllowOverride | 按 URL 前缀做处理,如缓存头 |
Options、AllowOverride这类指令放在Location里会直接报错(它们只允许出现在Directory容器中)。反过来,把文件系统路径当成Location的参数写,会静默不生效,因为没有任何请求的 URL 是那个样子。
四、403 排查顺序
按下面的顺序走,能覆盖绝大多数「权限全对但还是 403」的情况:
| 顺序 | 检查项 | 命令 | 修复 |
|---|---|---|---|
| 1 | 配置语法 | apachectl configtest | 按报错行号修 |
| 2 | 请求落在哪个 vhost | apachectl -S | 调整ServerName/ServerAlias或默认 vhost |
| 3 | Directory路径是否与DocumentRoot一致 | apachectl -S、查看站点配置 | 改成完全相同的路径 |
| 4 | 目录权限(含每级父目录的 x 位) | namei -l 路径、ls -l | chmod 755/644,补齐父目录 x 位 |
| 5 | SELinux 标签 | getenforce、ls -Zd、ausearch -m AVC | semanage fcontext加restorecon |
| 6 | 是否被规则拒绝 | error.log里的client denied by server configuration、Directory index forbidden | 改Require或Options |
error.log里的关键字指向完全不同的问题:client denied by server configuration是Require/Options规则,permission denied是文件系统权限或 SELinux,File does not exist是路径映射错误。先看日志再动手改,比盲改chmod 777高效得多。
常见坑点
坑一:改了 DocumentRoot,忘了同步改 Directory 容器的路径。
- ❌
DocumentRoot "/data/www"而Directory容器仍写着/var/www/html - ✅ 两处路径必须逐字符一致:
DocumentRoot "/data/www"配Directory "/data/www"
Apache 2.4 主配置里有一条兜底规则:顶层Directory容器对根路径的默认策略是拒绝访问。所以DocumentRoot指向的目录若没有被任何显式的Directory容器覆盖,就会继承这个拒绝策略,结果是一律 403。这是「改了 DocumentRoot 就 403」的头号原因。
坑二:在 RHEL 家族上跳过了 SELinux 那一层。
- ❌
chown -R apache:apache /data/www && chmod -R 755 /data/www之后仍然 403 - ✅ 加
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"和sudo restorecon -Rv /data/www
SELinux 的检查发生在标准权限检查之后,所以「权限看起来全对」的 403 基本都是它,用ausearch -m AVC -ts recent找拒绝记录是最快的确认方式。另外注意:/var/www之外以及/home下的目录,即使标签对了也可能因httpd_enable_homedirs之类的布尔值而未开放,具体以getsebool -a | grep httpd的输出为准。
坑三:父目录缺 x 权限,只修了叶子目录。
- ❌
sudo chmod 755 /data/www却漏了/data,访问仍 403 - ✅ 用
namei -l /data/www/index.html逐级确认每个组件都有 x 权限
Linux 的目录权限要求「路径上每一级都要可穿透」。/data若是700 root:root,即使/data/www是755,Apache 也进不去。诡异之处在于:以 root 身份ls /data/www完全正常,因为 root 绕过权限检查,「我在服务器上明明能看」会成为误导。
坑四:混用 2.2 的 Order/Allow 语法。
- ❌
Order allow,deny+Allow from all写在 2.4 的配置里 - ✅
Require all granted;按网段限制用Require ip 10.0.0.0/8;多条件用RequireAll/RequireAny容器
2.4 默认不加载mod_access_compat,旧语法会直接导致启动失败,而不是静默失效;如果确实看到旧语法却能启动,说明有人显式加载了兼容模块,那是应当尽快迁移的技术债。
坑五:把 Directory 容器的参数写成了 URL。
- ❌ 想给
/images这个 URL 路径加规则,却写成Directory /images - ✅ 按 URL 加规则用
Location /images;确实要给文件系统目录加规则,Directory的参数必须是真实存在的绝对路径
Directory匹配文件系统路径,Location匹配 URL 路径。两者混用的后果是「配置写了但完全没生效」,而且不会有任何报错。另外Options和AllowOverride只能放在Directory容器里,放进Location会被直接拒绝。
坑六:多个 vhost 时,请求落到了默认 vhost 上。
- ❌ 只改了
www.example.com那个 vhost 的DocumentRoot,用 IP 直接访问却看到另一个站点的内容 - ✅ 用
apachectl -S确认每个 IP:端口 上的默认 vhost,必要时把不再需要的默认站点停用(Debian 上a2dissite 000-default)
在同一个 IP:端口 上,第一个定义的VirtualHost会成为该组合的默认虚拟主机;请求的Host头不匹配任何ServerName/ServerAlias时,就落到这个默认 vhost 上。RHEL 家族上常见写法是ServerName _default_:80,Debian 家族默认的000-default.conf就是那个兜底者。这个坑的症状是「配置明明改对了,访问还是老页面」,很容易被误判为缓存问题。
坑七:目录下用软链接指向别处,忘了开 FollowSymLinks。
- ❌
Options -Indexes(未包含FollowSymLinks),DocumentRoot 里的软链接访问一律 403 - ✅
Options -Indexes +FollowSymLinks
Apache 2.4 中Options的默认值在未显式指定时是FollowSymLinks,但一旦你在Directory容器里写了任何Options,未列出的选项就会被清空。所以Options -Indexes这种「只想关掉目录列表」的写法,会连带把FollowSymLinks一起关掉,导致软链接失效。要保留就把+FollowSymLinks显式写出来。
总结
| 事项 | 结论 |
|---|---|
| DocumentRoot 的作用 | 只定义 URL 到文件系统的映射起点,不做授权 |
| 改它必须同步改 | Directory 容器路径、文件系统权限、SELinux 标签(RHEL 家族) |
| 403 三大嫌疑 | Directory 路径与 DocumentRoot 不一致、SELinux 标签、父目录缺 x 权限 |
| 授权语法 | 2.4 用Require all granted,2.2 的Order/Allow已废弃 |
| Directory 与 Location | 前者匹配文件系统路径,后者匹配 URL 路径,不能互换 |
| 默认值 | 两个发行版都是/var/www/html;老教程里的/var/www已过时 |
| 生效方式 | apachectl configtest通过后再systemctl reload |
一句话概括:DocumentRoot从来不是单独工作的,它和 Directory 容器、文件系统权限、SELinux 标签是一组必须同时改动的配置。绝大多数「改了 DocumentRoot 就 403」的问题,都能在apachectl -S、namei -l、ausearch -m AVC这三条命令的输出里找到答案,而不是靠chmod -R 777去试。发行版差异也务必注意:RHEL 家族是httpd、apache用户、conf.d直接生效;Debian 家族是apache2、www-data用户、必须a2ensite才生效。具体的指令可用范围与模块名称,请以你所用版本的apachectl -S输出与官方文档为准。