Amazon Web Services ブログ

AWS IoT Device Management セキュアトンネリングを利用して、SSHやリモートデスクトップでトラブルシューティングを行う方法

IoT のユースケースでは、デバイスで不具合や問題が起きた際にログを収集することができないため問題の切り分けが難しい、デバイスが遠隔地にありトラブルシューティングに時間やコストがかかる、といった声をよく耳にします。AWS IoT Device Management ではセキュアトンネリングという機能を提供しており、デバイスの属するネットワークの既存のインバウンドファイアウォールルールを更新することなく、遠隔地のデバイスに対してSSHやリモートデスクトップなどでログインし、状態の確認やログの収集などのトラブルシューティングを行ったり、製品のオーナーに対してリモートからの設定サポートを実施したりすることができます。

この記事では、AWS IoT Core で管理されているデバイスに対して、AWS IoT Device Management のセキュアトンネリングを利用して遠隔からトラブルシューティングを行う方法を紹介します。

事前準備

本記事の手順では、以下のデバイス・PCを利用します。

  • Ubuntu 18.04 がインストールされた Raspberry Pi 3 B+ もしくは Raspberry Pi 4
  • 接続用 PC

なお、セキュアトンネリングで利用する localproxy は Windows, Mac, Linux で実行可能なため、Ubuntu 18.04 以外のOSがインストールされたデバイスに対してもトラブルシューティングを実施することができます。

モノ・証明書・ポリシーの作成

IoT ポリシーの作成

AWS IoT のポリシーを作成します。このポリシーでは、AWS IoT への接続と、セキュアトンネリングのトンネルオープンの通知を受けるトピックのサブスクライブ・メッセージの受信を許可します。

  • AWS IoT のコンソール を開きます
  • 左側のメニューの 安全性 > ポリシー をクリックします
  • 右上の 作成 をクリックします
  • 名前: RaspberryPiTunnelPolicy を入力します
  • 「ステートメントを追加」の「アドバンストモード」をクリックします
  • 以下の JSON の ACCOUNT-ID, REGION をお使いのAWSアカウントID、リージョン(ap-northeast-1など)に置き換えて、テキストボックス内にペーストします
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": "arn:aws:iot:REGION:ACCOUNT-ID:client/${iot:Connection.Thing.ThingName}"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": "arn:aws:iot:REGION:ACCOUNT-ID:topicfilter/$aws/things/${iot:Connection.Thing.ThingName}/tunnels/notify"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Receive",
      "Resource": "arn:aws:iot:REGION:ACCOUNT-ID:topic/$aws/things/${iot:Connection.Thing.ThingName}/tunnels/notify"
    }
  ]
}
  • 右下の 作成 をクリックします

モノの作成

続いて、Raspberry Pi に対応する AWS IoT のモノを作成します。以下の手順を実行してください。

  • AWS IoT のコンソール を開きます
  • 左側のメニューの 管理 > モノ をクリックします
  • 右上の 作成 をクリックします
  • 単一のモノを作成する をクリックします
  • 名前RaspberryPi を入力し、次へ をクリックします
  • 1-Click 証明書作成 (推奨) > 証明書の作成 をクリックします
  • このモノの証明書、パブリックキー、プライベートキーの右のダウンロードをクリックします
  • 「また、AWS IoT のルート CA をダウンロードする必要があります」の下の 有効化をクリックします
  • 右下の ポリシーをアタッチをクリックします

証明書のダウンロードと有効化

  • 次の画面で、先ほど作成した RaspberryPiTunnelPolicyを検索してチェックボックスにチェックを入れ、右下のモノの登録をクリックします

秘密鍵・証明書のコピー

  • 上記でダウンロードした証明書、プライベートキーを Raspberry Pi に SCP などで転送して、ファイル名をraspberrypi.crtおよび raspberrypi.keyにリネームしておきます
# 証明書・プライベートキーを転送 (xxx の部分はご自身のファイル名に置き換えてください)
scp xxx.cert.pem <Raspberry Piのユーザ名>@<Raspberry PiのIPアドレス>:./raspberrypi.crt
scp xxx.private.key <Raspberry Piのユーザ名>@<Raspberry PiのIPアドレス>:./raspberrypi.key

これで、モノ、証明書およびポリシーが作成され、Raspberry Pi から AWS IoT Core へ接続する準備ができました。

localproxy のビルド (Raspberry Pi)

続いて、セキュアトンネリングを利用するための localproxy というアプリケーションのビルドを行います。セキュアトンネリングでは localproxy を接続元のPCおよび接続先のデバイス(Raspberry Pi)の両方にあらかじめインストールしておくことで、トンネルを介した遠隔での接続ができるようになります。

なお、本記事では 2020年7月現在のビルド手順を記載しておりますが、ビルド手順は変更される可能性があるため、最新版のビルド手順は GitHub のリポジトリ をご確認ください。

依存ライブラリのインストール

Raspberry Pi のターミナルで以下のコマンドを実行して、パッケージリストのアップデートと必要なライブラリのインストールを行います。

sudo apt update
sudo apt install -y build-essential cmake git libssl-dev python3-pip zlibc

任意の作業用ディレクトリで以下のコマンドを実行して、Boost をビルド&インストールします。なお、 Raspberry Pi 上で実行すると3〜4時間程度かかります。

wget https://dl.bintray.com/boostorg/release/1.69.0/source/boost_1_69_0.tar.gz -O /tmp/boost.tar.gz
tar xzvf /tmp/boost.tar.gz
cd boost_1_69_0
./bootstrap.sh
sudo ./b2 install

元の作業用ディレクトリに戻り、以下のコマンドを実行して Protobuf のビルド&インストールを行います。

wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protobuf-all-3.6.1.tar.gz -O /tmp/protobuf-all-3.6.1.tar.gz
tar xzvf /tmp/protobuf-all-3.6.1.tar.gz
cd protobuf-3.6.1
mkdir build
cd build
cmake ../cmake
make
sudo make install

元の作業用ディレクトリに戻り、以下のコマンドを実行して Catch2 をビルド&インストールします

git clone https://github.com/catchorg/Catch2.gitcd Catch2mkdir buildcd build
cmake ../
make
sudo make install

localproxy のビルド

Raspberry Pi のターミナルで以下のコマンドを実行して、localproxy のダウンロードとビルドを行います。

cd ~/
git clone https://github.com/aws-samples/aws-iot-securetunneling-localproxy
cd aws-iot-securetunneling-localproxy
mkdir build
cd build
cmake ../
make

~/aws-iot-securetunneling-localproxy/build/bin/localproxy ファイルが作成され、セキュアトンネリングで接続する準備が整いました。

localproxy のビルド (接続元PC)

接続元のPCでも、上記の手順と同様に localproxy をビルドしておきます。また、Docker が利用できる環境では、Dockerを利用してビルドを行うことも可能です。

git clone https://github.com/aws-samples/aws-iot-securetunneling-localproxy
cd aws-iot-securetunneling-localproxy
./docker-build.sh

Docker でビルドを行なった場合は、localproxy の実行も Docker コンテナ内で行います。以下のトンネル経由での接続の手順で ./docker-run.shを実行し、起動したシェルのlocalproxyバイナリを立ち上げて接続してください。Docker 外から localproxy に接続する場合は必要に応じて Docker 起動時の引数でポートを指定してください。

また、Windows でのビルド手順は こちらの手順 をご参照ください。

SSH もしくは RDP サーバのインストール (Raspberry Pi)

Raspberry Pi 側で SSH やリモートデスクトップ接続を待ち受けられるよう、サーバアプリケーションをインストールしておきます。リモートデスクトップ接続を行う場合は、Ubuntu に GNOME などのデスクトップ環境がインストールされ、.xsessionrc や /etc/xrdp/xrdp.ini などのファイルが適切に設定されていることを確認します。

# SSH を利用する場合
sudo apt install openssh-server
# リモートデスクトップを利用する場合
sudo apt install xrdp

サンプルアプリの実行

以下のプログラムを secure_tunnel.py として Raspberry Pi に保存します。このサンプルアプリでは AWS IoT Device SDK を用いて、$aws/things/モノの名前/tunnels/notify というトピックをSubscribe し、トンネルのオープンのメッセージが届いたら localproxy のアプリに適切な引数を渡して起動します。

import argparse
import json
import subprocess
import time

from awscrt import io, mqtt
from awsiot import mqtt_connection_builder

parser = argparse.ArgumentParser(description="Subscribe to a MQTT topic and launch localproxy")
parser.add_argument('--endpoint', required=True, help="Your AWS IoT custom endpoint")
parser.add_argument('--cert', help="File path to your client certificate")
parser.add_argument('--key', help="File path to your private key")
parser.add_argument('--root-ca', help="File path to root certificate authority")
parser.add_argument('--thing-name', help="Your thing name")
parser.add_argument('--localproxy', help="File path to your localproxy binary")
args = parser.parse_args()

SERVICE_PORTS = {"ssh": 22, "rdp": 3389}


def on_tunnels_notified(topic, payload):
    print(f"Received message: {payload}")
    payload = json.loads(payload)
    client_mode = payload["clientMode"]
    if client_mode != "destination":
        raise RuntimeError(f"unsupported client mode: {client_mode}")
    service = payload["services"][0].lower()
    if service not in SERVICE_PORTS:
        raise RuntimeError(f"unsupported service: {service}")
    port = SERVICE_PORTS[service]
    print(f"Launch localproxy for {service}")
    subprocess.Popen([args.localproxy,
                      "-t", payload["clientAccessToken"],
                      "-r", payload["region"],
                      "-d", f"localhost:{port}"])


def main():
    event_loop_group = io.EventLoopGroup(1)
    host_resolver = io.DefaultHostResolver(event_loop_group)
    client_bootstrap = io.ClientBootstrap(event_loop_group, host_resolver)

    mqtt_connection = mqtt_connection_builder.mtls_from_path(
        endpoint=args.endpoint,
        cert_filepath=args.cert,
        pri_key_filepath=args.key,
        client_bootstrap=client_bootstrap,
        ca_filepath=args.root_ca,
        client_id=args.thing_name,
        clean_session=False,
        keep_alive_secs=6)

    connect_future = mqtt_connection.connect()
    connect_future.result()
    print("MQTT connected")

    tunnel_topic = f"$aws/things/{args.thing_name}/tunnels/notify"
    subscribe_future, packet_id = mqtt_connection.subscribe(
        topic=tunnel_topic,
        qos=mqtt.QoS.AT_LEAST_ONCE,
        callback=on_tunnels_notified)
    subscribe_future.result()
    print(f"Topic subscribed: {tunnel_topic}")

    while True:
        time.sleep(1)


if __name__ == '__main__':
    main()

Raspberry Pi のターミナルで以下のコマンドを実行し、AWS IoT Device SDK Python v2 をインストールします。

pip3 install awsiotsdk

以下のコマンドを実行し AWS IoT のルートCA証明書をダウンロードします。

cd ~/
wget https://www.amazontrust.com/repository/AmazonRootCA1.pem

AWS IoT の接続先エンドポイントのURLを調べます。

  • AWS IoT のコンソール を開きます
  • 左側のメニューの 設定 をクリックします
  • カスタムエンドポイント 内の エンドポイント の値(xxxxx-ats.iot.ap-northeast-1.amazonaws.com のような形式)をコピーしておきます

以下のコマンドのエンドポイントの値をコピーしたものに置き換え、コマンドを実行してプログラムを開始します。

python3 secure_tunnel.py \
  --endpoint コピーしたエンドポイントの値 \
  --root-ca ~/AmazonRootCA1.pem \
  --cert ~/raspberrypi.crt \
  --key ~/raspberrypi.key \
  --thing-name RaspberryPi \
  --localproxy $HOME/aws-iot-securetunneling-localproxy/build/bin/localproxy

以下のようなログが表示されていれば、Raspberry Pi 側ではトンネルのオープンを待ち受ける準備ができています。

MQTT connected
Topic subscribed: $aws/things/RaspberryPi/tunnels/notify

トンネルのオープン

続いて、マネジメントコンソールからトンネルをオープンします。なお、2020年7月現在、トンネルのオープン1回あたり 6.00 USD (東京リージョンの場合) がかかりますので、ご注意の上ご利用ください。

  • AWS IoT のコンソール を開きます
  • 左側のメニューの 管理 > トンネル をクリックします
  • 右上の トンネルを開く をクリックします
  • 送信先の設定有効化 をクリックします
    • ThingNameRaspberryPi を選択します
    • サービスrdp を入力します (SSH 接続を行う場合は ssh と入力してください)
  • 右下の トンネルを開く をクリックします
  • 送信元のアクセストークンの右のダウンロードをクリックします
  • xxxxx-sourceAccessToken.txt のようなファイルがダウンロードされるので、ダウンロードしたテキストファイルを開き、中身のTokenをコピーしておきます

トンネル経由でのリモートデスクトップ接続

Raspberry Pi のターミナルに以下のようなログが出ていることが確認できれば、Raspberry Pi 側はトンネルオープンのMQTTメッセージを受信し、接続の準備ができています。

Received message: {...}
Launch localproxy for rdp
...
[2020-07-03T12:18:37.718301]{2433}[info]    Starting proxy in destination mode
...

以下のコマンド中の localproxy までのパス、リージョンコピーしたToken を自分の環境にあわせて置き換え、接続元のPCでコマンドを実行して、localproxy アプリを立ち上げます。

/path/to/your/aws-iot-securetunneling-localproxy/build/bin/localproxy \
  -r リージョン(ap-northeast-1など) \
  -s 3389 \
  -t コピーしたToken

問題なく接続されれば、以下のようなログが表示されます。

[2020-07-03T21:22:31.074761]{51161}[info]    Starting proxy in source mode
[2020-07-03T21:22:31.083563]{51161}[info]    Attempting to establish web socket connection with endpoint wss://data.tunneling.iot.ap-northeast-1.amazonaws.com:443
...
[2020-07-03T21:22:31.416155]{51161}[info]    Successfully established websocket connection with proxy server: wss://data.tunneling.iot.ap-northeast-1.amazonaws.com:443
[2020-07-03T21:22:31.418067]{51161}[info]    Listening for new connection on port 3389

リモートデスクトップのクライアントアプリを開き、localhost:3389 へ接続します。ユーザ名とパスワードを入力してログインすると、以下のように Raspberry Pi のデスクトップ画面が表示されることが確認できます。なお、2020年7月 現在、セキュアトンネリングで利用できるトンネルあたりの帯域幅は800Kbpsのため、動作が遅い場合には画面の解像度を小さくすることで改善する場合があります。

トンネルは1回オープンすると最大12時間まで利用することができますので、あとはトラブルシューティングのためにファイルやログを確認したり、設定を変更したりすることができます。

トンネル経由でのSSH接続

また、トンネル経由で SSH 接続をする場合も基本的な流れは同様です。接続元のPCで以下のコマンドを実行して、localproxy アプリを立ち上げます。

/path/to/your/aws-iot-securetunneling-localproxy/build/bin/localproxy \
  -r リージョン(ap-northeast-1など) \
  -s 8022 \
  -t コピーしたToken

問題なく接続されたことを確認したら、別のターミナルを開いて以下のコマンドを実行します。

ssh RaspberryPiのユーザ名@localhost -p 8022

ターミナルに ユーザ名@ubuntu $ のように表示され、トンネル経由で Raspberry Pi に SSH でログインしたことを確認できました。

さいごに

この記事では、AWS IoT Device Management のセキュアトンネリングを利用して、SSHやリモートデスクトップで遠隔からトラブルシューティングを行う方法を紹介しました。セキュアトンネリングの詳細については、開発者ガイドを参照してください。また、デバイスなしでセキュアトンネリングを試してみたい方はデモをお試しください。

実際のユースケースではセキュアトンネリングによるデバイス側のトラブルシューティングだけでなく、AWS IoT Core の接続やメッセージングなどのログを Amazon CloudWatch で確認したり IoT のモノのシャドウの値を確認したりすることで、クラウド側で確認できる情報からも問題の切り分けを行い、解決を目指します。セキュアトンネリングを利用することで、お客様の IoT 機器管理やトラブルシューティングが容易でスピーディになることを願っています。

著者について

mihira

三平 悠磨

ソフトウェアエンジニアとして会話AIやロボット開発を経験しました。AWS では IoT スペシャリストソリューションアーキテクトとして、お客様の IoT 関連案件を支援しています。