为一个或多个项目设置 Subversion
版本控制系统的历史相当悠久。它们已被证明在项目开发的许多阶段都非常有效,从源代码管理到文档和发布。在开源社区中,并发版本控制系统 (CVS) 已成为开发过程中的标准,是协调世界各地数十至数百名开发人员努力的重要工具。
然而,经过多年的成功案例,CVS 开始出现问题,主要与安全性和原子提交等重要功能的缺乏有关。最近,许多 CVS 替代方案开始出现在人们的视野中。其中一些对于关键部署仍然不成熟,而另一些则提出了全新的方法,使得平稳迁移变得不充分。
在众多新参与者中,Subversion 因其稳健性、与 CVS 的相似性以及创新的架构而受到许多开源开发人员的关注。Subversion 最近发布了 1.0 版本,已被用于许多开源项目中,包括 SpamAssassin、Linux 1394 火线支持项目和 SILO Sparc 引导加载程序。
与任何进入系统管理员手中的新玩具一样,安全性是处理 Subversion 时首先要考虑的问题。好消息是 Subversion 是在如今这个难以信任任何人的时代开发的。Subversion 采取了与 Apache Web 服务器耦合并将许多安全功能委托给 Apache 的明智方法。这种方法有许多优点
Apache 是一个成熟且广为人知的平台,用于处理身份验证、访问控制、保密性等安全功能。
Apache 受到严格监控,以查找安全漏洞:对新漏洞的响应时间非常短。
Apache 采用了许多最新的标准,使其成为一个始终保持最新的平台。
Apache 通常存在于已建立的网络基础设施中,因此无需打开新的网络端口并相应地调整防火墙。
Apache 在日志管理方面的灵活性简化了安全审计的任务,可以使用众所周知的工具来完成。
在本文中,我将处理一个复杂的 Subversion 存储库部署,并展示如何从系统管理员的角度解决安全问题。
我们的任务是为我们的项目部署一个 Subversion 存储库;它必须可以从我们的内部网络和互联网访问。我们的组织已经运行了一个 Apache Web 服务器,因此我们将使用它作为我们存储库的网关。因此,Subversion 配置必须满足以下要求
我们希望在存储库中托管多个项目,分为公共项目和私有项目。
我们希望我们的开发人员可以从世界任何地方无限制地访问他们参与的项目。
我们希望其他人拥有对我们的公共项目的只读访问权限。
鉴于这些要求,我们必须正确配置 Subversion 服务器以管理身份验证、访问控制、数据保密性和完整性。但是,Subversion 服务器以什么形式出现呢?这个问题没有唯一的答案,但常见的策略是将 Subversion 服务器构建为 Apache 2.0 共享模块,方法是扩展内置的 mod_dav.so Apache 模块。在这种配置中,Apache 负责处理上述许多安全方面,因此您无需学习另一种配置语言。只需根据您的新需求调整熟悉的 Apache 配置文件即可。
即使 Subversion 需要 Apache 2.0,这也不是什么大问题,因为您不需要将当前的 Apache 安装迁移到 2.0 系列。一个简单有效的策略是让 Apache 1.3 Web 服务器将任何 Subversion HTTP 请求代理到 Apache 2.0 Web 服务器。您可以稍后迁移您的 Apache 1.3 安装,或者如果您没有被迫迁移,则永远不要迁移。
图 1 说明了我们正在工作的环境。Subversion 客户端从互联网或受信任的子网连接到服务器。这里的“受信任”一词意味着密码不会被嗅探,要么是因为我们信任用户,要么是因为我们采取了其他对策。HTTP 请求(可能通过安全通道发送)进入服务器并包含来自 Delta-V 扩展的 DAV 方法。然后,Apache 1.3 将请求代理到 Apache 2.0 Web 服务器,后者开始检查。发出请求的用户通过纯 HTTP 身份验证或使用客户端证书进行身份验证。然后,做出访问控制决策并强制执行访问控制规则。接受的请求将传递到 Subversion 模块,该模块生成响应。
在安装 Subversion 之前,我们需要安装 Apache 2.0 Web 服务器。因此,下载并解压缩源代码 tarball 并启动 configure 脚本
sackville httpd-2.0.49 # ./configure --enable-mods-shared=most
命令行选项启用了大多数 Apache 模块,并将它们构建为共享模块。您可能需要微调命令行选项以包含(或排除)更多模块;例如,您可能需要 LDAP 模块才能针对 LDAP 服务器进行身份验证。要安装 Apache Web 服务器,请发出make && make install.
接下来,获取最新的 Subversion 源代码 tarball,解压缩源代码并启动 configure 脚本
sackville subversion-1.0.1 # ./configure --with-apxs=/path/to/apache2/bin/apxs \ --with-ssl
选项-with-apxs如果您将 Apache2 安装在默认位置,则可能不需要该选项。同样,如果您计划安装仅服务器 Subversion,则不需要选项-with-ssl,因为 SSL 支持由 Apache 的内置 mod_ssl.so 模块提供。此外,您可能需要指定共享库的位置。特别是,许多用户似乎在使用 Berkeley DB 库时遇到问题。如果您遇到问题,请仔细阅读 Subversion 用户邮件列表。
发出make && make install以构建和安装 mod_dav_svn.so 模块。如果一切顺利,您将在您的模块中找到 mod_dav_svn.so。
Subversion 安装过程应该已经在您的 Apache 配置文件中创建了正确的条目,以激活 mod_dav_svn.so 模块。此外,您应该看到 mod_authz_svn.so 模块的条目;它是 Subversion 访问控制机制的一部分,我们稍后将对其进行介绍。
在我们的设置中,Apache2 必须与 Apache1 并排存在,因此我们需要告诉 Apache2 监听端口 80 以外的端口 - 假设它是 8080 端口。由于 Apache2 是通过 Apache1 访问的,因此您应该在防火墙配置中阻止该端口,或者使 Apache2 绑定到环回接口。后一种解决方案比前一种更好,因为我们不需要依赖防火墙来丢弃来自外部主机的传入连接。您还应该应用常见的安全提示来增强 Apache2 安全性,我在此不再赘述。例如,带有 Subversion 模块的 Apache 在其错误消息中往往过于冗长,显示大多数已激活模块(SSL、DAV、Subversion 等)的版本号。安全纯粹主义者称这种行为为信息泄露;要最大程度地减少这种情况,请对 ServerTokens 指令采取操作。
现在是时候决定存储库的存放位置了。我们必须回答以下问题
我们的存储库将在 Apache2 URL 的哪个空间中可访问?由于 Apache2 被用作仅 Subversion 服务器,我们决定将服务器根目录作为我们存储库的根目录。
存储库在服务器的文件系统中的物理位置在哪里?我们在这里没有限制,因此我们选择 /svn 来包含所有与 Subversion 相关的文件。
我们的存储库将在外部 Apache1 URL 的哪个空间中存在?常见的策略是将 Subversion 存储库放在 /svn 目录中。
因此,/svn 目录的布局是
/svn/conf:包含 Apache2 和 Subversion 工作所需的所有文件,例如用户身份验证信息、访问控制策略等。
/svn/repository:包含公共项目和私有项目的两个子目录。在每个子目录中,我们使用 svnadmin 的 create 命令创建一个项目。
在 Apache2 httpd.conf 文件中,我们添加以下行
<IfModule mod_dav_svn.c> Include /svn/conf/mod_dav_svn.conf </IfModule>
包括文件 /svn/conf/mod_dav_svn.conf,我们将任何与 Subversion 相关的信息集中在同一位置,即目录 /svn。
要将所有 HTTP 请求从 Apache1 代理到 Apache2,请将以下条目添加到您的 Apache1 配置文件中
Proxy /svn/ http://localhost:8080/
在定义访问控制策略时,我们必须区分纯 HTTP 连接和 HTTPS 连接,因为密码在纯 HTTP 连接上不受保护。在以下行中,我们定义了 HTTP 连接的默认策略。我们将以下条目添加到 /svn/conf/mod_dav_svn.conf 文件中
Include /svn/conf/public_default_policy.conf Include /svn/conf/private_default_policy.conf
每个 *_default_policy.conf 都包含相应项目组的默认访问控制策略。我们希望公共项目具有只读 HTTP 公共访问权限,因此请将以下行添加到您的 /svn/conf/public_default_policy.conf 文件中
<Location /public> Dav svn # Tell Apache to use Subversion's own module # for HTTP's Dav extensions. SVNParentPath /svn/repository/public <LimitExcept GET PROPFIND OPTIONS REPORT> Order deny,allow Deny from all </LimitExcept> </Location>
此配置拒绝访问除 GET、PROPFIND、OPTIONS 和 REPORT 之外的任何 HTTP 方法,这些方法在只读会话期间使用。如果您有一个受信任的子网(假设为 192.168.0.0/24),您希望允许从中进行写入访问,您可以将上述配置代码段更改为
<Location /public> Dav svn SVNParentPath /svn/repository/public <LimitExcept GET PROPFIND OPTIONS REPORT> Order deny,allow Deny from all Allow from 192.168.0.0/24 </LimitExcept> </Location>
但是请注意,如果您不添加更多访问控制规则来限制访问,则从子网 192.168.0.0/24 连接的任何人都可以写入存储库。如果您需要严格的基于用户的访问控制,那么我建议您不要使用此默认策略。
私有项目组的访问控制策略拒绝任何人通过 HTTP 连接进行访问。您必须放入 /svn/conf/private_default_policy.conf 中的相应配置代码段是
<Location /private> Dav svn SVNParentPath /svn/repository/private Order deny,allow Deny from all </Location>
如果您希望允许从受信任的子网进行访问,请使用以下代码
<Location /private> Dav svn SVNParentPath /svn/repository/private Order deny,allow Deny from all Allow from 192.168.0.0/24 </Location>
为了在访问控制管理中实现更高的粒度,我们希望为每个项目定义单独的策略。我们将在此处分析我们拥有受信任子网的情况,因为它涉及的方面更多。在这种情况下,我们仅允许从受信任子网通过 HTTP 进行基于密码的身份验证。我们在 /svn/conf/private 和 /svn/conf/public 中包含的单独文件中指定每个项目的策略。为此,请将以下行添加到您的 /svn/conf/mod_dav_svn.conf 文件中
Include /svn/conf/policies/public/* Include /svn/conf/policies/private/*
假设我们有 foo 和 bar 公共项目。John 和 Bob 是 foo 的开发人员,而 John 和 Mike 是 bar 的开发人员。我们希望一个项目的开发人员仅能通过受信任子网上的 HTTP 完全访问他们开发的项目。首先,让我们填写用户的密码文件
sackville apache2 # bin/htpasswd -c /svn/conf/svnpasswd john ***** sackville apache2 # bin/htpasswd /svn/conf/svnpasswd bob ***** ....
然后,我们创建用户组文件:每个项目都与一个组关联,该组的名称形式为 (public|private)_projectname,并包含参与该项目的用户
public_foo: john bob public_bar: john mike
我们将此文件另存为 /svn/conf/svngroups。最后一个操作是将 /svn/conf/policies/public 目录中的文件与项目关联。foo 的访问控制策略文件名为 /svn/conf/policies/public/foo,包含以下行
<Location /public/foo> <LimitExcept GET PROPFIND OPTIONS REPORT> AuthType Basic AuthName "Public Subversion repository for project Foo" AuthUserFile /svn/conf/svnpasswd AuthGroupFile /svn/conf/svngroups Require group public_foo </LimitExcept> </Location>
我们可以将 AuthType、AuthUserFile 和 AuthGroupFile 移动到默认策略文件中,以避免复制配置条目。我们必须添加 Satisfy 指令,以要求用户在读/写会话期间从受信任的子网进行身份验证。因此,请按以下方式修改您的 public_default_policy.conf 文件
<Location /public> Dav svn SVNParentPath /svn/repository/public <LimitExcept GET PROPFIND OPTIONS REPORT> Order deny,allow Deny from all Allow from 192.168.0.0/24 Satisfy all </LimitExcept> </Location>
私有项目的配置非常相似;我们只需丢弃任何 LimitExcept 指令,以便 public_default_policy.conf 变为
<Location /private> Dav svn SVNParentPath /svn/repository/private Order deny,allow Deny from all Allow from 192.168.0.0/24 Satisfy all </Location>
并且私有项目 worldconquest 的访问控制策略文件是
<Location /private/worldconquest> AuthType Basic AuthName "Private Subversion repository for project WorldConquest" AuthUserFile /svn/conf/svnpasswd AuthGroupFile /svn/conf/svngroups Require group private_worldconquest </Location>
现在是时候考虑 HTTPS 连接了,它允许世界各地的用户访问存储库,从而在互联网等不安全通道上保证密码保密性和数据完整性。Apache 在单独的虚拟主机空间中管理 HTTPS,该空间使用如下配置进行设置
<VirtualHost _default_:443> ... </VirtualHost>
但是我们谈论的是哪个 Apache?外部 Apache1 还是内部 Apache2 加 Subversion?使用 HTTPS 的 Subversion 客户端当然可以连接到外部 Apache1 Web 服务器,并尝试与之建立安全连接。因此,我们必须为我们的外部 Apache1 Web 服务器配置 HTTPS。内部 Apache2 Web 服务器的代理请求也不需要通过安全连接传递,但是因为我们希望将访问控制策略集中在我们的 Apache2 Web 服务器中,所以我们必须为 Apache2 提供一种机制来区分来自外部安全通道的代理请求。
我们使用另一个端口(假设为 8081)来区分 HTTP 请求何时已使用 SSL 传递到 Apache1 Web 服务器。因此,当 HTTP 请求通过 SSL 到达 Apache1 时,它会在内部以明文形式代理到端口 8081,Apache2 在该端口上进行监听。与往常一样,请记住阻止来自外部主机的端口 8081 的传入连接,或者将 Apache2 绑定到环回接口(或两者都绑定)。
在 Apache1 配置文件中,将以下行添加到 SSL 虚拟主机指令
Proxy /svn/ http://localhost:8081/
现在告诉 Apache2 监听端口 8081,将以下条目添加到您的 httpd.conf 文件中Listen localhost:8081.
现在我们必须为通过端口 8081 进行的访问设置虚拟主机环境。与 HTTP 连接的主要区别在于基于源的访问控制:使用 HTTPS 连接,我们放弃了受信任和不受信任子网的概念。HTTPS 请求可以来自任何地方。
因此,默认策略文件必须包含端口 8081 的 VirtualHost 指令。这是我们放入 /svn/conf/public_default_policy.conf 文件中的公共项目的指令
<VirtualHost _default_:8081> <Location /public> Dav svn SVNParentPath /svn/conf/repository/public Order allow,deny Allow from all <LimitExcept GET PROPFIND OPTIONS REPORT> Order deny,allow Deny from all Satisfy all </LimitExcept> </Location> Include /tmp/LJ/policies/public/* </VirtualHost>
与往常一样,使用 Satisfy 子句允许我们仅向经过身份验证的用户授予写入访问权限。此外,我们回收了每个项目的配置文件(请参阅 Include 指令),因为它们不依赖于基于源的访问控制策略,但是如果需要,我们可以为另一个目的专门化它们。私有项目的默认策略类似
<VirtualHost _default_:8081> <Location /private> Dav svn SVNParentPath /svn/conf/repository/private Order allow,deny Allow from all Satisfy all </Location> Include /tmp/LJ/policies/private/* </VirtualHost>
在配置部分中,我提到了 mod_authz_svn.so 模块,该模块由 Subversion 安装过程创建和安装。此模块允许我们基于目录定义访问控制策略,从而提高粒度级别。
为什么我们需要一个单独的模块来处理基于目录的访问控制?我们不能使用 Apache 配置原语吗?这里的问题是传递给 Apache 以访问存储库的 URL,其形式为
/public/foo/!svn/act/b2a07a33-85d9-0310-857b-b13ae1b9c55b
正如我们所见,只有项目路径和名称是可见的:项目访问的目录隐藏在数字代码中。因此,我们无法直接应用基于 Apache Location 的访问控制原语。解决方案是将目录访问控制委托给 mod_authz_svn.so 模块,该模块能够解析数字代码并识别访问的目录。
mod_authz_svn.so 的访问控制策略在纯文本文件中指定,语法简单。这是我们用于公共项目的策略 (/svn/conf/policies/public_svn_authz)
[groups] foo = john, bob bar = john, mike [/] * = r [foo:/] @foo = rw [foo:/branch/john] bob = r [foo:/branch/bob] john = r [bar:/] @bar = rw
我们首先定义我们想要为其指定策略的用户组,然后我们列出访问控制规则。[/] 部分下的规则指定任何用户都可以读取任何项目的内容。对于每个项目(例如 [foo:/]),我们指定全局访问控制策略,并为内部目录指定它。无需在 /branch/john 目录中为用户 john 指定访问控制规则,因为它继承自更高级别目录中的规则。
此外,我们必须再次指定不同组的组成。作为良好的安全实践,我们应避免复制此类配置指令。这个问题可以通过简单地从项目配置文件中删除任何 AuthGroupFile 指令并将相关的 Require 指令更改为 Require valid-user 指令来解决,从而将组管理委托给 mod_authz_svn.so 模块。
此时,剩下的就是将指令 AuthzSVNAccessFile 添加到默认策略文件中,以告知 Apache 我们打算使用 mod_authz_svn.so 模块作为访问控制工具。为此,我们必须指定其配置文件。这是公共项目的配置文件
<Location /public> Dav svn SVNParentPath /tmp/LJ/svn/repository/public <LimitExcept GET PROPFIND OPTIONS REPORT> Order deny,allow Deny from all Allow from 127.0.0.1 Satisfy all </LimitExcept> AuthzSVNAccessFile /tmp/LJ/svn/conf/public_svn_authz </Location> <VirtualHost _default_:8081> <Location /public> Dav svn SVNParentPath /tmp/LJ/svn/repository/public Order allow,deny Allow from all <LimitExcept GET PROPFIND OPTIONS REPORT> Order deny,allow Deny from all Satisfy all </LimitExcept> AuthzSVNAccessFile /tmp/LJ/svn/conf/public_svn_authz </Location> Include /tmp/LJ/svn/conf/policies/public/* </VirtualHost>
您可能会问,为什么我们之前没有使用 mod_authz_svn.so 模块。如果我们没有任何受信任的子网,我们可以丢弃大部分 Apache 访问控制机制,而仅依赖 mod_authz_svn.so,即使这仅在 Subversion 1.0.1 中才成立。但是,由于在具有受信任子网的场景中,mod_authz_svn.so 的访问控制策略是基于用户的,因此我们需要混合使用基于源的访问控制和基于用户的访问控制。