macOS: TimeMachine バックアップを AWS 上に保全する

将来、**家に隕石が落ちてきた時の最後の砦**として、自分の PC にある日々の運用データ一式をクラウド上に保全して枕を高くして寝たい。また、**超強力な太陽嵐**が地球上に降り注いでも、データの整合性は可能な限り担保したい。

そんな夢を見たのでどこまで実現可能かを考えてみた。思考実験以上、運用実験未満。

### 現況

今のところ（高速回線環境がないので）最終的なバックアップ完了までこぎ着けていない。よって復元も試せていない。試行レポートをお待ちしています。

ゴール
------

[AWS](https://aws.amazon.com/) 上に macOS の `TimeMachine` 機能を使ってデータを保管したい。また、手元の Mac のディスクが壊れた時のリカバリー元としても使えるようにしたい。可能な限り高い信頼性を確保し、できるだけセキュアに。

## 要件

### ネットワーク

- 通信経路をセキュアに保つこと
- macOS の標準機能だけで VPN 接続を確立できること

### データ

- データは自分で暗号化・復号できること
- 信頼性の高い（ビット反転等に強い）ディスク環境を構築すること
- 可能な限り高い冗長性を確保できる（コントロールできる）こと
- バックアップだけでなく、リカバリモードで起動した Mac から OS の復元ができること

## 方針

* 特別な機器（ルータ等）を利用せず、ソフトウェアのみで実現する。
* 信頼性の高いツールを組み合わせて実現する。がんばりすぎない。
* サーバは１インスタンスで済ませる。

## ソリューション

検討した結果、ソリューションとして以下のソフトウェアを組み合わせることとした。

### VPN 環境: Algo

[Algo](https://github.com/trailofbits/algo) は、クラウドサービス上に IPSec （最近では WireGuard も可）を使った VPN インスタンスを簡単に構築できるツール。PC の設定ファイルも生成してくれるので便利。実行すると EC2 のインスタンスを立ててネットワークの設定まですべて行ってくれる。

OS は [Ubuntu](https://ubuntu.com/) の最新版がインストールされる。

### ファイルシステム: ZFS

みんな大好き [ZFS](https://ja.wikipedia.org/wiki/ZFS) には同一データの[複数コピーを保持する機能](https://docs.oracle.com/cd/E19253-01/819-6260/gevpg/)がある。複数のコピーがあると、ビット反転（腐敗）などが起きた時にメンテナンスコマンドにより整合性を回復できる。

またソフトウェア的に RAID を組める機能を利用し、ディスクのハードウェアエラーや操作ミス等もカバーする。

### データ暗号化: LUKS によるディスク暗号化

ディスクの暗号化機能は ZFS にも搭載されているが、Linux 上での安定性や可用性を考えて今回はこちらとした。

（他の方法との比較についてはのちほど論じる）

### ディスクデバイス: Amazon EBS

ディスク系のソリューションとなると [EFS](https://aws.amazon.com/jp/efs/) も候補に挙がるが、（AWS に任せず）自前で暗号化したいとなるとブロックデバイスである [EBS](https://aws.amazon.com/jp/ebs/) 一択となる。ただし、コストパフォーマンスを高めるために[タイプ](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html)は低速だが低コストの `Cold HDD (sc1)` を指定する。ネット越しのバックアップはネットワーク速度がボトルネックとなるのでディスク自体は低速でもまったく構わない。

また、必要に応じて EBS ボリュームの[スナップショット](https://aws.amazon.com/jp/ebs/features/?nc=sn&loc=1#Amazon_EBS_Snapshots)を定期的に取得してもよいだろう（ただし費用はけっこうかかる）。

### 通信プロトコル: netatalk

TimeMachine は AFP プロトコルで動作するので [netatalk](https://ja.wikipedia.org/wiki/Netatalk) サーバを稼働させる。なお現代では SMB (CIFS) でも可能なので [samba](https://www.samba.org/) を利用してもよさそうだ[^1]。 

## Algo のセットアップ

Algo のセットアップは詳しい記事がネットにたくさんあるので省略。いくつかポイントだけ。

### iptables の設定を追加

netatalk のためにポートを開放する。任意のプロトコルを通す設定を公式に追加する方法がわからないので、次のようなパッチを当ててごまかすこととした。

```sh
vi iptables.patch
```

```diff
--- 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
```

```sh
patch < iptables.patch
```

### 設定ファイルの書き換え

`config.cfg` を編集する。

#### IAM 作成と権限設定

専用の IAM を作成しておくとよい。[インストラクション](https://github.com/trailofbits/algo/blob/master/docs/cloud-amazon-ec2.md)にあるように、権限の欄に指定された [JSON](https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md#minimum-required-iam-permissions-for-deployment) を記述すること。

#### Python

python3 が必要。macOS には付属しないので brew 等で用意する。詳しくは algo の README を参照。3.8 ではエラーが出たので 3.7 推奨。

#### ユーザリスト

デフォルトで users が３つ指定されているが１つあればいいので、適当に減らしたりリネームする。`admin` とでもしておく。

#### インスタンスタイプ

2019 年は `t2.nano` を指定できたが、2020 年はメモリ 512 MB の環境では `cryptsetup` がメモリ不足でディスクの復号に失敗するようになっているので注意。安いインスタンスで動かせていたのに残念だ。`t2.micro` 以上にしておこう。ちなみに swap 領域を用意してもダメだった。

（なお、`t3` ファミリーでの起動も試したが現時点ではうまくいかなかった。おそらく [ENA](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/enhanced-networking-ena.html) 周りが関係するのだろう）

### 生成されたデータのバックアップ

algo コマンドによるセットアップ後に、`configs` 以下に秘密鍵や諸々のファイルが生成される。これらを別マシン/メディアへ安全に保全すること。もし可能ならバイト列を[石版](https://naga0001.at.webry.info/201805/article_5.html)に刻んで対爆仕様の金庫に放り込み、世界中に分散設置する。衛星に乗せるのもいいだろう。

インスタンスの立ち上げ
----------------------

実際に algo を実行しインスタンスを作成する。日本在住者なら `ap-northeast` リージョンを選択するとよい。

```sh
./algo
```

AWS コンソールから、自動作成された EC2 インスタンスの`削除保護`を有効にし、`シャットダウン動作`を「停止」に変更しておこう。

無事に作成されたら ssh でログインする。

```sh
ssh -i configs/algo.pem ubuntu@xx.xx.xx.xx
sudo su -
```

## ネットワークとマシンの準備

最新版にアップグレードしておく。

```sh
apt-get update
apt-get upgrade -y
```

必要なライブラリを入れる。

```sh
apt install -y cryptsetup zfsutils-linux netatalk
```

## ディスク領域の準備

### 方針

ZFS の機能を使って RAID を組む。

可能なら raidz2 オプションを採用し RAID6 を組みたいが、EBS (`Cold HDD (sc1)`) を４台も使うと最低でも $60/month がかかる。ディスク構成はあとから変更できるので、まずは２台で構築してみる。

なお EBS 自体の暗号化機能は使用しない。

### セットアップ

#### ディスク設計

今回は EBS ボリュームごとに ZFS のストレージプールを作成し、それらを raid として束ねることとした。

#### EBS ボリュームを用意する

EBS は EC2 インスタンスと同じアベイラビリティゾーンに作成すること。AWS コンソール上から EC2 インスタンスにアタッチする。

必要な数だけ作成する。ひとまず、タイプは sc1 で 500GB ものを２つ作り、それぞれタグの `Name` として `algo disk1` と `algo disk2` などとセットしておくとよいだろう。今回は EBS 暗号化は使わない。

通常なら /dev/sdf にアタッチされ、インスタンスからは `/dev/xvdf` として見えるはずだ。
２つめ以降は `/dev/xvdg`, `/dev/xvdh` ... などと振られる。

```sh
ls -lF /dev/xvd[f,g,h,i]
```

#### 暗号化ボリュームをセットアップする

次のようにして暗号化ボリュームをセットアップする。もちろん既存のファイルは全消去されるので注意すること。パラノイアなひとはすべて違うパスフレーズにするのも良いかもしれない。

```sh
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`  ... としてマウントする（名前は任意）。

```sh
cryptsetup luksOpen /dev/xvdf tmdisk1
cryptsetup luksOpen /dev/xvdg tmdisk2
...
```

#### ZFS プールに登録する

ここでは２台なので `mirror` を指定する。３台なら `raidz1`、４台なら `raidz2` ... というように設定できる。

```sh
zpool create -o ashift=12 -O normalization=formD tmdisk mirror \
	/dev/mapper/tmdisk1 \
	/dev/mapper/tmdisk2
```

状態を確認する。

```sh
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
```

#### ファイルシステムオプション

いくつか有用なオプションを付けておく。

```sh
zfs set compression=on tmdisk    # ディスク圧縮
zfs set copies=2 tmdisk    # 複数のコピーを保持
```

ここでは `copies=2` を設定している。もちろんそれだけ容量（と書き込み時間）を消費するので注意すること。ちなみに指定できる最大値は 3 となっている。

以下はお好みで。

```sh
zfs set atime=off tmdisk    # noatime 相当（ディスクのアクセス速度を上げたい）
zfs set relatime=on tmdisk    # relatime 相当（バランスがよさそうなので）
zfs set exec=off tmdisk    # noexec 相当（バイナリの実行を禁止。安全性を上げたい）
```

現在のオプション値を確認することもできる。

```sh
zfs get all tmdisk
```

なお、冗長性を確保するという目的のため、当然ながら `重複除去機能` は使用しない[^2]。

#### 書き込みテスト

ここでは 1GB のディスクで `copies=2` を指定しているので、圧縮が効かない場合にはせいぜい 500MB 程度しか書き込めないはずである。確かめる。

```sh
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` とした。

```sh
useradd -d /home/tmuser -g backup -m -s /usr/sbin/nologin -u 340 tmuser
passwd tmuser    # これが macOS からアクセスするときのパスワードとなる
```

ディスクの所有権を調整しておく。

```sh
chown -R tmuser:backup /tmdisk
chmod 750 /tmdisk    # あるいは 700 か
```

## netatalk

`/tmdisk` ボリュームを netatalk に登録する。`TMDisk` の名前は任意。macOS 上で見えるディスク名となる。

```sh
vi /etc/netatalk/AppleVolumes.default
```

```
/tmdisk "TMDisk" allow:tmuser cnidscheme:dbd options:usedots,upriv,tm
```

もしユーザ名を変えた場合は `tmuser` の部分を変更するのを忘れないように。また、デフォルトとして有効になっている `~/` の行が必要なければオフにしてもよいだろう。

```sh
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 以降だと次のようにする必要があるようだ。

```sh
vi /etc/netatalk/afp.conf
```

```
[TMDisk]
path = /tmdisk
time machine = yes
force user = tmuser
```

起動する。

```sh
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` に例えば次のように書いておくとよい。

```sh
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` が追加されているはずなのでこれを選び、「接続」をクリックする。

正しく接続されているかどうか確認する。

```sh
ping tmdisk-aws.local
```

### マウントする

Finder の「サーバへ接続 (cmd+K)」へアドレスを入れて接続する。

`afp://tmuser@tmdisk-aws.local/TMDisk`

試しになにかファイルを書き込んでみると次のように反映されるはずだ。

```sh
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 ボリュームを４つ用意し ZFS の `raidz2` を指定して RAID6 相当を組むと万全だろう。

ただ、（物理ディスクとは違って）EC2 上の仮想ディスクが壊れる可能性は相対的にかなり低いと思われるので、ヒューマンエラーを加味した落としどころとしては、３台で `raidz1` の RAID5 構成で十分だとは思う。

### 定期的なディスクのチェック

通常は `/etc/cron.d/zfsutils-linux` で定期的に `zpool scrub` コマンドが実行され、不整合は自動修復されるはずだ。デフォルトで月に１度だが、もう少し頻度を上げてもいいかもしれない。

### EBS スナップショットの定期作成

費用に余裕があれば、１日それぞれひとつのスナップショットを撮っておくとさらに安心だろう。ただし、かなり費用がかかる富豪的手法ではある。

仮に４つの 1TB ディスクを使用し、ディスクの使用量を各 50% とし、１日にひとつ、５つのスナップショットを保持した場合、現状では $500/月程度かかるだろう。

### OS 環境

細かいこといろいろ。

#### ホスト名

```sh
hostnamectl set-hostname tmdisk1.example.com
```

#### タイムゾーン

Algo デフォルトでは UTC なので JST にしておく。

```sh
timedatectl set-timezone Asia/Tokyo
```

#### 時刻同期

ntp を起動しておく。

```sh
apt-get install -y chrony
```

### IP 制限

可能なら、EC2 (VPC) のセキュリティグループで自宅/自社からのアクセスのみに制限しておこう。
ssh は `hosts.allow` へ記述する方法もある。

念のため netatalk も  `hosts.allow` で制限したいなら次のように書ける。

```
afpd : 10.19.48.1 : allow
afpd : ALL : deny
```

## 運用サイクル

一度セットアップすれば次のようなフローになる。

### OS 起動時

暗号化ディスクをマウントする。複数のディスクを使う場合は繰り返す。

```sh
cryptsetup luksOpen /dev/xvdf tmdisk1
cryptsetup luksOpen /dev/xvdg tmdisk2
...
```

ZFS 領域をマウントする。

```sh
zpool import tmdisk
zpool list -v
```

もしマウントされていなかったら手動で。

```sh
zfs mount tmdisk
```

### OS シャットダウン時

ふつうにマシンを shutdown/reboot しても問題ない。あえて手順を踏むなら次のようになる。

ZFS 領域をアンマウントする。

```sh
zfs unmount tmdisk
zpool export tmdisk
test -e /tmdisk && rmdir /tmdisk    # 次回マウント時に失敗するので消しておく
```

ディスクの暗号化を解除する。

```sh
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 上もそれに[合わせておく](https://docs.oracle.com/cd/E26924_01/html/E25824/gazss.html#gftgr)ほうが安全かもしれない。また、SMB を使う際にはまた[話が変わってくるようだ](https://decomo.info/wiki/blog/2018/2018-07-03)。

```sh
zfs set casesensitivity=caseinsensitive tmdisk
```

### ファイル名の UTF8 正規化問題

macOS のファイル名正規化は NFD なので、ZFS create 時に `-O normalization=formD` などと指定できるようだ。macOS に準拠させたいならどうすべきか。あるいは netatalk が差違を吸収してくれると期待してよいものか。

### クラウドからの OS リカバリは成功するのか

理論上は成功しそうだが、試せていない。実効的な速度が出るものなのかも不明。

### 回線速度

うちの **ADSL 12M** という化石のような回線だと、計算上、初回のフルバックアップに４０日ほどかかるのが難点といえば難点だ。少なくとも初回実行時は高速かつ従量課金されないネットワーク環境を用意したい。

発展
----

### 複数のサーバで別の TimeMachine バックアップを運用する

マルチ AZ 化やマルチリージョン化を試すと冗長性が高まってより安心か。

### 他のクラウドサービス上でも構築する

もし [Google Cloud](https://cloud.google.com) でも同等のシステムを構築できれば、マルチクラウド化して一段レベルの高い冗長性を確保できそうだ。 

### コールドストレージとして

定期バックアップするときだけサーバを立ち上げればセキュリティ上も安心な上に費用も安く上がる。例えば週に一度だけ [AWS Lambda](https://aws.amazon.com/jp/lambda/) 等で自動でインスタンスが立ち上がるようにしておく手もありそうだが、結局 cryptsetup へパスフレーズを渡さねばならない問題は残る（[AWS KMS](https://aws.amazon.com/jp/kms/) 等を活用できるかもしれない）。

検討事項
--------

### どの段階でデータを暗号化するか

今回は LUKS を用いたが、いくつかの方針が考えられる。

#### TimeMachine に暗号化させる

そもそも TimeMachine に「[バックアップを暗号化](https://support.apple.com/ja-jp/guide/mac-help/mh21241/mac)」するオプションがあるのでこれを使用する。ただし、サーバ上で実ファイルが見られなくなってしまうのが難点だが、お手軽確実ではある。

また、ディスクをマウントする時にパスフレーズを打たなくて済むので、サーバの自動起動・シャットダウンをプログラマブルにコントロールできるのは魅力だ。

#### LUKS で暗号化する

今回説明した方法。サーバ上から実ファイルが参照できるので、便利な場面は多いだろう。反面、サーバを稼働中にクラックされた場合の秘匿性に劣る。

とはいえ将来的に（プロプライエタリ製品である） macOS が使用できない環境を想定するのであれば、妥当なソリューションだと言えるだろう。

#### ZFS で暗号化する

未検証。使用する弊害はなさそうなので、LUKS の代わりにこちらを使用してもよさそうだ。

#### EBS で暗号化する

AWS に暗号化キーを預けることになるので用途にそぐわない。他の方法に比べてとくにアドバンテージがあるとも思えないので忘れてよいだろう。

#### 複数の手段で暗号化する

基本的に暗号の重ねがけは速度的にもセキュリティ強度的にも悪手[^3]ではある。個人的には上記のうちどれかを単体で採用すべきだと考えている。

おまけ
------

### HTTP プロキシを立てる

VPN 接続をしているとウェブに繋がらなくなるので、一時しのぎとしてインスタンス上で `Squid` を動かすことにする。

```sh
apt install -y squid
```

`/etc/squid/squid.conf` へ追記する。

```
acl localnet src 10.19.48.1
http_access allow localnet
```

リロードする。

```sh
systemctl reload squid
```

ポートを開ける。

```sh
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 バックアップを可逆的にクラウド上に置くことができた（理論上は）。

[^1]: Apple はむしろ SMB へスイッチ[したがっているようだ](https://applech2.com/archives/20180818-time-machine-over-smb-support-nas.html)。
[^2]: また、有効にするととんでもなくメモリを喰うという弊害もある。
[^3]: 重ねがけについては、確か「暗号技術入門」かなにかに記述があった。