将来、家に隕石が落ちてきた時の最後の砦として、自分の PC にある日々の運用データ一式をクラウド上に保全して枕を高くして寝たい。また、超強力な太陽嵐が地球上に降り注いでも、データの整合性は可能な限り担保したい。
そんな夢を見たのでどこまで実現可能かを考えてみた。思考実験以上、運用実験未満。
現況
今のところ(高速回線環境がないので)最終的なバックアップ完了までこぎ着けていない。よって復元も試せていない。試行レポートをお待ちしています。
ゴール
AWS 上に macOS の TimeMachine
機能を使ってデータを保管したい。また、手元の Mac のディスクが壊れた時のリカバリー元としても使えるようにしたい。可能な限り高い信頼性を確保し、できるだけセキュアに。
要件
ネットワーク
- 通信経路をセキュアに保つこと
- macOS の標準機能だけで VPN 接続を確立できること
データ
- データは自分で暗号化・復号できること
- 信頼性の高い(ビット反転等に強い)ディスク環境を構築すること
- 可能な限り高い冗長性を確保できる(コントロールできる)こと
- バックアップだけでなく、リカバリモードで起動した Mac から OS の復元ができること
方針
- 特別な機器(ルータ等)を利用せず、ソフトウェアのみで実現する。
- 信頼性の高いツールを組み合わせて実現する。がんばりすぎない。
- サーバは1インスタンスで済ませる。
ソリューション
検討した結果、ソリューションとして以下のソフトウェアを組み合わせることとした。
VPN 環境: Algo
Algo は、クラウドサービス上に IPSec (最近では WireGuard も可)を使った VPN インスタンスを簡単に構築できるツール。PC の設定ファイルも生成してくれるので便利。実行すると EC2 のインスタンスを立ててネットワークの設定まですべて行ってくれる。
OS は Ubuntu の最新版がインストールされる。
ファイルシステム: ZFS
みんな大好き ZFS には同一データの複数コピーを保持する機能がある。複数のコピーがあると、ビット反転(腐敗)などが起きた時にメンテナンスコマンドにより整合性を回復できる。
またソフトウェア的に RAID を組める機能を利用し、ディスクのハードウェアエラーや操作ミス等もカバーする。
データ暗号化: LUKS によるディスク暗号化
ディスクの暗号化機能は ZFS にも搭載されているが、Linux 上での安定性や可用性を考えて今回はこちらとした。
(他の方法との比較についてはのちほど論じる)
ディスクデバイス: Amazon EBS
ディスク系のソリューションとなると EFS も候補に挙がるが、(AWS に任せず)自前で暗号化したいとなるとブロックデバイスである EBS 一択となる。ただし、コストパフォーマンスを高めるためにタイプは低速だが低コストの Cold HDD (sc1)
を指定する。ネット越しのバックアップはネットワーク速度がボトルネックとなるのでディスク自体は低速でもまったく構わない。
また、必要に応じて EBS ボリュームのスナップショットを定期的に取得してもよいだろう(ただし費用はけっこうかかる)。
通信プロトコル: netatalk
TimeMachine は AFP プロトコルで動作するので netatalk サーバを稼働させる。なお現代では SMB (CIFS) でも可能なので samba を利用してもよさそうだ1。
Algo のセットアップ
Algo のセットアップは詳しい記事がネットにたくさんあるので省略。いくつかポイントだけ。
iptables の設定を追加
netatalk のためにポートを開放する。任意のプロトコルを通す設定を公式に追加する方法がわからないので、次のようなパッチを当ててごまかすこととした。
vi iptables.patch
--- roles/common/templates/rules.v4.j2.orig 2020-06-14 13:10:38.000000000 +0900
+++ roles/common/templates/rules.v4.j2 2020-06-14 13:11:21.000000000 +0900
@@ -81,6 +81,10 @@
# Accept DNS traffic to the local DNS resolver
-A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT
+# Netatalk
+-A INPUT -p tcp --dport 548 -m conntrack --ctstate NEW -j ACCEPT
+-A INPUT -p udp --dport 5353 -j ACCEPT
+
# Drop traffic between VPN clients
-A FORWARD -s {{ subnets|join(',') }} -d {{ subnets|join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }}
# Drop traffic to VPN clients from SSH tunnels
patch < iptables.patch
設定ファイルの書き換え
config.cfg
を編集する。
IAM 作成と権限設定
専用の IAM を作成しておくとよい。インストラクションにあるように、権限の欄に指定された JSON を記述すること。
Python
python3 が必要。macOS には付属しないので brew 等で用意する。詳しくは algo の README を参照。3.8 ではエラーが出たので 3.7 推奨。
ユーザリスト
デフォルトで users が3つ指定されているが1つあればいいので、適当に減らしたりリネームする。admin
とでもしておく。
インスタンスタイプ
2019 年は t2.nano
を指定できたが、2020 年はメモリ 512 MB の環境では cryptsetup
がメモリ不足でディスクの復号に失敗するようになっているので注意。安いインスタンスで動かせていたのに残念だ。t2.micro
以上にしておこう。ちなみに swap 領域を用意してもダメだった。
(なお、t3
ファミリーでの起動も試したが現時点ではうまくいかなかった。おそらく ENA 周りが関係するのだろう)
生成されたデータのバックアップ
algo コマンドによるセットアップ後に、configs
以下に秘密鍵や諸々のファイルが生成される。これらを別マシン/メディアへ安全に保全すること。もし可能ならバイト列を石版に刻んで対爆仕様の金庫に放り込み、世界中に分散設置する。衛星に乗せるのもいいだろう。
インスタンスの立ち上げ
実際に algo を実行しインスタンスを作成する。日本在住者なら ap-northeast
リージョンを選択するとよい。
./algo
AWS コンソールから、自動作成された EC2 インスタンスの削除保護
を有効にし、シャットダウン動作
を「停止」に変更しておこう。
無事に作成されたら ssh でログインする。
ssh -i configs/algo.pem ubuntu@xx.xx.xx.xx
sudo su -
ネットワークとマシンの準備
最新版にアップグレードしておく。
apt-get update
apt-get upgrade -y
必要なライブラリを入れる。
apt install -y cryptsetup zfsutils-linux netatalk
ディスク領域の準備
方針
ZFS の機能を使って RAID を組む。
可能なら raidz2 オプションを採用し RAID6 を組みたいが、EBS (Cold HDD (sc1)
) を4台も使うと最低でも $60/month がかかる。ディスク構成はあとから変更できるので、まずは2台で構築してみる。
なお EBS 自体の暗号化機能は使用しない。
セットアップ
ディスク設計
今回は EBS ボリュームごとに ZFS のストレージプールを作成し、それらを raid として束ねることとした。
EBS ボリュームを用意する
EBS は EC2 インスタンスと同じアベイラビリティゾーンに作成すること。AWS コンソール上から EC2 インスタンスにアタッチする。
必要な数だけ作成する。ひとまず、タイプは sc1 で 500GB ものを2つ作り、それぞれタグの Name
として algo disk1
と algo disk2
などとセットしておくとよいだろう。今回は EBS 暗号化は使わない。
通常なら /dev/sdf にアタッチされ、インスタンスからは /dev/xvdf
として見えるはずだ。
2つめ以降は /dev/xvdg
, /dev/xvdh
... などと振られる。
ls -lF /dev/xvd[f,g,h,i]
暗号化ボリュームをセットアップする
次のようにして暗号化ボリュームをセットアップする。もちろん既存のファイルは全消去されるので注意すること。パラノイアなひとはすべて違うパスフレーズにするのも良いかもしれない。
cryptsetup --verbose --verify-passphrase luksFormat --cipher aes-cbc-essiv:sha256 --key-size 256 /dev/xvdf
cryptsetup --verbose --verify-passphrase luksFormat --cipher aes-cbc-essiv:sha256 --key-size 256 /dev/xvdg
...
それぞれ tmdisk1
, tmdisk2
... としてマウントする(名前は任意)。
cryptsetup luksOpen /dev/xvdf tmdisk1
cryptsetup luksOpen /dev/xvdg tmdisk2
...
ZFS プールに登録する
ここでは2台なので mirror
を指定する。3台なら raidz1
、4台なら raidz2
... というように設定できる。
zpool create -o ashift=12 -O normalization=formD tmdisk mirror \
/dev/mapper/tmdisk1 \
/dev/mapper/tmdisk2
状態を確認する。
zpool list
zpool status
NAME SIZE ALLOC FREE EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
tmdisk 1008M 408K 1008M - 0% 0% 1.00x ONLINE -
pool: tmdisk
state: ONLINE
scan: none requested
config:
NAME STATE READ WRITE CKSUM
tmdisk ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
tmdisk1 ONLINE 0 0 0
tmdisk2 ONLINE 0 0 0
ファイルシステムオプション
いくつか有用なオプションを付けておく。
zfs set compression=on tmdisk # ディスク圧縮
zfs set copies=2 tmdisk # 複数のコピーを保持
ここでは copies=2
を設定している。もちろんそれだけ容量(と書き込み時間)を消費するので注意すること。ちなみに指定できる最大値は 3 となっている。
以下はお好みで。
zfs set atime=off tmdisk # noatime 相当(ディスクのアクセス速度を上げたい)
zfs set relatime=on tmdisk # relatime 相当(バランスがよさそうなので)
zfs set exec=off tmdisk # noexec 相当(バイナリの実行を禁止。安全性を上げたい)
現在のオプション値を確認することもできる。
zfs get all tmdisk
なお、冗長性を確保するという目的のため、当然ながら 重複除去機能
は使用しない2。
書き込みテスト
ここでは 1GB のディスクで copies=2
を指定しているので、圧縮が効かない場合にはせいぜい 500MB 程度しか書き込めないはずである。確かめる。
head -c 600m /dev/urandom > /tmdisk/data
head: error writing 'standard output': No space left on device
ls -lh /tmdisk/data
rm -f /tmdisk/data
これで secure かつ robust な /tmdisk
を準備できた。
ユーザ
netatalk へ接続するための Unix 実ユーザアカウントを適当に作成する。ここでは tmuser
とした。
useradd -d /home/tmuser -g backup -m -s /usr/sbin/nologin -u 340 tmuser
passwd tmuser # これが macOS からアクセスするときのパスワードとなる
ディスクの所有権を調整しておく。
chown -R tmuser:backup /tmdisk
chmod 750 /tmdisk # あるいは 700 か
netatalk
/tmdisk
ボリュームを netatalk に登録する。TMDisk
の名前は任意。macOS 上で見えるディスク名となる。
vi /etc/netatalk/AppleVolumes.default
/tmdisk "TMDisk" allow:tmuser cnidscheme:dbd options:usedots,upriv,tm
もしユーザ名を変えた場合は tmuser
の部分を変更するのを忘れないように。また、デフォルトとして有効になっている ~/
の行が必要なければオフにしてもよいだろう。
vi /etc/netatalk/afpd.conf
# for TimeMachine
- -tcp -noddp -uamlist uams_dhx.so,uams_dhx2_passwd.so -nosavepassword
もし afpd のログを出したければ次も追記するとよい。
- -setuplog "default log_debug /var/log/afpd.log"
追記:Ubuntu 20.04 以降だと次のようにする必要があるようだ。
vi /etc/netatalk/afp.conf
[TMDisk]
path = /tmdisk
time machine = yes
force user = tmuser
起動する。
systemctl restart netatalk
lsof -i:548
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
afpd 7239 root 4u IPv4 39717 0t0 TCP *:afpovertcp (LISTEN)
接続の準備
hosts ファイル
利便性向上のため、手元の macOS の /etc/hosts
に例えば次のように書いておくとよい。
sudo vi /etc/hosts
10.19.49.1 tmdisk-aws tmdisk-aws.local
この IP アドレスは Algo の roles/vpn/defaults/main.yml
にハードコードされているのでそのまま記述してよい。
実行する
VPN
まず VPN に接続する。「ネットワーク」環境設定に Algo VPN xx.xx.xx.xx IKEv2
が追加されているはずなのでこれを選び、「接続」をクリックする。
正しく接続されているかどうか確認する。
ping tmdisk-aws.local
マウントする
Finder の「サーバへ接続 (cmd+K)」へアドレスを入れて接続する。
afp://tmuser@tmdisk-aws.local/TMDisk
試しになにかファイルを書き込んでみると次のように反映されるはずだ。
ls -l /tmdisk/
drwxr-x--- 3 root backup 3 Sep 28 08:02 'Network Trash Folder'
drwxr-x--- 3 root backup 3 Sep 28 08:02 'Temporary Items'
drwxr-sr-x 1 tmuser backup 59601 Sep 5 05:15 test.jpg
あとは通常通り、TimeMachine 環境設定を開いて TMDisk
をバックアップ先に指定するだけだ。
UUID な .sparsebundle
が作られ、そのうちマシン名に変わるはずだ。
drwx--S--- 4 tmuser backup 7 Oct 9 10:42 5097C544-06B1-58F6-9B6B-2C1F065E3F9D.sparsebundle/
運用
ディスクの冗長化
費用に余裕があれば、数 TB の EBS ボリュームを4つ用意し ZFS の raidz2
を指定して RAID6 相当を組むと万全だろう。
ただ、(物理ディスクとは違って)EC2 上の仮想ディスクが壊れる可能性は相対的にかなり低いと思われるので、ヒューマンエラーを加味した落としどころとしては、3台で raidz1
の RAID5 構成で十分だとは思う。
定期的なディスクのチェック
通常は /etc/cron.d/zfsutils-linux
で定期的に zpool scrub
コマンドが実行され、不整合は自動修復されるはずだ。デフォルトで月に1度だが、もう少し頻度を上げてもいいかもしれない。
EBS スナップショットの定期作成
費用に余裕があれば、1日それぞれひとつのスナップショットを撮っておくとさらに安心だろう。ただし、かなり費用がかかる富豪的手法ではある。
仮に4つの 1TB ディスクを使用し、ディスクの使用量を各 50% とし、1日にひとつ、5つのスナップショットを保持した場合、現状では $500/月程度かかるだろう。
OS 環境
細かいこといろいろ。
ホスト名
hostnamectl set-hostname tmdisk1.example.com
タイムゾーン
Algo デフォルトでは UTC なので JST にしておく。
timedatectl set-timezone Asia/Tokyo
時刻同期
ntp を起動しておく。
apt-get install -y chrony
IP 制限
可能なら、EC2 (VPC) のセキュリティグループで自宅/自社からのアクセスのみに制限しておこう。
ssh は hosts.allow
へ記述する方法もある。
念のため netatalk も hosts.allow
で制限したいなら次のように書ける。
afpd : 10.19.48.1 : allow
afpd : ALL : deny
運用サイクル
一度セットアップすれば次のようなフローになる。
OS 起動時
暗号化ディスクをマウントする。複数のディスクを使う場合は繰り返す。
cryptsetup luksOpen /dev/xvdf tmdisk1
cryptsetup luksOpen /dev/xvdg tmdisk2
...
ZFS 領域をマウントする。
zpool import tmdisk
zpool list -v
もしマウントされていなかったら手動で。
zfs mount tmdisk
OS シャットダウン時
ふつうにマシンを shutdown/reboot しても問題ない。あえて手順を踏むなら次のようになる。
ZFS 領域をアンマウントする。
zfs unmount tmdisk
zpool export tmdisk
test -e /tmdisk && rmdir /tmdisk # 次回マウント時に失敗するので消しておく
ディスクの暗号化を解除する。
cryptsetup luksClose /dev/mapper/tmdisk1
cryptsetup luksClose /dev/mapper/tmdisk2
...
課題
実効容量を正しく認識してくれるか
ZFS の圧縮や複数コピーオプションを有効にしている関係上、実効領域が伸び縮みして見えている実ディスク容量と合わない(と思われる)。この差違を TimeMachine が正しく認識して消し込みを行ってくれるか不明。心配なら次のような対処法があり得るか。
- netatalk の設定で TimeMachine 領域の容量を明示的に指定する (volsizelimit:512000)
- ZFS 上で quota をかける (zfs set quota=500G tmdisk)
可能であれば、使用したい容量の 2.5 倍以上を確保しておくと安心かもしれない。
ファイル名の大文字小文字の区別問題
macOS 自体は case-insensitive
なので、ZFS 上もそれに合わせておくほうが安全かもしれない。また、SMB を使う際にはまた話が変わってくるようだ。
zfs set casesensitivity=caseinsensitive tmdisk
ファイル名の UTF8 正規化問題
macOS のファイル名正規化は NFD なので、ZFS create 時に -O normalization=formD
などと指定できるようだ。macOS に準拠させたいならどうすべきか。あるいは netatalk が差違を吸収してくれると期待してよいものか。
クラウドからの OS リカバリは成功するのか
理論上は成功しそうだが、試せていない。実効的な速度が出るものなのかも不明。
回線速度
うちの ADSL 12M という化石のような回線だと、計算上、初回のフルバックアップに40日ほどかかるのが難点といえば難点だ。少なくとも初回実行時は高速かつ従量課金されないネットワーク環境を用意したい。
発展
複数のサーバで別の TimeMachine バックアップを運用する
マルチ AZ 化やマルチリージョン化を試すと冗長性が高まってより安心か。
他のクラウドサービス上でも構築する
もし Google Cloud でも同等のシステムを構築できれば、マルチクラウド化して一段レベルの高い冗長性を確保できそうだ。
コールドストレージとして
定期バックアップするときだけサーバを立ち上げればセキュリティ上も安心な上に費用も安く上がる。例えば週に一度だけ AWS Lambda 等で自動でインスタンスが立ち上がるようにしておく手もありそうだが、結局 cryptsetup へパスフレーズを渡さねばならない問題は残る(AWS KMS 等を活用できるかもしれない)。
検討事項
どの段階でデータを暗号化するか
今回は LUKS を用いたが、いくつかの方針が考えられる。
TimeMachine に暗号化させる
そもそも TimeMachine に「バックアップを暗号化」するオプションがあるのでこれを使用する。ただし、サーバ上で実ファイルが見られなくなってしまうのが難点だが、お手軽確実ではある。
また、ディスクをマウントする時にパスフレーズを打たなくて済むので、サーバの自動起動・シャットダウンをプログラマブルにコントロールできるのは魅力だ。
LUKS で暗号化する
今回説明した方法。サーバ上から実ファイルが参照できるので、便利な場面は多いだろう。反面、サーバを稼働中にクラックされた場合の秘匿性に劣る。
とはいえ将来的に(プロプライエタリ製品である) macOS が使用できない環境を想定するのであれば、妥当なソリューションだと言えるだろう。
ZFS で暗号化する
未検証。使用する弊害はなさそうなので、LUKS の代わりにこちらを使用してもよさそうだ。
EBS で暗号化する
AWS に暗号化キーを預けることになるので用途にそぐわない。他の方法に比べてとくにアドバンテージがあるとも思えないので忘れてよいだろう。
複数の手段で暗号化する
基本的に暗号の重ねがけは速度的にもセキュリティ強度的にも悪手3ではある。個人的には上記のうちどれかを単体で採用すべきだと考えている。
おまけ
HTTP プロキシを立てる
VPN 接続をしているとウェブに繋がらなくなるので、一時しのぎとしてインスタンス上で Squid
を動かすことにする。
apt install -y squid
/etc/squid/squid.conf
へ追記する。
acl localnet src 10.19.48.1
http_access allow localnet
リロードする。
systemctl reload squid
ポートを開ける。
iptables -I INPUT 12 -p tcp --dport 3128 -m conntrack --ctstate NEW -j ACCEPT
永続化したいのであれば /etc/iptables/rules.v4
へ適宜書き込むなどする。
# Squid
-A INPUT -p tcp --dport 3128 -m conntrack --ctstate NEW -j ACCEPT
ネットワーク環境設定の Algo VPN xx.xx.xx.xx IKEv2
から右下の詳細...
を選び、プロキシ
タグで Web プロキシに tmdisk-aws.local
と 3128
をセットする。
メモ
macOS 10.14.6 のバグ
私の手元の macOS 10.14.6 では、 macOS Mojave 10.14.6 追加アップデート
を適用するまではすぐに TimeMachine がエラーになっていた。SMB 周りの修正が入っていたので関連するのかもしれない。
また、まだ私の環境では成功を確認できていない。回線が細すぎるせいか バックアップを準備中…
から先へ進むのにかなり時間がかかっているため。
遅い回線ではバックアップに失敗する
ある程度以上の回線速度(例えば 50Mbps 以上?)でないと失敗する。
まとめ
IPSec による VPN 接続と netatalk、それに ZFS の機能をフルに生かしたディスク構成により、クラウド事業者にメモリを直接覗かれない限り安全に TimeMachine バックアップを可逆的にクラウド上に置くことができた(理論上は)。
-
Apple はむしろ SMB へスイッチしたがっているようだ。 ↩
-
また、有効にするととんでもなくメモリを喰うという弊害もある。 ↩
-
重ねがけについては、確か「暗号技術入門」かなにかに記述があった。 ↩