← Back to Blog
EN中文

给公开博客装个"加密暗门":git-crypt 实战

多年来,我的个人博客一直是我的"数字花园"。它托管在 GitHub Pages 上,这意味着整个代码仓库都是公开的。这对于已发布的文章来说非常完美。但写作的另一半呢?那些乱七八糟的草稿、不成熟的想法、私密的日记、原始的研究笔记——它们该何去何从?

很长一段时间里,我的解决方案简单粗暴:在 .gitignore 文件里加上一个 _content/drafts/ 目录。这方法能用,但终究是一种妥协。我的私密笔记没有版本控制,没法在多台设备间同步,也无法真正地融入它们本该归属的项目里。我基本上是把它们藏了起来,让 Git "视而不见",同时也放弃了一套更可靠的工作流。

我想要一个能让我的公开博客和私密笔记在同一个仓库里共存的系统。公开内容保持公开,而私密内容则在推送到 GitHub 之前就经过版本控制和安全加密。这样我就能拥有一个跨设备同步的"单一事实源",同时又不必把我的"数字大脑"公之于众。

在探索了几个选项后,我最终锁定了一个堪称优雅的解决方案:git-crypt

混合仓库的架构设计

我的目标是让加解密过程对日常的 Git 工作流完全透明。当我在 Obsidian 里写作,或是运行我的 Node.js 构建脚本时,我希望看到的文件是正常的、解密后的明文。但当我运行 git push 时,任何在指定"私密"区域的文件都应该在离开我的电脑前被自动加密。

以下是我的几条硬核需求:

  1. 透明无感: 加密和解密必须是全自动的。我不想每次都手动敲命令去锁定和解锁文件。
  2. 兼容性好: 它绝不能干扰我本地的工具。我的静态网站生成器应该能读到明文,Obsidian 也必须能顺利地读写笔记。
  3. Git 原生体验: git statusgit diff 必须能正常工作。如果我只修改了加密文件里的一个词,Git 应该知道文件变了,而不是每次都因为密文完全不同而手足无措。

这最后一点,直接劝退了一些新潮的替代方案。比如,用 age 这样的工具配合 Git 的 clean/smudge 过滤器是一种流行模式。但许多现代加密工具会产生非确定性密文(出于安全考虑,同样的输入每次都会产生不同的输出)。这对安全是好事,但它会破坏 git diff --quiet 这类检查,而我的构建脚本恰恰依赖这个来判断内容是否有变。

git-crypt 完美地解决了这个问题。它使用确定性加密(具体来说是 AES-256-CTR),这意味着相同的文件和密钥总是会产生相同的加密输出。这保证了 git status 的干净,也让我的构建脚本能正常工作。它直接挂载到 Git 自带的 clean(提交时加密)和 smudge(检出时解密)过滤器系统上,整个过程完全透明。

下面是这个数据流的简化示意图:

                                  ┌──────────────────┐
                                  │   GitHub 仓库    │
                                  │   (加密文件)      │
                                  └──────────────────┘
                                          ▲  │
                                git push  │  │ git pull/clone
                                (加密)    │  │ (解密)
                                          │  ▼
┌──────────────────┐   git commit   ┌──────────────────┐
│    工作区        ├────────────────►   本地 Git 数据库   │
│   (明文文件)     │ (clean 过滤器)   │    (已加密)       │
└──────────────────┘   git checkout └──────────────────┘
      ▲ │            (smudge 过滤器)
      │ └────────────────────────────┘

┌──────────────────┐
│ 你和你的各种应用 │
│ (Obsidian, VSCode)│
└──────────────────┘

实战部署:一步步指南

搞定这套配置花了我不到十分钟。它就是那种"装上就能用"的省心工具。

1. 安装与初始化

首先,我用 Homebrew 安装 git-crypt

brew install git-crypt

然后,在我的博客仓库根目录下,执行初始化。

git-crypt init

这个简单的命令会在 .git 目录内设置好所有必需的钩子和配置。每个仓库只需设置一次。

2. 万分重要的密钥备份

git-crypt 默认使用对称密钥。这个密钥存储在你本地的 .git 目录里,并且不会被提交到版本库。如果你丢了这个密钥,你的加密文件就永远打不开了。这绝对是整个流程里无可争议的"千万别搞砸"环节。

我立刻把密钥导出到了一个安全的地方(我的密码管理器和一个加密U盘)。

git-crypt export-key ~/git-crypt-my-blog.key

请像对待密码一样对待这个密钥文件。不要把它提交到任何代码仓库。

3. 配置加密对象

接下来,我需要告诉 git-crypt 要加密哪些文件。这通过在仓库根目录创建一个 .gitattributes 文件来完成。这是 Git 的一个标准机制,用于给不同路径的文件定义属性,git-crypt 巧妙地利用了这一点。

我创建了一个 .gitattributes 文件,内容如下:

# 加密 private 目录下的所有东西
_content/private/** filter=git-crypt diff=git-crypt

# 加密 Obsidian 的配置目录
_content/.obsidian/** filter=git-crypt diff=git-crypt

这个配置指示 Git 对两个位置的文件应用 git-crypt 过滤器:

  1. _content/private/**:我所有私密草稿、笔记和日记的新家。
  2. _content/.obsidian/**:我的 Obsidian vault 配置目录,里面可能包含插件配置等敏感信息。

创建完这个文件后,我把它提交到了仓库。此后,任何匹配这些路径模式的新文件都将被自动加密。

4. 迁移与提交

配置就绪后,我做了以下几件事:

  1. 创建新的 _content/private/ 目录。
  2. 把以前被 git-ignore 的 _content/drafts/ 文件夹移动到 _content/private/drafts/
  3. 更新 .gitignore 文件,删掉原来对 _content/drafts/ 的忽略规则。

之前:

# .gitignore
_content/drafts/

之后:

# .gitignore
# 不再需要了,私密内容由 .gitattributes 处理。

最后,把所有新移动的文件、更新后的 .gitattributes.gitignore 文件添加到暂存区。

git add .gitattributes .gitignore _content/private
git commit -m "feat: 集成 git-crypt 用于私密笔记"
git push

就这样,我的私密笔记就被纳入了版本控制,并以完全加密的形式推送到了 GitHub。如果你现在去 GitHub 上浏览我的仓库,_content/private/ 目录下的文件就是一坨坨二进制乱码。但在我的本地电脑上,它们就是普普通通的 Markdown 文件。

日常工作流:无缝且无感

这套系统最棒的地方在于,我的日常工作流完全没有改变。我像往常一样在 Obsidian 里编辑私密笔记,写公开的博客文章,然后提交我的改动。git-crypt 在后台默默地完成了一切。

当我要配置一台新电脑时,流程也极其简单:

  1. 克隆仓库。此时,所有私密文件都会是加密后的二进制形态。
  2. 把备份好的密钥文件(git-crypt-my-blog.key)拷贝到新电脑上。
  3. 运行解锁命令:
git-crypt unlock ~/git-crypt-my-blog.key

瞬间,所有加密文件在工作区里都被解密了,马上就能开工。每台新设备,只需这一个一次性命令。

密钥到底存在哪?搞懂密钥的存储机制

刚配好 git-crypt 的时候,有个问题一度让我有点迷:导出的那个 key 文件和仓库内部的 key 到底是什么关系?搞清楚这个逻辑,密钥管理就一点也不慌了。

已配置过的机器上

运行 git-crypt init 之后,对称密钥就存储在你的 .git 目录内部:

.git/git-crypt/keys/default    # 148 字节,真正干活的 key

这就是 git-crypt 在每次 commit 和 checkout 时自动调用的密钥。你完全不需要手动指定它——它就在那儿默默工作。而且因为 .git/ 目录永远不会被 push 到远端,所以这个 key 始终只在本地。

导出的 Key 只是一份备份

当你执行 git-crypt export-key 时,你只是把那个内部密钥拷贝了一份到外部位置。导出的文件和 .git/git-crypt/keys/default逐字节相同的。它唯一的作用就是灾备:配置新设备,或者在本地 .git 目录丢失时恢复访问权限。

我的密钥管理策略

初始配置完成后,我做了三件事:

  1. git-crypt export-key ~/git-crypt-yuxu.ge.key 导出密钥
  2. 把它存到 1Password 里作为安全文档
  3. 删掉本地导出的副本——因为密钥已经在 .git/ 里面了,这个副本纯属多余
# 确认密钥已安全存入密码管理器后:
rm ~/git-crypt-yuxu.ge.key

密钥不需要固定放在 home 目录或任何特定路径下。它只需要存在于两个地方:.git/git-crypt/keys/default(自动工作)和你的密码管理器里(备份)。

全新设备上的完整恢复流程

在一台从未配置过的全新机器上,完整的恢复过程是这样的:

# 1. 安装 git-crypt
brew install git-crypt          # macOS
# apt install git-crypt         # Ubuntu/Debian

# 2. 克隆仓库(此时加密文件是二进制乱码)
git clone [email protected]:geyuxu/yuxu.ge.git
cd yuxu.ge

# 3. 从密码管理器导出 key 到临时文件
#   (从 1Password 下载,或从其他机器 scp 过来)

# 4. 解锁 —— 所有加密文件瞬间变回明文
git-crypt unlock /path/to/git-crypt-yuxu.ge.key

# 5. 删掉临时 key 文件(它已经被复制到 .git/ 里了)
rm /path/to/git-crypt-yuxu.ge.key

执行完第 4 步后,密钥就被复制到了 .git/git-crypt/keys/default,一切就绑定好了。之后所有的 pull/push 都会自动加解密。临时的 key 文件可以放心删掉。

避坑指南与总结

这套方案非常出色,但有几点需要牢记:

  • 密钥管理,重中之重: 再说一遍,如果你丢了对称密钥,你的数据就灰飞烟灭了。请务必在多个安全的地方备份它。
  • 合并冲突: 如果一个加密文件发生了合并冲突,Git 会在密文里给你展示冲突标记。它看起来就像天书。你必须在一台 git-crypt 已解锁的电脑上解决这些冲突,因为只有在那里你才能看到明文。
  • 团队协作: 我的设置用的是简单的对称密钥,因为只有我一个贡献者。如果你需要授权给一个团队,git-crypt 对 GPG 有很好的支持,可以让你添加多个拥有自己密钥的受信任用户。
  • 历史记录: 这个流程只会加密未来的文件。如果你不小心在过去提交过敏感文件,它们的历史记录仍然是明文的。你需要用 git-filter-repo 这样的工具来清洗历史。对我来说这不是问题,因为我是从一个被 .gitignore 的目录迁移过来的。

这次折腾,绝对是我个人知识管理体系里一次"白捡"的优化——纯赚。我现在用一个统一的仓库管理整个写作流程。公开文章和私密笔记并存,各自都有完整的版本历史,还能在所有设备间同步。而实现这一切,只需要一个简单、强大且透明的工具来搭起一座桥梁。