[PHP]posix_setgid関数とは?実グループIDを設定する方法を徹底解説

PHP

こんにちは!今回はPHPのPOSIX関数の中から、posix_setgid関数について詳しく解説していきます。プロセスの実グループID(Real GID)を永続的に設定する方法を、実践的なサンプルコードと共にお伝えします。

posix_setgid関数とは?

posix_setgidは、現在のプロセスの**実グループID(Real GID)**を設定する関数です。posix_setegidとは異なり、一度変更すると元に戻すことができません。主に権限降格(privilege dropping)に使用されます。

基本構文

posix_setgid(int $group_id): bool
  • 引数: 設定したいグループID(整数)
  • 戻り値: 成功時にtrue、失敗時にfalse

posix_setgidとposix_setegidの違い

この2つの関数の違いを理解することが重要です。

比較表

関数対象可逆性用途
posix_setgid実GID❌ 不可逆権限の永久的な降格
posix_setegid実効GID✅ 可逆権限の一時的な変更

動作の違い

<?php
echo "=== posix_setegid (一時的) ===\n";
$original_egid = posix_getegid();
echo "元の実効GID: {$original_egid}\n";

// 一時的に変更
posix_setegid(33); // www-data
echo "変更後: " . posix_getegid() . "\n";

// 元に戻せる
posix_setegid($original_egid);
echo "復元後: " . posix_getegid() . "\n";

echo "\n=== posix_setgid (永続的) ===\n";
$original_gid = posix_getgid();
echo "元の実GID: {$original_gid}\n";

// 永続的に変更
posix_setgid(33); // www-data
echo "変更後: " . posix_getgid() . "\n";

// 元に戻せない!
if (!posix_setgid($original_gid)) {
    echo "エラー: 元に戻せません(これが正常な動作)\n";
}
?>

実践的な使い方

例1: 基本的な権限降格

<?php
// 権限降格の基本パターン
function demonstratePrivilegeDrop() {
    echo "=== 権限降格のデモンストレーション ===\n\n";
    
    // 初期状態を確認
    $uid = posix_getuid();
    $gid = posix_getgid();
    
    $user = posix_getpwuid($uid);
    $group = posix_getgrgid($gid);
    
    echo "初期状態:\n";
    echo "  UID: {$uid} ({$user['name']})\n";
    echo "  GID: {$gid} ({$group['name']})\n\n";
    
    // root権限でのみ実行可能
    if ($uid !== 0) {
        echo "このデモンストレーションにはroot権限が必要です\n";
        return;
    }
    
    // www-dataユーザーの情報を取得
    $target_user = posix_getpwnam('www-data');
    if (!$target_user) {
        echo "www-dataユーザーが見つかりません\n";
        return;
    }
    
    echo "権限を降格します: www-data (UID:{$target_user['uid']}, GID:{$target_user['gid']})\n\n";
    
    // 重要: 先にGIDを変更してからUIDを変更
    // 1. グループIDを変更
    if (posix_setgid($target_user['gid'])) {
        echo "✓ GIDを変更しました\n";
    } else {
        echo "✗ GID変更に失敗\n";
        return;
    }
    
    // 2. ユーザーIDを変更
    if (posix_setuid($target_user['uid'])) {
        echo "✓ UIDを変更しました\n";
    } else {
        echo "✗ UID変更に失敗\n";
        return;
    }
    
    // 変更後の状態を確認
    echo "\n変更後の状態:\n";
    echo "  UID: " . posix_getuid() . "\n";
    echo "  GID: " . posix_getgid() . "\n";
    
    // root権限に戻そうとする(失敗する)
    echo "\nroot権限に戻ろうとしています...\n";
    if (!posix_setgid(0)) {
        echo "✓ 正常: root権限に戻れません(セキュリティ確保)\n";
    }
}

// 実行例
demonstratePrivilegeDrop();
?>

例2: デーモンプロセスの安全な起動

<?php
class SecureDaemon {
    private $daemon_user = 'daemon';
    private $daemon_group = 'daemon';
    private $pid_file = '/var/run/secure_daemon.pid';
    
    public function start() {
        // root権限での起動を確認
        if (posix_getuid() !== 0) {
            throw new Exception("このデーモンはroot権限で起動する必要があります");
        }
        
        echo "デーモンを起動中...\n";
        
        // Phase 1: 特権が必要な初期化
        $this->privilegedInitialization();
        
        // Phase 2: プロセスをデーモン化
        $this->daemonize();
        
        // Phase 3: 権限を降格
        $this->dropPrivileges();
        
        // Phase 4: メイン処理
        $this->run();
    }
    
    private function privilegedInitialization() {
        echo "Phase 1: 特権初期化中...\n";
        
        // 特権ポートをバインド(例: 80番ポート)
        echo "  - 特権ポートをバインド\n";
        
        // PIDファイルを作成
        $pid = posix_getpid();
        if (file_put_contents($this->pid_file, $pid) === false) {
            throw new Exception("PIDファイルの作成に失敗");
        }
        echo "  - PIDファイルを作成: {$this->pid_file}\n";
        
        // ログディレクトリのパーミッション設定
        $log_dir = '/var/log/secure_daemon';
        if (!is_dir($log_dir)) {
            mkdir($log_dir, 0755, true);
        }
        echo "  - ログディレクトリを準備\n";
    }
    
    private function daemonize() {
        echo "Phase 2: デーモン化中...\n";
        
        // 子プロセスを作成
        $pid = pcntl_fork();
        
        if ($pid == -1) {
            throw new Exception("フォークに失敗");
        } elseif ($pid) {
            // 親プロセスは終了
            exit(0);
        }
        
        // 新しいセッションリーダーになる
        if (posix_setsid() == -1) {
            throw new Exception("セッションの作成に失敗");
        }
        
        // 作業ディレクトリを変更
        chdir('/');
        
        // 標準入出力を閉じる
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        
        echo "  - デーモン化完了\n";
    }
    
    private function dropPrivileges() {
        // Phase 3: 権限降格
        // この時点でまだroot権限を持っている
        
        $daemon_user = posix_getpwnam($this->daemon_user);
        if (!$daemon_user) {
            throw new Exception("デーモンユーザーが見つかりません: {$this->daemon_user}");
        }
        
        $daemon_group = posix_getgrnam($this->daemon_group);
        if (!$daemon_group) {
            throw new Exception("デーモングループが見つかりません: {$this->daemon_group}");
        }
        
        // 補助グループリストを初期化
        if (!posix_initgroups($this->daemon_user, $daemon_group['gid'])) {
            throw new Exception("補助グループの初期化に失敗");
        }
        
        // 実グループIDを変更(不可逆)
        if (!posix_setgid($daemon_group['gid'])) {
            throw new Exception("GIDの変更に失敗");
        }
        
        // 実ユーザーIDを変更(不可逆)
        if (!posix_setuid($daemon_user['uid'])) {
            throw new Exception("UIDの変更に失敗");
        }
        
        // 権限降格の確認
        if (posix_getuid() === 0 || posix_getgid() === 0) {
            throw new Exception("権限降格に失敗しました!");
        }
        
        // ログに記録
        $this->log("権限を降格: {$this->daemon_user}:{$this->daemon_group}");
    }
    
    private function run() {
        // Phase 4: メイン処理
        $this->log("デーモンプロセス開始");
        
        // シグナルハンドラを設定
        $this->setupSignalHandlers();
        
        // メインループ
        while (true) {
            $this->doWork();
            sleep(5);
        }
    }
    
    private function setupSignalHandlers() {
        pcntl_signal(SIGTERM, function() {
            $this->log("SIGTERMを受信。終了します。");
            unlink($this->pid_file);
            exit(0);
        });
    }
    
    private function doWork() {
        // デーモンの実際の処理
        $this->log("処理実行中...");
    }
    
    private function log($message) {
        $timestamp = date('Y-m-d H:i:s');
        $log_file = '/var/log/secure_daemon/daemon.log';
        $uid = posix_getuid();
        $gid = posix_getgid();
        
        $log_entry = "[{$timestamp}] [UID:{$uid} GID:{$gid}] {$message}\n";
        file_put_contents($log_file, $log_entry, FILE_APPEND);
    }
}

// 使用例(root権限で実行する必要があります)
try {
    $daemon = new SecureDaemon();
    $daemon->start();
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
    exit(1);
}
?>

例3: Webアプリケーションの権限分離

<?php
class WebApplicationLauncher {
    private $web_user = 'www-data';
    private $web_group = 'www-data';
    
    public function launch() {
        echo "=== Webアプリケーション起動 ===\n\n";
        
        // root権限で起動
        if (posix_getuid() !== 0) {
            throw new Exception("root権限で起動してください");
        }
        
        echo "1. 初期化(root権限)\n";
        $this->initializeAsRoot();
        
        echo "\n2. 権限降格\n";
        $this->dropToWebUser();
        
        echo "\n3. アプリケーション実行\n";
        $this->runApplication();
    }
    
    private function initializeAsRoot() {
        // root権限が必要な初期化処理
        
        // ディレクトリの作成
        $dirs = [
            '/var/www/uploads' => 0755,
            '/var/www/cache' => 0755,
            '/var/www/sessions' => 0700,
        ];
        
        foreach ($dirs as $dir => $perms) {
            if (!is_dir($dir)) {
                mkdir($dir, $perms, true);
                echo "  ✓ ディレクトリ作成: {$dir}\n";
            }
        }
        
        // 所有権の設定
        $web_user = posix_getpwnam($this->web_user);
        foreach ($dirs as $dir => $perms) {
            chown($dir, $web_user['uid']);
            chgrp($dir, $web_user['gid']);
        }
        echo "  ✓ 所有権を設定\n";
        
        // 設定ファイルの保護
        $config_file = '/etc/webapp/database.conf';
        if (file_exists($config_file)) {
            chmod($config_file, 0600);
            echo "  ✓ 設定ファイルを保護\n";
        }
    }
    
    private function dropToWebUser() {
        $web_user = posix_getpwnam($this->web_user);
        $web_group = posix_getgrnam($this->web_group);
        
        if (!$web_user || !$web_group) {
            throw new Exception("Webユーザー/グループが見つかりません");
        }
        
        // 補助グループを設定
        posix_initgroups($this->web_user, $web_group['gid']);
        echo "  ✓ 補助グループを設定\n";
        
        // グループを変更(不可逆)
        if (!posix_setgid($web_group['gid'])) {
            throw new Exception("GID変更失敗");
        }
        echo "  ✓ GIDを変更: {$web_group['gid']} ({$this->web_group})\n";
        
        // ユーザーを変更(不可逆)
        if (!posix_setuid($web_user['uid'])) {
            throw new Exception("UID変更失敗");
        }
        echo "  ✓ UIDを変更: {$web_user['uid']} ({$this->web_user})\n";
        
        // 確認
        $this->verifyPrivilegeDrop();
    }
    
    private function verifyPrivilegeDrop() {
        $uid = posix_getuid();
        $gid = posix_getgid();
        
        if ($uid === 0 || $gid === 0) {
            throw new Exception("権限降格に失敗!まだroot権限です!");
        }
        
        // root権限に戻れないことを確認
        if (posix_setuid(0) || posix_setgid(0)) {
            throw new Exception("セキュリティ違反:root権限に戻れてしまいます!");
        }
        
        echo "  ✓ 権限降格を確認(root権限に戻れないことを確認)\n";
    }
    
    private function runApplication() {
        $uid = posix_getuid();
        $gid = posix_getgid();
        
        $user = posix_getpwuid($uid);
        $group = posix_getgrgid($gid);
        
        echo "  実行ユーザー: {$user['name']} (UID: {$uid})\n";
        echo "  実行グループ: {$group['name']} (GID: {$gid})\n";
        echo "  ✓ Webアプリケーションを安全に実行中\n";
        
        // この時点でroot権限は完全に失われている
        // アプリケーションのメイン処理を実行
    }
}

// 使用例
try {
    $launcher = new WebApplicationLauncher();
    $launcher->launch();
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
    exit(1);
}
?>

例4: マルチユーザーシステムでのジョブ実行

<?php
class UserJobExecutor {
    public function executeJobAsUser($username, callable $job) {
        echo "=== ユーザージョブ実行システム ===\n\n";
        
        // root権限で実行されていることを確認
        if (posix_getuid() !== 0) {
            throw new Exception("root権限が必要です");
        }
        
        // ユーザー情報を取得
        $user_info = posix_getpwnam($username);
        if (!$user_info) {
            throw new Exception("ユーザーが見つかりません: {$username}");
        }
        
        echo "ジョブを実行します:\n";
        echo "  ユーザー: {$username}\n";
        echo "  UID: {$user_info['uid']}\n";
        echo "  GID: {$user_info['gid']}\n";
        echo "  ホーム: {$user_info['dir']}\n\n";
        
        // 子プロセスでジョブを実行
        $pid = pcntl_fork();
        
        if ($pid == -1) {
            throw new Exception("フォークに失敗");
        } elseif ($pid == 0) {
            // 子プロセス:ユーザー権限でジョブを実行
            $this->dropToUser($user_info);
            
            try {
                // ユーザーのジョブを実行
                $result = $job();
                exit(0);
            } catch (Exception $e) {
                echo "ジョブ実行エラー: " . $e->getMessage() . "\n";
                exit(1);
            }
        } else {
            // 親プロセス:子プロセスの終了を待つ
            pcntl_wait($status);
            
            if (pcntl_wifexited($status)) {
                $exit_code = pcntl_wexitstatus($status);
                echo "\nジョブ完了(終了コード: {$exit_code})\n";
                return $exit_code === 0;
            }
            
            return false;
        }
    }
    
    private function dropToUser($user_info) {
        // 環境変数を設定
        putenv("HOME={$user_info['dir']}");
        putenv("USER={$user_info['name']}");
        putenv("LOGNAME={$user_info['name']}");
        putenv("SHELL={$user_info['shell']}");
        
        // 作業ディレクトリを変更
        chdir($user_info['dir']);
        
        // 補助グループを初期化
        posix_initgroups($user_info['name'], $user_info['gid']);
        
        // グループIDを変更(不可逆)
        if (!posix_setgid($user_info['gid'])) {
            throw new Exception("GID変更失敗");
        }
        
        // ユーザーIDを変更(不可逆)
        if (!posix_setuid($user_info['uid'])) {
            throw new Exception("UID変更失敗");
        }
        
        echo "  ✓ ユーザー '{$user_info['name']}' として実行中\n";
    }
}

// 使用例
$executor = new UserJobExecutor();

try {
    // ユーザー 'john' のジョブを実行
    $executor->executeJobAsUser('john', function() {
        echo "  → ジョブ実行中(現在のUID: " . posix_getuid() . ")\n";
        echo "  → ホームディレクトリ: " . getcwd() . "\n";
        echo "  → ファイルを作成中...\n";
        
        // ユーザーのホームディレクトリにファイルを作成
        file_put_contents('job_output.txt', 'Job completed successfully');
        
        echo "  → ジョブ処理完了\n";
    });
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例5: 権限降格チェッカー

<?php
class PrivilegeDropChecker {
    public function checkDropSafety() {
        echo "=== 権限降格の安全性チェック ===\n\n";
        
        $tests = [
            'testBasicDrop',
            'testCannotEscalate',
            'testGroupsCleared',
            'testEnvironmentSafe',
        ];
        
        $passed = 0;
        $failed = 0;
        
        foreach ($tests as $test) {
            try {
                if ($this->$test()) {
                    echo "✓ {$test}: PASS\n";
                    $passed++;
                } else {
                    echo "✗ {$test}: FAIL\n";
                    $failed++;
                }
            } catch (Exception $e) {
                echo "✗ {$test}: ERROR - {$e->getMessage()}\n";
                $failed++;
            }
        }
        
        echo "\n結果: {$passed} passed, {$failed} failed\n";
        
        return $failed === 0;
    }
    
    private function testBasicDrop() {
        // 基本的な権限降格のテスト
        $pid = pcntl_fork();
        
        if ($pid == 0) {
            // 子プロセス
            if (posix_getuid() !== 0) {
                exit(1); // 既にroot以外
            }
            
            // nobodyに降格
            $nobody = posix_getpwnam('nobody');
            if (!posix_setgid($nobody['gid']) || !posix_setuid($nobody['uid'])) {
                exit(1);
            }
            
            // 確認
            if (posix_getuid() === $nobody['uid'] && 
                posix_getgid() === $nobody['gid']) {
                exit(0); // 成功
            }
            exit(1);
        }
        
        pcntl_wait($status);
        return pcntl_wexitstatus($status) === 0;
    }
    
    private function testCannotEscalate() {
        // 権限昇格できないことを確認
        $pid = pcntl_fork();
        
        if ($pid == 0) {
            // nobodyに降格
            $nobody = posix_getpwnam('nobody');
            posix_setgid($nobody['gid']);
            posix_setuid($nobody['uid']);
            
            // root権限に戻ろうとする
            if (posix_setuid(0) || posix_setgid(0)) {
                exit(1); // 戻れてしまった(失敗)
            }
            
            exit(0); // 戻れなかった(成功)
        }
        
        pcntl_wait($status);
        return pcntl_wexitstatus($status) === 0;
    }
    
    private function testGroupsCleared() {
        // 補助グループがクリアされているか確認
        $pid = pcntl_fork();
        
        if ($pid == 0) {
            $nobody = posix_getpwnam('nobody');
            posix_initgroups('nobody', $nobody['gid']);
            posix_setgid($nobody['gid']);
            posix_setuid($nobody['uid']);
            
            $groups = posix_getgroups();
            
            // root(0)グループが含まれていないことを確認
            if (in_array(0, $groups)) {
                exit(1); // rootグループが残っている
            }
            
            exit(0);
        }
        
        pcntl_wait($status);
        return pcntl_wexitstatus($status) === 0;
    }
    
    private function testEnvironmentSafe() {
        // 環境変数が安全か確認
        $pid = pcntl_fork();
        
        if ($pid == 0) {
            $nobody = posix_getpwnam('nobody');
            
            // 環境変数をクリーンアップ
            putenv('HOME=' . $nobody['dir']);
            putenv('USER=nobody');
            
            posix_setgid($nobody['gid']);
            posix_setuid($nobody['uid']);
            
            // SUIDやSGIDが設定されていないことを確認
            $home = getenv('HOME');
            if ($home !== $nobody['dir']) {
                exit(1);
            }
            
            exit(0);
        }
        
        pcntl_wait($status);
        return pcntl_wexitstatus($status) === 0;
    }
}

// 使用例(root権限で実行)
$checker = new PrivilegeDropChecker();
$checker->checkDropSafety();
?>

注意点とトラブルシューティング

不可逆性の理解

<?php
// posix_setgidは一度実行すると元に戻せない

echo "初期GID: " . posix_getgid() . "\n";

// nobodyグループに変更
$nobody_group = posix_getgrnam('nobody');
posix_setgid($nobody_group['gid']);

echo "変更後GID: " . posix_getgid() . "\n";

// 元に戻そうとしても失敗する
if (!posix_setgid(0)) {
    echo "予想通り: root権限に戻れません\n";
    $error = posix_strerror(posix_get_last_error());
    echo "エラー: {$error}\n";
}

// これはセキュリティ機能です!
// 権限を降格したプロセスがroot権限を取り戻せないようにするため
?>

権限変更の正しい順序

<?php
// 重要: GID→UIDの順で変更する

function dropPrivilegesSafely($username) {
    $user = posix_getpwnam($username);
    
    if (!$user) {
        throw new Exception("ユーザーが見つかりません");
    }
    
    // ✓ 正しい順序
    // 1. 補助グループリスト
    posix_initgroups($username, $user['gid']);
    
    // 2. 実グループID
    if (!posix_setgid($user['gid'])) {
        throw new Exception("GID変更失敗");
    }
    
    // 3. 実ユーザーID(最後!)
    if (!posix_setuid($user['uid'])) {
        throw new Exception("UID変更失敗");
    }
    
    return true;
}

// ❌ 間違った順序
function dropPrivilegesWrong($username) {
    $user = posix_getpwnam($username);
    
    // UIDを先に変更すると...
    posix_setuid($user['uid']); // ここで一般ユーザーになる
    
    // GIDの変更権限を失う!
    posix_setgid($user['gid']); // 失敗する可能性が高い
}
?>

root権限チェック

<?php
function requireRoot($operation) {
    if (posix_getuid() !== 0) {
        throw new Exception(
            "{$operation}にはroot権限が必要です。\n" .
            "sudo php " . $_SERVER['PHP_SELF'] . " で実行してください"
        );
    }
}

// 使用例
try {
    requireRoot("権限降格");
    
    // root権限が必要な処理
    posix_setgid(33);
    posix_setuid(33);
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
    exit(1);
}
?>

エラーハンドリング

<?php
function safeSetGID($gid) {
    if (!posix_setgid($gid)) {
        $errno = posix_get_last_error();
        $error = posix_strerror($errno);
        
        switch ($errno) {
            case 1: // EPERM
                throw new Exception(
                    "権限がありません。GID {$gid} への変更が許可されていません"
                );
            case 22: // EINVAL
                throw new Exception("無効なグループID: {$gid}");
            default:
                throw new Exception(
                    "GID変更失敗: {$error} (errno: {$errno})"
                );
        }
    }
    
    // 変更が成功したか確認
    if (posix_getgid() !== $gid) {
        throw new Exception("GIDの設定に失敗しました(確認エラー)");
    }
    
    return true;
}

// 使用例
try {
    safeSetGID(33); // www-data
    echo "GIDを正常に変更しました\n";
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

関連する便利な関数

グループID関連関数の完全セット

<?php
// グループID取得
$gid = posix_getgid();       // 実グループID
$egid = posix_getegid();     // 実効グループID
$groups = posix_getgroups(); // 補助グループリスト

// グループID設定
posix_setgid($gid);          // 実GID設定(不可逆)
posix_setegid($egid);        // 実効GID設定(可逆)

// グループ情報取得
$info = posix_getgrgid($gid);      // GIDからグループ情報
$info = posix_getgrnam('www-data'); // 名前からグループ情報

// 補助グループ管理
posix_initgroups('username', $gid); // 補助グループを初期化

// ユーザーID関連(組み合わせて使用)
posix_getuid();    // 実UID
posix_geteuid();   // 実効UID
posix_setuid($uid); // 実UID設定(不可逆)
posix_seteuid($euid); // 実効UID設定(可逆)
?>

完全な権限降格テンプレート

<?php
function completePrivilegeDrop($username) {
    // root権限チェック
    if (posix_getuid() !== 0) {
        throw new Exception("root権限が必要です");
    }
    
    // ユーザー情報取得
    $user = posix_getpwnam($username);
    if (!$user) {
        throw new Exception("ユーザーが見つかりません: {$username}");
    }
    
    echo "権限降格を実行: {$username} (UID:{$user['uid']} GID:{$user['gid']})\n";
    
    // 1. 補助グループリストを設定
    if (!posix_initgroups($username, $user['gid'])) {
        throw new Exception("補助グループの初期化に失敗");
    }
    echo "  ✓ 補助グループを設定\n";
    
    // 2. 実効GIDを変更
    if (!posix_setegid($user['gid'])) {
        throw new Exception("実効GIDの変更に失敗");
    }
    echo "  ✓ 実効GIDを変更\n";
    
    // 3. 実GIDを変更(不可逆)
    if (!posix_setgid($user['gid'])) {
        throw new Exception("実GIDの変更に失敗");
    }
    echo "  ✓ 実GIDを変更(不可逆)\n";
    
    // 4. 実効UIDを変更
    if (!posix_seteuid($user['uid'])) {
        throw new Exception("実効UIDの変更に失敗");
    }
    echo "  ✓ 実効UIDを変更\n";
    
    // 5. 実UIDを変更(不可逆)
    if (!posix_setuid($user['uid'])) {
        throw new Exception("実UIDの変更に失敗");
    }
    echo "  ✓ 実UIDを変更(不可逆)\n";
    
    // 6. 検証
    verifyPrivilegeDrop($user['uid'], $user['gid']);
    
    echo "  ✓ 権限降格完了\n";
}

function verifyPrivilegeDrop($expected_uid, $expected_gid) {
    // UIDチェック
    if (posix_getuid() !== $expected_uid) {
        throw new Exception("実UIDが期待値と異なります");
    }
    
    if (posix_geteuid() !== $expected_uid) {
        throw new Exception("実効UIDが期待値と異なります");
    }
    
    // GIDチェック
    if (posix_getgid() !== $expected_gid) {
        throw new Exception("実GIDが期待値と異なります");
    }
    
    if (posix_getegid() !== $expected_gid) {
        throw new Exception("実効GIDが期待値と異なります");
    }
    
    // root権限に戻れないことを確認
    if (@posix_setuid(0) || @posix_setgid(0)) {
        throw new Exception("セキュリティ違反: root権限に戻れてしまいます");
    }
    
    // 補助グループにrootが含まれていないことを確認
    $groups = posix_getgroups();
    if (in_array(0, $groups)) {
        throw new Exception("セキュリティ違反: rootグループが残っています");
    }
}

// 使用例
try {
    completePrivilegeDrop('www-data');
    echo "\n安全に権限を降格しました。rootに戻ることはできません。\n";
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
    exit(1);
}
?>

まとめ

posix_setgid関数は以下の特徴があります。

✅ 実グループIDを永続的に変更 ✅ 一度変更すると元に戻せない(セキュリティ機能) ✅ 権限降格に必須 ✅ デーモンプロセスの安全な起動に使用 ✅ root権限からの不可逆な権限放棄

posix_setgidは、セキュアなシステムプログラミングの要です。root権限で起動したプロセスが、初期化完了後に一般ユーザー権限に降格し、二度とroot権限を取得できないようにすることで、セキュリティを大幅に向上させます。

ベストプラクティス

  1. 必ずGID→UIDの順: グループを先に変更してからユーザーを変更
  2. 補助グループも設定: posix_initgroups()を忘れずに
  3. 検証を実施: 権限降格後、root権限に戻れないことを確認
  4. 環境変数をクリーン: HOME、USER、SHELLなどを適切に設定
  5. ログに記録: 権限変更をログに残す

セキュリティの考慮事項

権限降格のチェックリスト

<?php
/*
権限降格の完全なチェックリスト:

[ ] 1. root権限で起動している
[ ] 2. 特権が必要な初期化を完了
[ ] 3. 補助グループリストを設定
[ ] 4. 実効GIDを変更
[ ] 5. 実GIDを変更(不可逆)
[ ] 6. 実効UIDを変更
[ ] 7. 実UIDを変更(不可逆)
[ ] 8. 環境変数をクリーンアップ
[ ] 9. root権限に戻れないことを確認
[ ] 10. rootグループが残っていないことを確認
*/
?>

よくある失敗パターン

<?php
// ❌ 失敗例1: 順序が間違っている
posix_setuid($uid);  // 先にUIDを変更すると...
posix_setgid($gid);  // GID変更の権限を失う

// ❌ 失敗例2: 補助グループを設定していない
posix_setgid($gid);
posix_setuid($uid);  // rootグループが残る可能性

// ❌ 失敗例3: 検証していない
posix_setgid($gid);
posix_setuid($uid);
// 本当に権限が降格されたか確認していない

// ✓ 正しい実装
posix_initgroups($username, $gid);  // 補助グループ
posix_setgid($gid);                 // GID変更
posix_setuid($uid);                 // UID変更
verifyDrop($uid, $gid);             // 検証
?>

セキュアな権限管理を! この記事が役に立ったら、ぜひシェアしてください。PHPのシステムセキュリティについて、他にも知りたいことがあればコメントで教えてくださいね。

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