第13章: セキュリティ

プラネタリスケールコンピュータは広大な攻撃対象面です。セキュリティサービスは、システムのダッシュボードにトークンベースの認証を提供し、実際の分散システムを保護する認証、認可、トークン管理の原則を実演します。

インターフェース

security/src/lib.rs セキュリティサービスは4つのプロシージャを公開します。CREATE_TOKENは名前と権限セットを持つ新しいトークンを生成します。VALIDATE_TOKENはトークンが有効かどうかをチェックし、関連するIDを返します。REVOKE_TOKENは侵害されたトークンを無効化します。LIST_TOKENSはダッシュボード用にすべてのアクティブなトークンを列挙します。

pub const CREATE_TOKEN_PROCEDURE: ProcedureId = 601;
pub const VALIDATE_TOKEN_PROCEDURE: ProcedureId = 602;
pub const REVOKE_TOKEN_PROCEDURE: ProcedureId = 603;
pub const LIST_TOKENS_PROCEDURE: ProcedureId = 604;

#[derive(Debug, Serializable, Deserializable)]
pub struct CreateTokenArgs {
    pub name: String,
    pub permissions: String,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct ValidateTokenResult {
    pub valid: i32,
    pub name: String,
    pub permissions: String,
}

トークンベース認証

security/src/main.rs セキュリティサービスは認証トークン——保護された操作へのアクセスを付与する不透明な文字列——を管理します。各トークンは名前(所有者の識別)、権限のセット、作成タイムスタンプを持ちます。フロントエンドは、すべての機密性の高いダッシュボード操作(状態を変更するPOSTリクエスト)でauth_tokenクッキーをチェックし、セキュリティサービスのVALIDATE_TOKENプロシージャを呼び出します。

struct TokenEntry {
    name: String,
    token: String,
    permissions: String,
    created_at: u64,
}

struct SecurityState {
    tokens: HashMap<String, TokenEntry>,
    rng_state: u64,  // xorshift64 seed
}

トークン生成:分散システムにおけるPRNG

security/src/main.rs トークン生成は興味深い問題を提起します:分散システムではランダム性はどこから来るのか?ハードウェア乱数生成器は低速です。暗号論的PRNG(/dev/urandomのような)はより良いですが、それでもシステムコールを伴います。教育目的の実装では、xorshift64——高速で自己完結型の擬似乱数生成器——を使用します:

fn xorshift64(&mut self) -> u64 {
    let mut x = self.rng_state;
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    self.rng_state = x;
    x
}

xorshift64生成器は周期264−1の一様分布64ビット値を生成します。2つの出力を連結して128ビットの16進トークンを形成します。本番システムでは暗号論的に安全なPRNGを使用しますが、xorshift64はコアコンセプトを実証します:シード値から一見ランダムな出力を生成する決定論的関数です。シードは起動時のシステムクロックから導出され、各インスタンスのトークンストリームを一意にします。

ブートストラップ問題

security/src/main.rs — 起動時のadminトークンシーディング トークンベースの認証は鶏と卵の問題を作り出します:すべての変更操作に有効なトークンが必要な場合、最初のトークンをどうやって作成するのか?初期のアプローチはブートストラップ例外——1つのルートを認証なしにすること——でしたが、それは公開デプロイメントで攻撃対象面を作成します。代わりに、起動時に環境変数からadminトークンをシードします:

let mut initial_state = SecurityState::new();
if let Ok(token) = std::env::var("ADMIN_TOKEN") {
    if !token.is_empty() {
        initial_state.tokens.insert(token.clone(), TokenEntry {
            name: "admin".to_string(),
            token,
            permissions: "admin".to_string(),
            created_at,
        });
    }
}

起動スクリプト(start.sh)はopenssl rand -hex 16でランダムトークンを生成し、ADMIN_TOKENとしてエクスポートし、運用者に表示します。運用者はブラウザコンソール(document.cookie = "auth_token=...;path=/")でクッキーを設定し、ダッシュボードを使用して追加のトークンを作成できます。トークン作成を含むすべてのPOSTルートには有効なauth_tokenクッキーが必要です。

このパターンは実際のシステムでは標準的です。Kubernetesはクラスター初期化時にブートストラップトークンを作成します。クラウドプロバイダーはプロビジョニング時にシードされたIAMルート認証情報を使用します。重要な洞察は、ブートストラップシークレットは認証なしのHTTPエンドポイントではなく、デプロイ環境に存在すべきだということです。

認可ミドルウェア

frontend/src/main.rs フロントエンドは認可をミドルウェア——各保護されたルートハンドラの前に実行される関数——として実装します:

async fn require_admin(headers: &str) -> bool {
    if let Some(token) = parse_cookie(headers, "auth_token") {
        let result = security::validate_token(SECURITY_ADDR, token).await;
        return result.valid == 1;
    }
    false
}

このパターン——リクエストから認証情報を抽出し、中央機関に対して検証し、結果に基づいてアクセスをゲーティングする——は、APIゲートウェイ、サービスメッシュ、Webフレームワークがプラネタリスケールで使用するのと同じパターンです。セキュリティダッシュボードではトークンの作成、アクティブなトークンの表示、侵害されたトークンの取り消しができます。

インテグリティ

認証は誰が操作できるかを制御しますが、量に対する保護にはなりません。有効なユーザーでもリクエストでシステムを圧倒でき、攻撃者はリソースを消費するのに認証情報を必要としません。インテグリティとは、敵対的な条件下でもシステムが機能し続けることを保証する実践です——レート制限、IPブラックホール化、深層防御です。

レート制限

loadbalancer/src/main.rsTokenBucket構造体 ロードバランサーはトークンバケットアルゴリズムを使用してIPごとのレート制限を実装します。各IPアドレスは最大30トークン(バースト容量)を保持するバケットを取得し、毎秒2トークン(毎分約120リクエストを持続)で補充されます。各リクエストは1トークンを消費します。バケットが空の場合、リクエストは429 Too Many Requestsで拒否されます:

fn try_consume(&mut self) -> bool {
    let now = Instant::now();
    let elapsed = now.duration_since(self.last_refill).as_secs_f64();
    self.tokens = (self.tokens + elapsed * RATE_REFILL).min(RATE_CAPACITY);
    self.last_refill = now;
    if self.tokens >= 1.0 {
        self.tokens -= 1.0;
        true
    } else {
        false
    }
}

トークンバケットはバーストを許容しつつ(ユーザーがページを読み込む際に複数のリソースを一度にフェッチする)持続的なレートを強制するため優雅です。代替手段の固定ウィンドウカウンターには境界問題があり、クライアントがウィンドウの境界をまたいでリクエストのタイミングを合わせることで制限の2倍を送信できます。

IPブラックホール化

loadbalancer/src/main.rsrecord_violation() レート制限だけでは不十分です。繰り返し拒否される攻撃者は、各拒否でCPUサイクルを消費します。ロードバランサーは自動的にエスカレートします:IPが60秒以内に10回の拒否を蓄積した場合、5分間ブラックホール化——禁止されます。ブラックホール化されたIPはリクエストボディが読まれる前に即座に429 Retry-After: 300を受け取ります:

fn record_violation(&mut self, ip: IpAddr) {
    let now = Instant::now();
    let (count, window_start) = self.violations
        .entry(ip).or_insert((0, now));
    if now.duration_since(*window_start) > BLACKHOLE_WINDOW {
        *count = 0;
        *window_start = now;
    }
    *count += 1;
    if *count >= BLACKHOLE_THRESHOLD {
        self.blacklist.insert(ip, now + BLACKHOLE_DURATION);
        self.violations.remove(&ip);
    }
}

バックグラウンドタスクが60秒ごとに3つのマップ(レート制限バケット、ブラックリストエントリ、違反カウンター)をスイープして期限切れの状態を削除します。これにより長時間稼働するデプロイメントからのメモリ成長を防ぎます。

深層防御

これらの保護は層として重なります。ロードバランサーは最初の防御線です:レート制限とブラックホール化はリクエストがバックエンドサービスに到達する前に行われます。フロントエンドは第2の防御線です:すべてのダッシュボードの変更操作はセキュリティサービスに対して検証された有効なauth_tokenクッキーを必要とします。モニタリングサービスは可視性を提供します——ロードバランサーはrate_limitedblackholedのメトリクスを報告し、運用者がリアルタイムで攻撃を検出できるようにします。

イントロスペクションエンドポイント(/__lb_status/__lb_strategy)はループバックアドレスに制限されており、外部ユーザーがバックエンドトポロジーを読み取ったりロードバランシング戦略を変更したりすることを防ぎます。すべてのバックエンドサービスは127.0.0.1にバインドされインターネットから到達不能です——ロードバランサーのみが0.0.0.0でリッスンします。これは本番リバースプロキシが使用するのと同じアーキテクチャです:強化された単一のエントリポイントが内部サービスにトラフィックを振り分けます。