Docker: 開発用コンテナで sshd を起動してサーバー化する
はじめに
私はねぇ、システムの Python 環境に直接 pip install
をかましまくった後で、クリーンな状態に戻そうと思い /usr/lib/python3/dist-packages
の中身を全部削除して、システムを破壊したことがあるんだ。それはとても甘美な経験だったと記憶しているよ。
しかしそれ以来、すべての開発環境を仮想環境にインストールしないと安心できない呪いにかかってしまったんだ。
実際のところ、本記事の内容は DevContainer を使えばよりドラマチックかつエロティックに解決できるのだが、私は頭が悪くナウでヤングなツールのツールに振り回されるのが大嫌いな老害なので枯れた技術に逃げることがある。
枯れた技術の素晴らしいところは何年経っても変わらないところである。一度覚えたらずっと使える。今回の内容の核となる以下のドキュメントは 10 年近く編集されていない(それはそれで不安である)。
SSH でアクセスできる開発用の Docker コンテナがあれば、サーバーを触るときのインターフェースと統一できるので私にとっては認知負荷が大きく下がる。ちょこっと編集したいからコマンドラインから SSH 接続して vim 叩こう、ができるととても安心感がある[1]。
というわけで作ってみたのだが、おそらく誰の参考にもならない。
本記事は上記の Docker 公式ドキュメントと以下の記事の、組み合わせと応用である。
検証した環境
クライアント
- macOS Ventura
サーバー
- Ubuntu 22.04 LTS
- Docker version 26.1.4
ベースとなる Docker イメージ
- Debian系(Ubuntuなど)
免責事項
SSH でアクセスできるコンテナを立てておくと、当然そこがセキュリティホールになりうるので自己責任でやってください。VSCode のサーバー立てても Extension にマルウェアや脆弱性が仕込まれてたりする可能性もあるわけなので危険性は変わらないですけど。
クリアすべき課題
クリアすべき課題は大きく分けて以下の2つである。
- Docker コンテナで sshd を起動し、外部からアクセスできるようにする
- コンテナ起動時にホスト側のユーザーと権限設定を合わせることで、ホスト側ボリュームをマウントしたときに作成されるファイルの権限が自然なものになるようにする
- 上記を手間をかけないでできるようにする
もちろん他にも付随して細々とした設定を行う必要はある。では順に解決しよう。
Docker コンテナで sshd を起動し、外部からアクセスできるようにする
冒頭で紹介した以下のドキュメントのほぼコピペで、まずはコンテナ内の root ユーザーへのアクセスを試す。この作業をする前に、当然サーバー側のファイアウォールやサーバー外のセキュリティ設定をいじってコンテナ側で sshd が待機するポートを開けておかねばならないので注意すること。
docker-sshd-example/
├── Dockerfile
└── docker-compose.yml
# ベースイメージは Debian か Ubuntu をベースにしているものにする
FROM ubuntu:22.04
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update --yes \
&& apt-get upgrade --yes \
&& apt-get install --yes --no-install-recommends \
# - `tini` is installed as a helpful container entrypoint,
# that reaps zombie processes and such of the actual executable we want to start
# See https://github.com/krallin/tini#why-tini for details
tini \
openssh-server \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN mkdir /var/run/sshd
RUN echo 'root:screencast' | chpasswd
# 以下の設定書き換えコマンドはバージョンによって微妙に変える必要がある
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
CMD ["tini", "-g", "--"]
services:
sshd:
build: .
image: sshd_example
container_name: sshd_container
ports:
- "2222:22"
# -D はフォアグラウンド実行、 -d はデバッグモードのオプション
# アクセスに成功するまではつけておくとよい
# デバッグモードでは一度アクセスを受け付けると
# それ以降のアクセスを受け付けなくなるので、常用するときは外す
command: ["/usr/sbin/sshd", "-D", "-d"]
以下のコマンドで起動できる。ユーザーに docker
の実行権限があれば sudo
はいらない。
sudo docker compose build
sudo docker compose up
# IPアドレスは適宜調整すること
# -v はデバッグオプションなのでアクセスに成功するまではつけておくとよい
ssh -v -p 2222 root@192.168.0.21
うまくいかないときはコンテナ内の /etc/ssh/sshd_config
を sudo docker container exec -it sshd_container bash
などで覗きに行き、設定が正しいかどうかを確認するとよい。特に root
でのパスワードログインなので PermitRootLogin
や PasswordAuthentication
が有効でないと弾かれる。
上記はデバッグモードを有効にしてあるので、もしうまくいかなければエラーメッセージを適当な生成AIにぶち込めばうまくいかない理由をある程度は教えてくれる。
コンテナ起動時にホスト側のユーザーと権限設定を合わせる
上記でのアクセスはコンテナ側が root
なので、ボリュームをマウントするなどして作業を永続化した際に、コンテナ側で生成したファイルのオーナーが root
となっていちいち操作に制限がかかり非常に厄介である。また、Dockerfile 内に書かれたパスワードでログインできてしまうことも問題で、自分だけが使う閉じた環境ならともかく会社などで使うには性善説が過ぎる。
Docker コンテナは --priviledged
オプションをつけて起動しない限り jail break されることはそうそうないが、root
でコンテナに入られるといろいろなツールをインストールされて踏み台化される危険はあるし、root
以外のユーザーでも Linux カーネルの既知の脆弱性を利用すれば root
への昇格は可能なので、そもそも他のユーザーにはコンテナに入らせないことを基本とすべきである。
コンテナ側にアクセスできるユーザーの情報を焼き込んでしまってもよいが、そうすると複数のサーバーや複数の Linux アカウントで同じ開発環境を作りたいときに、アクセスするすべてのユーザーの情報を Docker イメージに焼き込む必要があって、非常に面倒臭いというか実質そのような管理は不可能である。そんなことをするならば開発用の AWS インスタンスを個人ごとに立てたほうがまだ幾分賢い。
したがって普段使いするならばアクセスできるユーザーの情報はコンテナの起動時に指定できることがマストになる。
このような需要があって作成したスタートアップスクリプトが以下である。git clone
で簡単にイメージに導入できる。
今回の用途に使えるように、上記記事の時点より以下の2つの変更を追加した。
-
PRESERVE_ROOT=yes
を指定することで、目的のユーザーを作成した上でroot
ユーザーとしてコマンドを実行することができる。 - もともとの Jupyter 公式スクリプトに存在していた
run-hooks.sh
を復活させ、コンテナ起動時にユーザーが特定のディレクトリに置いたスクリプトを自動で実行できるようにした。
上記の変更を加えたことで、以下が可能になった。
- 目的のユーザーを作成した上で、
root
ユーザーでsshd
を起動できる。 -
sshd
の設定をコンテナ起動時に行うことができる。
sshd
の設定には以下の記事で作成した設定用スクリプトを手直しして使う。
docker-sshd-example/
├── .env
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── second-hook.d
│ ├── chpasswd.sh
│ └── ssh_setting.sh
└── work
└── home
└── josh
└── .ssh
├── .gitignore
└── authorized_keys
.ssh
にある .gitignore
は authorized_keys
を誤って push しないようにするためのお守りである。この work/home/josh
をコンテナ内の /home/josh
にマウントする。以後、コンテナ内ホームディレクトリ /home/josh
における作業はホスト側の work/home/josh
に永続化される。
マウントするに当たってはディレクトリやファイルの権限をきちんと設定しておかないと sshd
に怒られるので、以下のコマンドで権限を調整する。
cd work
chmod 750 home
cd home
chmod 700 .ssh
cd .ssh
chmod 600 authorized_keys
権限を設定したら authorized_keys
でユーザー(ここでは josh
)へのアクセスに用いる公開鍵を ssh-keygen -t ed25519
などで作成して書き込んでおく。
ssh-keygen -t ed25519
cat ~/.ssh/id_ed25519.pub >> work/home/josh/.ssh/authorized_keys
また、docker compose
がデフォルトで読み取る .env
に、コンテナ起動時に設定するユーザーの情報を書き込んでおく[2]。この手順も Makefile
に自動化しておいた。
DEV_USER ?= $(shell id -un)
DEV_UID ?= $(shell id -u)
DEV_GROUP ?= $(shell id -gn)
DEV_GID ?= $(shell id -g)
create_env:
echo "DEV_USER=${DEV_USER}" > .env
echo "DEV_UID=${DEV_UID}" >> .env
echo "DEV_GROUP=${DEV_GROUP}" >> .env
echo "DEV_GID=${DEV_GID}" >> .env
make create_env
準備ができたら各ファイルを以下のように修正する。
まず、Dockerfile
は root
でのアクセス設定を削除し、スタートアップスクリプトを追加する。
FROM ubuntu:22.04
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update --yes \
&& apt-get upgrade --yes \
&& apt-get install --yes --no-install-recommends \
sudo git ca-certificates \
# - `tini` is installed as a helpful container entrypoint,
# that reaps zombie processes and such of the actual executable we want to start
# See https://github.com/krallin/tini#why-tini for details
tini \
openssh-server \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# for sshd
RUN mkdir /var/run/sshd
# スタートアップスクリプト
RUN git clone https://github.com/wsuzume/devel-entrypoint.git \
&& cd devel-entrypoint \
&& /bin/bash install.sh
# 起動時に実行するスクリプトをコピー
COPY ./second-hook.d/ /usr/local/bin/second-hook.d/
# Configure container entrypoint
ENTRYPOINT ["tini", "-g", "--"]
次に docker-compose.yml
には以下の変更を加える。
- ホームディレクトリをマウントする。
- 起動時に作成するユーザーの情報を環境変数
INIT_USER
などで与える。 -
GRANT_SUDO=yes
により、コンテナ内ではパスワードなしでsudo
を可能にする -
PRESERVE_ROOT=yes
により、コマンドをroot
ユーザーで実行する -
init.sh
経由でsshd
を呼び出すことでスタートアップスクリプトを有効にする
sshd
起動時の -d
オプションはデバッグモード指定なのでアクセスに成功したら削除すること。
services:
sshd:
build: .
image: sshd_example
container_name: sshd_container
ports:
- "2222:22"
volumes:
- ./work/home/josh:/home/josh
environment:
- INIT_USER=${DEV_USER}
- INIT_UID=${DEV_UID}
- INIT_GROUP=${DEV_GROUP}
- INIT_GID=${DEV_GID}
- GRANT_SUDO=yes
- PRESERVE_ROOT=yes
command: ["init.sh", "/usr/sbin/sshd", "-D", "-d"]
以下はスタートアップスクリプトである init.sh
から実行されるスクリプトである。init.sh
に set -e
オプションが付与されているので、スクリプト中でエラーが発生するとそこで異常終了することに注意しつつスクリプトを書く必要がある。
chpasswd.sh
はユーザーのパスワードを変更する。GRANT_SUDO=yes
の指定により、作成されたユーザーについてはコンテナ内で sudo
をパスワードなしで実行できるので、極論、そのユーザーのパスワードは誰も知る必要がない。したがってパスワードはコンテナ起動時に毎回ランダムで生成する。
#!/bin/bash
echo "Random password set: ${INIT_USER}"
PASSWORD=$(openssl rand -base64 12)
echo "${INIT_USER}:${PASSWORD}" | chpasswd
ssh_setting.sh
は /etc/ssh/sshd_config
の設定を簡易化するスクリプトである。単純に sed
で置換するよりはロバストである。設定はお好みで。
#!/bin/bash
# SSH の設定
SSH_CONFIG="/etc/ssh/sshd_config"
SSH_CONFIG_BACKUP="/etc/ssh/sshd_config.bk"
SSH_PORT_NUMBER="22"
function change_setting () {
TARGET=$1
KEYWORD=$2
VALUE=$3
EXIST=$(grep "^${KEYWORD}" ${TARGET} || true)
EXIST_COMMENT=$(grep "^#${KEYWORD}" ${TARGET} || true)
if [ "${EXIST}" != "" ]; then
sed -i '/^'${KEYWORD}'/c '${KEYWORD}' '${VALUE}'' ${TARGET}
elif [ "${EXIST_COMMENT}" != "" ]; then
sed -i '/^#'${KEYWORD}'/c '${KEYWORD}' '${VALUE}'' ${TARGET}
else
echo -e "${KEYWORD} ${VALUE}" >> ${TARGET}
fi
}
if [ -f ${SSH_CONFIG_BACKUP} ]; then
echo "SSH setting is already done."
else
echo "Modify ${SSH_CONFIG}"
cp -i ${SSH_CONFIG} ${SSH_CONFIG_BACKUP}
# Port
change_setting ${SSH_CONFIG} Port ${SSH_PORT_NUMBER}
grep "^Port" ${SSH_CONFIG}
# PermitRootLogin
change_setting ${SSH_CONFIG} PermitRootLogin no
grep "^PermitRootLogin" ${SSH_CONFIG}
# PasswordAuthentication
change_setting ${SSH_CONFIG} PasswordAuthentication no
grep "^PasswordAuthentication" ${SSH_CONFIG}
# UsePAM
change_setting ${SSH_CONFIG} UsePAM no
grep "^UsePAM" ${SSH_CONFIG}
# PubkeyAuthentication
change_setting ${SSH_CONFIG} PubkeyAuthentication yes
grep "^PubkeyAuthentication" ${SSH_CONFIG}
# ChallengeResponseAuthentication
change_setting ${SSH_CONFIG} ChallengeResponseAuthentication no
grep "^ChallengeResponseAuthentication" ${SSH_CONFIG}
# PermitEmptyPasswords
change_setting ${SSH_CONFIG} PermitEmptyPasswords no
grep "^PermitEmptyPasswords" ${SSH_CONFIG}
# SyslogFacility
change_setting ${SSH_CONFIG} SyslogFacility AUTHPRIV
grep "^SyslogFacility" ${SSH_CONFIG}
# LogLevel
change_setting ${SSH_CONFIG} LogLevel VERBOSE
grep "^LogLevel" ${SSH_CONFIG}
# TCP Port Forwarding
#change_setting ${SSH_CONFIG} AllowTcpForwarding no
#grep "^AllowTcpForwarding" ${SSH_CONFIG}
# AllowStreamLocalForwarding
#change_setting ${SSH_CONFIG} AllowStreamLocalForwarding no
#grep "^AllowStreamLocalForwarding" ${SSH_CONFIG}
# GatewayPorts
#change_setting ${SSH_CONFIG} GatewayPorts no
#grep "^GatewayPorts" ${SSH_CONFIG}
# PermitTunnel
#change_setting ${SSH_CONFIG} PermitTunnel no
#grep "^PermitTunnel" ${SSH_CONFIG}
fi
以上の設定が終わったら、イメージをリビルドしてコンテナを立ち上げる。
sudo docker compose build
sudo docker compose up
# IPアドレスやユーザー名は適宜調整すること
# -v はデバッグオプションなのでアクセスに成功したら外してもよい
ssh -v -p 2222 -i ~/.ssh/id_ed25519 josh@192.168.0.21
試しに起動したコンテナに対して VSCode から SSH アクセスしたら普通に使えました。これだよ。求めていたのは。
おしまい
Docker で開発環境作るときはいつもユーザーの権限調整とか面倒だったけど、かなり手順を簡易化できたと思う。開発が捗るね!
-
この開発環境で作業するにはこのパソコンの VSCode から起動して、あれっ、なんか接続用のプラグインが無限ループしてない?ちょっと原因はなんですか、会社のプロキシに引っかかったかな?そもそもどうやってエラーを確認すればあ゛ぁ゛あ゛ぁ゛ぁ゛ぁ゛ぁ゛ぁ゛あ゛ぁ゛ぁ゛ぁ゛ぁ゛あ゛あ゛あ゛ぁ゛ぁ゛!! 開発が!!進まねぇだろうが!!!となったときの保険である。 ↩︎
-
明示的に指定する場合は
--env-file
オプションを用いる。複数ユーザーを切り替えたい場合も環境変数ファイルを複数作成して切り替えるだけでよい。同じサーバーで複数のコンテナを起動する場合は、docker compose -p [project_name] --env-file [env_file] up
のようにプロジェクト名も明示的に指定することでdocker compose
による制御の衝突を避けることができる。 ↩︎
Discussion