Cloudflare Tunnel をつくって TimeMachine の NAS バックアップ手段を SSH トンネルから移行

に公開

以前に SSH トンネルで TimeMachine のバックアップ保存先を外部ネットワークに配置してある NAS にする構築を行なった。しかし、速度の問題や、SSH トンネルの TCP over TCP の問題から、Cloudflare Tunnel 上で行えば良いのではないのかと気づいた。そこで、既存の自動マウントスクリプトを書き換えて Cloudflare Tunnel で繋ぐ快適でセキュアな TimeMachine 環境を整えた。

この記事のスコープ

書くこと:

  • セキュアな Cloudflare Tunnel の構築
  • Cloudflare Tunnel を使用した、仮想ディスクを自動マウントする仕組みの構築

書かないこと:

  • NAS・Samba 自体の構築
  • Launchd の plist ファイルの作成と実行手順

執筆時点の情報

項目 内容
執筆日 2026-03-28
クライアント OS macOS
NAS OS Ubuntu
使用ツール bash, lanuchd, Cloudflare Tunnel, TimeMachine, docker
想定読者 TimeMachine のバックアップ先を Cloudflare Tunnel を使用して外部 NAS にしたい人

手順

1. Cloudflare Tunnel を作る

Cloudflare の左サイドバーにある Tunnels を開いてトンネルを作成する。トンネル名は自由に決めてしまって問題ない。

トンネル名を入力して進むと、インストール手段が複数出てくる。今回はどの OS でも共通して動く Docker を使用する。しかし、実行環境における Docker の導入は省略する。Run tunnnel with Docker の下の docker run ~ コマンドをコピーして、Cloudflare Tunnel で繋ぎたい NAS の方で実行する。

しかし、このままだとバックグラウンドの実行ができないため、-dのオプションをつける。また、ドメインを指定してコンテナにアクセスできるようにするために、--network=hostのオプションを加える。

実行が完了すると、Cloudflare の接続ステータスが変化し、トンネルが正常に接続されましたと表示されるとトンネルの作成は完了だ。

2. アプリケーションルートを作る

Tunnel が作成されただけではまだ、接続できない。トンネルはあるが、正確な出口と入り方を定義しなければ使うことができない。

まず、Zero Trust からネットワークコネクタとすすみ、先ほど作成したトンネル名をクリックする。

上部にある公開されたアプリケーションルート公開されたアプリケーション ルートを追加するというボタンを押して、アプリケーションルートを作成する。ホスト名は任意の英数字でドメインは使用したいドメインを選択する。下部のサービスのところにあるタイプは、今回は Samba の通信を行うため、tcp を選択し、URL はlocalhost:445とする。ここまでできたら保存を押してアプリケーションルートの作成は完了。

3. サービストークンを作成する

まだこのままでは、誰もが通れてしまうような経路となってしまう。そこで、ポリシーを設定して自身しか通れないようにする。そのための手段として、今回はトークンを使用する。

先ほどのZero Trust から Access コントロールサービス資格情報 と進み、サービストークンを作成するというボタンを押す。

サービストークン名は任意の名前で、サービストークンの有効期間は各自の好みで設定する。無期限ではない方がセキュリティ面では良いが、有効期限が切れるとトークンの ID やクライアントシークレットを更新しなければいけないので面倒である。

サービストークンを作成するとクライアント IDクライアントシークレット が作成される。後ほど使用するため、これらをコピーしてメモに一時的に保存しておく。この画面から離れるとクライアントシークレットは消えてしまうので確実に保存しておくこと。もし、消えてしまったら、作成したサービストークンは削除して再度サービストークンを作成すること。

メモに書き留めたら、保存を押して、サービストークンの作成は完了。

4. ポリシーを作成する

先ほど作成したサービストークンを使用してポリシーを作成する。ポリシーを作成することで、トンネルに接続できる人を制限できるようになる。

Zero Trust から Access コントロールポリシー と進み、ポリシーを追加するというボタンを押す。

ポリシー名は任意の名前を入力するが、先ほどのアプリケーション名と同じ方がわかりやすくて良いだろう。アクションは Service Authに変更し、セッション時間はデフォルトの アプリケーション セッション タイムアウトと同じにする。ルールを追加するのところではセレクターをService Tokenにして、値のところに先ほど作成したトークンを選んで追加する。

ここまで終わったら保存を押してポリシーの作成は完了。

5. アプリケーションのポリシーを設定する

Zero Trust から Access コントロールアプリケーションとすすみ、アプリケーションを追加する というボタンを押す。すると、タイプの選択が出てくるため、セルフホストの選択するを押す。

アプリケーション名は任意の名前を入力する。セッション時間はデフォルトの 24 hours とする。そして、+ パブリックホスト名を追加というところを押して、サブドメインをアプリケーションルートの時に指定したサブドメインにする。ドメインは自身の保有しているドメインを選択する。つぎに、Access ポリシーのところで、規定のポリシーを選択というボタンを押して先ほど作成したポリシーを設定する。

ここまで終わったら次へのボタンを押す。この先はオプションの設定のため各自設定したい項目があれば設定する。今回は何も変更せずに次へ保存と進む。

これでセキュアな TCP のトンネルを作成することができた。

6. 仮想ディスクの自動マウントスクリプトを変更する

以前作成した SSh トンネルを使用した自動マウントスクリプトは以下で構築しているため、今回は変更点だけおさえる。

https://zenn.dev/yuito2742/articles/ca0edb19aaa9b7
https://zenn.dev/yuito2742/articles/fec1ef8bf4065a

6.1 最終的なスクリプト

tm_connect.sh
#!/bin/bash
fail=0
recovery=0

# 実際のホスト名は ~/.config/tm_connect.env に TM_SMB_HOSTNAME=your.hostname を記述
if [ -f "$HOME/.config/tm_connect.env" ]; then
    source "$HOME/.config/tm_connect.env"
fi

if [ -z "$TM_SMB_HOSTNAME" ]; then
    echo "Error: TM_SMB_HOSTNAME is not set. Please configure ~/.config/tm_connect.env"
    exit 1
fi

if [ -z "$CF_TOKEN_ID" ] || [ -z "$CF_TOKEN_SECRET" ]; then
    echo "Error: CF_TOKEN_ID or CF_TOKEN_SECRET is not set. Please configure ~/.config/tm_connect.env"
    exit 1
fi

CLOUDFLARED_CMD="/opt/homebrew/bin/cloudflared access tcp --hostname $TM_SMB_HOSTNAME --url localhost:10445 --service-token-id $CF_TOKEN_ID --service-token-secret $CF_TOKEN_SECRET"
HDIUTIL_LOG="/tmp/tm_hdiutil.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

while true
do
    # 1. cloudflared トンネルの生存確認
    if ! pgrep -f "/opt/homebrew/bin/cloudflared access tcp" > /dev/null; then
        # トンネルが死んでいる場合、残骸のマウントポイントを強制破棄
        if [ -d /Volumes/TMBackup ]; then
            log "Tunnel is dead. Force detaching virtual disk..."
            hdiutil detach /Volumes/TMBackup -force > /dev/null
        fi
        if [ -d /Volumes/TimeMachine ]; then
            log "detaching nas disk"
            diskutil unmount force /Volumes/TimeMachine > /dev/null
        fi

        # cloudflared トンネルの再作成
        $CLOUDFLARED_CMD &
        log "create cloudflared tunnel"
        sleep 5
    fi

    # 2. NAS ディスクのマウント
    if [ ! -d /Volumes/TimeMachine ]; then
        log "nas disk mounting"
        # osascript はフォアグラウンドで実行され、完了するまで待機する
        osascript -e 'mount volume "smb://localhost:10445/TimeMachine"'
        log "nas disk mounted"
    else
        # 3. 仮想ディスク ( sparsebundle ) のマウント
        if [ ! -d /Volumes/TMBackup ]; then
            log "virtual disk mounting"
            # 修復 ( fsck ) が走る場合はここでブロックして待機する、verbose で詳細をログに記録
            hdiutil attach -verbose /Volumes/TimeMachine/MacBackup.sparsebundle > "$HDIUTIL_LOG" 2>&1

            # マウント成否の確認
            if [ -d /Volumes/TMBackup ]; then
                log "virtual disk mounted"
                fail=0
            else
                log "virtual disk mount failed"
                fail=$((fail + 1))
            fi
        else
            fail=0
        fi
    fi

    if [ "$recovery" -gt "5" ]; then
        log "recovery virtual disk"
        hdiutil attach -verbose -readonly -noverify -noautofsck /Volumes/TimeMachine/MacBackup.sparsebundle >> "$HDIUTIL_LOG" 2>&1
        hdiutil detach /Volumes/TMBackup
        fail=0
        recovery=0
    fi

    # 4. 連続失敗時の自己回復ロジック
    if [ "$fail" -gt "5" ]; then
        log "Too many mount failures. Killing cloudflared to reset connection."
        pkill -f "/opt/homebrew/bin/cloudflared access tcp"
        fail=0
        recovery=$((recovery + 1))
    fi

    sleep 1
done

6.2 変更点

ホスト名や、Token の ID とクライアントシークレットは変数の中に保管する。このようなソースコードを共有するときなどにセンシティブなところを隠すことができる。

# 実際のホスト名は ~/.config/tm_connect.env に TM_SMB_HOSTNAME=your.hostname を記述
if [ -f "$HOME/.config/tm_connect.env" ]; then
    source "$HOME/.config/tm_connect.env"
fi

if [ -z "$TM_SMB_HOSTNAME" ]; then
    echo "Error: TM_SMB_HOSTNAME is not set. Please configure ~/.config/tm_connect.env"
    exit 1
fi

if [ -z "$CF_TOKEN_ID" ] || [ -z "$CF_TOKEN_SECRET" ]; then
    echo "Error: CF_TOKEN_ID or CF_TOKEN_SECRET is not set. Please configure ~/.config/tm_connect.env"
    exit 1
fi

トンネルに接続するときのコマンド。変数を使うことや、長いことからコマンド自体も変数として扱うようにしている。

CLOUDFLARED_CMD="/opt/homebrew/bin/cloudflared access tcp --hostname $TM_SMB_HOSTNAME --url localhost:10445 --service-token-id $CF_TOKEN_ID --service-token-secret $CF_TOKEN_SECRET"

標準出力のログには時刻をつけるようにして、いつのログなのかをわかりやすくした。

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

ssh から Cloudflare Tunnel に変更したため、プロセスが稼働しているかの確認や接続のコマンドは変更してある。

if ! pgrep -f "/opt/homebrew/bin/cloudflared access tcp" > /dev/null; then
# cloudflared トンネルの再作成
        $CLOUDFLARED_CMD &
        log "create cloudflared tunnel"

仮想ディスクの接続時の状態がブラックボックスだったため、--verboseで経過をログファイルに出力するように変更した。また、自己修復のプログラムのところで、あまりにも接続できない場合は、--readonlyで接続するようにしてから、detach して接続を再度試みるようにした。

HDIUTIL_LOG="/tmp/tm_hdiutil.log"
hdiutil attach -verbose /Volumes/TimeMachine/MacBackup.sparsebundle > "$HDIUTIL_LOG" 2>&1
if [ "$recovery" -gt "5" ]; then
        log "recovery virtual disk"
        hdiutil attach -verbose -readonly -noverify -noautofsck /Volumes/TimeMachine/MacBackup.sparsebundle >> "$HDIUTIL_LOG" 2>&1
        hdiutil detach /Volumes/TMBackup
        fail=0
        recovery=0
    fi

    # 4. 連続失敗時の自己回復ロジック
    if [ "$fail" -gt "5" ]; then
        log "Too many mount failures. Killing cloudflared to reset connection."
        pkill -f "/opt/homebrew/bin/cloudflared access tcp"
        fail=0
        recovery=$((recovery + 1))
    fi

直面した課題・エラー内容

launchd から cloudflared コマンドが見つからない

エラー:

/Users/test/develop/bin/tm_connect.sh: line XX: cloudflared: command not found

解決方法: cloudflared をフルパスで指定する。

tm_connect.sh
CLOUDFLARED_CMD="/opt/homebrew/bin/cloudflared access tcp ..."

原因: launchd は shell のログイン時の PATH を引き継がない。ターミナルから手動実行すると通るコマンドでも、launchd 経由では /usr/bin など最低限のパスしか参照されないため、Homebrew でインストールしたコマンドは見つからない。


launchctl list で exit code 78 が返る

エラー:

launchctl list | grep com.test.automount
- 78  com.test.automount

解決方法: スクリプトに実行権限を付与する。

chmod +x ~/develop/bin/tm_connect.sh

原因: スクリプトに実行ビット(-rwxr-xr-x)がついていなかった。launchd がスクリプトを直接実行しようとして EACCES で失敗し、exit code 78(EX_CONFIG)が返っていた。


スクリプトを書き換えたのに古い動作のままになる

症状: SSH から cloudflared に書き換えたはずなのに、ログに create ssh tunnel と出続ける。

解決方法: launchd を reload する。

launchctl unload ~/Library/LaunchAgents/com.tset.automount.plist
launchctl load ~/Library/LaunchAgents/com.test.automount.plist

原因: launchd はスクリプトを起動時にキャッシュするため、plist や参照しているスクリプトを変更しても、reload しない限り古い内容で動き続ける。


最後に。

SSH トンネルから Cloudflare Tunnel への移行は、スクリプトの変更量自体は少ないが、launchd 特有のはまりどころがいくつかあった。特に PATH の問題は、ターミナルでは動くのに launchd では動かないという状況が起きるため、launchd 経由で動かすコマンドは常にフルパスで書く癖をつけておくと良い。

Cloudflare Tunnel に移行したことで、ポート開放や SSH 鍵の管理が不要になった。サービストークンによるアクセス制御を Cloudflare 側で一元管理できるようになり、鍵ファイルをマシンに置く必要もなくなった。また、TCP over TCP の問題が起きる SSH トンネルと異なり、Cloudflare のネットワークを経由することでその問題が解消された。

インフラの管理コストを Cloudflare に委ねることで、自分が管理しなければいけない部分が減り、よりシンプルな構成になった。

Discussion