Soracom Airで繋がったデバイスにリモートからSSHする

2015.12.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

解決したいこと

IoT ソリューションで遠隔地にゲートウェイを設置したとします。 ゲートウェイからは Soracom Air で外部へ通信するのとして、逆方向、つまり、パブリック IP が振られたサーバからパブリック IP が振られていないゲートウェイに SSH する方法を紹介します。

システム全体図

remote port forwarding

Soracom AIRでインターネットに接続できるゲートウェイとパブリック IP が振られた Linux サーバ(以下「リモートサーバ」と呼びます)を用意します。

手順

ゲートウェイからリモートサーバへのSSH

リモートサーバの特定のポートをゲートウェイ(localhost)の SSH ポート(22)にフォワードするよう、リモートポートフォワードします。

GATEWAY_SERVER $ ssh -R REMOTE_PORT:localhost:22 remote-user@REMOTE_SERVER

リモートサーバからゲートウェイへのSSH

フォワードされているポート(REMOTE_PORT)に SSH すると、ゲートウェイに SSH できます。

REMOTE_SERVER $ ssh gateway-user@localhost -p REMOTE_PORT

ポートフォワードの手順を詳細に解説

SSH 時のポートフォワード向けオプション

今回は SSH をポートフォワード目的でのみ使い、 SSH 先でコマンド実行するわけではないので -N オプションをつけます。

バックグラウンド実行させる -f オプションを有効にします。

また、ポートフォワードが失敗しては意味が無いので、ポートフォワード失敗時にはプロセス終了する ExitOnForwardFailure=yes のオプションを有効にします。

ExitOnForwardFailure オプションを有効にしていると、ポートがリモートですでに LISTEN しているなどしてポートフォワード出来ない場合

Error: remote port forwarding failed for listen port 4321

というエラーが発生して SSH プロセスが終了します。

SSH 時コネクション向けオプション

SSH コネクションが確立されているかチェックするために、一定間隔(ServerAliveInterval=60)でメッセージを送信し、指定回数(ServerAliveCountMax=3)応答がなければ、切断するようにします。

SSH 先のホストチェック向けオプション

初めて SSH する場合やIPが同じだけれどもフィンガープリントが変わった場合、インタラクティブな確認が求められます。 SSH 先は安心できるホストだという前提のもと、SSH 時にこれらのチェックをスキップするために以下のオプションを追加します。

  • StrictHostKeyChecking=no (フィンガープリントのチェックをしない)
  • UserKnownHostsFile=/dev/null (接続先のフィンガープリントを 保存しない)

SSH コマンド全体

ゲートウェイのSSHコマンド

以上をまとめると、ゲートウェイが実行する SSH コマンドは以下の様になります。

$ ssh -o StrictHostKeyChecking=no \
      -o UserKnownHostsFile=/dev/null \
      -o ServerAliveInterval=60 \
      -o ServerAliveCountMax=3 \
      -o ExitOnForwardFailure=yes \
      -i /root/.ssh/ops.pem \
      -N \
      -f \
      -R 8888:localhost:22 ec2-user@55.66.77.88

リモートサーバのSSHコマンド

トンネルを掘った後は、リモートサーバが実行する SSH コマンドは以下です。

$ ssh localhost -p 8888

SSH オプションを設定ファイルに引っ越す

リモートポートフォワードはコマンドオプションが長いので、設定ファイル(~/.ssh/config)に引っ越します。設定ファイルのパーミッションは自分だけが読み書きできるように設定します($ chmod 600 ~/.ssh.config)

Host ops
  HostName 55.66.77.88
  User ec2-user
  IdentityFile ~/.ssh/ops.pem
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  ServerAliveInterval 60
  ServerAliveCountMax 3
  ExitOnForwardFailure yes
  RemoteForward 8888 localhost:22

このように設定しておくと、ゲートウェイからはシンプルに

$ ssh -N -f ops

と実行するだけです。

接続が切れた時に再接続させる

ネットワーク障害やリモートサーバーの再起動などでSSH接続が切断することが考えられます。

autossh を使うと、SSH 切断時に再接続の面倒をみてくれます。自前で再接続のロジックをシェルスクリプトで書くよりもずっと確実です。

autossh のインストール

URL http://www.harding.motd.ca/autossh/

Amazon Linux

$ sudo yum install -y autossh --enablerepo=epel

Ubuntu

$ sudo apt-get install -y autossh

autossh のオプション

コマンドは簡単で、 $ ssh OPTIONS に対して $ autossh -M 0 OPTIONS とするだけです。

今回の場合は、元の SSH コマンドが $ ssh -N -f ops だったため $ autossh -M 0 -N -f ops となります。

autossh には独自にSSHコネクションをモニタリングする機能が存在します。 man ページを読むと、autossh 標準のモニタリングより OpenSSH のモニタリングを推奨しているため、autossh のモニタリングを無効化する -M 0オプションを渡しています。

autossh の Man ページから引用します。

Setting the monitor port to 0 turns the monitoring function off, and autossh will only restart ssh upon ssh's exit. For example, if you are using a recent version of OpenSSH, you may wish to explore using the ServerAliveInterval and ServerAliveCountMax options to have the SSH client exit if it finds itself no longer connected to the server. In many ways this may be a better solution than the monitoring port.

autossh の再接続確認

リモートサーバーを意図的に再起動させるなどしてトンネル用接続を切断させてみました。

Dec 20 12:52:25 ip-172-*-*-* autossh[2392]: port set to 0, monitoring disabled
Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: starting ssh (count 1)
Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: ssh child pid is 2394
Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: ssh exited with error status 255; restarting ssh
Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: starting ssh (count 2)
Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: ssh child pid is 2395
  • ssh exited with error status 255; restarting ssh
  • starting ssh (count 2)

などと SSH の切断を検知して、再接続していることがわかりますね。

ログは /var/log/messages から取りました。

ぷらっとホーム OpenBlocks BX1 起動時にリモートサーバに SSH させる

/etc/rc.local に autossh コマンドを追記するだけです。

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
autossh -N -f ops # XXX : ADDED LINE
exit 0

リモートサーバー情報を後から変更する

やんごとなき理由により

  • リモートサーバーの IP が変わった
  • リモートサーバーの SSH 鍵が変わった

といったことがあるかもしれません。 ゲートウェイ設置後にリモートサーバーへの接続情報を変更する方法を考えてみましょう。

Soracom Air のメタデータ について

SORACOM Air には「メタデータサービス」という機能が存在します。

AWS EC2 のメタデータのように、SIM から接続している場合のみ、その SIM に属するメタデータを読み書きするサービスです。

SORACOM Air メタデータサービスのご紹介 - SORACOM Blog

メタデータサービスは標準では無効化されており、グループ単位で有効・無効にします。 有効にすると、SIM 情報や通信に関する情報などを取得出来ます。

Soracom Air のユーザーデータでリモートサーバ情報を連携

AWS EC2 と同じく、ユーザー自身がデータをカスタマイズできるユーザーデータ機能もサポートしています。

このユーザーデータ欄にリモートサーバの IP や SSH 鍵などの情報を登録しておき、ゲートウェイが定期的にユーザーデータを参照して同期するようにしておけば、リモートサーバが変更になっても、SSH できます。

目指すところは AWS IoT Device Shadows と同じかと思います。

SSH 先が複数あるケースも考慮すると、ユーザーデータは次の様なデータ構造が良いでしょうか。

{
    "ops": {
      "KeyMaterial": "-----BEGIN RSA PRIVATE KEY-----\nMII...SNIP...hE=\n-----END RSA PRIVATE KEY-----",
      "HostName": "11.22.33.44",
      "RemotePort": 8080
    }
}

SORACOM管理画面から設定します。

soracom-userdata-sample

ユーザーデータの取得は AWS EC2 と同じく、特定のエンドポイントを叩くだけです。

$ curl -s http://metadata.soracom.io/v1/userdata
{
  "ops": {
    "KeyMaterial": "-----BEGIN RSA PRIVATE KEY-----\nMII...SNIP...hE=\n-----END RSA PRIVATE KEY-----",
    "HostName": "11.22.33.44",
    "RemotePort": 8080
  }
}

レスポンスの JSON をパースして SSH 鍵や ~/.ssh/config ファイルを再生成するプログラムを用意してやれば OK ですね。

SSH 接続先サーバのホストキーなどはチェックしないようにしているため、接続先が変わっても、確認を求められることはありません。

まとめ

クラウドだけをいじっている人は「SSH したら負けかなと思っている」などと(わざと)煽る人もいますが、現実問題として IoT ソリューションではリモートからデバイスの状態をモニターできるようにし、リモートからデバイスを管理できるようにしないと、運用で疲弊するだけです。

泥臭い IoT をより楽に回せるなアイデアがあれば、今後も共有していきたいと思います。

参照