20250922 Ubuntu24.04+nerdctl+vscode devcontainers 研究

昨日今日は集中的にdevcontainerを研究していた。というのはnpmの汚染がなんかすごい感じになっていて(曖昧)とりあえずnpmからダウンロードしたパッケージに悪さをされることはもう避けられないっぽいので、それでも耐えられる環境づくりがテーマだ。

もうひとつ、nerdctl(rootless)でやるというのも裏テーマ。僕の私物PCにはdockerの代わりにnerdctlしか入ってないからだ。特に深い理由はないが、なんかdockerはそろそろ標準じゃなくなるっぽいという話を数年前に聞いて以降そうしている。

AIに手伝わせながら作ったのがこんな感じだ。

{
    "name": "Node.js & TypeScript",
    "build": {
        "dockerfile": "ContainerFile"
    },
    "postCreateCommand": "rm -rf /tmp/vscode-ssh-auth-* && npm ci --ignore-scripts",
    "containerEnv": {
        "TZ": "Asia/Tokyo",
        "NPM_CONFIG_IGNORE_SCRIPTS": "true",
        "SSH_AUTH_SOCK": "",
        "SECRET": "dummy"
    },
    "runArgs": [
        "--cap-drop=ALL",
        "--security-opt", "no-new-privileges",
        "--pids-limit", "512",
        "--tmpfs", "/tmp:rw,noexec,nosuid,nodev",
        "--tmpfs", "/var/tmp:rw,noexec,nosuid,nodev",
    ]
}
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm

RUN usermod -aG root node

--cap-drop=ALLとかno-new-privileges、--pids-limit、/tmp潰しとかは基本らしいのだがSSH_AUTH_SOCKの周りは結構調査が必要だった。vscodeが作るdevcontainerはデフォルトでホストのssh-agentのソケットをコンテナ内にマウントして露出してしまい、これを止めるオプションはない。コンテナ内のsshはSSH_AUTH_SOCK経由でソケットファイルの場所を知るので、それを上書きしてしまえば良い…とAIは言うが、ソケットの置き場は/tmpで名前も明らかにそれとわかる感じなので名前だけ隠してもコンテナ内で悪意あるプログラムが動きうるという前提なら何も隠せてない。なのでpostCreateCommandでrmしている。

ContainerFileでusermodしているのはこうしておかないとホストとコンテナ内のファイルパーミッションが合わず、コンテナ内からファイルに書き込めないから。

nerdctlが正式サポートされていないのである程度コツが必要だった。AppArmorがユーザー名前空間の利用を制限している問題は、エラーログに解決法が書いてあり、その通りにこれを実行したら直った。

cat <<EOT | sudo tee "/etc/apparmor.d/usr.local.bin.rootlesskit"
# ref: https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces
abi <abi/4.0>,
include <tunables/global>

/usr/local/bin/rootlesskit flags=(unconfined) {
  userns,

  # Site-specific additions and overrides. See local/README for details.
  include if exists <local/usr.local.bin.rootlesskit>
}
EOT
sudo systemctl restart apparmor.service

rootlesskitとbuildkitは必要。runコマンドの--sig-proxyオプションも必要なのでnerdctlのバージョンはv2.0.0以上。

以下のように、ローカルのイメージをベースにして更にイメージをビルドしようとすると失敗することがある。devcontainerも何らかの差分ビルドをやっているのか、こういうエラーが起きることがあった。これはbuildkitをcontainerdを使うように設定すると直った

chao@chao-home:~/nerdctl-test$ nerdctl build -f Dockerfile.B .
[+] Building 1.1s (2/2) FINISHED                                                                                                                        
 => [internal] load build definition from Dockerfile.B                                                                                             0.0s
 => => transferring dockerfile: 107B                                                                                                               0.0s
 => ERROR [internal] load metadata for docker.io/library/my-test-image:latest                                                                      1.1s
------
 > [internal] load metadata for docker.io/library/my-test-image:latest:
------
Dockerfile.B:1
--------------------
   1 | >>> FROM my-test-image:latest
   2 |     RUN echo "This is image B" > /image-b.txt
   3 |     
--------------------

error: failed to solve: my-test-image:latest: failed to resolve source metadata for docker.io/library/my-test-image:latest: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed

FATA[0001] no image was built

秘密鍵はコンテナにマウントしたくないのでgit pushはdevcontainerの外で行うことにするが、git commitは悩みどころだ。commit hookで様々なnpmスクリプトが動くことを考えるとコンテナ内にしたいが、CIを勝手に回されるリスクを避けるためにはcommit署名を使うべきで、署名鍵はコンテナ内に入れたくないので、commitはコンテナ外ですることになる。結局commitをコンテナ内でやりつつ署名用gpg鍵のパスフレーズをホスト側で毎回要求することで解決した。

# ~/.gnupg/gpg-agent.conf
default-cache-ttl 1
max-cache-ttl 5

この設定だと、コンテナ内でgpg鍵を使おうとしたときに、毎回ホストマシン側にパスフレーズ入力ダイアログが現れる。vscodeはgpg-agentも勝手にコンテナ内にフォワーディングしているのでこれ以外の設定は不要。

次の段階としてインターネットアクセスもドメインで制限したいのだが、--cap-drop=ALLをつけているためコンテナからiptablesを操作できず、かなり面倒な形になりそうだ。そこまでしなくてもいいかなあ。でもここまで来たならやりたいよな。あとはnodeのpolicyとか、ビルド成果物のチェック、動作確認用のブラウザもサンドボックス内で実行とかもやっていきたい。

Raspberry Pi Zero WHでNASしたい、失敗録1

配線

私はRaspberry Pi Zero WHを所有していて、これまで自宅の室温を記録してサーバーに送信していた。突然NASを組みたくなり、これを利用できないかと考えた。

ざっくりラズパイにストレージが接続さえできればあとはどうにでもなりそうだったので、アダプター類を購入して準備した。具体的には、Zero WHはUSB micro-B凹端子しかないので、ここから

  • USB micro-B凸 to USB A凹 変換
  • USB A凸 to USB A凹 ハブ(給電機能つき)
  • USB A凸 to SATA

の3段階を経てタンスに打ち捨てられていたHDDに接続。つないでlsblkするだけで認識できた。

HDDの準備

母艦側PCでHDDの中身を適当にサルベージしたあと、フォーマットしてLUKS暗号化しつつBtrfsでパーティションを作成。このあたりは全部UbuntuのdiskユーティリティでGUI操作可能だった。ただしGUI上でBtrfsを選択するためにはaptでbtrfs-progsを入れておく必要がある。

Raspberry Piのソフトウェアレイヤーの準備

よくあるチュートリアルのようにOpenMediaVaultを入れようとしたのだが、そのためにはRaspberry PiのOSがGUIをサポートしないバージョンでなければならない。そのために面倒な思いをしてOSを入れ直した。しかし改めてOMVを入れようとしたが、一般的なインストールスクリプトはRaspberry Pi Zeroには対応してないらしい。

RPi revision code :: 9000c1
This RPi1 is not supported (not true armhf).  Exiting...

仕方なくsambaに方針転換してインストールしてみた。

HDDをマウントする

LUKSで暗号化されたストレージは先に

sudo cryptsetup luksOpen /dev/sda storage

する必要があるのだが、なんとこれを実行するメモリが足りなかった(そんなことある!?)

Not enough available memory to open a keyslot.

ので、HDDを再フォーマットしてチャレンジします…

A/2=

1/28(火)

仕事が全然進まずつらい。帰りに同僚とラーメン。銀座のラーメンはお上品でどうも物足りない。ラーメンは野蛮なほうが好きだ。

とても寒い。自宅から出なくていいなら天気はハチャメチャになってくれたほうが楽しいのだが、会社員の身ではそうも言っていられない。寒いのも靴が濡れるのも嫌だ。

30日OS本はのんびり5日目の文字の描画をやっている。ピクセルデータをVRAMに投入していくだけなので簡単と思いきや、妙に小さく潰れた「A」が表示された。QEMUに解像度の設定などを渡す必要があるのかと思って精査してみると、単に上半分しか描画できてないだけだった。


Aが表示されるはずと思いこんで見るとAじゃないですかこれ?


これが本物のA

Aの上半分もAに見えるの、フラクタルだ。

定番外し/F4C3

1/14(火)

今日は昼食を食べに外に出て、いつもの店のいつもは頼まないメニューを頼んでみたのだがやはりいつものメニューのほうが良かった。冒険はリスクを伴う。

今日もぼちぼちOS30日本を進めた。3日目が終了。HLTRETをループするだけの関数をアセンブリで書き、それをCで読み込んで実行するプログラムを書いてコンパイルする。そうしてできたファイルをバイナリエディタで読んでみると、ちゃんと末尾がF4, C3になっていて感動。

もう2時か。さすがに遅い。

『第19回 自作OSもくもく会』に参加した

1/12(日)

https://atnd.org/events/110955

界隈の方に誘われ、同僚とともに参加した。フロントエンドエンジニアの自分が自作OSをやってどうなるのか。まだ初めたばかりなのであまり大きなことは言えないが、低レイヤーでも高レイヤー(?)でも共通する考え方のようなものがあって、低レイヤーの知識を得ることによって物事をより抽象的に捉えられるようになるかもしれないな、という感触がある。