Dockerコンテナ内でsshdを実行してはいけない理由

Dockerを使い始めた人がよくする質問といえば、「どうすればコンテナに入れますか?」です。その質問に対して、「コンテナ内でSSHサーバを起動すればいいよ」と答える人たちがいますが、これは非常にマズいやり方です。なぜその方法が間違いなのか、そして代わりにどうすればよいのかをこれから紹介します。

注:本記事へのコメントやシェアは、
Dockerブログにアップされた標準版から行ってください。よろしくお願いします。

コンテナでSSHサーバを起動すべきではない

…もちろん、コンテナ自体がSSHサーバである場合は除きます。

SSHサーバを起動したくなる気持ちは分かります。それはコンテナの”中に入る”簡単な方法だからです。この業界の人ならほぼ全員がSSHを一度は使ったことがあります。多くの人がSSHを日常的に使用し、公開鍵や秘密鍵、パスワード入力の省略、認証エージェント、そして時にはポート転送やその他の細かいことにまで精通しています。

以上のことを考慮すると、コンテナ内でSSHを起動しろというアドバイスが出るのも妥当だと言えます。しかし、それについてはよく考える必要があるでしょう。

例えば、RedisやJava WebサービスのDockerイメージを構築するとします。そこで、いくつか質問させてください。

  • 何のためにSSHが必要なのですか?

SSHが使われるのは、おそらく、バックアップやログのチェック、またはプロセスの再起動やコンフィグの微調整をするためでしょう。もしかすると、gdbやstraceのようなツールでサーバをデバッグするためにも用いられるかもしれません。しかし、SSHなしでこれらのことを行う方法があるので、のちほど紹介します。

  • 鍵やパスワードはどう管理しますか?

大抵の場合は、鍵やパスワードをイメージの中に組み込むか、ボリュームの中に格納するかのどちらかだと思います。では、鍵やパスワードをアップデートしたい時はどうしますか。鍵やパスワードをイメージの中に組み込む場合は、イメージを再構築して再デプロイし、コンテナを再起動する必要があります。このやり方でも大して煩わしいわけではありませんが、簡潔だとは言えません。はるかに優れたソリューションは、これらの認証情報をボリュームに格納し、ボリュームの中で管理することです。これは良い方法ですが、深刻な欠点もあります。この場合、コンテナにボリュームへの書き込み権限を与えてはいけません。さもないと、コンテナが認証情報を破損するかもしれないからです(その結果、コンテナへのログインができなくなってしまいます)。認証情報が複数のコンテナで共有されている場合は、さらに面倒なことになるでしょう。そんな時、コンテナ内にSSHさえなければ、心配事が1つ減るはずですよね?

  • セキュリティのアップグレードはどう管理しますか?

SSHサーバは安全ですが、それでも、セキュリティ上の問題が発生した場合は、SSHを使っている全てのコンテナをアップグレードする必要があります。つまり、全てのコンテナを再構築、および再起動しなくてはなりません。たとえ無害なmemcachedのサービスを使うだけでも、セキュリティ勧告に従って最新の安全対策をする必要があります。なぜなら、コンテナの攻撃対象領域は不意に拡大するからです。この場合も、コンテナ内にSSHさえなければ、心配事を分散できるはずですね?

  • “SSHサーバだけを追加”すればうまくいきますか?

それだけでは不十分です。Dockerは1つのプロセスを監視するため、SSHサーバに加え、MonitSupervisorのようなプロセスマネージャが必要です。複数のプロセスが必要な場合は、トップレベルにプロセスマネージャを1つ追加して他のプロセスをカバーできるようにしなくてはなりません。しかしこれでは、無駄のないシンプルなコンテナがずいぶんと複雑になってしまいます。アプリケーションの終了時(正常終了の場合もクラッシュの場合も)には、Dockerからではなく、プロセスマネージャから情報を得なくてはなりません。

  • あなたはコンテナにアプリケーションを格納することの他に、アクセスポリシーやセキュリティコンプライアンスまで任されていますか?

小規模な会社ではそれほど大きな問題にはなりません。しかし、大規模な会社で、コンテナにアプリケーションを格納する役割をあなたが担っているとしたら、リモートアクセスポリシーに責任を持つのは、おそらくあなた以外の人でしょう。また会社には、誰がアクセス権を持ち、どのように、そしてどんな種類のオーディットトレイルが必要なのかを定めた厳格なポリシーがあるかもしれません。そのようなケースでは、コンテナにSSHサーバを置くようなことは、絶対にしないほうがいいでしょう。

では、どのようにすればいいのでしょうか?

データをバックアップするには?

データはボリュームに置くべきです。そして、別のコンテナを実行しましょう。--volumes-fromオプションをつけて、そのコンテナと事前に作ったコンテナのボリュームを共有します。そうすれば新たなコンテナがバックアップを担い、必要なデータにアクセスできます。

その上、利点もあります。バックアップまたは長期保存するデータをストレージに送るために新たなツール(例えば、s3cmdやそれに類似するもの)をインストールしなければならない場合、メインサービスを担うコンテナの代わりに、専用のバックアップコンテナで、そのインストールのジョブを行えることです。このほうが簡潔ですね。

ログをチェックするには?

ボリュームを使いましょう。そうです、またボリュームです。特定のディレクトリにログを保存するとして、そのディレクトリがボリュームだとしたら、(例の--volumes-fromを使って)”ログを調査する”別のコンテナを起動しましょう。ここで必要なことを全て実行します。

先ほどと同じように、専門のツール(ack-grepか何か)が必要な場合、メインコンテナを初期状態に保ちながら、別のコンテナにそのツールをインストールできるわけです。

サービスを再起動するには?

事実上、シグナルを送ればあらゆるサービスを再起動できます。/etc/init.d/foo restartservice foo restartといったコマンドを打つと、ほとんどの場合、特定のシグナルがプロセスに送られるでしょう。docker kill -s を使って、シグナルを送信できます。

シグナルを受け付けないサービスもありますが、そういったサービスは特別なソケットでコマンドを受け付けます。TCPソケットであれば、シグナルをネットワーク経由で接続することになりますし、UNIXソケットであれば、ボリュームを使うことになります。またボリュームが登場しましたね。コントロールソケットが特定のディレクトリ配下になるように、コンテナとサービスを立ち上げてください。そのディレクトリとは、ボリュームのことです。その上で、ボリュームにアクセスする新たなコンテナを起動すれば、ソケットを使えるようになります。

複雑な手順だと思うかもしれませんが、決してそうではありません。fooというサービスが/var/run/foo.sockにソケットを作成し、正常に再起動するためにfooctl restartを実行する必要があるとしましょう。その場合はサービスを-v /var/runで単に起動してください(またはDockerfileにVOLUME /var/runを追加するという方法もあります)。再起動したい時は、--volumes-fromオプションをつけてコマンドをオーバーライドしつつ、同じイメージを実行すればいいのです。つまり以下のような感じになります。

# Starting the service
CID=$(docker run -d -v /var/run fooservice)
# Restarting the service with a sidekick container
docker run --volumes-from $CID fooservice fooctl restart

とてもシンプルですね。

コンフィグを編集するには?

もしコンフィグに永続的な変更を加えるのであれば、イメージ内で行うべきです。そうしないと、新たなコンテナを起動した時に古いコンフィグが残っていて、変更が失われてしまうことになります。そうなるとSSHアクセスを実行することができませんよね。

「でも、例えば、新たなバーチャルホストを追加するような時、サービスの実行中に、コンフィグを変更しないとならないんです」という方もいるでしょう。

そのようなケースは、あれを使ってください。そうです、ボリュームです。コンフィグはボリュームにあり、そのボリュームは”コンフィグ編集”専用のコンテナと共有されているはずです。このコンテナ内では使いたいツールを使えます。つまりSSHとお好みのエディタ、またはAPIコールを受け付けるWebサービスや、外部ソースから情報を取得するcrontabなど、何でも使えるのです。

繰り返しになりますが、こうすることで心配事を分散させることができます。あるコンテナはサービスを実行し、別のコンテナはコンフィグを更新する役割を担うというようなことです。

「でも、異なる値をテストするために一時的な変更をしたいだけなんです」と言いたい人もいるかもしれませんね。

その場合は、次のセクションを確認してみてください。

サービスのデバッグをするには?

この時ばかりは、どうしてもコンテナ内にシェルを導入する必要があります。なぜならば、gdbやstraceを実行したり、コンフィグを微調整したりしなければならないからです。

この場合、nsenterが必要です。

nsenterとは

nsenterは名前空間(namespace)に入る(enter)ことを可能にする、ちょっとしたツールです。正確に言えば、nsenterは既存の名前空間に入ること、あるいは新たな名前空間でプロセスを作成することを可能にします。「さっきから言っている名前空間って何ですか?」と疑問に思った方のために補足しておきますが、名前空間とは、コンテナを構成する基本要素の1つです。

簡単に説明すると、nsenterを使うことで既存のコンテナにシェルを導入できます。コンテナがSSHや特別な目的を持ったデーモンを実行していなくても、それは可能です。

nsenterの入手方法

GitHubのjpetazzo/nsenterを読んでください。早い話が、次のコマンドを実行すればいいのです。

docker run -v /usr/local/bin:/target jpetazzo/nsenter

すると、nsenter/usr/local/binにインストールされ、すぐに使用できます。nsenterは、(util-linuxパッケージの)ディストリビューションに含まれている場合もあります。

使い方は?

まず、入りたいコンテナのPIDを指定します。

PID=$(docker inspect --format {{.State.Pid}} <container_name_or_ID>)

そしてコンテナに入ります。

nsenter --target $PID --mount --uts --ipc --net --pid

するとコンテナ内にシェルが導入されます。これだけです。

特定のスクリプトやプログラムの実行を自動化したい場合は、それらを引数としてnsenterに追加してください。シンプルなディレクトリの代わりにコンテナを利用するという点を除けば、nsenterの動作はchrootと少し似ています。

リモートアクセスをするには?

リモートホストからコンテナに入る必要がある場合は、(少なくとも)2つの方法があります。

  • SSHでDockerホストに入り、nsenterを使う
  • SSHでDockerホストに入り、特定のコマンド(つまりnsenter)を実行する特別なキーを置く

1番目の方法はとても簡単です。しかし、Dockerホストへのrootアクセスが必要です(したがってセキュリティの観点からは、いい方法とは言えません)。

2番目の方法は、SSHのauthorized_keysファイルの中でcommand=パターンを使用するやり方です。”クラシックな” authorized_keysファイルについてはご存じだと思いますが、以下のような感じになります。

ssh-rsa AAAAB3N…QOID== jpetazzo@tarrasque

(もちろん、実際のキーはこれよりもずっと長く、通常、何行にも及びます)

特定のコマンドを実行させることもできます。SSHキーを使ってリモートホストからシステム上の使用可能なメモリをチェックできるようにしたいが、完全なシェルアクセスを許可したくない場合、authorized_keysファイルに次のように記述してください。

command="free" ssh-rsa AAAAB3N…QOID== jpetazzo@tarrasque

すると、指定したキーで接続した時、シェルが導入される代わりに、freeコマンドが実行されます。それ以上のことは行われません。

(厳密に言えば、no-port-forwardingを追加したほうがいいでしょう。詳しくはmanページのauthorized_keys(5)を参照してください)

この仕組みの重要なポイントは、役割を分けることにあります。アリスは、コンテナ内にサービスを置きますが、リモートアクセスやロギングなどには関与しません。ベティは、特殊な状況でのみ使用されるSSHレイヤを(不可解なトラブルをデバッグするために)追加します。シャーロットは、ロギングの管理をします。こんな感じです。

まとめ

コンテナ内でSSHサーバを実行するのは本当に大間違いなのでしょうか。正直に言うと、それほど悪いことではありません。Dockerホストにアクセスできない時は、非常に便利な手段です。しかし、コンテナ内にシェルを導入しなければならないという点は変わりません。

今回は、コンテナ内でSSHサーバを実行しなくてもいい方法をいくつか紹介しました。これらの方法であれば、これまでのように必要な機能を全て手に入れることができ、その上、はるかに簡素なアーキテクチャを構築できます。

Dockerの利用者は、自分にとって最適なワークフローを選ぶことができます。しかし、”コンテナを小さなVPSにしよう”といった時流に乗る前に、解決方法は他にもあるということを覚えておいてください。これで、情報に基づいた判断が下せるはずです。