餡子付゛録゛

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

サプライチェーン攻撃と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)