オフサイトバックアップに平文を送っていた話
友人宅のNASへオフサイトバックアップを組んだとき、`zfs send`に`-w`を付け忘れていたことに数ヶ月気づきませんでした。暗号化データセットは既定の送信方法だと送信元でいったん復号されるため、平文がネットワーク越しに流れていたことになります。ZFSネイティブ暗号化の鍵管理と性能コスト、そしてこのraw sendの落とし穴を整理します。
友人宅のNASへオフサイトバックアップをzfs send | zfs recvで組んだとき、しばらく-w(raw send)を付け忘れていたことがあります。気づいたのは数ヶ月後、別件でraw sendの挙動を調べていたときでした。暗号化データセットは、既定の送信方法だと送信元でいったん復号してから送信ストリームを組み立てます。つまりその数ヶ月間、友人宅までの経路を平文が流れていたことになります。幸い自宅と友人宅の間はVPN越しだったので実害はなかったはずですが、冷や汗ものでした。
ZFSネイティブ暗号化(OpenZFS 0.8以降)は、データセット単位で暗号化をオン/オフできる手軽さが魅力です。zfs create -o encryption=on の1行で始められる一方、鍵管理の設計を誤ると「あとから暗号化だけ外す/入れる」がほぼ不可能だったり、自分がやったようにレプリケーション先に平文が漏れたりする落とし穴があります。この記事では、仕組みと鍵管理の実務、性能コストがどこから来るのかを整理し、LUKS/dm-cryptとどちらを選ぶべきかの判断材料をまとめます。
データセット単位の暗号化という設計
ZFSネイティブ暗号化は、プールやvdevではなくデータセット(ファイルシステム/zvol)単位で有効化します。同じプール内に暗号化されたデータセットと平文のデータセットを混在させられるのが、ディスク全体を覆うLUKSとの根本的な違いです。
zfs create -o encryption=on -o keyformat=passphrase tank/secure
このコマンドを実行すると、パスフレーズの入力を求められ、tank/secure 以下(子データセットを含む)が暗号化されます。重要なのは、暗号化はデータセット作成時にしか有効化できないという制約です。すでに存在する平文のデータセットを後から暗号化に切り替えることはできません。既存データを暗号化したい場合は、新しく暗号化データセットを作成し、zfs send | zfs recv でデータを移送してから旧データセットを破棄する、という手順が必要になります。
暗号化ルートと鍵の継承
暗号化を有効にしたデータセットは、それ自身が「暗号化ルート(encryption root)」になります。配下に作成する子データセットは、デフォルトでは親と同じ鍵を継承し、暗号化ルートも共有します。
zfs create tank/secure/vms # tank/secure の鍵を継承
zfs create -o keyformat=passphrase tank/secure/isolated # ここで新しい暗号化ルートを作る
tank/secure/isolated のように子データセット作成時に改めて keyformat を指定すると、そこが新しい独立した暗号化ルートになります。テナントごとに鍵を分離したいマルチテナント用途では、この「暗号化ルートの分岐」を意図的に設計する必要があります。逆に言えば、親子で同じ鍵を共有している限り、鍵のロード/アンロードは暗号化ルート単位でまとめて行われます。
暗号化ルートの継承と分岐
→ keyformatを指定せずに作成した子データセット(左)は親の暗号化ルートに属したまま鍵を共有する。作成時に改めてkeyformatを指定した子データセット(右)は、そこを起点に独立した暗号化ルートへ分岐する。
鍵の形式とライフサイクル
keyformat には3種類あります。
| keyformat | 用途 |
|---|---|
passphrase | 人間が入力する。内部的にPBKDF2で鍵導出(反復回数はpbkdf2iters、デフォルト350,000回程度) |
raw | 32バイトの生鍵。鍵管理システム(KMS)から供給する用途 |
hex | 生鍵の16進数表現。scriptから扱いやすい |
鍵の保存場所は keylocation プロパティで指定します。prompt(対話入力、デフォルト)、file:///path/to/keyfile(ファイル)、https://...(HTTPS経由)が使えます。自動起動時にプロンプトを出せないファイルサーバーでは file:// を使い、鍵ファイル自体のパーミッションとバックアップ管理を別途設計するのが一般的です。
主な運用コマンドは次の4つです。
zfs load-key tank/secure # 鍵をロード(データにアクセス可能にする)
zfs unload-key tank/secure # 鍵をアンロード(ロック、mountも解除される)
zfs mount -l tank/secure # ロード+マウントを一度に
zfs change-key tank/secure # 鍵のローテーション、keyformat/keylocationの変更
zfs unload-key でロックした状態のデータセットは、zpool status や zpool scrub などプール管理コマンドの対象には引き続きなりますが、中身のファイルは一切見えません。パスフレーズを紛失した場合、バックドアや復旧手段は存在しません。 鍵のバックアップ(パスフレーズの安全な保管、あるいは鍵ファイルのオフサイト保管)は暗号化を有効にする前に設計しておくべき最優先事項です。
性能コスト: AES-NIの有無で桁が変わる
ZFSネイティブ暗号化はAES-GCM(encryption=on のデフォルトは aes-256-gcm)を使用し、認証付き暗号(AEAD)としてブロックごとにMAC(改ざん検知タグ)を付与します。この暗号化処理は、CPUがAES-NI命令セットに対応しているかどうかで体感が変わるほど性能が変わります。ソフトウェア実装のAESにフォールバックすると、CPU負荷がボトルネックになり、暗号化なしでは出ていたはずのIOPS/スループットを引き出せなくなることがあります。
まず自分の環境がAES-NIに対応しているかを確認しましょう。
grep -o aes /proc/cpuinfo | head -1
openssl speed -evp aes-256-gcm
ホームラボの古い/省電力志向のCPU(一部のAtom系、初期のARM系NASなど)ではAES-NIが無いか無効化されていることがあり、その場合は暗号化の追加コストが無視できません。現行世代の一般的なx86_64サーバー/デスクトップCPUであれば、AES-NIを使ったオーバーヘッドは実務上ほぼ無視できる水準に収まるのが一般的ですが、実際の負荷はワークロードのI/Oパターンに左右されるため、自分の環境で fio などを使い暗号化ON/OFFの2構成を比較測定することを推奨します。
処理順序も押さえておく価値があります。ZFSは 圧縮 → 暗号化 の順でブロックを処理します。先に圧縮してから暗号化するため、暗号化を有効にしても圧縮率(容量削減効果)はそのまま享受できます。逆の順序(先に暗号化)だと、暗号文はランダムに近いデータになり圧縮がほぼ効かなくなるため、この順序はZFSの設計上理にかなっています。
raw sendを付け忘れると平文が流れる
冒頭の話がまさにこれです。暗号化データセットを zfs send でレプリケーションする際、デフォルトの送信方法では送信元でいったん復号してから送信ストリームを組み立てます。つまり受信側で鍵をロードしなくてもデータは書き込めますが、ネットワーク経路やスクリプトの取り扱いに気をつけないと平文が露出する経路が生まれます。
信頼できないレプリケーション先(オフサイトのバックアップストレージ、クラウド上のZFSインスタンスなど)に送る場合は、-w(raw send)オプションを使います。
zfs send -w tank/secure@snap1 | zfs recv backup/secure
通常のsend (zfs send)
→ 送信元でいったん復号するため、パイプ/ネットワークを流れる区間は平文。受信側は鍵を持たなくても書き込める。
raw send (zfs send -w)
→ 暗号化されたブロックをそのまま転送。受信側に鍵を渡さずに、信頼できないバックアップ先へ安全にレプリケーションできる。
raw sendは暗号化された状態のブロックをそのまま転送するため、受信側は鍵を持っていなくてもデータを受け取れ、かつ中身を読めません。 バックアップ先が侵害されても保存データは暗号化されたままという設計にできる一方、raw受信したデータセットは受信側単独では復号できないため、鍵(またはパスフレーズ)を災害復旧の手順として別途安全に保管しておく必要があります。また、raw send/recvでは送信元と受信先で圧縮方式や暗号化パラメータを変更できない点にも注意してください。
LUKS/dm-cryptを選ぶべきとき
ネイティブ暗号化は万能ではありません。次のようなケースではLUKS/dm-cryptによるフルディスク暗号化の方が適しています。
- スワップ領域やプール外のパーティションも含めて暗号化したい。 ネイティブ暗号化はデータセット単位のスコープなので、ZFS管理外の領域はカバーしません。
- 鍵管理をシンプルに保ちたい。 LUKSはブート時に1回鍵を解除すればディスク全体が使えます。データセットごとの暗号化ルート設計・鍵ローテーション運用を持ち込みたくない小規模環境では、運用の複雑さがネイティブ暗号化のメリットを上回ることがあります。
- 物理的な盗難対策が主目的。 電源断状態のディスクを盗まれた場合の保護であれば、フルディスク暗号化で十分要件を満たせます。
逆に、同じプール内でテナント/用途ごとに異なる鍵を使い分けたい、信頼できないレプリケーション先に安全にバックアップしたい(raw send)、特定のデータセットだけ暗号化コストを払いたい といった要件があるなら、ネイティブ暗号化のデータセット粒度の柔軟性が活きます。
あの一件以来のルール
友人宅へのバックアップスクリプトは今、送信コマンドを組み立てる関数の中に-wをハードコードして、暗号化データセットを扱うパスでは絶対に外せないようにしています。過信せず、スクリプト側で強制する方が安全です。
- 暗号化はデータセット単位で有効化し、既存の平文データセットに後付けはできません。暗号化するなら新規データセットを作り
zfs sendで移送します - 暗号化ルートは親子で継承されます。テナント/用途ごとに鍵を分けたいなら、子データセット作成時に新しい
keyformatを指定して分岐させます - パスフレーズ/鍵の紛失にバックドアはありません。バックアップ計画を有効化前に決めておきましょう
- 性能コストの主因はAES-NIの有無です。
openssl speed -evp aes-256-gcmで確認し、ON/OFFを実測してから判断してください - 信頼できないレプリケーション先へは
zfs send -w(raw send)を使い、忘れないようスクリプト側で強制する仕組みを作りましょう