From 90fd54a6a170fa4c33e78ed41a79c43e03cd2167 Mon Sep 17 00:00:00 2001 From: Coldin04 Date: Tue, 29 Jul 2025 18:16:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Vaultwarden=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=A4=87=E4=BB=BD=E8=84=9A=E6=9C=AC=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env-ex | 20 ++++++ README.md | 162 +++++++++++++++++++++++++++++++++++++++++++++++ backup.py | 154 ++++++++++++++++++++++++++++++++++++++++++++ backup.sh | 19 ++++++ requirements.txt | 3 + 5 files changed, 358 insertions(+) create mode 100644 .env-ex create mode 100644 README.md create mode 100644 backup.py create mode 100644 backup.sh create mode 100644 requirements.txt diff --git a/.env-ex b/.env-ex new file mode 100644 index 0000000..ee677c1 --- /dev/null +++ b/.env-ex @@ -0,0 +1,20 @@ +# Cloudflare R2 +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= +R2_ACCOUNT_ID= +R2_BUCKET_NAME=backup +R2_REGION=auto + +# Vaultwarden 和备份相关 +COMPOSE_DIR=/root/vaultwarden +BACKUP_SOURCE_DIR=/root/vaultwarden/vw-data +BACKUP_TEMP_FILE=/tmp/vw_backup.tar.gz +BACKUP_ENCRYPTED_FILE=/tmp/vw_backup.tar.gz.enc +ENCRYPT_PASSWORD=Password + +SLOT_COUNT=3 +BACKUP_PREFIX=back-vault-s + +# Telegram Bot +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f77fc0 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# Vaultwarden 自动备份脚本 + +本项目用于自动化备份 Vaultwarden 密码管理服务的数据,支持加密、云端存储、备份轮换和 Telegram 通知,适用于个人和小型团队的数据安全需求 + +> README.md是由Copilot AI 生成的,可能包含一些不准确或不完整的信息,请根据实际情况进行调整和补充。如果遇到情况请提交 Issue 或 PR 进行修正,万分感谢! + +--- + +## 项目背景 +Vaultwarden 是 Bitwarden 的轻量级开源实现,常用于自建密码管理服务。数据安全至关重要,定期自动备份能有效防止数据丢失。本脚本实现了备份、加密、上传、轮换和通知的全流程自动化。 + +## 功能亮点 +- 自动备份 Vaultwarden 的数据库、配置文件、私钥和附件目录 +- 使用 openssl 强加密备份文件,保障数据安全 +- 上传加密备份至 Cloudflare R2 对象存储 +- 自动轮换旧备份,保留指定数量,节省存储空间 +- 备份失败时自动通过 Telegram 推送通知 +- 支持多平台(Windows/Linux) + +--- + +## 环境准备 + +### 1. 克隆或下载项目 +将本项目代码下载到任意目录,例如: +``` +c:\Users\你的用户名\Downloads\vaultwarden_backup +``` +或 +``` +/home/youruser/vaultwarden_backup +``` + +### 2. 配置环境变量 +1. 将 `.env-ex` 文件重命名为 `backup.env`,并根据实际情况填写各项配置。 +2. 各项配置说明: + - `R2_ACCESS_KEY_ID`:Cloudflare R2 的 Access Key + - `R2_SECRET_ACCESS_KEY`:Cloudflare R2 的 Secret Key + - `R2_ACCOUNT_ID`:Cloudflare R2 的账号 ID + - `R2_BUCKET_NAME`:R2 存储桶名称 + - `R2_REGION`:R2 区域(通常填 auto) + - `BACKUP_SOURCE_DIR`:Vaultwarden 数据目录(如 `/opt/vaultwarden/data` 或 `C:\vaultwarden\data`) + - `BACKUP_TEMP_FILE`:临时 tar 文件路径(如 `/tmp/vaultwarden-backup.tar.gz` 或 `C:\temp\vaultwarden-backup.tar.gz`) + - `BACKUP_ENCRYPTED_FILE`:加密后文件路径(如 `/tmp/vaultwarden-backup.tar.gz.enc`) + - `ENCRYPT_PASSWORD`:备份加密密码(请妥善保存) + - `SLOT_COUNT`:保留的备份数量(如 3) + - `BACKUP_PREFIX`:备份文件前缀(如 `back-vault-s`) + - `TELEGRAM_BOT_TOKEN`:Telegram Bot Token(可选) + - `TELEGRAM_CHAT_ID`:Telegram Chat ID(可选) + +3. 配置示例: +``` +R2_ACCESS_KEY_ID=xxxxxx +R2_SECRET_ACCESS_KEY=xxxxxx +R2_ACCOUNT_ID=xxxxxx +R2_BUCKET_NAME=vault-backup +R2_REGION=auto +BACKUP_SOURCE_DIR=/opt/vaultwarden/data +BACKUP_TEMP_FILE=/tmp/vaultwarden-backup.tar.gz +BACKUP_ENCRYPTED_FILE=/tmp/vaultwarden-backup.tar.gz.enc +ENCRYPT_PASSWORD=你的强密码 +SLOT_COUNT=3 +BACKUP_PREFIX=back-vault-s +TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 +TELEGRAM_CHAT_ID=123456789 +``` + +--- + +## 依赖安装 + +### Python 依赖 +请确保已安装 Python 3.7 及以上版本。 + +#### 方式一:使用 requirements.txt +``` +pip install -r requirements.txt +``` + +#### 方式二:手动安装 +``` +pip install boto3 python-dotenv requests +``` + +### 系统依赖 +- Windows:请确保 `sqlite3.exe` 和 `openssl.exe` 已加入环境变量(可用 scoop/choco 安装) +- Linux: +``` +sudo apt install sqlite3 openssl +``` + +--- + +## 备份流程详解 +1. 备份数据库:使用 sqlite3 的 .backup 命令生成安全副本 +2. 打包数据:将数据库、配置文件、私钥和附件目录打包为 tar.gz +3. 加密备份:用 openssl AES-256-CBC 加密备份文件 +4. 上传备份:将加密文件上传至 Cloudflare R2 +5. 轮换备份:自动删除超出 SLOT_COUNT 的旧备份 +6. 清理临时文件:删除本地临时文件 +7. 通知推送:如有异常,自动推送 Telegram 消息 + +--- + +## 运行方法 +在项目目录下执行: +``` +python backup.py +``` + +如需定时自动运行,可结合系统计划任务: +- Windows:任务计划程序 +- Linux:crontab + +--- + +## 数据安全说明 +- 备份文件采用 AES-256-CBC 加密,密码由 ENCRYPT_PASSWORD 指定 +- 加密密码请妥善保存,遗失将无法恢复备份内容 +- 云端存储采用 Cloudflare R2,需正确配置密钥 + +--- + +## Telegram 通知配置 +如需异常通知,请在 Telegram 创建 Bot 并获取 Token,查找你的 Chat ID 并填写到 `backup.env`。 + +- Bot 创建教程:https://core.telegram.org/bots#creating-a-new-bot +- Chat ID 获取方法:可用 @userinfobot 查询 + +--- + +## 备份轮换机制 +- 每次备份后自动检查云端备份数量,超出 SLOT_COUNT 时自动删除最旧的备份 +- 备份文件命名格式:`{BACKUP_PREFIX}{日期时间}.tar.gz.enc` + +--- + +## 常见问题与故障排查 +- **依赖未安装**:请检查 Python 包和系统工具是否安装齐全 +- **权限问题**:请确保数据目录和临时文件路径有读写权限 +- **上传失败**:检查 R2 配置和网络连接 +- **加密失败**:确认 openssl 命令可用,密码无特殊字符 +- **Telegram 未推送**:检查 Bot Token 和 Chat ID 是否正确 + +--- + +## FAQ +- Q: 如何恢复备份? + A: 下载加密备份文件,使用 openssl 解密后解包 tar 文件即可。 +- Q: 可以只备份数据库吗? + A: 可自行修改 backup.py,只保留数据库相关打包逻辑。 +- Q: 支持多平台吗? + A: 支持 Windows 和 Linux,MacOS 亦可。 + +--- + +## 贡献与反馈 +如有建议、问题或需求,欢迎提交 Issue 或 PR。 + +--- + +> 本项目旨在简化 Vaultwarden 的备份流程,提升数据安全性。感谢您的使用! diff --git a/backup.py b/backup.py new file mode 100644 index 0000000..339ace5 --- /dev/null +++ b/backup.py @@ -0,0 +1,154 @@ +import os +import tarfile +import boto3 +import datetime +import hashlib +import shutil +import requests +from dotenv import load_dotenv +from subprocess import run + +# 加载配置 +load_dotenv('./backup.env') # 或 '.env',看你放哪 + +# 环境变量 +access_key = os.getenv('R2_ACCESS_KEY_ID') +secret_key = os.getenv('R2_SECRET_ACCESS_KEY') +account_id = os.getenv('R2_ACCOUNT_ID') +bucket = os.getenv('R2_BUCKET_NAME') +region = os.getenv('R2_REGION', 'auto') + +source_dir = os.getenv('BACKUP_SOURCE_DIR') +tar_path = os.getenv('BACKUP_TEMP_FILE') +enc_path = os.getenv('BACKUP_ENCRYPTED_FILE') +enc_password = os.getenv('ENCRYPT_PASSWORD') + +slot_count = int(os.getenv('SLOT_COUNT', 3)) +backup_prefix = os.getenv('BACKUP_PREFIX', 'back-vault-s') + +# 创建 R2 客户端 +session = boto3.session.Session() +client = session.client( + service_name='s3', + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + endpoint_url=f'https://{account_id}.r2.cloudflarestorage.com', + region_name=region, +) + +# 发送 Telegram 消息 +def send_telegram_message(text): + bot_token = os.getenv('TELEGRAM_BOT_TOKEN') + chat_id = os.getenv('TELEGRAM_CHAT_ID') + if not bot_token or not chat_id: + print('[!] 未配置 Telegram 推送,无法发送通知') + return + url = f"https://api.telegram.org/bot{bot_token}/sendMessage" + payload = {"chat_id": chat_id, "text": text} + try: + requests.post(url, data=payload, timeout=10) + except Exception as e: + print(f'[!] Telegram 推送失败: {e}') + +""" +def tar_backup(): + print('[*] 打包数据...') + with tarfile.open(tar_path, "w:gz") as tar: + tar.add(source_dir, arcname=os.path.basename(source_dir)) +""" +def sqlite_backup(): + print('[*] 使用 sqlite3 .backup 命令备份数据库...') + db_file = os.path.join(source_dir, 'db.sqlite3') + backup_db_file = os.path.join(source_dir, 'db-backup.sqlite3') + if os.path.exists(db_file): + cmd = [ + "sqlite3", db_file, + f".backup '{backup_db_file}'" + ] + run(cmd, check=True) + else: + print('[!] 未找到数据库文件,跳过数据库备份') + return backup_db_file + +def tar_backup(): + print('[*] 只备份必要数据...') + backup_db_file = sqlite_backup() + with tarfile.open(tar_path, "w:gz") as tar: + # 主数据库 + db_file = os.path.join(source_dir, 'db.sqlite3') + if os.path.exists(db_file): + tar.add(db_file, arcname='db.sqlite3') + # 配置文件 + config_file = os.path.join(source_dir, 'config.json') + if os.path.exists(config_file): + tar.add(config_file, arcname='config.json') + # 私钥 + rsa_file = os.path.join(source_dir, 'rsa_key.pem') + if os.path.exists(rsa_file): + tar.add(rsa_file, arcname='rsa_key.pem') + # 附件目录(如有需要) + attachments_dir = os.path.join(source_dir, 'attachments') + if os.path.exists(attachments_dir): + tar.add(attachments_dir, arcname='attachments') + if os.path.exists(backup_db_file): + os.remove(backup_db_file) + +def encrypt_backup(): + print('[*] 加密备份...') + cmd = [ + "openssl", "enc", "-aes-256-cbc", "-salt", "-pbkdf2", + "-in", tar_path, + "-out", enc_path, + "-k", enc_password + ] + run(cmd, check=True) + +def upload_backup(): + now = datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S') + object_key = f"{backup_prefix}{now}.tar.gz.enc" + print(f'[*] 上传备份 {object_key} ...') + with open(enc_path, 'rb') as f: + client.upload_fileobj(f, bucket, object_key) + return object_key + +def rotate_backups(): + print('[*] 检查并轮换旧备份...') + res = client.list_objects_v2(Bucket=bucket, Prefix=backup_prefix) + if 'Contents' not in res: + print('[+] 无需轮换,当前仅有一个备份') + return + + backups = sorted( + [obj['Key'] for obj in res['Contents']], + key=lambda k: k + ) + if len(backups) <= slot_count: + print(f'[+] 当前备份数 {len(backups)} 未超过 {slot_count},无需删除') + return + + to_delete = backups[0:len(backups)-slot_count] + for key in to_delete: + print(f'[-] 删除过旧备份 {key}') + client.delete_object(Bucket=bucket, Key=key) + +def cleanup(): + print('[*] 清理临时文件') + for f in [tar_path, enc_path]: + if os.path.exists(f): + os.remove(f) + +def main(): + try: + tar_backup() + encrypt_backup() + upload_backup() + rotate_backups() + cleanup() + print('[✓] 所有备份流程完成') + except Exception as e: + err_msg = f'[!] 备份流程出错: {e}' + print(err_msg) + send_telegram_message(err_msg) + +if __name__ == "__main__": + main() diff --git a/backup.sh b/backup.sh new file mode 100644 index 0000000..364b1b8 --- /dev/null +++ b/backup.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 读取配置 +source ./backup.env + +echo "[INFO] 停止 Vaultwarden 容器" +cd "$COMPOSE_DIR" +docker compose stop vaultwarden + +echo "[INFO] 运行备份 Python 脚本" +python3 backup.py + +echo "[INFO] 启动 Vaultwarden 容器" +docker compose start vaultwarden + +echo "[INFO] 备份完成" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1b086a1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +boto3 +python-dotenv +requests