ランサムウェアに決して書き換えられない「データの耐火金庫」:
ZFS不変スナップショットと行単位の鍵(RLS)のしくみ
監修:加藤 明生(株式会社オープンナレッジ 創業者・代表取締役CTO / データベーススペシャリスト 第DB-2013-04-00083号 / 登録情報処理安全確保支援士(第007617号))
この記事の要点と4つの章のご案内
| 章構成 | 主なテーマ | 要点と確認ポイント |
|---|---|---|
| 1. 基本と全体像 | 身近なたとえで見る仕組み:「鉛筆で書き直す帳簿」と「ガラスケースに封印される万年筆の帳簿」 | 最近のランサムウェア(身代金ウイルス)は、社内ネットワークに侵入するとまず「つながっている外付けHDDや共有フォルダ、クラウド同期フォルダのバックアップ」を探し出し、それを消去・暗号化してから最後に本体のパソコンをロックします。そのため、通常の上書き可能なバックアップでは復旧できないケースが多発しています。 |
| 2. 現場での進め方と対策 | ひと目でわかる図解:なぜ「db01金庫室」は外から攻撃できないのか | データを変更するときに「元のページを消しゴムで消して書き直す」のではなく、「元のページには一切触れず、新しいページに変更分だけを書き足す(Copy-on-Write)」という仕組みです。過去の時点の記録はガラスケースの中に封じ込められたように読み取り専用となるため、万が一ウイルスが侵入しても過去の記録を書き換えることができず、数秒で感染前の状態へ巻き戻せます。 |
| 3. 関連する制度とルール | 今のバックアップ方法で、万が一のとき何時間で業務を再開できるか試してみませんか? | 一切ありません。当社のデータベース(PostgreSQL 19)では、プログラム側の画面だけでなく、データベースの最も深い場所にある「1行1行のデータそのもの」に会社ごとの専用の鍵(行レベルセキュリティ:RLS)をかけています。そのため、契約しているご本人の鍵がない限り、他の会社のデータは1行たりとも読み出すことができない構造になっています。 |
| 4. 詳しい技術仕様としくみ | 実践解説:大事なデータを守る「3つの鉄則」 | 現代のランサムウェア(身代金ウイルス)は、パソコン本体だけでなく「つなぎっぱなしの外付けHDDやクラウドのバックアップ」まで先に破壊します。これを構造的に防ぐ唯一の方法が、過去のデータに物理的なロックをかけて誰も上書きできないようにする「書き換え不能(イミュータブル)スナップショット」です。 |
現代のランサムウェア(身代金ウイルス)は、パソコン本体だけでなく「つなぎっぱなしの外付けHDDやクラウドのバックアップ」まで先に破壊します。これを構造的に防ぐ唯一の方法が、過去のデータに物理的なロックをかけて誰も上書きできないようにする「書き換え不能(イミュータブル)スナップショット」です。
オープンナレッジの専用データベース基盤(db01)は、インターネットから直接触れられない専用内線網(プライベートVLAN)の奥に置かれ、「ZFSによる不変バックアップ」と「PostgreSQL 19の1行ごとの専用鍵(RLS)」の二重守りで、お客様の帳簿と図面を守り抜きます。
身近なたとえで見る仕組み:「鉛筆で書き直す帳簿」と「ガラスケースに封印される万年筆の帳簿」
一般的なハードディスクやクラウドの保存方法は、いわば「鉛筆と消しゴムで書くノート」です。新しい数字を書くとき、前の数字を消しゴムで消して同じ場所に上書きします。便利ですが、もし悪意のある侵入者が入ってきたら、ノートの最初から最後まで全部消しゴムで消して、デタラメな暗号に書き換えてしまいます。
一方、私たちが採用している「FreeBSD ZFS(ゼット・エフ・エス)」という金庫室ファイルシステムは、「元のページには一切触れず、変更があれば必ず新しいページに書き足し、過去のページは15分ごとに強化ガラスのケースへ封印する(Copy-on-Writeスナップショット)」という動き方をします。ガラスケースに入った過去のページは、外からどんな命令を送っても消すことも書き換えることもできません。万が一のときも、ガラスケースを開けて「感染5分前のページ」を1秒で取り出すだけで、何事もなかったかのように業務を再開できるのです。
ランサムウェア被害に遭った企業のうち、バックアップを取得していたにもかかわらず「バックアップ自体も暗号化・削除されて復元できなかった」ケースが約7割以上に達しています。
2026年開始の経済産業省「サプライチェーン強化に向けたセキュリティ対策評価制度(★3)」および厚労省「医療情報ガイドライン第6.0版・第7.0版」において、攻撃者に消されないバックアップとネットワーク分離が重要要件となっています。
ZFSのCopy-on-Writeスナップショットと高速メモリキャッシュ(ARC)により、容量何百ギガのデータであっても、コピー待ち時間ゼロで瞬時に過去時点へロールバック可能です。
ひと目でわかる図解:なぜ「db01金庫室」は外から攻撃できないのか
| 比較項目 | 一般的なWebサーバー同居型DB・外付けHDD | オープンナレッジ db01 金庫室アーキテクチャ |
|---|---|---|
| ネットワークの置き場所 | Webサイトと同じサーバー内(玄関のすぐ横に金庫がある状態) | 外部から直接通信できない専用内線(vLAN)の奥に安全な分離 |
| ランサムウェア侵入時のバックアップ | つながっているバックアップ先も一緒に暗号化されてしまう | ZFSの読み取り専用スナップショットにより書き換えを100%拒否 |
| 顧客データの混線防止 | 担当プログラマーが毎回WHERE句(検索条件)を手書きして区別 | PostgreSQL 19のRLS(行単位の鍵)でデータベース側が自動強制 |
| 万が一の復旧時間(RTO) | 数週間〜数ヶ月(データ復元に1,000万円以上かかる事例も多数) | スナップショットの切り替えにより数秒〜数分で感染前へ復元 |
今のバックアップ方法で、万が一のとき何時間で業務を再開できるか試してみませんか?
ランサムウェア遮断・データ乱雑度(エントロピー H(X))リアルタイム図解
ランサムウェアが業務ファイルを暗号化する瞬間、バイト列の乱雑さ(シャノン・エントロピー H(X))は理論上限の 8.0 bits/byte 付近(H(X) ≥ 7.95)へ急上昇します。
下の3つのボタンをクリックして、バイト分布波形と100ms(0.1秒)でのプロセス凍結・ZFSスナップショット復元の挙動を確認できます。
安全設計
サンプル1問・3項目試算はブラウザ内で即時動作します。学習プラットフォーム連携アカウントは現在リニューアル準備中となっており、本ページの自己点検・シミュレーター機能はそのままご利用いただけます。電話番号入力や営業連絡はありません。
実践解説:大事なデータを守る「3つの鉄則」
1. 玄関(Webサーバー)と金庫室(データベース)は原則として同じ部屋に置かない
多くのレンタルサーバーや従来のシステム(WordPress等)では、ホームページを表示する「受付窓口」と、顧客名簿や注文履歴が入った「データベース」が同じ1台のサーバーの中に同居しています。これでは、万が一ホームページのプラグインの隙間から侵入された瞬間、隣にある金庫まで開けられてしまいます。オープンナレッジでは、受付窓口(ap01)と金庫室(db01)を物理的に分け、インターネットからは一切見えない専用の内線(プライベートvLAN)だけでつなぐことで、外からの直接攻撃を物理的に遮断しています。
2. 「3-2-1-1-0の法則」——最後の「1」は書き換え不能(イミュータブル)であること
世界の防災・セキュリティ基準では、データを「3つ以上のコピーで持ち、2種類の異なる媒体に保存し、1つは離れた場所(別拠点)に置き、1つは原則として書き換えできない状態(イミュータブル/オフライン)にし、復元テストのエラーを0にする」というルールが推奨されています。ZFSファイルシステムは、データブロックごとに256ビットの指紋(チェックサム)を記録してハードディスクの微細な磁気劣化(ビット腐敗)すら自動で検知・修復し、スナップショットを瞬時に別拠点へ暗号化転送します。
3. PostgreSQL 19の「行レベルセキュリティ(RLS)」による全体個室化
画面を作るプログラムがどれほど気をつけていても、人間の書く検索条件(SQL)だけに頼っていては「うっかり条件を書き忘れて他社のデータが見えてしまった」という事故のリスクが残ります。私たちは、データベース(PostgreSQL 19)のエンジン自体に「ログインしているお客様のテナント番号と一致する行しか、どんな命令が来ても原則として返さない」という強制ロック(Row Level Security)を組み込んでいます。これにより、マンションの1室1室に独立したオートロックがかかっているのと同じ安心を実現しています。
今日から確認できる!御社のバックアップ安心度 3項目チェック
クリックしてチェックを入れてみてください。
db01.ok.st(vLAN分離・OpenZFS・PostgreSQL 19)運用検証マトリクス
外付けHDDの常時接続による連鎖暗号化リスクを排除し、ネットワーク分離・不変スナップショット・行レベルセキュリティ(RLS)が正常に機能していることを確認する検証基準です。
| 防御レイヤー | 検証コマンド | 合格基準(不変条件) |
|---|---|---|
| vLAN 境界分離 (FreeBSD pf) | pfctl -sr | grep '5432' | 外部WANからの5432番ポート通信を全遮断し、内部vLAN(ap01)のみ許可 |
| OpenZFS 不変スナップショット | zfs get compressratio,readonly zroot/pgdata | LZ4圧縮が有効であり、スナップショット領域が読み取り専用(readonly=on)で保護されていること |
| PostgreSQL 19 FORCE RLS | SELECT relrowsecurity, relforcerowsecurity FROM pg_class; | 全マルチテナント表で t / t が設定され、テナント間の混線が構造的に起きないこと |
宇宙線(太陽からの微粒子)でハードディスクの「0」が「1」に変わってしまうことがある?
実は、何年も大量のデータを保存していると、ハードディスクの経年劣化や微弱な磁気・宇宙線の影響で、ごく稀にデータの中の「0」と「1」が1ビットだけ勝手に反転してしまう「サイレント・データ破損(ビット腐敗)」という現象が起きます。普通のシステムはこれに気づけず、数年後にファイルを開いたとき「なぜか開けない!」となってしまいますが、私たちが使う「ZFS」は全データに「指紋(チェックサム)」をつけて鏡合わせで保存しているため、1ビットでも変化があれば自動で気づいて正しいデータへ自己修復してくれます。
関連資料・解説記事
週末は外付けHDDのケーブルを抜く
まずは今お使いの外付けHDDを「コピーが終わったら抜いて鍵付き棚にしまう」だけでも、週末の感染被害を大幅に防げます。
不変バックアップ・中小・小規模事業者向けのクラウド見守り・業務自動化支援
ケーブルの抜き差しに頼らず、自動で書き換え不能なストレージ領域へデータを保護する設計と運用支援を個別のご相談・ご提案にて提供しています。
データベース・バックアップ現状点検
国家資格「データベーススペシャリスト」「登録情報処理安全確保支援士」が、御社の現在のバックアップ構成を丁寧に診断します。
FreeBSD 15.1-STABLE OpenZFS 128-Byte blkptr_t マークル木・ARCキャッシュ遷移と PostgreSQL 19 FORCE RLS プランナ内部構造
当社の唯一の中枢データベースである db01.ok.st(FreeBSD 15.1-STABLE + OpenZFS + PostgreSQL 19 + pgvector 0.8+ + Redis 8.10.2)は、ランサムウェアによるバックアップ同時暗号化(MITRE ATT&CK T1490 / T1486)とマルチテナント横断漏洩(CWE-284)をストレージ物理層とクエリプランナ層の双方で遮断します。OpenZFS のすべてのデータブロックは 128バイトのブロックポインタ構造体 blkptr_t(sys/fs/zfs/sys/spa.h)によって参照され、最大3つの Data Virtual Address(DVA[0..2])と、子ブロック自体の 256-bit チェックサム(Fletcher-4 または SHA-256、blk_cksum.zc_word[0..3])を親ブロック内部に保持する自己検証型マークル木(Merkle Tree)を形成します。
書き込みトランザクション群(Transaction Group: TXG)のコミット時、ZFS は既存ブロックを一切上書きせず(Copy-on-Write)、新しい物理セクタへ変更ブロックと親ブロックを連鎖的に割り当て、ツリーの根である 1KB の Uberblock(リングバッファ128スロット)をアトミックに更新します。このため、15分周期で取得されるスナップショット(@snap_txg)は過去の Uberblock ルートハッシュによって暗号学的に固定され、OS 上の特権プロセスであっても既存スナップショット内のバイト列をインプレース改ざんすることは構造上不可能です。さらに PostgreSQL 19 のヒープページサイズ(BLCKSZ = 8192 bytes、PageHeaderData 24 bytes + ItemIdData 配列)と ZFS データセット zroot/pg19/data(recordsize=8K・primarycache=metadata・compression=lz4)および zroot/pg19/wal(recordsize=128K・logbias=latency)を一致させることで、部分ページ更新時の Read-Modify-Write I/O 増幅を 1.0 倍に抑え込んでいます。
リレーショナル層では、PostgreSQL 19 の ENABLE ROW LEVEL SECURITY に加えてテーブル所有者・特権ロールに対してもポリシー適用を強制する FORCE ROW LEVEL SECURITY を全テーブルに宣言しています。クエリ書き換え器(src/backend/rewrite/rowsecurity.c の get_row_security_policies())は、通常の WHERE 句評価よりも前段の security_barrier サブクエリ相当の保護レベルで tenant_id = (SELECT current_setting('app.tenant_id', false)::uuid)(InitPlan 定数化)を注入します。これにより、攻撃者が COST 0.0001 の悪意ある関数を用いて行データをゼロ除算エラー等でオラクル推論する Qual Pushdown サイドチャネル攻撃(CVE-2017-7486 等と同型のプランナ漏洩)を構造的に封じ込めています。
| Offset / Layer | Structure / Field (spa.h / bufpage.h) | Size / Alignment | Cryptographic / Planner Role |
|---|---|---|---|
blkptr_t +0x00 | dva_t blk_dva[0..2] (VDEV + Offset + Grid) | 48 Bytes (3 × 16B) | Triple-mirror / RAID-Z physical block addressing |
blkptr_t +0x30 | uint64_t blk_prop (lsize, psize, lz4, type) | 8 Bytes (Bitfield) | Logical=8192B (BLCKSZ) / Compressed Physical Size |
blkptr_t +0x50 | uint64_t blk_birth (txg_id creation epoch) | 16 Bytes (phys_birth) | Copy-on-Write Snapshot immutability boundary |
blkptr_t +0x60 | zio_cksum_t blk_cksum (zc_word[0..3]) | 32 Bytes (256-bit) | Parent-stored Merkle hash of child payload |
PG19 PageHeader | pd_lsn (8B) + pd_checksum (2B) + pd_lower/upper | 24 Bytes / 8KB Page | WAL LSN ordering + FNV-1a 16-bit page checksum |
PG19 rowsecurity | security_barrier Qual Priority (security_level=0) | Planner Rewrite AST | Blocks untrusted predicate pushdown before RLS filter |
-- ============================================================================
-- 1. FreeBSD 15.1-STABLE OpenZFS Dataset Alignment for PostgreSQL 19 (db01.ok.st)
-- zfs create -o recordsize=8K -o compression=lz4 -o atime=off \
-- -o primarycache=metadata zroot/pg19/data
-- zfs create -o recordsize=128K -o compression=lz4 -o logbias=latency zroot/pg19/wal
-- ============================================================================
-- ============================================================================
-- 2. PostgreSQL 19 Strict Multi-Tenant Isolation with FORCE ROW LEVEL SECURITY
-- ============================================================================
CREATE TABLE IF NOT EXISTS ledger_journals (
journal_id UUID PRIMARY KEY DEFAULT uuidv7(),
tenant_id UUID NOT NULL,
tx_timestamp TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
debit_account VARCHAR(32) NOT NULL,
credit_account VARCHAR(32) NOT NULL,
amount_jpy BIGINT NOT NULL CHECK (amount_jpy >= 0),
payload_sha256 BYTEA NOT NULL CHECK (octet_length(payload_sha256) = 32)
);
-- Enable AND Force RLS even for table owner / admin connections
ALTER TABLE ledger_journals ENABLE ROW LEVEL SECURITY;
ALTER TABLE ledger_journals FORCE ROW LEVEL SECURITY;
-- Fail-Closed Policy: InitPlan constant folding via scalar subquery (SELECT ...)
CREATE POLICY tenant_strict_isolation_policy ON ledger_journals
AS RESTRICTIVE
FOR ALL
USING (tenant_id = (SELECT current_setting('app.tenant_id', false)::uuid))
WITH CHECK (tenant_id = (SELECT current_setting('app.tenant_id', false)::uuid));
-- Composite index aligned with ZFS 8K recordsize and RLS leading column
CREATE INDEX CONCURRENTLY idx_ledger_journals_tenant_ts
ON ledger_journals (tenant_id, tx_timestamp DESC)
INCLUDE (amount_jpy, payload_sha256);
// ============================================================================
// 3. Rust 2024: OpenZFS 128-Byte Block Pointer (`blkptr_t`) Fletcher-4 Verifier
// ============================================================================
#[repr(C, align(8))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ZfsBlkptr128 {
pub blk_dva: [[u64; 2]; 3], // 0x00..0x30 (48 bytes: 3 DVAs)
pub blk_prop: u64, // 0x30..0x38 (8 bytes: lsize, psize, compression, checksum type)
pub blk_pad: [u64; 2], // 0x38..0x48 (16 bytes)
pub blk_phys_birth: u64, // 0x48..0x50 (8 bytes)
pub blk_birth: u64, // 0x50..0x58 (8 bytes: logical birth TXG)
pub blk_fill: u64, // 0x58..0x60 (8 bytes: non-zero block count under subtree)
pub blk_cksum: [u64; 4], // 0x60..0x80 (32 bytes: 256-bit Fletcher-4 / SHA-256 Merkle hash)
}
const _: () = assert!(core::mem::size_of::<ZfsBlkptr128>() == 128);
/// Computes OpenZFS Fletcher-4 256-bit checksum over an 8KB PostgreSQL heap page
/// and verifies it against the parent block pointer's stored Merkle hash.
#[inline]
pub const fn verify_zfs_fletcher4_8k(page_words: &[u32], expected_cksum: [u64; 4]) -> bool {
let mut a: u64 = 0;
let mut b: u64 = 0;
let mut c: u64 = 0;
let mut d: u64 = 0;
let mut i = 0;
while i < page_words.len() {
a = a.wrapping_add(page_words[i] as u64);
b = b.wrapping_add(a);
c = c.wrapping_add(b);
d = d.wrapping_add(c);
i += 1;
}
a == expected_cksum[0] && b == expected_cksum[1] && c == expected_cksum[2] && d == expected_cksum[3]
}
db01.ok.st では現在、PostgreSQL 19 の主要機能(① INSERT ... ON CONFLICT DO SELECT による不要なWAL書き込みゼロの冪等応答、② 外部拡張不要のネイティブ REPACK (CONCURRENTLY)、③ インデックス並列 Autovacuum、④ 動的非同期I/Oワーカー io_method = worker、⑤ uuidv7() によるB-tree局所性維持と Skip Scan、⑥ COPY ... TO STDOUT (FORMAT JSON) による高速JSONストリーミング、⑦ WAIT FOR LSN による読み取り整合性保証、⑧ pg_plan_advice による実行計画固定と jit = off)を標準適用しています。あわせて、次期メジャーバージョンである PostgreSQL 20 に向けた以下の5領域の先行検証を進めています。
| 先行研究領域(PostgreSQL 20 R&D) | 対象アーキテクチャ・国際標準規格 | db01.ok.st における検証目的と期待効果 |
|---|---|---|
| ① マルチスレッド・バックエンド移行 | Single-Process Multithreaded postgres |
従来の1接続1プロセスモデルからスレッドモデルへの移行を見据え、FreeBSD 15.1 上でのコンテキストスイッチ削減と共有メモリ効率を検証。 |
| ② SQL/PGQ グラフ問合せ & テンポラル制約 | ISO/IEC 9075-16 GRAPH_TABLE / SQL:2011 FOR PORTION OF |
学習サイト(OK-STUDY Pro)の知識グラフ探索や、契約・会計期間の重複防止(WITHOUT OVERLAPS)を標準SQL構文で記述する検証。 |
| ③ 全経路 非同期I/O(Write/WAL AIO)& SIMD集計 | Full-Path Direct/Async I/O + Vectorized Executor | OpenZFS ARC と PostgreSQL バッファプール間の非同期書き込み経路および列指向SIMDスキャンのスループット測定。 |
| ④ ネイティブ増分ビュー更新(IVM) | CREATE INCREMENTAL MATERIALIZED VIEW |
仕訳帳(ledger_journals)への追記と同時に試算表ビューを差分更新し、バッチ集計待ち時間をゼロ化する検証。 |
| ⑤ 次世代ベクトル索引(DiskANN / 量子化HNSW) | Next-Gen pgvector Streaming DiskANN & Halfvec/Bit Quantization |
30TB 文書ガバナンス基盤とのハイブリッド検索(BM25 + Dense Vector RRF)における省メモリ・高速探索の検証。 |