March 27, 2013

できる ssh

この記事は 2013 年 3 月 27 日に「Syn の独り言」で書いた記事を移行したものです。

ssh って便利だ。でも、「リモートログインするためのコマンド」程度の認識しかない人も多いのでは?自分は、この小さい働き者が大好きだ。なので、今回はその紹介

長いので先にアジェンダを書くと、こんな感じ

rsa 鍵を使って認証する

鍵認証とは、秘密鍵と公開鍵を割り符のように使う認証だ。ssh サーバーは公開鍵を持っており、それに対応する秘密鍵を持っているユーザーの認証を通す。

百聞は一見にしかずと言う事で、とりあえず rsa 鍵を作ってみて欲しい。ssh クライアント (ssh コマンドを実行する側)で

$ ssh-keygen -t rsa

と実行すると、公開鍵と秘密鍵が作成される。作成中に鍵の保存場所とパスワードが聞かれるが、ここは適当に決めて欲しい。

ただ、(ssh に限らず)鍵を使うアプリケーションのほとんどはデフォルトでこのパスを見に行くので特に理由がなければ変更しない事をおすすめする。

パスワードは認証に使うものではなく秘密鍵を暗号化するための物だ。秘密鍵を盗まれた時の時間稼ぎにしかなっていない事に注意しよう。

また、ssh 鍵を作成する際の -t rsa というオプションは rsa2 アルゴリズムを使用すると言う事だ。歴史的経緯により rsa1, dsa なども併記してあるサイトがあるが、2013 年 3月現在では rsa2 の方がデファクトスタンダードだ。悪い事は言わない、rsa2 を使っとけ。

さて、デフォルトのパスで作成すると、~/.ssh/id_rsa と ~/.ssh/id_rsa.pub というファイルが出来る。~/.ssh/id_rsa が秘密鍵で ~/.ssh/id_rsa.pub が公開鍵だ。

この ~/.ssh/id_rsa.pub を ssh サーバー (接続される方) の ~/.ssh/authorized_keys に記載すると、この鍵を用いて ssh の認証ができるようになる。

https://dq1xd0takmkns.cloudfront.net/img/2b183f88b7591b3dfcd303c5906c4291.png

クライアントの id_rsa.pub をなんとかしてサーバーに転送したら、

$ mkdir -p ~/.ssh
$ chmod 700 ~/.ssh
$ cat id_rsa.pub >> ~/.ssh/authorized_keys
$ chmod 600 ~/.ssh/authorized_keys

とすればよい。cat の部分は id_rsa.pub の中身を authorized_keys へ追記しているだけなので、エディタで authorized_keys を開いてコピペでも OK だ。.ssh や authorized_keys のパーミッションは、Linux では本来どうでも良いはず(と、記憶している)が、Redhat 等の一部の OS ではこうしないとセキュリティーチェックにより動作しないので念のため。

または、最初からパスワード認証を用いて ssh クライアントからログイン出来る場合は、ssh クライアントで

$ ssh-copy-id [user@]hostname

を実行しても良い。

これ以降、ログインする際は必ず鍵認証を使うのであれば、認証用のパスワードは不要になる。

サーバーで

$ sudo passwd -d user

とするのも良いだろう。こうするとユーザーのパスワードは空になり、秘密鍵が無いとログイン出来なくなる。通常の ssh サーバーは空パスワードのログインを禁止しているためだ。(/etc/ssh/sshd_config に "PermitEmptyPasswords yes" と書かれていなければ大丈夫)

「パスワードが存在しない」というのは、パスワードクラックに対する最強の防御策だ。ただし、コンソールからは空パスワードでログインできるので注意しよう。

複数のユーザーやクライアントからサーバーにログインしたい場合は、それぞれのクライアントで鍵を生成して両方の id_rsa.pub をサーバーの authorized_keys に登録すれば良い。手順を見てもらえるとわかるが、id_rsa.pub は authorized_keys へ追記する形となっているので同一の authorized_keys へ複数の id_rsa.pub を登録できる。

また、同じ鍵で複数のサーバーやサイトにログインできるようにする事も問題ない。同じ id_rsa.pub を複数サーバーの authorized_keys や GitHub などに登録できる。

普通は、作業マシンでは秘密鍵(と、ペアの公開鍵)が 1個、それ以外のサーバーでは authorized_keys に公開鍵が複数登録されている状態になると思う。(authorized_keys に 1個しか登録されていないと、その作業マシンが壊れた際に少し危険)

https://dq1xd0takmkns.cloudfront.net/img/6100eb57b07edd5e97fd97eb2e351168.png

ただし、決して1個の秘密鍵を2人のユーザーや2台のマシンで共有してはいけない。

(公開鍵はやってもいいよ。)

転送した際にどんなミスがあるか分からない。盗聴されるかもしれない。

欲を言えば、移動やコピー、標準出力への出力も可能な限り控えたい所だ。cat で内容を出力するだけでもだめだ。どこかに画面出力のログが残っているかもしれないし、画面を後ろから見ている人がいるかもしれない。移動やコピーについても、アクセス権を間違えて誰かに見られる状態になるかもしれない。また、別パーティションにコピーされて別ハードディスクに保存されればハードディスク盗難のリスクが増える。

「大げさな」と思うかもしれないが、鍵を盗まれるのはそれだけ重大なことだ。バグの無いプログラムが無いように、ミスの無い人間もいない。こういう些細な気遣いが、重大な事件を起こすかどうかの境目だと思う。

ちなみに、aws (Amazon Web Service) ではサーバーログインのための秘密鍵をダウンロードするが、上記の理由から一時的な物と考えた方が良いだろう。最初のログイン時に初期パスワードを変更するサービスのように、初回ログイン時に鍵を変更したい。

ssh-agent で秘密鍵を登録しておく

ssh する度に秘密鍵のパスワードを入力するのは面倒だ。そんな時は、ssh-agent を用いれば良い。

client 上で

$ ssh-agent
$ ssh-add [path_to_secret_key]

とすると、秘密鍵のパスワードを聞かれるので入力しよう。すると、次からパスワードを入力する手間が省ける。

詳細にいうと、ssh-agent は /tmp にディレクトリを作成し、そこに UDS (Unix Domain Socket) を作成する。daemon としてバックグラウンドで動作する ssh-agent はそのソケットを通じて ssh が鍵の情報をやり取りするのだ。

UDS のパスや ssh-agent の PID は shell 変数に保存される。なので、同じ shell 変数が使用される限り異なる端末でも使用可能だ。screen や tmux などの仮想端末を使用する際は、仮想端末を立ち上げる前にこの作業を行うと全ての window で ssh-agent を使用できる。

なお、この作業は後述の agent forward にも通じている。秘密鍵にパスワードを設定していなかったとしても、agent forward を使用する場合は必要だ。

認証情報を踏み台ごしに使用する

例えば以下のように、目的のサーバーまで ssh でアクセスする際に間に踏み台(Step)を挟む場合。(ネットワークの都合などで直接 ssh 出来ない事は良くある。)

https://dq1xd0takmkns.cloudfront.net/img/0e30dfbb101456c2d315dbec2e26bbb6.png

踏み台で鍵を作っても良いが、管理が面倒になる。秘密鍵はローカルサーバーだけ、踏み台と目的のサーバーの authorized_keys にはローカルサーバーの公開鍵を登録する方法。

手順はこんな感じになる。

  1. ローカルサーバーの公開鍵を踏み台、サーバー両方の authorized_keys に登録する

  2. ローカルサーバーで ssh-agent と ssh-add を実行する

  3. ローカルサーバーから踏み台へ ssh ログインする際に -A オプションを付与する

    $ ssh -A step_server
    
  4. 踏み台サーバーから目的のサーバーへログインする

    $ ssh server_name
    

(踏み台で ssh-agent や ssh-add を実行してはダメ)

すると、2回目のログインでもローカルの鍵情報が使える。

これは1回目の ssh が踏み台でも UDS を作成し、ローカルの認証情報を踏み台からでも使えるようにするからだ。鍵そのものを転送するわけではないのでセキュリティー的にはそこまで心配する必要は無い。

ssh の度に -A オプションをつければ Agent Forward は下記のような多段構成でも出来る。そのため、Agent Forward を使用すれば秘密鍵はローカルマシンでのみ作成すればよくその他のサーバーにはローカルで作成した共有鍵を登録すれば良い事になる。

https://dq1xd0takmkns.cloudfront.net/img/0e0491ef5d48581bbed870bc146b2ebf.png

ただし、セキュリティーが完全に担保されるわけではない事は覚えておいた方が良い。最も大きいリスクは、踏み台の UDS を悪用される事だろう。この UDS は ssh でログインしたユーザーしか使用できないようにパーミッションが設定されているが root ユーザーからはアクセス権を無視して使用出来る。

root を信頼できないサーバーを踏み台にする際は控えた方が良いだろう。

リモート上のコマンドを直接実行する

話は少し変わって、ssh だけでコマンドまで実行する方法。

$ ssh [user@]hostname command [arg1 [arg2 [...]]]

と実行すると、hostname 上でコマンドを実行してくれる。

例えば、自分の環境でやってみるとこんな感じ

$ ssh sagittarius.chaos.com ls -a /usr
  ..
  bin
  games
  include
  lib
  local
  sbin
  share
  src

念のため言っておくと、sagittarius.chaos.com というのは自分で管理している VM だ。

リモート上でコマンドを実行する時は、通常のログイン時と同じ環境変数(ホームディレクトリなど)が設定されている。そのため、

$ ssh wbcchsyn@sagittarius.chaos.com ls

と実行すると、wbcchsyn のホームディレクトリのファイル一覧を表示する。

ちなみに、ssh に -t オプションをつけると、コマンド実行前にリモートサーバーで仮想端末 (tty) を作成してくれる。何が嬉しいかと言うと、パスワード入力や yes,no の確認等を促す、tty にのみ表示される文字列も ssh ごしに表示される。対話的なコマンドを実行する際に便利だ。

$ ssh sagittarius.chaos.com touch /tmp/hoge
$ ssh sagittarius.chaos.com rm -i /tmp/hoge
> rm: remove regular empty file `/tmp/hoge'?

もちろん、ここで y と入力すると削除されるし、n と入力すると削除はキャンセルされる。

こういう tty にのみ表示される文字列については、以前 だからそんな事に spawn を使うなと! にかいたので、良かったら見て下さい。

リモートの GUI を使う

Linux の GUI は、Client-Server システムになっている。例えば firefox の場合、firefox が X-Client となり「このような内容の表示をして欲しい」と X-Server に伝える。firefox のリクエストを受け付ければ X-Server はディスプレイに firefox の GUI を表示する。

X-Client と X-Server は同じサーバーで動く事が多いが、異なるサーバーで動かす事も可能だ。X-Server は Windows でも cygwin を使えばインストール出来る。

GUI を表示させたいマシンで X-Server を立ち上げたら、X-Client を動かしたいサーバーへ-X オプションをつけて ssh 接続し、コマンドを実行する。この時、ssh でログインしてからコマンドを実行しても良いが、先ほどの方法で直接実行しても良い。

$ ssh -X sagittarius.chaos.com firefox

みたいに。

ちなみに、これをやると firefox が終わるまで terminal のプロンプトが戻ってこない。それが嫌な場合は最後に & をつけてバックグラウンドで実行するとかして頑張って下さい。

自分はよく Windows から ubuntu の emacs を使うが、その際は cygwin の xterm から

$ run ssh -X sagittarius.chaos.com emacs&

とやるとうまくいく。

まあ、ここいらのテクニックは OS や cygwin のバージョンによっても異なるので適当に試して下さい。

あと、当然 OS-X 以降の mac からでも出来ます。(それ以前は知らない)

config ファイルにデフォルト設定を記載しておく

ssh の度に -A とか -X を入力したり、長い FQDN を記載するのは面倒だ。そんな時は、~/.ssh/config にその設定を記載すれば良い。

書式としては下記のような感じだ。

Host *
StrictHostKeyChecking no

Host sagittarius
HostName sagittarius.chaos.com
User wbcchsyn
ForwardAgent yes
ForwardX11 yes

Host aries
HostName aries.chaos.com
...

Host の次が、ssh コマンドで実際に入力するホスト名。ここで * と入力すると下記のオプションは全てのホストに対して適用される。

StrictHostKeyChecking を yes にすると、初めて ssh ログインする時はに聞かれる ssh-server の fingerprint 確認にデフォルトで yes と答えてくれる。HostName は本当のホスト名(又は IP)。ssh が名前解決に使う。User はそのホストにログインする時のユーザー名、ForwardAgent を yes にすると、-A オプションをデフォルトでつける。ForwardX11 を yes にすると、-X オプションをデフォルトでつける。(-X オプションをつけていても、違いは X-Client のコマンドを実行した時だけでそれ以外は普通の ssh コマンドとして使える。)

上記の場合、

$ ssh sagittarius

と入力すると

$ ssh -AX wbcchsyn@sagittarius.chaos.com

を実行した事になる。初めてログインするサーバーでも FingerPrint の確認は出てこない。

その他にも色々とオプションがあるので、興味のある人は man ssh_config を見て下さい。

ファイルを効率よく転送する

リモートで実行した結果がローカルの端末に表示されたりローカルで入力した y や n の文字列がリモートで実行されるのは、ローカルの標準入出力とリモートの入出力がパイプでつながっているからだ。

これを利用すると、ファイルの転送を行ったり出来る。たとえば、mysqldump で MySQL をバックアップした物を gzip 圧縮してバックアップサーバーに保存したい場合。バックアップサーバーから

$ ssh sagittarius.chaos.com mysqldump -uroot -pMyPassword \
  --all-databases \
  | gzip -c  > path_to_backup

のように実行すると、1行で見事リモートバックアップが実行される。

解説すると、mysqldump は mysql のデータをダンプして標準出力に渡す。

これをローカル(バックアップサーバー)がパイプで受け取り、gzip 圧縮をする。gzip の -c オプションは結果を標準出力に出すというコマンドだ。gzip は通常、ファイルを受け取ってそのファイル名に ".gz" 拡張子をつけた名前で保存するが、今回はファイルでは無く標準入力から受け取っているので、一回標準出力に出す。

最後に、gzip の結果をパイプでファイルに落としている。

このスクリプトの良い所は mysqldump と gzip が並列に動いている事だ。mysqldump した物をファイルに落としてから gzip 圧縮するより圧倒的に速い。

ただし、このスクリプトには 1点だけ改良の余地がある。mysqldump の結果を転送してから、ローカルで圧縮している点だ。圧縮してから転送した方が、帯域を節約できる。

帯域を節約したい場合は、以下のようにすればよい。

$ ssh sagittarius.choas.com mysqldump -uroot -pMyPassword \
  --all-databases \
  '|' gzip -c > path_to_backup

gzip の前のパイプ ( | ) を quotation ( ' ) でくくっているので、ローカルではこの | はパイプではなく文字列と見なされる。つまり、ローカルの none が '|' を文字列 | とみなし、リモートに転送する。リモートでは | は quotation でくくられていないので、通常のパイプとみなされる。よって、mysqldump の結果が gzip で圧縮されてからローカルに転送される。

以上をふまえて、cron で mysqldump を用いた日時バックアップをするスクリプトは以下で充分だ。

#!/bin/sh

tmp=`mktemp` && \
ssh sagittarius.choas.com mysqldump -uroot -pMyPassword \
  --all-databases \
  '|' gzip -c > "$tmp" && \
mv $tmp /var/backup/dump_`date +'%Y%m%d'.gz`

一応言っておくと、mktemp は一時ファイルを安全に作成してくれる。その一時ファイルに mysqldump の結果を保存し、成功したら /var/backup/dump_YYYYMMDD.gz にリネームする。

途中で失敗した場合、このコマンドは最後まで行かないので /var/backup 以下には正しいバックアップしか残らない。

また、これらの none コマンドは失敗したらその場で stderr にエラーメッセージが出る。(特別な場合は例外があるかもしれないが。)

メール送信の設定さえしておけば、cron は stdout や stderr に表示された文字列があるとその内容をメール送信する。なので、失敗したらメール通知してくれる仕組みまでバッチリだ。

なお、おまけだが quotation ( ' ) でくくるとリモートで解釈されるのはパイプ ( | ) だけではない。&&、;、||、* なども同じだ。また、変数 ($HOME など)もそのままではローカルの値が、quotation ( ' ) でくくるとリモートの値が使用される。

ディレクトリごと効率よく転送する

先ほどの例では gzip を使ったファイル転送の例を上げたが、tar を使えばディレクトリごと転送出来る。たとえば、Apache で公開するための静的コンテンツを ~/git/html というディレクトリで git 管理しているとする。これを、sagittairus.chaos.com の /var/www/html に転送するには、以下のようにすればよい。

$ tar czf - -C ~/git html | \
  ssh wbcchsyn@sagittarius.chaos.com \
  sudo tar xzf - -C /var/www

ただし、このコマンドを実行するには sudo 周りの権限や設定をする必要がある。例えば、こんな感じ。

$ sudo sh - c \
  "echo 'wbcchsyn ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/wbcchsyn"
$ sudo chown root:root /etc/sudoers.d/wbcchsyn
$ sudo chmod 0440 /etc/sudoers.d/wbcchsyn
$ sudo sed -i \
  's/^Defaults[ \\t]*requiretty/#Defaults requiretty/' \
  /etc/sudoers

ユーザー wbcchsyn にパスワード無しで全コマンドを sudo 実行出来る権限を付与し、sudo 一般の設定として、tty が無くとも実行出来るようにしている。(一部のディストリビューションでは tty が無いと sudo を実行出来ないように設定してある。)

tar は通常、

$ tar czf file_name directory1 [directory2 [...]]

という書き方で directory1, directory2, ... を圧縮、アーカイブし、file_name というファイルに出力する。しかし、ファイル名を - とすると出力を標準出力へ出す。gzip の時と同様に、出力をパイプでリモートへ渡すためだ。

-C オプションは、「このディレクトリにチェンジディレクトリしたつもりになって」という意味。

例えば、

$ cd ~
$ tar czf html.tar.gz git/html

とした場合、html.tar.gz を解凍すると最初に git というディレクトリができて、その下に html ディレクトリが展開される。

しかし、

$ cd ~/git
$ tar czf html.tar.gz html

とすると、html.tar.gz を解凍した際に html ディレクトリが作業ディレクトリ直下に直接展開される。

-C をつけた

$ tar czf - -C ~/git html

は ~/git/html を、解凍した時に html ディレクトリが直接出来る形でアーカイブし、標準出力に渡す。tar が作成したアーカイブの標準出力はパイプに渡され、sagittarius.chaos.com でやはり tar を用いて解凍される。圧縮時と同様、ファイル名の - は標準入力を、-C オプションは「このディレクトリに cd したつもりになって」という意味だ。

この1行で、 ローカルの ~/git/html が sagittarius.chaos.com の /var/www/html へ転送される。

ただ、この作業も改善の余地が多いにある。

まず、最初にこの作業では .git ディレクトリも転送されてしまう。.git ディレクトリは git でレポジトリを管理するための物であり、開発中には必要だが本番では必要ない。なので、tar でアーカイブを作成する際にこのディレクトリは除くように --exclude '.git' オプションを付与する。

また、tar はデフォルトではファイルのオーナーやパーミッションも保存してしまう。しかし、おそらくローカルではオーナーは作業用ユーザーであり、リモートでは apache をオーナーにしたいだろう。そのような場合、圧縮時に --owner='apache' オプションを付与する。

まとめると、~/git/html を sagittarius.chaos.com の /var/www/html に転送する手順は以下のようになる。

$ tar czf - -C ~/git --excoude '.git' --owner='apache' \
  | ssh wbcchsyn@sagittarius.chaos.com sudo tar xzf - -C /var/www

Port Forward で簡易 トンネリング・VPN を貼る

例えば、学校や会社の FW の中で動いている http サーバーに外からアクセスしたいような場合。Port Forward (ポートフォワード) を用いれば FW をすり抜ける事が出来る。

https://dq1xd0takmkns.cloudfront.net/img/3751ca61a7f3bde674c45b14f7318787.png

上記のように、FW 内の aries で動いてる http サーバーに FW 外部の taurus からアクセスしたい場合。同じく FW 内にある sagittarius から例えば以下のように taurus に予めポートフォワードの ssh を接続しておけばよい。

$ ssh -R 8000:aries:80 taurus

この状態で taurus から localhost の 8000番ポートにアクセスすると、aries の 80 番ポート(http の内容)が表示される。(ssh の接続を切るとポートフォワードも切れてしまうので気をつけよう。)

ちなみに、taurus と sagittarius 間の通信は ssh により暗号化されているので FW の外部における盗聴の可能性は限りなく低い。

しかし、この状態では taurus の 8000番ポートには taurus 自身からしかアクセス出来ない。taurus 以外のマシンからでも taurus の 8000番ポート経由で aries の 80番ポートにアクセスするためにはtaurus の ssh デーモンを GatewayPorts オプションを有効にした上で

$ ssh -R 0.0.0.0:8000:aries:80 taurus

と実行する。(もちろん、ネットワーク的に taurus の 8000 番へアクセス出来る事は必要。)

ssh デーモンの GatewayPorts を有効にするには、/etc/ssh/sshd_config に <code>GatewayPorts yes</code>と書き込んで ssh デーモンを再起動すればよい。

このように、目的のサーバーと同じ FW 内から外部に ssh アクセスする事でポートフォワードする場合の一般的なオプションは以下になる。

$ ssh -R [bind_address]:port:host:hostport [user]hostname

なお、ssh 接続の向きは上記と逆にする事も可能だ。

https://dq1xd0takmkns.cloudfront.net/img/59bafde04800a242353a4ca719a23799.png

上記のように、taurus から sagitarius に ssh コネクションを張れる場合は、taurus 上で

$ ssh -L 8000:aries:80 sagittarius

とすると tausus から localhost の 8000番経由で aries の 80番ポートにアクセス出来る。

-R の時と同様、このままでは taurus 以外のマシンからは taurus の 8000番ポートへアクセス出来ない。他のマシンからでも taurus の 8000番ポート経由で aries の 80番ポートへアクセスしたい場合は

$ ssh -L 0.0.0.0:8000:aries:80 sagittarius

と実行する。-L の場合は -R と違って ssh デーモンの GatewayPorts の設定が必要ない事は少し嬉しい。

-L と -R は ssh の向きが違うだけで動作はほとんど同じだ。-L を使うと ssh client 上でポートを開き、目的のサーバーへポートフォワードする。-R を使うと ssh server でポートを開き、目的のサーバーへポートフォワードする。

-L と -R は client を左側、server を右側に書いた時、ポートを開く場所が left, right という事らしい。ただ自分は「client は L の文字が含まれ、server は R の文字が含まれる」と覚えている。

今回の例では、-R を使うと、taurus から aries を閲覧中に万一 ssh コネクションが切れると、sagittarius から再度 ssh を張り直さなくては行けない。実用にのせるためには sagittairus で ssh コネクションを監視するか、taurus からも sagittarius にログイン出来る仕組みを作る必要がある。

そのため、今回の例に限って言えば個人的には -L を使う方が良いと思う。-L を使う場合は、taurus から sagittairus へ ssh アクセスできれば全て解決だ。

どちらの場合も言える事だが、ポートフォワードは実質的に FW に穴をあける事に等しい。そもそも、FW 的にアクセス出来ないのはそれだけの理由があるかもしれない。多くの場合、ネットワーク管理者がユーザーのポートフォワードを禁止する事は難しいのが現状だが、だからと言って乱用は避けた方が良いだろう。

以上、駆け足でしたが ssh の紹介でした。