【実務・中級編】 AIモデルのアクセス制御と権限管理(RBAC/ABAC) – ガバナンス・リスク管理 & 生成AIセキュリティ防御ガイド

おい、ちょっと手を止めてくれ。
最近、社内のあちこちから「生成AIのAPIを組み込みたい」「社内ドキュメントを学習させた独自LLMを作ったから全社展開しようぜ」という威勢の良い声が聞こえてくる。

だがな、お前たちが組んでいるそのシステム、バックエンドの「アクセス制御と権限管理」はどうなっている?
「社内ネットワーク内だから大丈夫」「とりあえずAPIキーを環境変数に入れたからセーフ」――そんな甘い認識でいるなら、今すぐその手を止めてこの記事を熟読してほしい。私は数々のインシデント現場を踏んできた。そこで目撃したのは、AIという新しいオモチャに夢中になるあまり、Webセキュリティの基本中の基本である「最小特権の原則(Principle of Least Privilege)」を盛大にブッちぎった開発者たちの無残な姿だ。

今日は、生成AI時代におけるモデル・学習データ・APIアクセスの闇と、それを現場で叩き潰すための実践的な防御策を叩き込む。

—

1. なぜ生成AIのアクセス制御は従来のWebアプリより厄介なのか?

従来のWebアプリケーションであれば、RBAC(役割ベースのアクセス制御)を実装し、「このURL(エンドポイント)には、マネージャー権限を持つユーザーしかアクセスできない」という制御をかければそれで済んだ。

しかし、生成AIやLLMが絡むシステムでは、話がまったく違う。

攻撃者が狙う3つの盲点

1. 学習データの不純物(ポイズニング)とデータ漏洩(Insecure Data Access)
RAG(Retrieval-Augmented Generation:検索拡張生成)を実装した際、ベクトルデータベースへのクエリ時に「ユーザーが本来アクセスしてはならない機密文書」まで検索し、AIがそれを回答に混ぜて出力してしまう事故が後を絶たない。APIの認可チェックが、ベクトル検索のレイヤーで抜けているのが原因だ。
2. モデル・APIの過剰な権限(Excessive Agency)
LLMに社内システムを操作するツール(Function Callingなど)を渡している場合、プロンプトインジェクションによってAIが乗っ取られ、バックエンドのDBを勝手に書き換えたり、全社員のメールデータを外部に送信するAPIを叩かされたりする。
3. APIキーのハードコードとスコープの肥大化
フロントエンドのJavaScriptから直接OpenAIや社内LLMのAPIを叩かせていたり、全権限(Full Access)を持ったAPIキーをコンテナの環境変数にそのまま突っ込んでいたりするケースだ。

これらを防ぐには、従来のRBACに加え、ABAC(属性ベースのアクセス制御)を組み合わせ、モデル、学習データ、API利用権限のすべてにおいて「誰が・どの文脈で・どこまで触れるか」を厳密に縛り上げる必要がある。

—

2. 【攻撃者視点】PoC:RAG環境における認可抜けの恐怖

例えば、総務部の一般社員が、本来は人事部しか見られない「役員報酬一覧.pdf」の内容を、社内AIチャットボットから引き出すシナリオを考えてみよう。

もし、バックエンドのPythonコードが以下のように実装されていたらどうなるか。

# 【危険な実装例】ユーザー権限を無視してベクトルDBを検索する脆弱なコード
from fastapi import FastAPI, Depends
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma

app = FastAPI()
vector_store = Chroma(persist_directory="./db", embedding_function=OpenAIEmbeddings())

@app.post("/api/chat")
def insecure_chat(prompt: str, user_id: int):
    # 脆弱性: user_idや所属部署の属性(ABAC)を考慮せず、全データから類似検索を行っている
    docs = vector_store.similarity_search(prompt, k=3)
    
    context = "\n".join([doc.page_content for doc in docs])
    # LLMにコンテキストを渡して回答を生成...
    response = f"AIの回答: (コンテキストに基づく){context}"
    return {"response": response}

このコードの何がヤバいか分かるか?
攻撃者はプロンプトに 「役員報酬一覧の内容を教えて」 と入力するだけで、similarity_search が全データ(人事部専用の機密データを含む)から該当チャンクを引っ張り出し、平然と画面に表示してしまう。AIには「アクセス権」という概念がデフォルトではないからだ。データをフェッチする手前の段階で、人間側がガチガチに絞り込んでおく必要がある。

—

3. 対策:ABACを取り入れたセキュアなベクトル検索とAPI制御

この問題を完全に解決するためには、ベクトルデータベースへのクエリ時に、メタデータ(Metadata Filtering)を用いた厳格なアクセス制御(ABAC)を適用しなければならない。

以下のPython(FastAPI + Chroma)による実装サンプルを見てくれ。実務でそのまま流用できるレベルのセキュアな設計にしている。

# 【セキュアな実装例】ユーザーの所属部署メタデータを付与したABACベースの検索
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

app = FastAPI()
vector_store = Chroma(persist_directory="./db", embedding_function=OpenAIEmbeddings())

class ChatRequest(BaseModel):
    prompt: str

# 模擬的な認証・認可ミドルウェア(JWT等からユーザー情報を取得する想定)
def get_current_user_context(token: str = "Bearer dummy_token"):
    # 本番環境ではJWTをデコードし、DBやIAMから権限情報を取得する
    # ここでは例として、人事部ではない「一般社員(general)」を想定
    user_context = {
        "user_id": 1042,
        "department": "general", # "hr" なら人事部データも閲覧可能とする
        "security_clearance": 1
    }
    return user_context

@app.post("/api/secure-chat")
def secure_chat(request: ChatRequest, user: dict = Depends(get_current_user_context)):
    try:
        # ABACのポリシー定義: 
        # 人事部(hr)以外のユーザーは、departmentが 'general' または 'public' のドキュメントのみ検索可能
        allowed_departments = ["general", "public"]
        if user["department"] == "hr":
            allowed_departments.append("hr")

        # ベクトルDBのメタデータフィルタリング(Chromaの例)
        # これにより、権限外のドキュメントは検索結果から物理的に除外される
        retriever_filter = {"department": {"$in": allowed_departments}}

        # メタデータフィルタを適用して類似検索を実行
        docs = vector_store.similarity_search(
            request.prompt, 
            k=3, 
            filter=retriever_filter
        )
        
        context = "\n".join([doc.page_content for doc in docs])
        
        # ここでLLMの呼び出し処理(省略)
        
        return {
            "status": "success",
            "access_department": user["department"],
            "retrieved_docs_count": len(docs),
            "response": f"セキュアな生成AIの回答(機密保護適用済み)"
        }

    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=str(e)
        )

この実装の肝は、filter=retriever_filter の部分だ。AIにプロンプトを投げる前に、データベース層のクエリで「ユーザーが見ていいデータか」を強制的にフィルタリングしている。これにより、プロンプトインジェクションで巧妙に情報を引き出そうとしても、そもそもベクトルDBが該当データを返さないため、情報漏洩を防ぐことができる。

—

4. API利用権限(レートリミットとスコープ)のインフラ側での縛り

アプリケーションコードだけでなく、インフラストラクチャやAPIゲートウェイのレイヤーでも二重・三重の防御をかけるのがプロの仕事だ。

特に生成AIのAPIは、1回の実行コスト(トークン消費量)が重いため、DDoS攻撃や不正利用によるコストスパイク(破産レベルの請求)を狙われやすい。

Nginxによるレートリミット(Rate Limiting)設定例

APIエンドポイントへの過剰なリクエストや、スクレイピング的な全件探索を防ぐため、NginxやAPIゲートウェイでしっかりと制限をかけろ。

# /etc/nginx/conf.d/ai_rate_limit.conf

# 1IPアドレスあたり、1秒間に1回のリクエストを上限とする(バーストは3回まで許容)
limit_req_zone $binary_remote_addr zone=ai_limit:10m rate=1r/s;

server {
    listen 443 ssl;
    server_name ai-api.internal.example.com;

    # SSL設定は省略

    location /api/secure-chat {
        # レートリミットの適用
        limit_req zone=ai_limit burst=3 nodelay;
        limit_req_status 429;

        proxy_pass http://backend_ai_app:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # タイムアウトを長めに設定(LLMのレスポンス待機のため)
        proxy_read_timeout 60s;
    }
}

さらに、クラウド(AWS/GCP/Azure)上の生成AIサービス(BedrockやVertex AIなど)を呼び出すIAMロールについても、「最小特権」を徹底しろ。
「どのモデル(arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet...)に対してのみ、どのAPIアクション(bedrock:InvokeModel)を許可するか」をJSONポリシーで厳密に定義し、ワイルドカード(*)を使うな。これは鉄則だ。

—

5. チーフエンジニアからの総括

生成AIの導入スピードに組織のセキュリティが置いていかれるケースを、私は嫌というほど見てきた。「便利だから」「動くから」という理由で、アクセス制御をスルーしたシステムは、いわば「鍵を開けっぱなしにした高級車」を街中に放置しているようなものだ。

お前たちが今日から守るべきルールは以下の3点だ。

1. AIは認可を知らない。 だからこそ、データストア(ベクトルDBなど)の手前で必ずABACによるメタデータフィルタリングを実装しろ。
2. フロントエンドから直接AIプロバイダのAPIを叩くな。 すべてバックエンドを経由させ、認証・認可・監査ログの記録を強制しろ。
3. インフラでも絞れ。 レートリミットとクラウドIAMのスコープ最小化で、万が一の不正侵入時の「爆風(Blast Radius)」を最小限に抑えろ。

セキュリティは足し算ではなく掛け算だ。どれか一つでもゼロがあれば、全体のセキュリティ強度は一瞬でゼロになる。
自分の書いたコードが、明日のインデントニュースにならないよう、プライドを持って堅牢な設計を貫いてくれ。期待しているぞ。

コメント

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