【実務・中級編】 アクセス制御におけるOwnableとAccessControlの使い分け – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

OwnableかAccessControlか?その境界線で死ぬか生きるか

現場で「とりあえずOpenZeppelinのOwnable入れとけば安心でしょ」と考えているなら、今すぐその考えを捨てろ。我々が守っているのは単なるWebアプリじゃない。背後で物理的な機器を動かすIoTや、数億円規模の流動性が絡むスマートコントラクトだ。

一度の権限設定ミスが、全資産のドレインや、制御システムならプラントの緊急停止に直結する。今日は、アクセス制御の設計において「なぜOwnableでは足りないのか」、そして「どうやってRBAC(ロールベースアクセス制御)で鉄壁を築くのか」という泥臭い話をしよう。

—

1. 脆弱性の本質:権限の「単一障害点」を殺せ

多くの開発者が陥る罠は、スマートコントラクトやAPIの管理権限を「オーナー(1人)」に委ねすぎることだ。

Ownableの盲点

Ownableは「神(Owner)」を1人だけ作る。この神が秘密鍵を紛失したら?あるいは、神のPCがマルウェアに感染して鍵が盗まれたら?その瞬間、システムは攻撃者の所有物になる。これを「単一障害点(Single Point of Failure)」と呼ぶ。

RBAC(AccessControl)の哲学

対してAccessControlは、権限を細分化する。「更新担当(MINTER_ROLE)」「停止担当(PAUSER_ROLE)」「設定変更担当(ADMIN_ROLE)」のように分ければ、たとえ一つの鍵が盗まれても被害は限定的だ。

—

2. 攻撃者が狙う「権限昇格」のPoCシナリオ

想像してほしい。君が管理しているIoTゲートウェイと接続されたスマートコントラクトがあるとする。攻撃者は、本来ADMIN_ROLEしか呼べない関数を、不完全なアクセス制御の隙を突いて呼び出そうとする。

もし、関数のアクセス制御が「誰でも呼べる状態」または「オーナーチェックが関数名と一致していない」場合、攻撃者は以下のJSコード(ethers.js使用)でコントラクトを乗っ取る。

// 攻撃用スクリプトの断片
async function exploit() {
    const targetContract = new ethers.Contract(ADDR, ABI, attackerSigner);
    
    // 本来ADMINしか呼べないはずの関数を呼び出し、管理者を自分に書き換える
    // もし内部のrequire(msg.sender == owner)が欠けていれば、即座に成功する
    const tx = await targetContract.transferOwnership(attackerAddress);
    await tx.wait();
    console.log("コントラクトを掌握しました");
}

この「たった一行の書き換え」を防ぐためには、ロールの階層化と、関数実行ごとの厳密なチェックが不可欠だ。

—

3. 実践:セキュアな設計と実装サンプル

では、OpenZeppelinのAccessControlを使った、実務で使える鉄壁の実装例を見ていこう。Solidityでの設計だが、Web APIの認可ロジックでも考え方は同じだ。

推奨される実装例(Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/AccessControl.sol";

contract SecureController is AccessControl {
    // ロールを定数として定義する
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

    constructor() {
        // デプロイした人を両方の管理者にする(運用時はマルチシグ化を推奨)
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ADMIN_ROLE, msg.sender);
    }

    // 特定の操作(例:IoT機器の再起動指令)はオペレーター権限のみ許可
    function restartDevice() public onlyRole(OPERATOR_ROLE) {
        // ここに制御ロジック
    }

    // 権限管理そのものはADMINのみ
    function grantOperatorRole(address account) public onlyRole(ADMIN_ROLE) {
        grantRole(OPERATOR_ROLE, account);
    }
}

運用上のポイント:マルチシグの導入

コードがどれだけセキュアでも、鍵が1つなら意味がない。AccessControlの管理アドレスには、必ずGnosis Safe(Safe)のようなマルチシグウォレットを指定しろ。「3人中2人の承認がないと権限変更できない」という仕組みが、ヒューマンエラーと内部不正を防ぐ最後の砦になる。

—

4. クラウド/インフラ層での防壁(Nginx設定例)

Web3のコントラクトだけでなく、それを叩くバックエンドAPIも同様だ。Nginxのallow/denyやAPI GatewayのIAMポリシーで、アクセス元を絞り込むのは基本中の基本だ。

# 特定の管理IP以外からの管理者APIへのアクセスを遮断
location /admin/ {
    allow 192.168.1.50; # 社内踏み台サーバーのみ許可
    deny all;
    
    # さらに認証なしで通さない
    auth_basic "Restricted Area";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

—

最後に:セキュリティは「性悪説」で構築せよ

現場で最も危険なのは、「今のところ誰も攻撃してきていないから大丈夫」という楽観論だ。

1. Ownableを卒業せよ: 権限が1箇所に集中しているなら、即座にAccessControlへリファクタリングする計画を立てろ。
2. 最小権限の原則を徹底せよ: 関数ごとに「本当にこのロールが必要か?」を問い直せ。
3. 監査を自動化せよ: Slitherのような静的解析ツールをCI/CDパイプラインに組み込み、アクセス制御が漏れているコードがマージされないようにしろ。

権限管理は、システムの心臓部だ。そこを雑に扱うエンジニアに、未来のIoTやWeb3インフラは任せられない。君たちの手で、強固な防壁を築いてくれ。何かあればいつでも相談に乗る。健闘を祈る。

コメント

タイトルとURLをコピーしました