AWS IoT CoreにVPN環境からアクセス

2022/1/28 更新

再度試したところ、このままではうまくいかなかったので、追記しました。

* Route 53のプライベートホストゾーンの設定を追加
*  `update-resolv-conf`を`update-systemd-resolved`の変更

---

IoT CoreがPrivateLinkをサポートしたとのことで、早速試しました。
https://aws.amazon.com/jp/about-aws/whats-new/2021/07/aws-iot-core-supports-vpc-endpoints/

このような形で構成することでインターネット上にデータが出ない構成となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180441/3c1547d6-6735-6c0b-bcb7-55a53cae1e26.png)


公式ドキュメントからの引用ですが、以下のような制約があります。
https://docs.aws.amazon.com/iot/latest/developerguide/IoTCore-VPC.html

* VPCエンドポイントは、現在IoTデータエンドポイントのみサポートされています。
* MQTTのキープアライブ期間は230秒に制限されています。この期間を超えるキープアライブは自動的に230秒に短縮されます
* 各VPCエンドポイントは、合計100,000台の同時接続デバイスをサポートします。それ以上の接続数が必要な場合は、「IoT CoreでVPCエンドポイントを拡張する」を参照してください。
* VPCエンドポイントは、IPv4トラフィックのみをサポートします。
* VPCエンドポイントは、カスタムドメインを除き、ATS証明書のみを提供します。
* VPCエンドポイントのポリシーは、現時点ではサポートされていません。


---

実際に試してみましたが、設定がまあまあな量となりました。
VPCの勉強になりました。

# VPC

## VPCの作成

| 項目 | 値 |
| --- | --- |
| 名前 | private-iot-vpc |
| IPv4 CIDR ブロック | 10.0.0.0/16 |
| IPv6 CIDR ブロック | IPv6 CIDR ブロックなし |
| テナンシー | デフォルト |

作成後、DNSの設定を変更します。

* アクション→DNS ホスト名を編集

| 項目 | 値 |
| --- | --- |
| DNS ホスト名 | 有効化|

## サブネットの作成

| 項目 | 値 |
| --- | --- |
| VPC ID | private-iot-vpc |

* サブネットの設定

| 項目 | 値 |
| --- | --- |
| サブネット名 | private-iot-vpc-subnet-1 |
| アベイラビリティーゾーン | ap-northeast-1a |
| IPv4 CIDR ブロック | 10.0.1.0/24 | 

(* IoT向けのエンドポイントがap-northeast-1dは未対応のようなので避けましょう）

## セキュリティグループの作成

### Client VPN用

| 項目 | 値 |
| --- | --- |
| セキュリティグループ名  | private-iot-sg-clientvpn |
| 説明| private-iot-sg-clientvpn |
| VPC | private-iot-vpc | 

* インバウンドルール

| 項目 | 値 |
| --- | --- |
| タイプ  | カスタム TCP |
| プロトコル | TCP |
| ポート範囲 | 8883 | 
| ソース | Anywhere-IPv4 |

* アウトバウンドルール

| 項目 | 値 |
| --- | --- |
| タイプ  | すべてのTCP |
| プロトコル | TCP |
| ポート範囲 | 0 - 65535 | 
| ソース | Anywhere-IPv4 |

### PrivateLink用

| 項目 | 値 |
| --- | --- |
| セキュリティグループ名  | private-iot-sg-privatelink |
| 説明| private-iot-sg-privatelink |
| VPC | private-iot-vpc | 

* インバウンドルール

| 項目 | 値 |
| --- | --- |
| タイプ  | すべてのTCP |
| プロトコル | TCP |
| ポート範囲 | 0 - 65535 | 
| ソース | Anywhere-IPv4 |

* アウトバウンドルール

| 項目 | 値 |
| --- | --- |
| タイプ  | カスタム TCP |
| プロトコル | TCP |
| ポート範囲 | 8883 | 
| ソース | Anywhere-IPv4 |

# PrivateLink

## PrivateLinkの作成

左メニューのエンドポイントから作成

| 項目 | 値 |
| --- | --- |
| サービスカテゴリ | AWS サービス |
| サービス名 | com.amazonaws.ap-northeast-1.iot.data |
| VPC | private-iot-vpc | 
| サブネット | private-iot-vpc-subnet-1 |
| セキュリティグループ | private-iot-sg-privatelink |

# Route 53

## IoT Coreのエンドポイントの確認

Route 53の設定の前に、IoT Coreのエンドポイントを確認します。
IoT CoreのエンドポイントはAWSアカウントごとに固有となります。
自分のアカウントのエンドポイントはCLIで取得可能です。

```console
aws iot describe-endpoint --endpoint-type iot:Data-ATS
```

## プライベートホストゾーンの作成

Route 53の画面でホストゾーンを作成します。

| 項目 | 値 |
| --- | --- |
| ドメイン名 | iot.ap-northeast-1.amazonaws.com |
| タイプ | プライベートホストゾーン |
| ホストゾーンに関連付けるVPC | private-iot-vpc |

## レコードの作成

IoT Coreのエンドポイントのレコードを作成します。

| 項目 | 値 |
| --- | --- |
| レコード名 | 取得したIoT Coreエンドポイントの先頭（xxxxxxxxxxxxxx-ats）の部分 |
| レコードタイプ | A |
| 値 | エイリアスを有効にし、VPCエンドポイントへのエイリアスから作成したIoT Core用のVPCエンドポイントを選択。２つリストアップされますが、vpce-xxxx.data.iot.ap-northeast-1.vpce.amazonaws.comという方でうまくいきました |


# ClientVPN用証明書をACMに登録

## 証明書の作成

Client VPNの認証方法のうち、相互認証 (証明書ベース)を行うための証明書を作成します。
https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-admin/client-authentication.html

```bash:RaspberryPi
git clone https://github.com/OpenVPN/easy-rsa.git
cd easy-rsa/easyrsa3
./easyrsa init-pki # PKI 環境を初期化
./easyrsa build-ca nopass # 認証局 (CA) を構築
./easyrsa build-server-full server nopass # サーバー証明書とキーを生成
./easyrsa build-client-full client1.domain.tld nopass # クライアント証明書とキーを生成
```

`~/cert`ディレクトリーに必要なものをまとめます。

```bash:RaspberryPi
mkdir ~/cert/
cp pki/ca.crt ~/cert/
cp pki/issued/server.crt ~/cert/
cp pki/private/server.key ~/cert/
cp pki/issued/client1.domain.tld.crt ~/cert/
cp pki/private/client1.domain.tld.key ~/cert/
```

## 証明書の登録

ACM(AWS Certificate Manager)の画面で証明書をインポート

* server

| 項目 | 値 |
| --- | --- |
| 証明書本文 | server.crtの`BEGIN CERTIFICATE`から`END CERTIFICATE`まで |
| 証明書のプライベートキー | server.keyの内容|
| 証明書チェーン | ca.crtの内容 |

* client1.domain.tld

| 項目 | 値 |
| --- | --- |
| 証明書本文 | client1.domain.tld.crtの`BEGIN CERTIFICATE`から`END CERTIFICATE`まで |
| 証明書のプライベートキー | client1.domain.tld.keyの内容|
| 証明書チェーン | ca.crtの内容 |


# ClientVPN

## Client VPNエンドポイントの作成

左メニューの仮想プライベートネットワーク（VPN）→クライアント VPN エンドポイント
クライアント VPN エンドポイントの作成

| 項目 | 値 |
| --- | --- |
| 名前タグ | private-iot-clientvpn |
| クライアント IPv4 CIDR | 172.31.0.0/16 |

* 認証情報

| 項目 | 値 |
| --- | --- |
| サーバー証明書 ARN | server |
| 認証オプション | 相互認証の使用 | 
| クライアント証明書 ARN | client1.domain.tld |

* 接続ログ記録

| 項目 | 値 |
| --- | --- |
| クライアント接続の詳細を記録しますか? | いいえ |

* その他のオプションパラメータ

| 項目 | 値 |
| --- | --- |
| DNS サーバー 1 IP アドレス | 10.0.0.2 |

(* VPCのCIDRの末尾が2がAWS提供のDNSサーバーのIPアドレスです）
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/VPC_DHCP_Options.html#AmazonDNS

## 関連付け設定

関連付けタブから関連付け

| 項目 | 値 |
| --- | --- |
| VPC | private-iot-vpc |
| 関連付けるサブネットの選択 | private-iot-vpc-subnet-1 |

## セキュリティグループ設定

セキュリティグループタブからセキュリティグループの適用

| 項目 | 値 |
| --- | --- |
| セキュリティグループ ID | private-iot-sg-clientvpn |

## 認証設定

認証タブから受信の承認

| 項目 | 値 |
| --- | --- |
| アクセスを有効にする送信先ネットワーク | 10.0.0.0/16 |
| アクセスを付与する対象 | すべてのユーザーにアクセスを許可する |


# Raspberry Pi

## OpenVPNのインストール

```bash
sudo apt update
sudo apt install openvpn openvpn-systemd-resolved -y
```

AWSのマネジメントコンソールのクライアントVPNエンドポイントの設定画面
上部にあるクライアント設定のダウンロードから設定ファイルを取得


設定ファイルの末尾に以下を追記。これをしないとAWS側のDNSサーバーが参照されません

```text
script-security 2
up /etc/openvpn/update-systemd-resolved
down /etc/openvpn/update-systemd-resolved
```

設定ファイルはこんな感じです

```
client
dev tun
proto udp
remote cvpn-endpoint-xxxxxxxxxxxxxxxxx.prod.clientvpn.ap-northeast-1.amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
<ca>
-----BEGIN CERTIFICATE-----

-----END CERTIFICATE-----

</ca>


reneg-sec 0

script-security 2
up /etc/openvpn/update-systemd-resolved
down /etc/openvpn/update-systemd-resolved
```

# 接続

```bash
sudo openvpn --config downloaded-client-config.ovpn --cert cert/client1.domain.tld.crt --key cert/client1.domain.tld.key
```



```console
pi@raspberrypi:~ $ sudo openvpn --config downloaded-client-config.ovpn --cert cert/client1.domain.tld.crt --key cert/client1.domain.tld.key 
Sat Jul 17 09:47:16 2021 OpenVPN 2.4.7 arm-unknown-linux-gnueabihf [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Apr 28 2021
Sat Jul 17 09:47:16 2021 library versions: OpenSSL 1.1.1d  10 Sep 2019, LZO 2.10
Sat Jul 17 09:47:16 2021 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
Sat Jul 17 09:47:16 2021 TCP/UDP: Preserving recently used remote address: [AF_INET]35.75.87.106:443
Sat Jul 17 09:47:16 2021 Socket Buffers: R=[180224->180224] S=[180224->180224]
Sat Jul 17 09:47:16 2021 UDP link local: (not bound)
Sat Jul 17 09:47:16 2021 UDP link remote: [AF_INET]35.75.87.106:443
Sat Jul 17 09:47:16 2021 TLS: Initial packet from [AF_INET]35.75.87.106:443, sid=3ff2acdd fabd509e
Sat Jul 17 09:47:16 2021 VERIFY OK: depth=1, CN=Easy-RSA CA
Sat Jul 17 09:47:16 2021 VERIFY KU OK
Sat Jul 17 09:47:16 2021 Validating certificate extended key usage
Sat Jul 17 09:47:16 2021 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
Sat Jul 17 09:47:16 2021 VERIFY EKU OK
Sat Jul 17 09:47:16 2021 VERIFY OK: depth=0, CN=server
Sat Jul 17 09:47:16 2021 Control Channel: TLSv1.2, cipher TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA
Sat Jul 17 09:47:17 2021 [server] Peer Connection Initiated with [AF_INET]35.75.87.106:443
Sat Jul 17 09:47:18 2021 SENT CONTROL [server]: 'PUSH_REQUEST' (status=1)
Sat Jul 17 09:47:18 2021 PUSH: Received control message: 'PUSH_REPLY,dhcp-option DNS 10.0.0.2,redirect-gateway def1 bypass-dhcp,route-gateway 172.31.0.33,topology subnet,ping 1,ping-restart 20,ifconfig 172.31.0.34 255.255.255.224,peer-id 0,cipher AES-256-GCM'
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: timers and/or timeouts modified
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: --ifconfig/up options modified
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: route options modified
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: route-related options modified
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: peer-id set
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: adjusting link_mtu to 1624
Sat Jul 17 09:47:18 2021 OPTIONS IMPORT: data channel crypto options modified
Sat Jul 17 09:47:18 2021 Outgoing Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Sat Jul 17 09:47:18 2021 Incoming Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Sat Jul 17 09:47:18 2021 ROUTE_GATEWAY 192.168.0.1/255.255.255.0 IFACE=wlan0 HWADDR=dc:a6:32:c2:07:8a
Sat Jul 17 09:47:18 2021 TUN/TAP device tun0 opened
Sat Jul 17 09:47:18 2021 TUN/TAP TX queue length set to 100
Sat Jul 17 09:47:18 2021 /sbin/ip link set dev tun0 up mtu 1500
Sat Jul 17 09:47:18 2021 /sbin/ip addr add dev tun0 172.31.0.34/27 broadcast 172.31.0.63
Sat Jul 17 09:47:18 2021 /etc/openvpn/update-resolv-conf tun0 1500 1552 172.31.0.34 255.255.255.224 init
dhcp-option DNS 10.0.0.2
Too few arguments.
Too few arguments.
Sat Jul 17 09:47:18 2021 /sbin/ip route add 35.75.87.106/32 via 192.168.0.1
Sat Jul 17 09:47:18 2021 /sbin/ip route add 0.0.0.0/1 via 172.31.0.33
Sat Jul 17 09:47:18 2021 /sbin/ip route add 128.0.0.0/1 via 172.31.0.33
Sat Jul 17 09:47:18 2021 WARNING: this configuration may cache passwords in memory -- use the auth-nocache option to prevent this
Sat Jul 17 09:47:18 2021 Initialization Sequence Completed

```


# 接続テスト

（IoT Coreのデバイスの作成や証明書の取得は省略。。。）

VPN接続したままだとインターネットに繋がらないので一旦VPNを切断して実行

```bash:raspberrypi
mkdir ~/iot-sample
cd ~/iot-sample/
python3 -m venv .venv
source .venv/bin/activate
pip install awsiotsdk # AWS IoT Device SDK v2 for Python
wget https://raw.githubusercontent.com/aws/aws-iot-device-sdk-python-v2/main/samples/pubsub.py #サンプルプログラム
```

ここまでできたら再度VPNを接続してPubSubのテスト

エンドポイントはインターネット経由でつなぐときと同じです。

```console:RaspberryPi
python pubsub.py \
  --endpoint xxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com \
  --root-ca AmazonRootCA1.pem \
  --cert xxxxxxxxxx-certificate.pem.crt \
  --key xxxxxxxxxx-private.pem.key
```

```console:RaspberryPi
Connecting to xxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com with client ID 'test-3c246580-3dc8-4268-9daf-731b3e42288b'...
Connected!
Subscribing to topic 'test/topic'...
Subscribed with QoS.AT_LEAST_ONCE
Sending 10 message(s)
Publishing message to topic 'test/topic': Hello World! [1]
Received message from topic 'test/topic': b'"Hello World! [1]"'
Publishing message to topic 'test/topic': Hello World! [2]
Received message from topic 'test/topic': b'"Hello World! [2]"'
Publishing message to topic 'test/topic': Hello World! [3]
Received message from topic 'test/topic': b'"Hello World! [3]"'
Publishing message to topic 'test/topic': Hello World! [4]
Received message from topic 'test/topic': b'"Hello World! [4]"'
Publishing message to topic 'test/topic': Hello World! [5]
Received message from topic 'test/topic': b'"Hello World! [5]"'
Publishing message to topic 'test/topic': Hello World! [6]
Received message from topic 'test/topic': b'"Hello World! [6]"'
Publishing message to topic 'test/topic': Hello World! [7]
Received message from topic 'test/topic': b'"Hello World! [7]"'
Publishing message to topic 'test/topic': Hello World! [8]
Received message from topic 'test/topic': b'"Hello World! [8]"'
Publishing message to topic 'test/topic': Hello World! [9]
Received message from topic 'test/topic': b'"Hello World! [9]"'
Publishing message to topic 'test/topic': Hello World! [10]
Received message from topic 'test/topic': b'"Hello World! [10]"'
10 message(s) received.
Disconnecting...
Disconnected!
```

うまくいきました。

ちなみに、エンドポイントのIPアドレスは、VPN内のプライベートIPアドレスとなっています。

```console:RaspberryPi
(.venv) pi@raspberrypi:~/iot-private $ ping xxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com
PING xxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com (10.0.1.105) 56(84) bytes of data.
^C
--- xxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

(.venv) pi@raspberrypi:~/iot-private $ 
```

やったね。

# インターネット経由での接続を拒否する方法

ここまでで、VPNを使ったIoT Coreまでの通信はできました。
追加で、 **インターネット経由での接続を拒否し、PrivateLink経由のみを許可**　する設定を紹介します。

設定は、IoTポリシーで、ConditionにVPCエンドポイントからのアクセスに限定します。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:*",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:SourceVpce": "vpce-0bae8fd9e8c2b2879"
        }
      }
    }
  ]
}
```

こうすると、VPNオフでインターネット経由での通信の場合にはエラーとなります。

```console:RaspberryPi（VPNオフ状態で実行）
(.venv) pi@raspberrypi:~/iot-private $ python pubsub.py --endpoint xxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com --root-ca AmazonRootCA1.pem --cert xxxxxxxxxx-certificate.pem.crt --key xxxxxxxxxx-private.pem.key
Connecting to xxxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com with client ID 'test-25dc3da9-5177-4a27-85f7-21fdf9456369'...
Traceback (most recent call last):
  File "pubsub.py", line 133, in <module>
    connect_future.result()
  File "/usr/lib/python3.7/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "/usr/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result
    raise self._exception
awscrt.exceptions.AwsCrtError: AWS_ERROR_MQTT_UNEXPECTED_HANGUP: The connection was closed unexpectedly.
(.venv) pi@raspberrypi:~/iot-private $ 
```

# おまけ（Raspberry PiのOS起動時にVPNに自動接続する方法）

`downloaded-client-config.ovpn`の拡張子を`.conf`に変更します。
（ついでにファイル名も`clientvpn.conf`にしました）

そして、ファイル中に接続に使用する証明書とキーを追記します。
（`<key></key>`の部分と`<cert></cert>`の部分です。）

```text:clientvpn.conf
client
dev tun
proto udp
remote cvpn-endpoint-xxxxxxxxxxxxxxxxx.prod.clientvpn.ap-northeast-1.amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
<ca>
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
</ca>
<key>
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
</key>
<cert>
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
</cert>

reneg-sec 0

script-security 2
up /etc/openvpn/update-systemd-resolved
down /etc/openvpn/update-systemd-resolved

```

ファイルを用意できたら、所定の位置にコピーします。

```bash
sudo cp clientvpn.conf /etc/openvpn/client/clientvpn.conf
```

systemctlの自動起動設定を追加（clientvpnの部分がconfファイルのファイル名になります）

```bash
sudo systemctl enable openvpn-client@clientvpn.service
sudo systemctl start openvpn-client@clientvpn.service
```

# ClientVPN用の証明書について

公式ドキュメントにあるEasy-RSA 3を使った方法で検証しましたが、「AWS Certificate Manager プライベート認証局」で生成した証明書でも良いようです。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/client-vpn-authenticate-private-ca-acm/