昨日今日は集中的に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とか、ビルド成果物のチェック、動作確認用のブラウザもサンドボックス内で実行とかもやっていきたい。