餡子付゛録゛

ソフトウェア開発ツールの便利な使い方を紹介。

Gitを(コンテナと組み合わせて)サプライチェーン攻撃から強くするためのRust製ラッパー

だれか書いている気がするのですが、Gitを(コンテナと組み合わせて)サプライチェーン攻撃から強くするためのRust製ラッパーを書いてみました。サプライチェーン攻撃とAIエージェントの暴走に強い、開発環境のコンテナー運用の話の続きです。

gitのフックは、サプライチェーン攻撃対策の障害

サプライチェーン攻撃対策で開発環境のコンテナ化をしたときに、運用上の脆弱性になるのがGitのhooksです。

  • コンテナー内にsshの秘密鍵などのクレデンシャル情報が置いてあると危険なので、GitHubなどへのpush/fetch/pullはホストで行うべきです。
  • コンテナーがマルウェアで汚染されたときに開発中のアプリケーションのソースコードが入ったGitリポジトリーは安全とは限らず、リポジトリー内のスクリプトなどの実行ファイルはホストから動かしてはいけません。
  • ホストでGitを用いると、その仕組みからリポジトリー内のhooksに設置されたスクリプトを自動実行してしまいます。
  • リポジトリー外からcloneやfetch/pullをしたとしても、Gitはclone元でgit-upload-packを動かします。git-upload-packにはcore.alternateRefsCommand / uploadpack.packObjectsHookが仕掛けられます。

たぶん公式の対策はない

ホストでhooksを動かさなければよいわけですが、Gitのグローバルオプションでhooksを無効にすることはできないようです。

~/.gitconfig に、

[core]
    hooksPath = /dev/null

と書くことで、.git/hooksにあるスクリプトの実行を防ぐことはできるのですが、.git/config に hooksPath = ./hooks と書かれていたら機能しません。グローバル設定よりもローカル設定が有効だからです。./git/hooks を消すか実行属性をなくせば良い気もしますが、hooksPath = ./other_place と書かれてother_placeに悪意のあるスクリプトを配置されたら意味がないです。

コマンドライン引数

コマンドライン引数でhooksPathを指定するのはどうでしょうか。

git -c core.hooksPath=/dev/null

ローカル設定よりも優先されます。しかし、Git 2.54からgitconfigにhookが書けるようになりました。configでの指定は、hooksPathよりも優先されるので、このコマンドライン引数では防止できません。

hookが動かないことを確認してからgitを動かすラッパー

解決策としてhookが動かないことを確認してからgitを動かすラッパーを書いてみました。tomlクレートを使った方がよい気もしますが、正規表現で単純に実装しています。リファクタリングをして、正規表現を用いない素朴な実装になりました。長くなったので、GitHubに置いています。

github.com

Rustのファイルなので、ホストで

git clone https://github.com/uncorrelated/chkgithooks
cargo build --release

とすると、target/release/chkgithooksができるので、

git config --add --global core.hooksPath /dev/null
mkdir -p ~/.local/bin
cp -pr target/release/chkgithooks ~/.local/bin/git
cp ~/.bashrc ~/.bashrc.bak
cat - <<EOL >> ~/.bashrc
export TRUE_GIT_PATH=/usr/bin/git # aptで入れるとここに入ります
export PATH=~/.local/bin:\$PATH
EOL
source ~/.bashrc

とすると、ホストでgitをラッパーとして機能します。ただし目的からホストでフックは使えなくなるのでご注意ください。コンテナ内のgitはラップしないので、フックも使えます。

ラッパーなしにgitを叩きたい場合は、$TRUE_GIT_PATH/git (か /usr/bin/git あたり)を呼びます。

追記(2026/10/17 05:20):.git/configでのhookの設定の記述方法を誤解していたので、コードを修正した。

追記(2026/10/17 16:00):リファクタリングを行って、正規表現を使わないようにした。

追記(2026/10/24 17:03):fsmonitorなどのhook以外の(他人に設定されると)危険な要素もチェックするようにした。

誰かすでに対策を立ててはいないのか

gitのフックを悪用した攻撃はすでに行われており、問題は広く知られているはずです。対策も、発想としては難しくはないです。誰か同じようなことをやっていると思うのですが、見当たらないんですよね。こういうラッパーは何かと問題を引き起こしがちなので、なるべく避けたいところではあるのですが。

サプライチェーン攻撃とAIエージェントの暴走に強い、開発環境のコンテナー運用(Dockerfileによるイメージ作成版)

泥縄式に操作をしていると不安になる人のために、「サプライチェーン攻撃とAIエージェントの暴走に強い、開発環境のコンテナー運用」のDockerfileを使う例を用意しました。

前のエントリーよりも、より実践的にしています。

  • Javaアプリ開発用としてインストールするパッケージを選んでいますが、用途に応じて適時書き換えてください。
  • デフォルトのユーザー名を javapg にして、podman-build時に引数で uncorrelated にしていますが、お好みのユーザー名に書き換えてください。rootパスワードも同様です。
  • ユーザーホーム名はユーザー名にあわせて変更されます。
  • ユーザーホームの変更により、podman runの引数が変更になります。
  • コンテナーのユーザーホームのホストでの位置は、$HOME/working.jdk に変更しました。

目次

Dockerfileの作成

cd /var/tmp
export DockerfileName=Dockerfile.jdk
cat - << "EOL" > ${DockerfileName}
FROM ubuntu:26.04

# 非対話モードの設定
ENV DEBIAN_FRONTEND=noninteractive

# 1. タイムゾーンの設定
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# 2. パッケージの更新と各種ツールのインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
    tzdata \
    # ロケール関連
    locales \
    language-pack-ja \
    manpages-ja \
    manpages-ja-dev \
    fonts-noto-cjk \
    fonts-noto-cjk-extra \
    # X Window System クライアント
    openbox \
    obconf \
    xterm \
    x11-xkb-utils \
    x11-apps \
    dbus-x11 \
    x11-xserver-utils \
    python3-xdg \
    # 日本語入力 (fcitx5)
    fcitx5 \
    fcitx5-mozc \
    fcitx5-config-qt \
    fcitx5-frontend-gtk3 \
    fcitx5-frontend-gtk4 \
    fcitx5-frontend-qt5 \
    # 開発ツール (Java & Ant & Maven)
    openjdk-26-jdk \
    openjdk-26-doc \
    openjdk-26-source \
    ant \
    maven \
    # その他のツール
    ca-certificates \
    vim-tiny \
    wget \
    curl \
    git \
    # システム設定
    && dpkg-reconfigure --frontend noninteractive tzdata \
    && locale-gen ja_JP.UTF-8 \
    && update-locale LANG=ja_JP.UTF-8 \
    # クリーニング
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 3. rootパスワードの設定
ARG ROOT_PASSWORD="your_root_password"
RUN echo "root:${ROOT_PASSWORD}" | chpasswd

# 4. 変数の定義
ARG USERNAME="javapg"
ARG GROUPNAME="developer"

# 5. ユーザー・グループの名称変更、ホームディレクトリのリネームと所有権の更新
RUN groupmod -n ${GROUPNAME} ubuntu && \
    usermod -l ${USERNAME} -d /home/${USERNAME} -m ubuntu

# 6. クライアント環境の設定 (指定したユーザーで実行)
USER ${USERNAME}
WORKDIR /home/${USERNAME}

# 7. .bashrc への追記(ヒアドキュメントを排除)
RUN printf '%s\n' \
    '# Java環境変数' \
    'export JAVA_HOME=/usr/lib/jvm/default-java' \
    '' \
    '# 日本語環境・IM設定' \
    'export LANG=ja_JP.UTF-8' \
    'export GTK_IM_MODULE=fcitx' \
    'export QT_IM_MODULE=fcitx' \
    'export XMODIFIERS=@im=fcitx' \
    'export DefaultIMModule=fcitx' \
    'alias openbox-session="dbus-launch --exit-with-session openbox-session &"' \
    >> /home/${USERNAME}/.bashrc

# 8. openbox の autostart 作成
RUN mkdir -p /home/${USERNAME}/.config/openbox && \
    printf '%s\n' \
    'setxkbmap jp &' \
    'fcitx5 &' \
    > /home/${USERNAME}/.config/openbox/autostart

CMD ["/bin/bash"]
EOL

ブラウザー

Seleniumを使うときなどでブラウザーが必要な場合は、2. パッケージの更新と各種ツールのインストールのブロックのあとに、以下のスニペットを挟んでください。

# 2.5. Mozilla公式リポジトリの追加とFirefoxのインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    wget \
    gpg \
    # GPG用のディレクトリーを作成
    && install -d -m 0755 /etc/apt/keyrings \
    && install -d -m 0700 /root/.gnupg \
    # Mozilla公式のGPG公開鍵を登録
    && wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null \
    # 公開鍵がインストールされたか確認 (35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3)
    && gpg -n -q --import --import-options import-show /etc/apt/keyrings/packages.mozilla.org.asc | awk '/pub/{getline; gsub(/^ +| +$/,""); if($0 == "35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3") print "\nThe key fingerprint matches ("$0").\n"; else {print "\nVerification failed: the fingerprint ("$0") does not match the expected one.\n"; exit 1}}' \
    # aptリポジトリーにMozilla公式を追加
    && echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null \
    # Mozilla公式の優先順位をubuntuリポジトリーよりも高くする
    && printf "Package: *\nPin: origin packages.mozilla.org\nPin-Priority: 1000\n" | tee /etc/apt/preferences.d/mozilla > /dev/null \
    # aptでFirefoxをインストール
    && apt-get update \
    && apt-get install -y --no-install-recommends firefox firefox-l10n-ja fonts-wqy-microhei \
    # ダウンロードしたファイルを削除
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

コンテナーイメージの作成

export DockerfileName=Dockerfile.jdk
export USERNAME=uncorrelated
export GROUPNAME=zombie
export ROOT_PASSWORD=password
export IMAGE_TAG=ubuntu:jdk

podman build \
  -f ${DockerfileName} \
  --build-arg USERNAME=${USERNAME} \
  --build-arg GROUPNAME=${GROUPNAME} \
  --build-arg ROOT_PASSWORD=${ROOT_PASSWORD} \
  -t ${IMAGE_TAG} .

コンテナー内ユーザーホームのホストへのコピー

非対話モードでコンテナーを起動して、ディレクトリーをコピーした後、即座に終了します。
なお、既に${WORKDIR}が存在している場合、${WORKDIR}/${USERNAME} としてコピーされます。

export USERNAME=uncorrelated
export IMAGE_TAG=ubuntu:jdk
export WORKDIR=$HOME/working.jdk

podman run --name devcon ${IMAGE_TAG}
podman cp devcon:/home/${USERNAME} ${WORKDIR}
podman rm devcon

コンテナーの起動

ホストにXephyrがインストール済みとします。--userns=keep-idをつけているので、初回は時間がかかることに注意してください。

export USERNAME=uncorrelated
export GROUPNAME=zombie
export IMAGE_TAG=ubuntu:jdk
export WORKDIR=$HOME/working.jdk
export DISPLAY_number=4
export DISPLAY_devcon=:${DISPLAY_number}

Xephyr $DISPLAY_devcon -nolisten tcp -br -screen 1024x768 &

sleep 1 # 1秒待って、Xephyr起動前にpodmanが動くのを回避

podman run --name devcon \
-e DISPLAY=$DISPLAY_devcon \
-v /tmp/.X11-unix/X${DISPLAY_number}:/tmp/.X11-unix/X${DISPLAY_number}:ro \
--userns=keep-id:uid=$(id -u),gid=$(id -g) \
--user ${USERNAME}:${GROUPNAME} \
--storage-opt overlay.mount_program=/usr/bin/fuse-overlayfs \
-v ${WORKDIR}:/home/${USERNAME}:Z \
-it ${IMAGE_TAG} bash

これでホストのディレクトリーをユーザーホームに使って、コンテナー内で作業ができます。また、openbox-session で、X Window Systemを使うことができます。

コンテナーの再開

Xephyrが終了しているとして、再開する場合は以下のようになります。

export DISPLAY_number=4
export DISPLAY_devcon=:${DISPLAY_number}

Xephyr $DISPLAY_devcon -nolisten tcp -br -screen 1024x768 &
podman start -ia devcon

サプライチェーン攻撃とAIエージェントの暴走に強い、開発環境のコンテナー運用

ここ数年間で、プログラミング言語のパッケージ(ライブラリー)や統合開発環境の拡張が汚染される事件の他、バイブコーディングのAIエージェントの暴走の被害が頻繁に報告されるようになりました。

これらの問題への対応として、開発環境のコンテナー運用*1(の準備)を行います。流行りのDev Containerを使っても良いのですが、VS CodeではないNetBeansなどのマイナー開発環境で使いづらいので避けます。

設定/作業のポイントは、

  • コンテナーのユーザーホームをホストのワーキングディレクトリーにする*2
  • コンテナーのユーザーとホストのユーザーのUIDとGIDをあわせる
  • コンテナーでもホストのX Window System(かWayLand)でGUIを使えるようにする
  • Dockerfileではなくexport / importでコンテナーのレイヤーを簡素化する(Dockerfileによるイメージ作成版は別に用意)

の4点です。運用環境ではなく開発環境なので、緩く作業していきます。

目次

コンテナーイメージのダウンロードとワーキングディレクトリーの作成

まずは何かのLinuxディストリビューションのDocker Imageを落としてきましょう。ここではpodmanを使いますが、dockerでもオプションは同様です。

export dist_tag='ubuntu:26.04'
podman pull $dist_tag

ダウンロードしたコンテナーイメージから、devcon と言う名前をふってコンテナーを作成・起動します。

podman run --name devcon -it $dist_tag bash

--nameは名前を指定するオプションです。無いと自動でつけられます。-iはホストの標準入力をコンテナーに送るオプション、-tはコンテナーに仮想端末を割り当てるオプションで、2つつけるとインタラクティブに使えるようになります。

コンテナーのrootパスワードを設定しておきます。

echo "your_root_password" | passwd -s

一般ユーザーのアカウントの追加は、最初からある ubuntu ユーザーを使うことにして省略します。

後ででもできますが、apt-updateとapt-installでできるパッケージのインストールもします。

軽量ウィンドウマネージャーOpenbox(とその設定ツールobconf)と、X用アプリを入れます。python3-xdgは、OpenboxがXDG標準のパスにある設定ファイルを読み込むのに使うツールです。今回の作業範囲ではなくてもエラーメッセージが出るだけですが、入れておきます。

apt update
apt install -y openbox obconf x11-apps python3-xdg

CTRL+dでシェルを終了して、コンテナーを停止した後、コンテナーのubuntuユーザーのホームをホスト側にコピーします。

podman cp devcon:/home/ubuntu ~/devcon

コンテナーをコンテナーイメージに保存して、コンテナーを終了します。

podman commit devcon devcon:latest
podman rm devcon

devcon:latest というコンテナーイメージができます。

コンテナーの起動

ホストが使っているX Serverをコンテナーに使わせるとセキュリティー上の問題が生じるため、ホストにコンテナー用のX Serverを用意します。

今回はXephyrを使うことにしました。インストールされていない場合は、以下のように入れます。

sudo apt install -y xserver-xephyr

ホストのX Serverが :0 を使っているので、ディスプレイ番号を :2 にして起動します。

export DISPLAY_number=2
export DISPLAY_devcon=:${DISPLAY_number}
Xephyr $DISPLAY_devcon -nolisten tcp -br -screen 1024x768 &

-nolisten tcpはTCP経由でのアクセスを制限するオプションで、-brは背景色に黒を指定するオプションです。-screenはスクリーンサイズです。

ホストをUbuntu Linux 26/Linux Mintで動かしている場合はkeymap関係で警告が出ますが、無視します。

ls /tmp/.X11-unix
X0  X2

で、X2が新たにつくられます。

コンテナーを作成・起動します。-userns=keep-idの副作用で、初回起動は待たされるので覚悟してください。幸い、2回目以降は待たされません。

podman run --name devcon \
-e DISPLAY=$DISPLAY_devcon \
-v /tmp/.X11-unix/X${DISPLAY_number}:/tmp/.X11-unix/X${DISPLAY_number}:ro \
--userns=keep-id:uid=$(id -u),gid=$(id -g) \
--user ubuntu:ubuntu \
--storage-opt overlay.mount_program=/usr/bin/fuse-overlayfs \
-v ~/devcon:/home/ubuntu:Z \
-it devcon:latest bash

-e DISPLAY=$DISPLAY_devconと-v /tmp/.X11-unix/X${DISPLAY_number}:/tmp/.X11-unix/X${DISPLAY_number}:roは、コンテナーからX Window Systemを使うための設定です。

--storage-opt overlay.mount_program=/usr/bin/fuse-overlayfsは、Podmanの動作を変更することで、--userns=keep-idを使った時の初回起動時間の問題を解消しています。

-v ~/devcon:/home/ubuntu:Zは、ホストの~/devconをコンテナーの/home/ubuntuに割り当て、SELinuxが動いていれば、SELinuxでのラベルを割り当てます。

なお、--memory=8g --cpus=4 --pids-limit=1024 とすると、最大メモリー8GB、最大利用CPU4、最大プロセス数1024と制限がかかります。コンテナーが使うリソースに制限を加えたい場合は、適時、数字を変えてつけましょう。

これでコンテナー内で

openbox &
xeyes

とすると、懐かしいマウスのポインターをじっと見つめる目玉が動き出します。

再開

xeyesを消して、CTRL + dで中断します。再開するときは、

podman start -ai devcon

です。-aはコンテナーのコンソール出力をホストのコンソール出力にattachするオプションです。

なお、podman-startの前にXephyrを動かしていないと、/tmp/.X11-unix/X${DISPLAY_number}がないとエラーになります。

開発環境を整える

/home/ubuntu に保存したファイルはホストの ~/devcon に入るので、公式サイトからダウンロードしたプログラミング言語の最新のバイナリーや統合開発環境と、それらの設定ファイルなどは特に操作なく揮発せずに保存されます。書いたソースコードなども /home/ubuntu 以下に保存しておけば、揮発せずに保存されます。ホスト側でシームレスにgitなどで操作が行えます。

クレデンシャル情報

sshの秘密鍵などをコンテナーに入れたら隔離している意味がなくなるので、GitHubなどへの同期などはホスト側で行ってください。なお、コンテナーからの操作を許すため、ユーザーホームのリポジトリーが汚染されている可能性はあり、Gitは便利機能により、汚染されたリポジトリーに弱いので、脆弱性をカバーするラッパーのコマンドを挟んだ方が良いです。

AIエージェントにpushさせたい場合は、AIエージェント用の鍵ペアを用意しましょう。

日本語化

コンテナー内で日本語を使う場合は、関連したパッケージをインストールします。

# ubuntuユーザーであればrootになる
su -

# タイムゾーンのパッケージのインストール(途中で設定なし)
ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
apt install tzdata

echo "Asia/Tokyo" > /etc/timezone
dpkg-reconfigure --frontend noninteractive tzdata

# 日本語入力に必要なパッケージのインストール
apt install -y fcitx5 fcitx5-mozc dbus-x11 x11-xserver-utils fcitx5-config-qt fcitx5-frontend-gtk3 fcitx5-frontend-gtk4 fcitx5-frontend-qt5 language-pack-ja manpages-ja manpages-ja-dev fonts-noto-cjk fonts-noto-cjk-extra x11-xkb-utils

# ロケールの設定
update-locale LANG=ja_JP.UTF-8

次に、ホストの ~/devcon(コンテナーでは /home/ubuntu)で設定ファイルを編集・作成します。

cd ~/devcon
mkdir -p .config/openbox
cat - << 'EOL'  >> .bashrc
export LANG=ja_JP.UTF-8
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export DefaultIMModule=fcitx
alias openbox-session="dbus-launch --exit-with-session openbox-session &"
EOL
cat - << 'EOL' > .config/openbox/autostart
setxkbmap jp &
fcitx5 &
EOL

Openboxの説明ページで .config/openbox/environment に環境変数を設定するようにあるものを見かけるのですが、どうも説明通りに動かないので、今回は.bashrcに書きました。どのみちLANGの設定は.bashrcに書く必要があります。

Openboxの起動

openboxコマンドではなくて、openbox-sessionを使います。

openbox-session

エイリアスを使ってdbus-launch経由で起動することで、dbus-daemonも同時に走らせます。openbox-sessionをそのまま使う場合は末尾に&が要りますが、今回はエイリアスにつけているので不要です*3。

fcitx5の設定ツールが動かして、fcitx5を起動するか聞かれなければ、正しく設定できています。

fcitx5-configtool

文字列の入力欄で半角/全角を押すか、SHIFT + SPACEを押せば、IMEを起動できます。

fcitx5-configtool

統合開発環境を使う場合はSHIFT + SPACEは無効化しておきましょう。Toggle Input MethodのControl + Spaceの横のゴミ箱アイコンを押して、ウィンドウの右下のApplyを押します。

文字サイズ

やたら文字サイズが小さいのですが、Openbox Configuration ManagerのAppearenceのFontsで大きく設定できます。

右クリックでApplication → Settings → …でも起動できますが、コマンドラインから

obconf

でも起動できます。

ターミナル

xtermでも良いのですが、今回はRust製のOpenGLによる高速なターミナルエミュレーターを入れます。

openboxを終了した状態で、

su -
unset WAYLAND_DISPLAY # Wayland環境でX Window Systemを使わせる; ホストがWaylandでは無い場合、コンテナー起動オプションにWayland用の設定がある場合は不要
apt install alacritty

とし、openbox-sessionでOpenboxを起動すると、右クリックメニューのTerminal emulatorで、alacrittyが動きます。

ここまでの手順では生じませんが、複数のターミナルエミュレーターが入っていて、切り替える必要がある場合は、rootで

update-alternatives --config x-terminal-emulator

としてください。

右クリックのメニュー

ubuntuユーザーで

cp /etc/xdg/openbox/menu.xml ~/.config/openbox/menu.xml

と menu.xml の雛形をコピーしてきて、XMLファイルを編集します。変更の反映はOpenboxの再起動です。

CA証明書

サーバーと通信を行うAIエージェントはコレがないと正常動作しません。

su -
apt install ca-certificates

curlか何かをインストールした後、httpsからのファイルダウンロードをしようとしてCA証明書がコンテナーに入っていないことに気づく気もするのですが、ホストにあれこれダウンロードさせていると気づかないかもなので。

ブラウザー

ホストのブラウザーを使えば間に合うことが多い気もしますが、アプリケーションからブラウザーを呼ぶ必要があるときはインストールします。

su -
apt update
apt install -y ca-certificates wget gpg
# GPG用のディレクトリーを作成
install -d -m 0755 /etc/apt/keyrings
install -d -m 0700 /root/.gnupg
# Mozilla公式のGPG公開鍵を登録
wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null 
# 公開鍵がインストールされたか確認; 35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3 はMozillaのGPG公開鍵のフィンガープリント
gpg -n -q --import --import-options import-show /etc/apt/keyrings/packages.mozilla.org.asc | awk '/pub/{getline; gsub(/^ +| +$/,""); if($0 == "35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3") print "\nThe key fingerprint matches ("$0").\n"; else print "\nVerification failed: the fingerprint ("$0") does not match the expected one.\n"}' 
# aptリポジトリーにMozilla公式を追加
echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null 
# Mozilla公式の優先順位をubuntuリポジトリーよりも高くする
echo -e "Package: *\nPin: origin packages.mozilla.org\nPin-Priority: 1000" | tee /etc/apt/preferences.d/mozilla  > /dev/null 
# aptでFirefoxをインストール
apt update
apt install -y --no-install-recommends firefox fonts-wqy-microhei 
# ダウンロードしたファイルを削除
apt clean

Ubuntu版はsnapを使うので都合が悪いので、Mozilla公式版にしています。

サウンド

aptでpulseaudioもinstallしておき、podman-runのオプションに

 -e PULSE_COOKIE=/tmp/pulse/cookie \
 -e PULSE_SERVER=unix:/tmp/pulse/native \
 -v /run/user/1000/pulse/native:/tmp/pulse/native \
 -v ~/.config/pulse/cookie:/tmp/pulse/cookie:ro \

を加えておくと、コンテナー内のアプリケーションが音を鳴らす事が出来ます*4。

Wayland対応

X Window Systemの利用を前提にしましたが、Waylandでの設定の紹介もあります。

Xで動くアプリケーションもまだ多く残存しているので、両方を設定するのが良さそうです。

# 以下3行は再掲
export DISPLAY_number=2
export DISPLAY_devcon=:${DISPLAY_number}
Xephyr $DISPLAY_devcon -nolisten tcp -br -screen 1024x768 &

export C_XDG_RUNTIME_DIR=/home/ubuntu/.runtime-dir
export C_WAYLAND_DISPLAY=$WAYLAND_DISPLAY

# labwcがインストールされているときは使う
if [ ! -z $(which labwc) ]; then
  export XKB_DEFAULT_LAYOUT=jp
  labwc &
  export PID_labwc=$!
  sleep 1 # 1秒のウェイト; labwcが起動するのを待つ; -sはプロセスID不明になるので使えない
  echo "PID: $PID_labwc"
  export C_WAYLAND_DISPLAY=$(lsof -w -a -p $PID_labwc -U 2>&1 | awk '/wayland/ {split($9, a, "/"); print a[length(a)]}' | head -n 1)
  echo "WAYLAND_DISPLAY=$C_WAYLAND_DISPLAY" # 空欄のときはlabwc起動前にプロセスが使っているファイル一覧を取得してしまっている蓋然性が高いので、ウェイトを増やす
  echo 'Run "GTK_IM_MODULE= fcitx5 &" when you need a Japanese IME in the Wayland compositor.'
fi

if [ ! -z "$C_WAYLAND_DISPLAY" ]; then
  podman run --name devcon \
  -e DISPLAY=$DISPLAY_devcon \
  -v /tmp/.X11-unix/X${DISPLAY_number}:/tmp/.X11-unix/X${DISPLAY_number}:ro \
  -e WAYLAND_DISPLAY=$C_WAYLAND_DISPLAY \
  -e XDG_RUNTIME_DIR=$C_XDG_RUNTIME_DIR \
  -v $XDG_RUNTIME_DIR/$C_WAYLAND_DISPLAY:$C_XDG_RUNTIME_DIR/$C_WAYLAND_DISPLAY:ro \
  --device /dev/dri \
  --userns=keep-id:uid=$(id -u),gid=$(id -g) \
  --user 1000:1000 \
  --storage-opt overlay.mount_program=/usr/bin/fuse-overlayfs \
  -v ~/devcon:/home/ubuntu:Z \
  -it devcon:latest bash
fi

ホストにWayland compositorのlabwcがインストールされているときは、labwcを起動して使うようにします。紛らわしいインターフェイスのマルウェアが、ホストのアプリケーションに混じって騙してくるのを防止できます。

openboxがコンテナーで動く一方、labwcはホストで動くので、labwc起動後に走るスクリプトでIMEを起動することはできません。必要であればコンテナーが動いてから起動します。

コンテナーの環境変数XDG_RUNTIME_DIRをユーザーホームの下においていますが、よくある解説の /tmp/runtime-dir におくと、--userns=keep-idを使っているため、以下のようなメッセージが出ます。

dbus[5]: Unable to set up transient service directory: XDG_RUNTIME_DIR "/tmp/runtime-dir" is owned by uid 0, not our uid 1000 

このメッセージが出た後、コンテナー内で

su - 
mkdir -p /tmp/runtime-dir
chown -R ubuntu:ubuntu /tmp/runtime-dir

としてディレクトリーとファイルの所有権を変えると抑制できますが、煩雑なのでユーザーホームの下に置きました。

イメージの保存とレイヤーの整理

ユーザーホーム以外のコンテナーに入る変更は、podman commitをしないと保存されません。パッケージを追加した後などは、CTRL + dで中断して、

podman commit devcon devcon:latest

で上書きしましょう。

ただし、これはコンテナーイメージのレイヤーの追加になります。必要以上に容量が肥大化しますし、127までしかレイヤーは重ねられないという制限があります。

コンテナーは頻繁に終了する必要はなく、CTRL + dの中断とpodman start -ai devconの再開で作業していけばよいですが、定期的にイメージ化した方が良いでしょう。

レイヤーが多くなりすぎた場合は、export / importでレイヤーを整理しましょう。

コンテナーが停止状態で、

podman export devcon -o devcon.bak

とすると、devcon.bakにデータを吐き出すので、

podman rmi -f devcon:latest
podman import devcon.bak devcon:latest
rm -fr devcon.bak

と、コンテナーイメージを消してからインポートし、バックアップを消しましょう。

開発ツールの隔離目的で使う分で必要になるかは分かりませんが、レイヤーと一緒に消えるメタ情報は、podman runするときに --workdir /root や --entrypoint /bin/bash で指定することができます。メタ情報をイメージつけたい場合は、-c "USER=ubuntu" -c "WORKDIR=/home/ubuntu" -c "ENTRYPOINT=/bin/bash" をpodman importにつけます。

Dockerfileを作らずに場当たり的にイメージを変更するのはbad-practiceではないかと思われるかも知れませんが、開発中は予定外にインストールやアップデートが起きるものです。インストールしたパッケージ等のメモをとっておいてDockerfileを書き直し、イメージを作りなおすよりは手軽に運用できます。

ホストとコンテナーのファイル共有にあるリスク

コンテナーがマルウェアに感染されると、共有しているユーザーホームにマルウェアを配置されるリスクがあります。ホストから共有ディレクトリー内のファイルは原則実行しないことが肝要です。ソースコードの改竄もありえるので、コンパイル後のコードもリスクがあります。スクリプトやソースコードを確認し、マルウェア混在がないと信じられるときだけ実行しましょう。

*1:仮想マシンでも良いのですが、コンテナーの方が必要リソースが少ない利点があります。

*2:コンテナーが汚染されてマルウェアが混入している場合、ホストでコンテナーで作成したファイルを実行するとホストも汚染されうる事になります。サプライチェーン攻撃では、外部パッケージ/ライブラリーのインストールをトリガーに動くものがほとんどで、AIエージェントがウイルスを仕込んだと言う話もまだ聞いたことはないですが、原則としてコンテナーで作成したファイルはホストで安易に実行しないようにしましょう。

*3:dbus-launchに&をつけ忘れるとキー入力を持っていかれて困ることになるので、変則的ですがエイリアスの方につけました。

*4:Dockerコンテナ上のGUIアプリで音を出す(Linux)

悪意のあるVS Code拡張からシステム破壊とクレデンシャル漏洩を予防するためのUbunt Linux/AppArmorの設定

今年5月に悪意あるVS Code拡張機能による情報漏えい事件があったのですが、Ubuntu LinuxかLinux Mintを使っている場合は、AppArmorによってVisual Studio Codeに強制アクセス制限をしてしまうのが手軽なようです*1。

  • 広範にファイル読み込み、TCP/IP通信は許可
  • リポジトリーを置くワーキングディレクトリー(本稿では ~/working がそれだとします)とVS Codeの設定ファイルと一時ファイルのディレクトリー以外は書き込みを許さない
  • ユーザーホームの .ssh/ などへの読み込みを禁止する
  • ユーザーホーム以外のマルウェアに参照されたくないファイルは、従来からのファイルパーミッションに任せる

方針でVS Codeに強制アクセス制限をかけます。これで、悪意ある拡張が秘密鍵などのクレデンシャル情報を盗んだり、システム領域などにトロイの木馬を仕掛けたりするのを防げます。

ワーキングディレクトリーにマルウェアが仕掛けられ、かつ、それをVS Code以外から実行してしまうと強制アクセス制限の網から逃れられる事になりますが、だいぶリスクは軽減できるでしょう。なお、VS CodeからGitなどでリモートリポジトリーを更新できなくなる弊害があります。

Ubuntu 24のAppArmorの設定ファイルの修正

Ubuntu Linux 24のAppArmorの以下の設定ファイルは初期値で壊れています。

  • /etc/apparmor.d/abstractions/pasta
  • /etc/apparmor.d/abstractions/passt
mount options=(rw, runbindable) /,

を

mount options=(rw, runbindable) -> /,

と -> を挿入して置き換え、

"" -> "/tmp/", 

を

-> "/tmp",

と""を削除してお聞ければ修正できます。

sudo aa-logprofをしていたら、

ERROR: Operation {'runbindable'} cannot have a source. Source = AARE('/')

と言うエラーが出で気づいたのですが、最初は何事かと思いました。

VS CodeのApp Armor

Ubuntu Linux 24の初期状態のプロファイル /etc/apparmor.d/code では、VS Codeの実行ファイルである /usr/share/code/bin/code に何の制限もかかっていません。

方針に沿って、以下のように書き換えます。

/etc/apparmor.d/code

abi <abi/4.0>,

include <tunables/global>

profile vscode /usr/share/code{/bin,}/code flags=(attach_disconnected) {

# 標準的な設定はインクルードで済ます
  include <abstractions/base>
  include <abstractions/dbus-session>
  include <abstractions/fcitx>
  include <abstractions/fonts>
  include <abstractions/freedesktop.org>
  include <abstractions/lightdm>
  include <abstractions/nameservice>
  include <abstractions/passt>
  include <abstractions/postfix-common>
  include <abstractions/user-tmp>

  network,

# デバッガーがVS Codeから起動していないプロセスにattachする場合に必要になるが、
# メモリー上のクレデンシャル情報の漏洩や、キーロガー等のマルウェアの混入を許すので、
# 許可しない
#  ptrace (read, trace) peer=unconfined,

  deny @{HOME}/.ssh/ r,
  deny @{HOME}/.ssh/** rw,
  deny @{HOME}/Documents/** rw,
  deny @{HOME}/Desktop/** rw,
  deny @{HOME}/Music/** rw,
  deny @{HOME}/Pictures/** rw,
  deny @{HOME}/Videos/** rw,
  deny @{HOME}/Templates/** rw,

# 標準的な設定(abstractions/*)では入らないがVS Codeの動作に必要
  /dev/shm/* rwk,
  /proc/** rw,
#  /sys/devices/system/** r, # aa-logprofでは /sys/devices/system/node/** の読み込みあり
  /usr/** mrix,
  /usr/share/code/chrome-sandbox px,

  owner @{HOME}/ r,
  owner @{HOME}/** mrixk, # ユーザーホームに配置したコンパイラーなどの改ざん防止で書き込みは許可しない
  owner @{HOME}/.cache/** mrwixk,
  owner @{HOME}/.config/Code/** mrwixk,
  owner @{HOME}/.copilot/** mrwixk,
  owner @{HOME}/.pki/** rwk,
  owner @{HOME}/.vscode-shared/** mrwixk,
  owner @{HOME}/.vscode/** mrwixk,
  owner @{HOME}/working/** mrwixk,

  userns,

}

許可は明示的に書かないと禁止になります。

flags=(attach_disconnected)は、コンテナーやサンドボックスが使われた時に、AppArmorがアクセス拒否をしないようにするための設定です。

denyをつけておくと、他の行に関わらず禁止になります。

@{HOME}は /root や /home/uncorrelated を指すマクロです。

パーミッションの種類は、

m mmap() による実行可能領域へのメモリマッピングを許可(DLLのロード等に必要)
k ファイルの排他ロック(flock や fcntl)を許可
r 読み込み
w 書き込み
ix 現在のプロファイルを継承して実行(親と同じ制限)
px 指定の固有プロファイルに遷移して実行(親と異なる制限)

になります。

usernsは、非特権ユーザーがuser namespaceを作成することを許可する命令で、昔はデフォルトでした。

ファイルを読み込ませたら、AppArmorにこのプロファイルを再読込させます。

sudo apparmor_parser -r /etc/apparmor.d/code

これで、悪意のあるVS Code拡張にだいぶ強くなります。

動作確認

大雑把に変更が反映されているから確認しましょう。~/.ssh/にあるファイルをVS CodeからCTRL + Oをして読もうとすると、

VS Codeのファイルアクセスのエラーメッセージ

となります。VS Codeの子プロセスからも同様なので、VS Code内のシェルからもアクセスできません。

VS Codeの挙動がおかしいときは

これで問題無いとは思うのですが、プロファイルの制限が強すぎる可能性が高いおかしい挙動に突き当たった場合は、AppArmorの設定ガイドライン通り、flags=(attach_disconnected)をflags=(complain,attach_disconnected)に書き換えてプロファイルを再読込させて、ターミナルでsudo aa-logprofを動かしならがらVS Codeを操作してください。

ただし、新たにインストールした無名な拡張にとってプロファイルの制限が強すぎる場合は、無名な拡張を疑った方がよいかもです。いつも使っている有名な拡張でも、アップデート後に制限に引っかかるようになった場合は、サプライチェーン攻撃で汚染されている可能性もあります。

*1:開発環境全体をDockerコンテナーに入れてしまうことも考えたのですが、VS Codeの更新頻度から煩雑になり過ぎるのでやめました。なお、更新頻度が高くてもユーザーホーム以下のファイルとディレクトリーしか更新しない場合は、そう困るわけではないです。

正規表現によるMarkdownの表の列の追加と削除

Markdown記法の文書中に表を入れることもあります。列名と文字寄せしか装飾が無い簡素な場合は、

|id|name|
|:---:|:---|
|001|noah|
|002|sophia|

と言うようにバーティカルバー(|)で区切りを入れ、:---(左寄せ):---:(中央寄せ)---:(右寄せ)を指定する行の一つ上の行が列名、下の行が列名以外の1行目以降の行となります。意図せず一行の表になったりするので*1罠な面もあるのですが、便利なときもある文法です。

編集していて厄介なのが、列の追加と削除です。CSVと相互変換できるツールもありますし、VSCodeであれば表を直接編集する事もできるのですが、一般のテキストエディターであれば正規表現で置換する方が手っ取り早いかもしれません。Perl互換正規表現が使えるとして、

操作 置換対象 置換文字
J個目の|の後に|を追加 ^((?:[^|\n]*\|){J}) $1|
J個目の|の後、|までを削除 ^((?:[^|\n]*\|){J})[^|\n]*\| $1

で複数行の置換を行えば、列の追加と削除になります。Jは任意の数字に変えて使ってください。列追加の場合は、文字寄せを追記しないと表として解釈されないかもしれません。

*1:行頭が | であるかで表なのか判別していて滅多に遭遇しませんが、利用者の意図を汲んでくれるわけではないです。なお、表ではないが行頭がバーティカルバーの場合は \| もしくは&#x7c;と数値参照入力で書きます。

PlantUMLでER図を描こう

PlantUMLは総本山のドキュメントも充実していますし山のように紹介記事もあるので、生成AIもよく知っているツールで資料的に困る事はないのですが、PlantUMLでER図を描く紹介の準備をしたいと思います。

インストール

Javaアプリケーションなので、Javaのインストールが必要です。まだインストールしていない場合は、Java 11以降をインストールしましょう。安定版の最新で問題ないです。Ubuntu Linuxの場合は、

sudo apt install openjdk-25-jdk -y

でインストールできます。

PlantUMLはGPLv2版のPlantUMLをダウンロードしてきます。何かに組み込んで使う場合でない場合は、GPL版で困ることはないです。バージョンは、本稿執筆時は1.2026.6が最新でした。

cd ~
wget https://github.com/plantuml/plantuml/releases/download/v1.2026.6/plantuml-gplv2-1.2026.6.jar

続けて.bashrcにaliasを追記して、souceで読み込みなおします。

cp -pr .bashrc .bashrc.bak
grep -v plantuml .bashrc.bak > .bashrc
echo "alias plantuml='java -Xmx256m -jar ~/plantuml-gplv2-1.2026.6.jar -Playout=smetana'" >> .bashrc
source .bashrc

JVMの利用メモリー量を256MBに抑え、標準のGraphvizではなく内蔵レンダリングエンジンのsmetanaを使う指定をしています。Graphviz(dotコマンド)はこれで無くても大丈夫です。

エンティティーを記述したファイルを作成

ER図のエンティティーを記述したファイルを作成しましょう。エンティティーは説明のために使いまわしがあるので、別ファイルにしておくほうが便利です。

entities.puml

@startuml

' 主キーuser_idと、nameとユニークなemailを持つ「ユーザー」テーブルを定義し、UserというPlantUMLで使う名前をつける
entity "ユーザー" as User {
  + user_id : int
  --
  name : text
  *email : text
}

' Userの表示をカスタマイズして、マスターだとはっきりわかるようにする
User << (M, cyan) Master >>

' 主キーorder_idとorder_dateと外部キーのuser_idを持つ「注文」テーブルを定義し、Orderという名前をつける
entity "注文" as Order {
  + order_id : int
  --
  order_date : date
  # user_id : int <<FK(User,user_id)>>
}

' Orderの表示をカスタマイズして、トランザクションだとはっきりわかるようにする
Order << (T, lightgreen) Transaction >>

' removeの説明用エンティティ
entity "無用" as Useless {
  + trash_id : int
}

@enduml

@startumlと@endumlはUMLであることを示します。PlantUMLではクラス図をカスタマイズしてER図にしています。
シングルコーテーションはコメントをあらわします。/'と'/で挟むことでコメントをブロック指定できます。
--はエンティティー表示の区切りになります。==を使うと二重線になります。
要素の頭についている+は主キー、*はユニーク、#は外部キーをあらわすアイコンになります。

  1. user_id : intと、要素名 : 型名になっていますが、型名は自由に入ります。<>も<>と書いてもエラーになったりしません。

リレーションを記述したファイルを作成

エンティティーを記述したファイルをインクルードして、リレーションを記述します。

relations.puml

@startuml example_ER

/'
 entities.pumlを読み込む
 includeだと何回も読み込んで展開できる
'/
!include_once entities.puml
/' 不要なエンティティーが描画されないように削除 '/
remove Useless

/'
(E)や(C)といった目印文字を表示する
表示しない場合は hide circle
'/
show circle

/'
  UserとOrderの関係を1対0以上で右並びで表示し、リレーションの線の上に注釈を書く
'/
User ||--r--o{ Order : This is a relation.

@enduml

@startumlの後のexample_ERは図表の名前で、example_ER.pngのようなファイルがつくられることになります。省略時はファイル名が用いられますが、IDEによっては必須です。
||--r--o{の--は線種です。--は実線、..は破線になります。
エンティティーの並びは、線の間にd(上から下)r(左から右)u(下から上)l(右から左)のどれかをはさみます。省略可能です。

リレーションの種類

関係 記号
1 対 0か1 ||--o|
0か1 対 1 |o--||
1 対 1 ||--||
1 対 0以上 ||--o{
0以上 対 1 }o--||
1 対 1以上 ||--|{
1以上 対 1 }|--||
1以上 対 1以上 }|--|{

エンティティーを枠でくくる

エンティティーをpackageでまとめると、ひとまとめとして示すための枠をつけられます。

relations.pumlの!include_onceを以下のようにpackageでくくるような方法でも、使えます。

package "注文"  {
    !include_once entities.puml
    remove Useless
}

packageの代わりに、rectangleやframeという選択肢もあり、見栄えが変わります。

注釈をつける

エンティティーの周りに注釈を入れられます。

note left of User : 成人男性

packageの括弧の中に入れると枠線内に描画され、外に入れると枠線外に描画されます。

スタイルの設定

エンティティーやリレーションの色や背景色、フォントをまとめて指定する場合は、styleタグの中でスタイルを定義します(PlantUML Styles: Modern CSS-like Styling for Diagrams)。以前はskinparamで指定していたのですが、廃止になりました。

コンパイルする

以下でexample_ER.pngをつくってくれます。

plantuml relations.puml
PlantUMLで描いたER図

エイリアスを設定していなければ、

java -Xmx256m -jar ~/plantuml-gplv2-1.2026.6.jar -Playout=smetana' relations.puml

となります。

エンティティーを枠でくくって注釈をつけた場合は、以下のようになります。

エンティティーを枠でくくって注釈をつけた場合


SVGで出したい時は-tsvg、EPSで出したい場合は-tepsをオプションにつけます。

plantuml relations.puml -tsvg
plantuml relations.puml -teps

plantuml --helpで使い方が出てきますが、出力ファイル名を変えたい場合はパイプ出力を使いましょう。

plantuml -pipe < relations.puml > result.png

Dynamic Panel Data (Serial Correlation) Model

北村 (2005)で紹介されている*1のですが、あまり使われていない手法を紹介します。

\begin{align}
y_{it} &= \mu_i + X_{it} \beta + w_{it} \\
w_{it} &= \rho w_{i,t-1} + u_{it}
\end{align}

個体i、時点t、従属変数y_{it}、固定効果\mu_i、説明変数 X_{it}、係数\beta、誤差項 w_{it}として、 w_{it}が自己回帰しているモデルです。\rhoは自己回帰項の係数、u_{it}がi.i.d.の確率変数です。

これをParis-Winsten変換をかけて推定してみましょう。

DGP

データセットは生成します。DGPのβは {}^t\!(0, -1, 2)です。

DGP <- function(N = 10, T = 10){
    u <- matrix(rnorm(N*T, sd = 1), N, T)
    w <- u
    rho <- 0.5
    for(j in 2:T){
        w[, j] <- rho*w[, j - 1] + u[, j]
    }
    beta <- c(0, -1, 2)
    mu <- rep(seq(0, 100, length.out = N), T)
    t <- rep(1:T, each = N)
    i <- rep(1:N, T)
    X <- cbind(1, matrix(runif(N*T*(length(beta) - 1)), N*T, length(beta) - 1))
    y <- mu + X %*% beta + c(w)
    df <- data.frame(y, X[, -1], as.factor(i), as.factor(t))
    colnames(df) <- c("y", "x", "z", "i", "t")
    df
}

set.seed(1023)
df01 <- DGP(20, 10)
head(df01, 3)
           y         x         z i t
1 -0.5524994 0.3613191 0.2426655 1 1
2  7.0878804 0.9227929 0.5528946 2 1
3 10.6797476 0.7026300 0.5465261 3 1

推定

まず、LSDVで推定します*2。

r_lm01 <- lm(y ~ x + z + i + t, df01)
coef(summary(r_lm01))[1:3, ]
               Estimate Std. Error    t value     Pr(>|t|)
(Intercept) -0.07491713  0.4589730 -0.1632277 8.705341e-01
x           -0.39637808  0.2938260 -1.3490232 1.791342e-01
z            1.77550099  0.2677757  6.6305520 4.317182e-10

推定された係数は、DGPの係数と乖離が大きめです。

sum(tapply(w_hat, df01$i, \(x) sum(diff(x)^2)))/sum(w_hat^2)
[1] 1.08872

パネルデータ版のダービン=ワトソン比も1付近で、微妙ですが系列相関の可能性を示唆します。

 \rhoを推定します。

T <- length(levels(df01$t)) # 時点の数
N <- length(levels(df01$i)) # 個体の数
w_hat <- residuals(r_lm01) # 誤差項
lhs <- df01$t != 1 # t=1時点のデータ以外の行
rhs <- df01$t != T # t=T時点のデータ以外の行
# ρを推定
r_lm02 <- lm(w_hat[lhs] ~ w_hat[rhs] + 0)
(rho_hat <- coef(r_lm02)[1])
w_hat[rhs] 
 0.3557187 

DGPでは0.5なのでやや小さめですが、とりあえずは気にしない事にします。

Paris-Winsten変換をかけてみましょう。

# Paris-Winsten変換
y <- with(df01, y[lhs] - rho_hat*y[rhs])
x <- with(df01, x[lhs] - rho_hat*x[rhs])
z <- with(df01, z[lhs] - rho_hat*z[rhs])
# lmコマンドを使う都合で、ダミー変数(と切片項の1)はParis-Winsten変換せず、推定に使う時点の観測値をそのまま使う
i <- with(df01, i[lhs])
t <- with(df01, t[lhs])
# 差分モデルで推定
r_lm03 <- lm(y ~ x + z + i + t)
coef(summary(r_lm03))[1:3, ]
              Estimate Std. Error    t value     Pr(>|t|)
(Intercept)  0.2182374  0.3830573  0.5697253 5.697163e-01
x           -0.7630830  0.2382007 -3.2035302 1.658259e-03
z            1.9556396  0.2256916  8.6650984 6.714954e-15

切片項以外は大きく改善しました。固定効果モデルなので、切片項は個体効果ダミーと合算して評価すべきです。この変化は問題ありません。改善です。

固定効果の推定値と切片項の合計値を見ると、概ねDGPの値に近いです。0から100まで個体数で等分した値に近くなっています。

i_ptr <- 4:(2 + N)
coef(r_lm03)[1] + c(0, coef(r_lm03)[i_ptr]/(1 - rho_hat)) # Paris-Winsten(事後?)変換
                   i2         i3         i4         i5         i6         i7 
 0.2182374  5.7498597 10.7469271 15.5186148 21.4453556 26.5662748 31.7242678 
        i8         i9        i10        i11        i12        i13        i14 
37.1064273 42.5975526 45.7352265 53.0230941 57.9536065 64.6846864 68.1316753 
       i15        i16        i17        i18        i19        i20 
74.4829777 79.4217145 85.6120205 89.1234461 94.7430600 99.4748170 

ウェイト付き最小二乗法になるので、因子型を自動展開して入れたダミー変数以外の係数の標準誤差はそのまま使えます。ダミー変数の係数をParis-Winsten(事後?)変換する場合は、標準誤差もスケールしてください。

Baltagi and Li (1997)によるより良い \hat{\rho}

 \rhoの推定ですが、Baltagi and Li (1997)で以下の小標本のシミュレーションでより効率のよい手法が提案されています。

Q <- function(s, r_lm){
    df <- r_lm01$model
    N <- length(levels(df$i))
    T <- length(levels(df$t))
    u <- residuals(r_lm)
    r <- 0
    for(t in (s + 1):T){
        t0 <- t - s
        r <- r + sum(u[df$t == t] * u[df$t == t0])
    }
    r / (N*(T - s))
}
rho_hat_by_Q <- function(r_lm) (Q(1, r_lm) - Q(2, r_lm)) / (Q(0, r_lm) - Q(1, r_lm))
(rho_hat <- rho_hat_by_Q(r_lm01))
[1] 0.5679262

DGPの係数は0.5なので、確かにこちらの方がパフォーマンスが良いです。これを使った推定をすると、

              Estimate Std. Error    t value     Pr(>|t|)
(Intercept)  0.2140068  0.3794652  0.5639695 5.736171e-01
x           -0.7967706  0.2212629 -3.6010139 4.302451e-04
z            1.9605123  0.2098229  9.3436523 1.214826e-16

となります。こちらも微妙に改善されました。

*1:Baltagi (2005) "Econometric Analysis of Panel Data" のpp.84–86にもあります

*2:誤差項が出ればwith推定でも大丈夫です。