云计算百科
云计算领域专业知识百科平台

用 Borg 搭建集中式备份服务器:Ansible 与 Salt 多客户端部署实战

  • 运维
  • 存储

【免费下载链接】borg

Deduplicating archiver with compression and authenticated encryption.

项目地址:
https://gitcode.com/gh_mirrors/bo/borg

点击查看 免费下载

本指南以 Borg 仓库中的 docs/deployment/central-backup-server.rst 为基础,完整讲解如何在一台备份服务器上为多个客户端(桌面机、Web 服务器、应用服务器)提供隔离的仓库存储:单 UNIX 用户 + borg serve –restrict-to-path 强制路径限制 + authorized_keys 强制命令,并给出 Ansible 与 Salt 两套可复用的自动化部署配置。读完本文,你将掌握集中式 Borg 备份服务的目录规划、SSH 强制命令限制、客户端仓库初始化方式,以及越权访问时错误信息的排查方法,并理解其背后的源码实现原理。

适用前提:本文方案与 Borg 2 的说明

官方文档明确指出,本示例早于 Borg 2,使用的是传统 ssh:// 传输方式(由 borg serve 提供服务)和 borg init 命令:

  • 在 Borg 2 中,ssh:// 传输仅用于 legacy borg 1.x(v1)仓库;
  • 对于当前版本的仓库,应改用 rest:// 仓库:Borg 通过 SSH 连接并在远程主机上运行一个 borgstore REST 服务器(即 borg serve –rest);
  • 创建仓库应使用 borg repo-create 而非 borg init。

因此,如果部署的是 Borg 1.x 环境,本文的 ssh:// 示例可直接使用;如果使用 Borg 2 及更新版本,请参考仓库中的 hosting-repositories.rst(其中给出了 command="borg serve –rest –restrict-to-repository …",restrict 的现代写法)与 repo-create.rst(例如 borg repo-create –encryption=aes256-ocb)作对应调整。本文后续内容严格遵循原文档,以 ssh:// + borg serve 的经典集中式服务器为例展开。

机器规划(Machines)

本方案涉及多台机器,文档中以各自的完全限定域名(FQDN)来指代:

  • 备份服务器:backup01.srv.local
  • 客户端:
    • John Doe 的桌面机:johndoe.clnt.local
    • Web 服务器 01:web01.srv.local
    • 应用服务器 01:app01.srv.local

核心设计思想是:所有客户端共享同一个备份服务器的 UNIX 账号,通过 SSH 公钥 + 强制命令实现一对一的路径隔离,而不是为每个客户端创建独立的系统账号。

备份服务器的用户与组(User and group)

仓库服务器应当为所有客户端创建一个单一的 UNIX 用户。文档推荐的用户与组及附加设置如下:

项目推荐值
用户(User) backup
组(Group) backup
Shell /bin/bash(或其他能够运行 borg serve 命令的 shell)
家目录(Home) /home/backup

大多数客户端应以 root 用户发起备份,以便捕获完整的用户、组和权限信息(例如备份 /home 目录时)。

目录布局(Folders)

备份服务器上建议采用如下目录结构:

  • 用户家目录:/home/backup
  • 仓库路径(存储池):/home/backup/repos
  • 各客户端的受限路径(/home/backup/repos/<client fqdn>):
    • johndoe.clnt.local → /home/backup/repos/johndoe.clnt.local
    • web01.srv.local → /home/backup/repos/web01.srv.local
    • app01.srv.local → /home/backup/repos/app01.srv.local

即每个客户端在存储池下拥有一个以自己 FQDN 命名的独立子目录,所有仓库都位于该子目录内部。

访问限制:borg serve –restrict-to-path

Borg 通过如下命令把客户端限制在其自身路径内:

borg serve –restrict-to-path /home/backup/repos/<client fqdn>

客户端能够访问 /home/backup/repos/<client fqdn> 内部的任何文件与子目录,但无法访问任何其他目录。如果需要允许某个客户端访问多个相互独立的目录,可以重复传递多个 –restrict-to-path 标志,例如:

borg serve –restrict-to-path /home/backup/repos/<client fqdn> –restrict-to-path /home/backup/repos/<other client fqdn>

这在「多台机器属于同一个人,且此人应能访问自己所有机器的备份」的场景下非常有用。

authorized_keys 中的强制命令

每个客户端只允许一把 SSH 公钥。本例中为 johndoe.clnt.local、web01.srv.local、app01.srv.local 各添加一把密钥,它们以单一 UNIX 账号 backup@backup01.srv.local 访问备份。$HOME/.ssh/authorized_keys 中的每一把密钥都带有强制命令(forced command)与限制项,格式如下:

command="cd /home/backup/repos/<client fqdn>;
borg serve –restrict-to-path /home/backup/repos/<client fqdn>",
restrict <keytype> <key> <host>

注意: 上述文本必须写成单行!

该密钥行添加的选项依次完成三件事:

  • 切换工作目录:cd /home/backup/repos/<client fqdn>
  • 以受限路径运行 borg serve:borg serve –restrict-to-path /home/backup/repos/<client fqdn>
  • 限制 SSH 功能:restrict 关键字禁用了端口转发、Agent 转发、X11 转发、PTY 分配以及 ~/.ssh/rc 的执行等具有安全风险的能力。
  • 由于有了 cd 命令,服务器会自动切换当前工作目录,客户端无需知道远程仓库的绝对或相对路径,可以直接通过如下形式访问仓库:

    ssh://<user>@<host>/./<repo>

    注意: 上述设置会忽略客户端提供的、通常会附加到 borg serve 命令后的所有命令行参数(除少数白名单项外,见下文「SSH_ORIGINAL_COMMAND 白名单机制」)。

    源码层面的限制实现

    –restrict-to-path 的底层行为可以在源码中得到印证:

    • 参数解析位于 serve_cmd.py:–restrict-to-path 使用 action="append",可重复指定多次,帮助文本明确说明「Access to all subdirectories is granted implicitly; PATH does not need to point directly to a repository」(所有子目录隐式可访问,PATH 不必直接指向仓库)。此外还有 –restrict-to-repository PATH,它只允许访问 PATH 处的单个仓库(不包含子目录),且 PATH 处可以是不存在的空目录、允许客户端在那里初始化仓库。
    • 路径校验位于 legacy/remote.py 的 RepositoryServer.open():先对路径做 os.path.realpath() 解析,再给路径补上结尾分隔符(os.path.join(path, "")),然后逐条与每个受限路径做带结尾斜杠的前缀匹配(path_with_sep.startswith(restrict_to_path_with_sep))。这样设计是为了避免 –restrict-to-path /foo 误放行 /foobar 这类路径;全部不匹配时抛出 PathNotAllowed。
    • PathNotAllowed 异常定义于 helpers/errors.py,相关测试(见 argparsing_test.py 与 serve_cmd_test.py)验证了「允许路径之下的 backend 可通过、路径之外被拒绝」的行为。

    客户端操作:初始化仓库与越权示例

    初始化仓库

    客户端(如 John Doe 的桌面机)初始化 pictures 仓库:

    borg init ssh://backup@backup01.srv.local/./pictures

    /./ 表示相对于 SSH 登录后的工作目录(即服务器已通过强制命令 cd 进入的 <client fqdn> 目录),这是推荐写法。

    也可以使用完整路径(仅作演示,实际不建议使用;服务器会自动切换工作目录到 <client fqdn> 目录):

    borg init ssh://backup@backup01.srv.local/home/backup/repos/johndoe.clnt.local/pictures

    越权访问被拒绝

    当 johndoe.clnt.local 试图访问其限制之外的路径时(例如 John Doe 想把备份写到 web01 的路径下):

    borg init ssh://backup@backup01.srv.local/home/backup/repos/web01.srv.local/pictures

    会得到如下错误:

    ~~~ SNIP ~~~
    Remote: borg.remote.PathNotAllowed: /home/backup/repos/web01.srv.local/pictures
    ~~~ SNIP ~~~
    Repository path not allowed

    这正是 RepositoryServer.open() 中前缀校验失败后抛出 PathNotAllowed、并经远程 RPC 异常重建机制传回客户端的结果。从仓库文档与测试可知,PathNotAllowed 对应退出码 83(见 hosting-repositories.rst 及 serve_cmd_test.py 中对 PathNotAllowed.exit_mcode 的断言)。

    用 Ansible 自动化部署

    Ansible 负责完成所有系统级操作:添加用户、创建目录、安装与配置软件。原文给出的完整 playbook 如下:

    – hosts: backup01.srv.local
    vars:
    user: backup
    group: backup
    home: /home/backup
    pool: "{{ home }}/repos"
    auth_users:
    – host: johndoe.clnt.local
    key: "{{ lookup('file', '/path/to/keys/johndoe.clnt.local.pub') }}"
    – host: web01.clnt.local
    key: "{{ lookup('file', '/path/to/keys/web01.clnt.local.pub') }}"
    – host: app01.clnt.local
    key: "{{ lookup('file', '/path/to/keys/app01.clnt.local.pub') }}"
    tasks:
    – package: name=borg state=present
    – group: name="{{ group }}" state=present
    – user: name="{{ user }}" shell=/bin/bash home="{{ home }}" createhome=yes group="{{ group }}" groups= state=present
    – file: path="{{ home }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory
    – file: path="{{ home }}/.ssh" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory
    – file: path="{{ pool }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory
    – authorized_key: user="{{ user }}"
    key="{{ item.key }}"
    key_options='command="cd {{ pool }}/{{ item.host }};borg serve –restrict-to-path {{ pool }}/{{ item.host }}",restrict'
    with_items: "{{ auth_users }}"
    – file: path="{{ home }}/.ssh/authorized_keys" owner="{{ user }}" group="{{ group }}" mode=0600 state=file
    – file: path="{{ pool }}/{{ item.host }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory
    with_items: "{{ auth_users }}"

    几个要点:

    • 通过变量 pool = "{{ home }}/repos" 统一管理存储池路径,auth_users 列表以「主机名 + 公钥文件」的形式声明客户端;
    • authorized_key 模块的 key_options 中拼接出与手动配置完全一致的强制命令:command="cd {{ pool }}/{{ item.host }};borg serve –restrict-to-path {{ pool }}/{{ item.host }}",restrict;
    • 注意 authorized_keys 与各客户端目录都以 0700/0600 权限收敛,SSH 对这两个位置的权限有严格要求;
    • playbook 中 johndoe.clnt.local 的 auth_users 使用了 .clnt,而文档「Machines」一节中为 .clnt,两者在原文中即存在细微不一致,实际使用时请统一为你环境中真实的 FQDN。

    用 Salt 自动化部署

    以下是与之等价的 Salt 配置,运行于 Debian 系统:

    Install borg backup from pip:
    pkg.installed:
    – pkgs:
    – python3
    – python3-dev
    – python3-pip
    – python-virtualenv
    – libssl-dev
    – openssl
    – libacl1-dev
    – libacl1
    – build-essential
    – libfuse-dev
    – fuse
    – pkg-config
    pip.installed:
    – pkgs: ["borgbackup"]
    – bin_env: /usr/bin/pip3

    Setup backup user:
    user.present:
    – name: backup
    – fullname: Backup User
    – home: /home/backup
    – shell: /bin/bash
    # CAUTION!
    # If you change the ssh command= option below, it won't necessarily get pushed to the backup
    # server correctly unless you delete the ~/.ssh/authorized_keys file and re-create it!
    {% for host in backupclients %}
    Give backup access to {{host}}:
    ssh_auth.present:
    – user: backup
    – source: salt://conf/ssh-pubkeys/{{host}}-backup.id_ecdsa.pub
    – options:
    – command="cd /home/backup/repos/{{host}}; borg serve –restrict-to-path /home/backup/repos/{{host}}"
    – restrict
    {% endfor %}

    要点说明:

    • Salt 方案在 Debian 上通过 pkg.installed 安装 Python 及构建依赖(含 libfuse-dev/fuse 用于挂载、libacl1-dev/libacl1 用于 ACL 支持),再经 pip.installed 安装 borgbackup 包;
    • 使用 user.present 创建 backup 用户(家目录 /home/backup、shell /bin/bash);
    • {% for host in backupclients %} 循环为每个客户端从 salt://conf/ssh-pubkeys/{{host}}-backup.id_ecdsa.pub 拉取公钥,并写入与 Ansible 版本相同的强制命令与 restrict 限制;
    • 注意注释中的警告:如果修改了 ssh command= 选项,除非先删除 ~/.ssh/authorized_keys 文件再重新生成,否则改动不一定会正确推送到备份服务器(Salt 的 ssh_auth.present 对既有授权行的更新行为所致)。

    增强方向(Enhancements)

    原文档指出,上述方案是一个简单且有效的配置,但也可以进一步增强,以支持(有限集合的)客户端提供的命令:

    • 可以编写一个用于启动 borg serve 的包装脚本(wrapper);
    • 或者增强 Borg 自身——通过检测 SSH_ORIGINAL_COMMAND 环境变量来自动识别它是否运行在 SSH 之下。

    如果 Borg 内置了 SSH 自动检测,则无需外部包装脚本,也无需部署其他解释器或应用程序。这一点在当前仓库中已经部分实现:borg serve 对 ssh 强制命令有专门支持——当环境变量 SSH_ORIGINAL_COMMAND 存在时,它会忽略客户端命令行中除白名单外的选项,只从该变量取值(详见 serve.rst)。

    SSH_ORIGINAL_COMMAND 白名单机制

    从仓库文档 serve.rst 可以确认 borg serve 的强制命令细节:

    • 客户端发送的原始命令行会通过 SSH_ORIGINAL_COMMAND 传给服务器,borg serve 仅接受其中的受控白名单选项:
      • 控制日志级别与调试主题的选项,如 –verbose、–info、–debug、–debug-topic 等;
      • –lock-wait:允许客户端控制等待锁的超时时间;
      • –backend:允许客户端选择具体要使用的仓库(配合 borg serve –rest 使用);强制命令始终钉住限制——客户端给出的 backend 会被校验是否落在 –restrict-to-path / –restrict-to-repository 允许的范围内。
    • 明确不在白名单中的有:–rest(强制命令钉住模式:是当前 rest:// 仓库还是 legacy Borg 1.x 仓库)、–restrict-to-path、–restrict-to-repository、–umask、–permissions。
    • 客户端原命令中出现的环境变量(如 BORG_XXX)不会被解释,而是被忽略;如需在 borg serve 侧设置环境变量,必须写在系统级位置(如 /etc/environment)或强制命令本身中。

    因此,即便客户端试图在命令行中附加额外的 borg serve 参数,其模式与路径限制也始终由 authorized_keys 中的强制命令决定——这正是本集中式备份方案安全性的核心。

    生产环境加固建议

    基于原文档并结合作者后续文档与测试,以下加固项强烈建议在部署集中式备份服务器时一并落实:

  • SSH 保活配置:borg serve 的 stdin/stdout/stderr 与服务器端 sshd 进程相连。网络中断时 sshd 可能长时间无法感知客户端已断开,导致 borg serve 一直持有仓库锁,后续操作报 Failed to create/acquire the lock。解决办法:
    • 客户端 ~/.ssh/config 或 /etc/ssh/ssh_config 中: Host backupserver
      ServerAliveInterval 10
      ServerAliveCountMax 30
    • 服务器端 /etc/ssh/sshd_config 中: ClientAliveInterval 10
      ClientAliveCountMax 30
    • 客户端每 10 秒发送一次保活,30 次无响应(约 300 秒)后终止连接,borg serve 随之优雅退出并释放锁;配合 –lock-wait 600 可留出充足等待时间。超时数值可按实际环境调整。
  • 最小化 Shell:当客户端不可信时,建议将备份用户 shell 改为精简实现(chsh -s /bin/sh BORGUSER)。OpenSSH 以 "$SHELL" -c "$COMMAND" 执行 authorized_keys 中配置的命令,使用精简 shell 可显著缩小攻击面。注意 /bin/sh 只是示例,并不一定是「精简实现」。
  • 权限收紧:如 Ansible playbook 所示,/home/backup、/home/backup/.ssh、/home/backup/repos 及每个客户端目录均设为 0700,authorized_keys 设为 0600,这是 SSH 正常运行所必需的严格权限。
  • 依赖文档:本方案在 Borg 2 下的等价实现(–rest + –restrict-to-repository + borg repo-create)请参阅 hosting-repositories.rst;borg serve 参数全表见 serve.rst.inc;安全模型相关讨论见 internals/security.rst。
  • 参考

    • 本部署方案原文:central-backup-server.rst
    • borg serve 用法与 SSH 强制命令白名单:serve.rst
    • Borg 2 的仓库托管方案(–rest 模式):hosting-repositories.rst
    • 仓库创建命令 borg repo-create:repo-create.rst
    • serve 子命令实现与 –restrict-to-path/–restrict-to-repository 解析:serve_cmd.py
    • 路径限制前缀校验实现(RepositoryServer.open):legacy/remote.py
    • 相关测试:serve_cmd_test.py、argparsing_test.py

    赞

    分享

    • 运维
    • 存储

    【免费下载链接】borg

    Deduplicating archiver with compression and authenticated encryption.

    项目地址:
    https://gitcode.com/gh_mirrors/bo/borg

    点击查看 免费下载

    上一篇:
    Vue右键菜单组件v-contextmenu完全使用指南:打造极致用户体验的终极教程

    下一篇:
    如何用 Selenium Grid 4 的 relay 功能把多个 Appium 服务器接入同一个 Hub?

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 用 Borg 搭建集中式备份服务器:Ansible 与 Salt 多客户端部署实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!