<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>バックアップ on Off-Grid</title><link>https://offgrid.taikiito.com/tags/%E3%83%90%E3%83%83%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97/</link><description>Recent content in バックアップ on Off-Grid</description><generator>Hugo</generator><language>ja-JP</language><lastBuildDate>Mon, 05 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://offgrid.taikiito.com/tags/%E3%83%90%E3%83%83%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97/index.xml" rel="self" type="application/rss+xml"/><item><title>バックアップを「消されても戻せる」形にする</title><link>https://offgrid.taikiito.com/guides/backup-ransomware-resistant/</link><pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate><guid>https://offgrid.taikiito.com/guides/backup-ransomware-resistant/</guid><description>毎晩クラウドへバックアップしていたが、サーバーが乗っ取られたらバックアップごと消されると気づいた。鍵の権限削減・削除が伝播しない複製バケット・サーバーの外で動く見張りの3層で、ハードウェアを増やさずに作り直した記録。踏んだ落とし穴も全部書いた。</description><content:encoded><![CDATA[<p>写真・パスワード・ファイルを自分のサーバーに集めたあと、毎晩クラウドストレージ(Backblaze B2)へバックアップを取る仕組みを作って安心していた。ところが鍵の権限を実際に調べてみたら、<strong>サーバーを乗っ取られた時点でバックアップも全部消せる状態</strong>だった。それを作り直した記録。</p>
<h2 id="前提">前提</h2>
<ul>
<li>常時起動しているLinux環境と、そこから<code>rclone</code>(いろいろなクラウドストレージへファイルを同期するコマンド)で日次バックアップを取っている状態</li>
<li>バックアップ先はBackblaze B2(安価なオブジェクトストレージ。1GBあたり月$0.005前後)</li>
<li>構築の土台は<a href="/guides/self-hosting-basics/">自分の「サーバー」を持つとはどういうことか</a></li>
</ul>
<h2 id="何が問題だったか">何が問題だったか</h2>
<p>心配していたのは<strong>ランサムウェア</strong>(侵入したうえでファイルを暗号化し、戻す鍵と引き換えに金銭を要求する攻撃)のような、サーバーを乗っ取られるケース。</p>
<p>「B2側に30日間のバージョン保持(上書き・削除された古い版を30日間取っておく設定)があるから、仮に荒らされても戻せる」と考えていた。これが成立していなかった。</p>
<p>サーバーに置いていたB2のアプリケーションキー(APIから操作するための鍵。権限を選んで発行できる)の権限を一覧してみると、18個の権限が付いていた。問題はこの3つ。</p>
<ul>
<li><code>deleteFiles</code> — ファイルの版そのものを完全削除できる</li>
<li><code>writeBucketLifecycleRules</code> — ライフサイクルルール(古い版をいつ消すかの設定)を書き換えられる。「0日で消す」に変えれば30日の保持は即座に無意味になる</li>
<li><code>writeBucketReplications</code> — 複製ルールを止められる</li>
</ul>
<p>つまり<strong>鍵を1本盗られたら、バックアップ本体も、それを守るはずの設定も、まとめて壊せる</strong>。実際に同期スクリプトが使っていたのは<code>listBuckets, listFiles, readFiles, writeFiles, deleteFiles</code>の5つだけで、残りは余分だった。</p>
<p>もう1つ、根本的な制約を自分に課した。<strong>「予備のハードウェアをもう1台持つ」案は採らない</strong>。将来は拠点を移して身軽に暮らしたいと思っているので、機械を持ち続ける前提の設計にはしたくなかった。ハードウェア不要・手作業ゼロで回る形に限定して考えた結果、3層構成になった。</p>
<h2 id="層0-鍵の権限を必要最小限にする">層0: 鍵の権限を必要最小限にする</h2>
<p>まずサーバー上の鍵を、実際に使う5権限ちょうどの新しい鍵に差し替えた。</p>
<p><strong>ここで分かった制約: B2のWeb管理画面の鍵発行UIでは権限を細かく選べない</strong>。「読み書き」のような粗い単位しかないので、<code>b2</code>コマンド(Backblaze公式のCLI = コマンドライン操作ツール)が必須になる。全権限を持つマスター鍵を扱う作業なので、サーバー上ではなく手元のPCから実行した。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">b2 account authorize
</span></span><span class="line"><span class="cl">b2 key create --bucket &lt;バケット名&gt; &lt;鍵の名前&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  listBuckets,listFiles,readFiles,writeFiles,deleteFiles
</span></span><span class="line"><span class="cl">b2 key list --long     <span class="c1"># 権限が5つちょうどか確認</span>
</span></span><span class="line"><span class="cl">b2 key delete &lt;旧鍵のID&gt;
</span></span></code></pre></div><ul>
<li><strong><code>b2 account authorize</code>と<code>b2 key create</code>の出力は、どこにも貼り付けない</strong>。鍵の本体(パスワード相当の文字列)が含まれる。自分はこの作業中にターミナルの出力をそのままAIチャットに貼ってしまい、鍵が会話ログに残った。同日中にマスター鍵の再発行と露出した鍵の削除で無効化したので実害はなかったが、<strong>確認は<code>b2 key list --long</code>で行う</strong>のが正しい(鍵のIDと権限しか出さず、本体は出ない)。</li>
<li><strong>鍵の状態確認はWeb管理画面を信じない</strong>。削除済みの鍵が残って見えたり、新規作成した鍵が出てこなかったりした。<code>b2 key list</code>を正とする。</li>
</ul>
<p>層0は次の層1と<strong>セット</strong>。層0をやらずに複製ルールだけ作っても、<code>writeBucketReplications</code>を持った鍵でルールごと止められてしまう。</p>
<h2 id="層1-削除が伝播しない複製先を作る">層1: 削除が伝播しない複製先を作る</h2>
<p>B2には<strong>Cloud Replication</strong>という、バケット(ストレージ上のフォルダのような入れ物)の内容を別のバケットへ自動複製する機能がある。決定的な仕様はこれ。</p>
<blockquote>
<p><strong>削除と hideマーカー(B2が「削除された」印として置く目印)は複製されない。</strong></p>
</blockquote>
<p>つまり複製先は、構造的に<strong>append-only</strong>(追加だけができて削除が伝わらない)なミラーになる。元のバケットでファイルが消されても、複製先には残り続ける。これがランサムウェア対策の本体になる。</p>
<p>同一アカウント内の別バケットへの複製で十分と判断した。理由は、<strong>サーバー上の鍵は元のバケットに限定されているので、別バケットには構造的に届かない</strong>から。別アカウントが必須になるのは「別リージョンへ複製する場合」だけで、同一アカウント・同一リージョンの別バケットへの複製は普通にできる。別アカウント構成と差が出るのは「B2のログイン自体を乗っ取られた場合」と「リージョン障害」の2つだけで、前者は後述の2段階認証のほうが本質的な対策になる。</p>
<p>複製元を読む鍵・複製先に書く鍵は<strong>B2が自動で2本発行し、どちらもB2の内部にしか存在しない</strong>(サーバー上に置かれない)。侵害経路にならないのがこの構成の強み。</p>
<p>複製先のライフサイクル設定でUIに落とし穴が2つあった。</p>
<ul>
<li>🔴 <strong>「Keep only the last version」(最新の版だけ残す)を選んではいけない</strong>。上書き攻撃を受けた瞬間に正常な旧版が消え、複製先を用意した意味がなくなる。必ず<code>Use custom lifecycle rules</code>を選ぶ。</li>
<li>🔴 <strong><code>Days Till Hide</code>は必ず空にする</strong>。ここに数字を入れると、現役のバックアップ本体が自動削除される。埋めるのは「古い版を何日後に消すか」の1箇所だけ。</li>
</ul>
<p>自分は<strong>古い版を180日で削除</strong>に設定した。毎晩上書きされるデータベースのダンプ(写真サーバー188MB + ファイルサーバー18MB + 認証サーバー13MB ≒ 日220MB)が積み上がって年80GB増える計算だったため。180日なら約40GBで頭打ちになる。<strong>保護が弱まるわけではない</strong>——削除は複製されないので消されたファイルには無関係で、上書き攻撃は次の層2が1週間以内に検知する。</p>
<p>複製元141.5GB・118,503ファイルに対して、複製先は149.0GB・118,976ファイルで完了。<strong>複製先のほうが大きいのは設計どおり</strong>(元で消えた・上書きされたファイルが残り続けるので膨らむ)。初回コピーは数日かかった。</p>
<ul>
<li><strong>初回コピーの進捗はAPIでは測れない</strong>。B2が複製の状態フラグを付けるのはルール作成後に新規アップロードされたファイルだけで、既存ファイルの後追いコピー分は全件「未設定」のまま。進捗はWeb管理画面の複製先バケットのファイル数で見るしかない。</li>
<li><strong>初回コピー完了を待つ必要はない</strong>(層2の判定は「減った/増えていない」なのでコピー中に誤報は出ない)。ただし<strong>ライフサイクル設定は初回コピー完了まで触らない</strong>——コピー中の変更は直後のファイルが消える危険があると公式ドキュメントに明記されている。</li>
<li>コストは複製分で<strong>月$0.70</strong>。複製の転送料は無料。</li>
</ul>
<h2 id="層2-サーバーの外に見張りを置く">層2: サーバーの外に見張りを置く</h2>
<p>複製が本当に続いているかを誰も見ていなければ、仕組みは静かに壊れる。そこで<strong>デッドマンスイッチ</strong>(定期的な生存信号が途絶えたら警報を出す仕掛け)を用意した。</p>
<p>肝は<strong>サーバーの外で動かすこと</strong>。サーバー監視には別途Uptime Kumaを使っているが、これはサーバー自身の上で動いているので、<strong>サーバーごと死んだら自分の死を通報できない</strong>。</p>
<p>実装はGitHub上の非公開リポジトリ1つ + <strong>GitHub Actions</strong>(GitHubが提供する、決めた時刻やイベントでスクリプトを自動実行する仕組み)。週1回、複製先バケットを走査して、次のどれかに当たったらTelegramへ通報する。</p>
<ol>
<li>ファイル件数が前回より減った</li>
<li>合計サイズが前回比1%を超えて減った</li>
<li>21日間まったく増えていない</li>
<li>B2へ接続・認証できない</li>
</ol>
<p>観測値はリポジトリ内のJSONファイルにコミットして、次回の比較基準にする。外部ライブラリは使わず標準ライブラリだけでB2のAPIを直接叩く作りにした(依存が増えると、それ自体が壊れる原因になる)。見張り用の鍵は<strong>複製先バケット限定・読み取り専用</strong>なので、こちらはWeb画面の発行UIで作れる。</p>
<ul>
<li>件数の減少は「複製先を直接触られた」ことのシグナルとして意味が強い(削除は複製されないので、正常運用では減らない)。</li>
<li><strong>上書き破壊の検知は不完全</strong>だと正確に理解しておく。同サイズ・より大きいサイズで上書きされた場合は検知できない。ただしその場合も版管理で旧版が残るので復元は可能で、守りは版管理側が担っている。</li>
<li><strong>GitHub Actionsの定時実行は大幅に遅れる</strong>。<code>0 1 * * 1</code>(毎週月曜01:00 UTC)と書いたジョブが実際に走ったのは06:16 UTCで、<strong>5時間16分遅れ</strong>だった。<code>:00</code>は最も混雑する分なので、奇数分(<code>17 1 * * 1</code>など)にずらすと改善する。週次監視なら遅延自体は無害だが、<strong>時刻を当てにした説明を書かないこと</strong>。</li>
<li><strong>通報が空振りする状態で放置してはいけない</strong>。鍵をGitHubのSecrets(リポジトリに暗号化して保存する秘密情報)に登録する前に初回の定時実行が走り、認証情報が無くて失敗した。設計どおりの挙動だが、これを放置すると毎週失敗通知が来て、<strong>デッドマンスイッチの警報を無視する習慣がつく</strong>。それが一番の実害。</li>
</ul>
<h2 id="踏んだ落とし穴-鍵の差し替えが設定ファイルを作り直していた">踏んだ落とし穴: 「鍵の差し替え」が設定ファイルを作り直していた</h2>
<p>層0の鍵差し替えには、以前自分で書いた<code>rotate-b2-key.sh</code>というスクリプトを使った。これが<code>rclone</code>の設定ファイル(<code>rclone.conf</code>)を<strong>丸ごと作り直す</strong>作りになっていた。</p>
<p>同じ日の朝に、送信前に自前で暗号化するための設定ブロックをこのファイルに追加していた。スクリプトはそのブロックを知らないので、鍵の差し替え自体は成功したのに<strong>直後のバックアップが「設定ブロックが見つからない」で全滅</strong>した。</p>
<ul>
<li><strong>教訓: 構成に新しい設定ブロックを足したら、そのファイルを書き換える既存スクリプトを洗い出す</strong>。「鍵の差し替え」という名前から、設定ファイル全体を作り直しているとは読み取れなかった。</li>
<li>スクリプトは、丸ごと上書きをやめて該当2行だけを置換する形に直した。加えて<strong>書き込む前に「設定ブロックが減っていないか」「新しい鍵IDが入ったか」を検査し、1つでも失敗したら一切書かずに中止する</strong>ようにした。</li>
<li>🔴 <strong>もう1つの地雷</strong>: 設定ファイルは<code>root</code>用と一般ユーザー用の2箇所にあり、復旧を片方だけやって放置していた。バックアップは<code>root</code>で動くので「成功」と出続け、<strong>一般ユーザーで実行したときだけ静かに失敗する</strong>状態になっていた。</li>
<li><strong>教訓: 「両方必要」と書いた手順は、実施後に必ず両方を実測で確認する</strong>。<code>grep &quot;^\[&quot; &lt;設定ファイル1&gt; &lt;設定ファイル2&gt;</code>のようにセクション名だけ出すコマンドなら値が漏れないので、安心して結果を見られる。</li>
<li>おまけの罠として、<code>ssh サーバー 'bash -s' &lt;&lt;'EOF'</code>形式のヒアドキュメント(複数行のスクリプトを流し込む書き方)は<code>ssh</code>の標準入力と競合して出力が二重化し、成功したのか失敗したのか読めなくなった。<strong>サーバー操作は1行の<code>ssh サーバー '...'</code>に分解して1つずつ流す</strong>のが確実だった。</li>
</ul>
<h2 id="2段階認証が1段階だった">2段階認証が1段階だった</h2>
<p>作業中にもっと基本的な穴が見つかった。「2段階認証は済んでいる(ログインのたびにメールでコードが来る)」と認識していたが、これはBackblazeが既定で有効にしている<strong>メールMFA</strong>(多要素認証)だった。</p>
<p><strong>コードの送り先も、パスワードリセットの送り先も、同じメールアドレス</strong>。つまりメールボックス1つ取られると2要素が1要素に潰れる。同一アカウント構成を選んだ結果、このアカウントが全データの唯一のオフサイトコピーを持っているので、ここが設計上の最大の残留リスクだった。</p>
<p><strong>TOTP</strong>(認証アプリが30秒ごとに生成する6桁コード方式)に切り替えて解消した。</p>
<ul>
<li>認証アプリは<strong>Ente Auth のオフラインモード</strong>(アカウント登録なしで使える)を選んだ。Google Authenticatorは、自分がGoogleアカウントを削除する方針で同期が使えないこと、Google Playが載らない端末への移行を検討していることの2点で不適だった。アプリ登録が必要なものだと、復旧手段がまた別の会社のアカウントに依存して、断ち切った鎖を作り直すことになる。</li>
<li><strong>バックアップコードは必ず生成してパスワードマネージャーに保存する。ただしTOTPそのものは同じ金庫に入れない</strong>(同じ場所に入れたら1要素に戻る)。TOTPはスマートフォンのアプリに、バックアップコードはVaultwardenに置いた。</li>
</ul>
<h2 id="残る鶏と卵">残る「鶏と卵」</h2>
<p>この構成にも、原理的に閉じない循環が残っている。</p>
<ul>
<li>B2に入るには → TOTP(スマートフォン)</li>
<li>スマートフォンを失くしたら → バックアップコード(Vaultwarden)</li>
<li>VaultwardenはサーバーVPS上 → サーバーを失ったらB2から復元が必要 → B2に入る必要がある</li>
</ul>
<p><strong>スマートフォンとサーバーを同時に失うと詰む</strong>。対処は「バックアップコードを紙でもう1箇所持つ」しかない。これは<a href="/guides/1password-to-vaultwarden/">パスワードの移行の記事</a>で書いた、バックアップの取り出し鍵自体をどこに置くかという問題とまったく同じ構造になっている。<strong>自前で持つほど、最後の1本をどこに置くかという問題に必ず行き着く。</strong></p>
<h2 id="結果とやらなかったこと">結果と、やらなかったこと</h2>
<p>鍵の権限削減・append-onlyな複製先・サーバー外の見張りの3層が稼働状態になった。追加費用は複製分の月$0.70だけで、ハードウェアは1台も増えていない。</p>
<p>やらなかったことも書いておく。<strong>Backblazeというプロバイダ1社に依存している</strong>のは未対策のまま。アカウントの凍結や会社都合に備えるなら、別会社のストレージへもう1本(追記専用の設定で月€4前後)が答えになるが、今回は見送った。手を広げる前に、今ある3層が本当に回り続けるかを数ヶ月見ておきたかった。</p>
<p><strong>更新履歴</strong> — 2026-10-01: 初版</p>
]]></content:encoded></item><item><title>バックアップ先を2つから1つに減らした</title><link>https://offgrid.taikiito.com/guides/backup-consolidation/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0000</pubDate><guid>https://offgrid.taikiito.com/guides/backup-consolidation/</guid><description>非公開のGitリポジトリとクラウドストレージの2系統にバックアップしていたが、片方を捨てて1系統に絞った。2系統は「2倍安全」ではなく「壊れたら困るものが2つ」で、どちらも検証されていなかった。一本化に必要だった棚卸しの手順、復元に使う鍵をどこに置くか、実際にやった復元テストまでの記録。</description><content:encoded><![CDATA[<p>自分のサーバーに写真・パスワード・ファイルを集めたあと、バックアップを<strong>2系統</strong>持っていた。GitHubの非公開リポジトリ(自分だけが見られるGitの保管場所)への日次バックアップと、クラウドのオブジェクトストレージ(Backblaze B2)への日次バックアップ。多いほうが安全だと思っていた。</p>
<p>調べたら、どちらも完全ではなく、どちらも検証されていなかった。GitHub側を廃止して1系統に絞り、代わりに「本当に戻せるか」を確かめる作業を足した。</p>
<h2 id="前提">前提</h2>
<ul>
<li>常時起動しているLinuxサーバー1台に、パスワード管理・写真・ファイル置き場・認証を同居させた状態。土台は<a href="/guides/self-hosting-basics/">自分の「サーバー」を持つとはどういうことか</a></li>
<li>道具は<code>rclone</code>(いろいろなクラウドストレージへファイルを同期するコマンド)。送り先はBackblaze B2(安価なオブジェクトストレージ。1GBあたり月$0.006、最初の10GBは無料)</li>
<li>もう1系統は、作業用ディレクトリ一式を毎日<code>git commit</code>して非公開リポジトリへ<code>push</code>する自作の仕組み</li>
</ul>
<h2 id="2系統が実際に守っていたもの">2系統が実際に守っていたもの</h2>
<p>同じものを2箇所に置いているつもりだったが、並べたら役割が違っていた。GitHub側はテキスト類(会話ログ・設定・メモ)と添付ファイルで、強みは<strong>履歴</strong>——「このファイルは3週間前どう書かれていたか」を追える。B2側はデータベースのダンプ・写真ライブラリ・ファイル置き場の実データ・パスワードの保管庫で、強みは<strong>容量</strong>。つまり二重化ではなく<strong>分担</strong>で、片方だけでは静かに欠けが出る状態だった。</p>
<p>実際に測ったら、添付ファイルが<strong>B2に638件、GitHubに460件、重複ゼロ</strong>。時期で真っ二つに割れていた。<code>git clone</code>だけで戻せば638件が消え、B2だけで戻せば460件が消える。</p>
<h2 id="2倍安全ではなく壊れたら困るものが2つ">「2倍安全」ではなく「壊れたら困るものが2つ」</h2>
<p>分担になっていた理由は単純で、<strong>2つあると、どちらも真剣に見なくなる</strong>。</p>
<p>B2側の同期スクリプトのログを読んだら、<strong>34回の起動のうち完走は5回だけ</strong>だった。最後の処理で<code>sudo</code>がパスワードを求めて止まり、<code>set -e</code>(途中で失敗したらそこで中断する設定)で全体が終わっていた。データ本体の各工程は成功していたので実害はなかったが、問題はそこではない。<strong>「完走しました」のマーカーが1ヶ月出ていないのに、気づいていなかった。</strong></p>
<p>これが冗長なバックアップ経路の本当のコストだと思う。ディスク代でもなく手間でもなく、<strong>どちらも検証されないこと</strong>。「もう片方があるから」と思っている限り、片方が壊れても痛みが出ないので、気づく機会そのものが無くなる。</p>
<h2 id="githubがこの用途に向いていなかった理由">GitHubがこの用途に向いていなかった理由</h2>
<ol>
<li><strong>サイズと中身</strong>。Gitはテキストには強いが、<strong>画像のようなバイナリは版が増えるたびに丸ごと溜まる</strong>。<code>.git</code>(履歴の実体)が346MBあり、作業用ディレクトリ751MBの半分近くを占めていた(1年後には847MB)</li>
<li><strong>履歴は消せない</strong>。消さないことが価値の仕組みなので、間違って入れたものが残る。実際、秘密情報を含むディレクトリ(APIトークン類やサーバーの秘密鍵)が<strong>気づかないうちに毎日pushされ続けていた</strong>。「除外設定に入れてある」と自分のメモに書いてあったが、入っていなかった</li>
<li><strong>他人のアカウント方針の上に乗っている</strong>。容量制限も規約も将来どう変わるかは自分では決められない</li>
<li><strong>そもそも用途が違った</strong>。置いていたのは日々の記録で、ソースコードではない。全ての版を永久に取っておく必要がない</li>
</ol>
<p>一方で**「履歴が追える」価値は本物だった<strong>ので代わりを用意した。B2の</strong>バージョン保持**(上書き・削除された古い版を一定期間取っておく設定)で、自分のバケット(ストレージ上のフォルダのような入れ物)は旧バージョンを30日保持している。実際にデータベースのダンプは26世代遡れた。30日の「取り消し」が成立していればGitの履歴機能の大部分は代替できる。なお<strong>公開しているもの(ブログ・ポートフォリオ)はGitHubのままにしている</strong>。廃止したのは「バックアップ置き場として使うこと」だけ。</p>
<h2 id="一本化の本体は棚卸しだった">一本化の本体は棚卸しだった</h2>
<p>ここが一番再利用できる部分だと思う。片方を止める作業の本体は、<strong>「GitHub側にしか無いものが、B2側に全部入っているか」を1件ずつ確認すること</strong>。やらずに止めると、欠けたことに気づくのは復元が必要になった日になる。</p>
<p><strong>1. 止める側の中身を、実物から列挙する</strong></p>
<p>設計書ではなく実際のファイル一覧から出す。自分はここで一度間違えた。手元のメモの箇条書きだけを読んで「テキスト類がB2に入っていない」と誤判断したが、スクリプト本文を開いたら既に実装済みだった。<strong>メモで判断せず、スクリプト本文とファイル一覧を実見する。</strong></p>
<p><strong>2. 残す側の対象リストと突き合わせる</strong></p>
<p>ここで<strong>本物の穴が見つかった</strong>。環境変数をまとめたファイル(<code>.env</code>。各種サービスのトークンが入っている)が、<strong>GitHubにもB2にも無かった</strong>。2系統あったのに、どちらにも無いものが存在した。</p>
<p><strong>3. Gitの履歴そのものを、ファイルとして退避する</strong></p>
<p>履歴は普通のファイル同期では運べない。<code>git bundle</code>(リポジトリ全体を1ファイルに固める機能)を使った。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bundle create /tmp/history-20260928.bundle --all
</span></span><span class="line"><span class="cl">git bundle verify /tmp/history-20260928.bundle
</span></span><span class="line"><span class="cl">rclone copy /tmp/history-20260928.bundle &lt;暗号化remote&gt;:&lt;保存先&gt;/
</span></span></code></pre></div><ul>
<li>🔴 <strong><code>root</code>で<code>git</code>を叩くと<code>detected dubious ownership</code>で弾かれる</strong>。ディレクトリの所有者ユーザーで実行する</li>
<li>出来上がりは831MB。<strong>往復まで確認した</strong>——取り出してmd5(内容の指紋)が一致し、<code>git clone &lt;bundleファイル&gt; &lt;ディレクトリ&gt;</code>で162コミットが復元でき、履歴の中にしか無いPDFを元のサイズどおり取り出せた</li>
<li>🔴 <strong>一度きりのスナップショット</strong>。以後履歴が伸びても自動では更新されない</li>
</ul>
<p><strong>4. 止める前に「止める側にしか無いコミット」がゼロだと確認する</strong></p>
<p><code>git rev-list --count &lt;リモート&gt;/main --not --all</code>が<code>0</code>であること。</p>
<p><strong>5. 浮いた分を別の軸に回す</strong></p>
<p><strong>テキスト類だけを対象にした軽い同期を1日4回</strong>動かすようにした(全体の同期は従来どおり夜1回)。これでRPO(障害時に失われる最大の時間幅)が<strong>最大24時間から最大6時間</strong>に縮んだ。重い写真ライブラリに触らないので約10秒で完走する。加えて30日を超える長期の記録として、<strong>テキスト類のみの日付付き<code>tar.gz</code>スナップショットを週1回</strong>置いた。実測16MBなので年0.8GB、費用はほぼゼロ。</p>
<ul>
<li>🔴 <strong>バックアップを取る道具自体がバックアップ対象外だった</strong>。同期スクリプトを置いたディレクトリは、どちらの系統にも入っていなかった。サーバーが全損したらデータは戻るが<strong>戻し方を書いた道具が無い</strong></li>
<li>🔴 <strong>データベースのダンプを取る行に管理者パスワードが直書きされていた</strong>。<strong>権限600の<code>.env</code>に切り出して読み込む</strong>形にし、読めなければ<code>exit 1</code>、変数が空なら<code>: &quot;${VAR:?未設定}&quot;</code>で止める(黙って空のパスワードで走らせないため)。渡し方は<code>-p'…'</code>のような引数ではなく環境変数経由にする——引数は<code>ps</code>で他のユーザーから見える。なお<strong>平文の秘密はスクリプト本文を直しても消えない</strong>。過去の退避ファイルと稼働中のコンテナの環境変数に同じ値が残っており、本当に消すにはパスワードの変更が必要だった</li>
</ul>
<h2 id="何を金庫に入れるか">何を金庫に入れるか</h2>
<p><strong>バックアップを復元するための鍵が、バックアップの中にしか無いと、永久に開かない。</strong> いわゆるブートストラップ問題(起動するために起動済みの何かが必要になる循環)。自分の場合、B2から取り出すための接続情報(<code>rclone</code>の設定ファイル)はサーバーの中だけにあった。毎日バックアップは成功していたが、<strong>サーバーが丸ごと壊れた瞬間に、無事なデータを開ける人が誰もいなくなる</strong>。</p>
<blockquote>
<p><strong>再発行できる鍵は金庫に入れない。再発行できない鍵だけ入れる。</strong></p>
</blockquote>
<ul>
<li><strong>入れる(再発行不能)</strong>: 暗号化パスワード(失うとバックアップが永久に読めない石になる)、ストレージ事業者のアカウントの2段階認証バックアップコード、復元用の接続情報</li>
<li><strong>入れない(再発行可能)</strong>: 各種APIトークン、ストレージのアプリケーションキー。上位のアカウントにログインできれば1分で作り直せる</li>
<li><strong>置くべきは鍵そのものではなく、上位の認証</strong>(アカウントのログイン情報と2段階認証)。それがあれば下位の鍵は何度でも生やせる</li>
<li>🔴 <strong>何でも放り込むと、金庫自体が肥大した単一障害点になる</strong></li>
</ul>
<p>置き場所はパスワードマネージャー(<a href="/guides/1password-to-vaultwarden/">1PasswordからVaultwardenに移した</a>もの)にした。スマートフォンのアプリは一度ログインすればオフラインのキャッシュが残るので、サーバーが全損してもそこから取り出せる。ただし金庫自身がサーバー上で動いているので、<strong>スマートフォンとサーバーを同時に失うと詰む</strong>。対処は「紙でもう1部持つ」しかない。</p>
<h2 id="暗号化とやらなかったこと">暗号化と、やらなかったこと</h2>
<p>実査したら<strong>事業者側の暗号化(SSE)すら無効</strong>になっていて、写真は生のJPEGのままディスクに置かれていた。「バケットは非公開だから最低限守られている」という感覚は間違いだった。2種類あることを区別しておく。</p>
<ul>
<li><strong>事業者側の暗号化</strong>: 倉庫が金庫に入れてくれるが<strong>鍵は倉庫が持つ</strong>。退役ディスクの流出には効くが事業者自身には無力で、手間はゼロ。🔴 <strong>有効にしても遡らない</strong></li>
<li><strong>送る前に自分で暗号化</strong>: 自分で鍵をかけた箱を渡す。事業者にもアカウント乗っ取りにも強いが、<strong>鍵を失えば永久に読めない</strong>し、既存分は全量を再アップロードになる</li>
</ul>
<p>やったのは前者を有効化したうえで、<strong>テキスト類・秘密情報・データベースのダンプだけを後者にする</strong>こと。<code>rclone</code>には<code>crypt</code>という、送信前に暗号化して保存名も不可読にする仕組みがある。初回は6,710オブジェクト・69MBで5分43秒、以後は差分で数十秒だった。</p>
<p><strong>やらなかったこと</strong>。</p>
<ul>
<li><strong>写真ライブラリ131GBは自分で暗号化していない</strong>。全量の再アップロードになるのと、暗号化する動機(「事業者に読まれたくない」のか「凍結で消えるのが嫌」なのか)を整理できていなかったため。後者ならもう達成済みで、前者ならまだ未達。<strong>整理できていないまま重い工事をしない</strong></li>
<li>🔴 <strong>パスワードの保管庫も、意図的に暗号化していない</strong>。中身はそれ自体がゼロ知識暗号化(サービス側も読めない方式)済みで平文ではなく、何より<strong>暗号化パスワードの保管場所がその金庫なので、暗号化すると循環依存になる</strong></li>
</ul>
<h2 id="誰も戻したことのないバックアップはバックアップではない">誰も戻したことのないバックアップは、バックアップではない</h2>
<p><strong>1. 生のファイルから実際に復元して突き合わせる。</strong> 124ファイルが完全一致した。暗号化remote経由でちゃんと復号できることも同時に確かめられる。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rclone copy &lt;暗号化remote&gt;:&lt;対象パス&gt; /tmp/restore-test/
</span></span><span class="line"><span class="cl">diff -rq /tmp/restore-test/ /home/&lt;user&gt;/&lt;元のディレクトリ&gt;
</span></span></code></pre></div><p><strong>2. 週次スナップショットを取り出して<code>tar xzf</code>で展開する。</strong> 6,644ファイル・74MBが正常に展開でき、自動処理の定義ファイル16本まで含まれていた。</p>
<p><strong>3. 全ファイルを照合する。</strong> 差分19件。中身を見たら<strong>すべて同期が走った時刻より後に生成されたファイル</strong>で、取りこぼしはゼロだった。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rclone check --one-way &lt;ローカル&gt; &lt;暗号化remote&gt;:&lt;対象パス&gt;
</span></span></code></pre></div><p><code>--one-way</code>はリモートにしかない古い版を差分に数えないので、検証にはこちらを使う。</p>
<p>併せて<strong>事業者からの通知メールの読み方</strong>を決めた。「マスターキーが生成されました」のような通知は、<strong>自分がコンソールを触っていた日なら本人操作、触っていない日なら本物の警戒シグナル</strong>。先に線引きしないと、通知を全部無視する習慣がつく。</p>
<h2 id="残った1系統の弱いところ">残った1系統の弱いところ</h2>
<ul>
<li><strong>事業者1社への依存は未対策</strong>。アカウントの凍結や会社都合には何の備えもない。別会社のストレージへ追記専用でもう1本(月€4前後)が答えだが、見送った</li>
<li><strong>削除の伝播と容量の兼ね合いが未整理</strong>。送信側の鍵から削除権限を外すと<code>rclone sync</code>が使えず<code>copy</code>しかできなくなり、ローカルで消したものが残り続けて容量が増える。守りと費用がここで正面衝突する</li>
<li><strong>一番大きい穴は、サーバーを乗っ取られた場合</strong>。1系統しかないので、送信に使っている鍵を盗られたらバックアップ本体を消される。これは別テーマとして切り出し、鍵の権限削減・削除が伝播しない複製先・サーバーの外で動く見張りの3層で作り直した。そちらは<a href="/guides/backup-ransomware-resistant/">バックアップを「消されても戻せる」形にする</a>に書いた</li>
</ul>
<p>費用は全バックアップ合計125GBで<strong>月約$0.69</strong>。一本化で減ったのは費用ではなく、<strong>気にかけておく仕組みの数</strong>だった。</p>
<p><strong>更新履歴</strong> — 2026-10-05: 初版</p>
]]></content:encoded></item></channel></rss>