erebe/wstunnel で SSL 証明書を使う

wstunnel という名前のものはいくつかあるようですが、erebe/wstunnel が Rust (かつては Haskell!) で書かれていて良さそうに思いました。コマンドが SSH と似ているので使いやすいです。

https://github.com/erebe/wstunnel

ちゃんとしたところから発行された証明書でポート 443 を使おうとしたところ少し迷ったので、記事にします。


## 注意点

**最低限のオプションしか指定せずにサーバーを起動すると、他人から踏み台として使われてしまいます。**

以下では次のようにして安全性を高めています。

- server の `--restrict-to` オプションで SSH のように認証が必要なポートを指定する
- クライアント証明書で認証する

必ず、意図しない利用者が利用できないようにしてください。


## 証明書の取得

https://qiita.com/hanotch/items/50831eafb75fd8e89ed8

上記記事の手順で証明書を入手しました。内容は以下です。

- certificate.crt: 私のサーバーの証明書
- ca_bundle.crt: ルートおよび中間証明書
- private.key


## 実行

### サーバー側

事前に certificate.crt と ca_bundle.crt を結合しておきます。

```console
$ cat certificate.crt ca_bundle.crt > chained.crt
```

あとは起動するだけです。

```console
$ ./wstunnel server --tls-certificate chained.crt --tls-private-key private.key wss://[::]:8080 --restrict-to 127.0.0.1:22
```

### クライアント側

クライアントのポート 22 をサーバーの SSH ポートにトンネルする例です。ちゃんとした証明書なので --tls-verify-certificate をつけても動作します。

```console
$ ./wstunnel client --tls-verify-certificate -L tcp://10022:127.0.0.1:22 wss://my-server:8080
```

あとは SSH するだけ。

```console
$ ssh -p 10022 localhost
```


## サービス化して実行

ポート 443 を使うにはサービス化するのが良さそうです。

### サーバー側

以下は使えるのを SSH だけに制限する例です。

```console
$ sudo cat <<EOF >/etc/systemd/system/wstunnel.service
[Service]
ExecStart=/path/to/wstunnel server --tls-certificate /path/to/chained.crt --tls-private-key /path/to/private.key wss://[::] --restrict-to 127.0.0.1:22

[Install]
WantedBy=multi-user.target
EOF
$ sudo systemctl start wstunnel
```

これで WSS のデフォルトであるポート 443 で待ち受けます。

再起動後も有効にするには以下を実行します。

```console
$ sudo systemctl enable wstunnel
```

### クライアント側

デフォルトなので WSS のポートの指定がいらなくなります。SSH するところは先ほどと同じです。

```console
$ ./wstunnel client --tls-verify-certificate -L tcp://10022:127.0.0.1:22 wss://my-server
```


## クライアント証明書の利用

以下のページの手順を少しアレンジした手順をご紹介します。開発／テスト用で、プロダクション用ではありませんのでご注意ください。

https://github.com/erebe/wstunnel/blob/main/docs/using_mtls.md

### CA 業務で必要になるファイルやディレクトリーの作成

まずは openssl で必要になるファイルやディレクトリーを作成します。

```console
$ mkdir -p $HOME/wstunnel/ca/{certs,csr,crl,newcerts,private}
$ cd $HOME/wstunnel/ca/
$ echo 1000 > serial
$ touch index.txt
```

### 設定ファイルの作成

以下のようにして、openssl.cnf を作成します。

```conf:openssl.cnf
[ ca ]
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = $ENV::HOME/wstunnel/ca
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand

# The root key and root certificate.
private_key       = /path/to/private.key
certificate       = $dir/certs/ca.cert.pem

# For certificate revocation lists.
crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 30

# SHA-1 is deprecated, so use SHA-2 instead.
default_md        = sha256

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_loose

[ policy_loose ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = optional
emailAddress            = optional

[ req ]
# Configuration for a certificate signing request.
default_bits        = 2048
distinguished_name  = req_distinguished_name
string_mask         = utf8only
# SHA-1 is deprecated, so use SHA-2 instead.
default_md          = sha256
# Extension to add when the -x509 option is used.
x509_extensions     = v3_ca

[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

[ v3_ca ]
# Configuration for a certificate authority.
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ client_cert ]
# Configuration for client certificates.
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Configuration for server certificates.
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
# Configuration for CRLs.
authorityKeyIdentifier=keyid:always
```

`private_key = /path/to/private.key` のところは実際の private.key のパスに書き換えてください。

### CA の自己署名証明書の作成

```console
$ openssl req -config openssl.cnf -key /path/to/private.key -new -x509 -days 7300 -sha256 -extensions v3_ca -subj / -out certs/ca.cert.pem
```

`-subj /` により subject の中身を空にしています。入力したい場合にはこのオプションを外してインタラクティブに設定するなどしてください。

### クライアントでキーと署名要求 (Certificate Signing Request) を作成

```console
$ openssl genrsa -out wstunnel-client-1.pem 2048
$ openssl req -key wstunnel-client-1.pem -new -subj / -out wstunnel-client-1.csr.pem
```

作成した CSR は scp などでサーバーに送ってください。

### 署名要求に署名

サーバーで CSR に署名します。

```console
$ openssl ca -config openssl.cnf -extensions client_cert -days 375 -in wstunnel-client-1.csr.pem -out wstunnel-client-1.cert.pem
```

以下のように聞かれるのでどちらも y と回答します。

```console
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
```

作成した wstunnel-client-1.cert.pem は scp などでクライアントに送ってください。

### wstunnel server 起動

```console
$ ./wstunnel server --tls-certificate /path/to/chained.crt --tls-private-key /path/to/private.key --tls-client-ca-certs ca/certs/ca.cert.pem wss://[::]:8443
```

### wstunnel client 起動

```console
$ ./wstunnel client --tls-certificate /path/to/wstunnel-client-1.cert.pem --tls-private-key /path/to/wstunnel-client-1.pem -L tcp://10022:localhost:22 wss://20.37.102.222:8443
```

### トラブルシューティング

Windows なら Git Bash の openssl を使うなどして、接続確認します。

Mac の場合は LibreSSL の openssl コマンドでは `-cert_chain` オプションがなく、Homebrew で OpenSSL を入れるとかしないと確認できません。

```console
$ openssl s_client -connect my-server:8443 -key wstunnel-client-1.pem -cert wstunnel-client-1.cert.pem -cert_chain ca.cert.pem -state
```

ダメな場合は Certificate Unknown などと出ると思います。私のアレンジのせいかもしれませんので、冒頭で紹介した、配布元の mTLS のページを参考にするなどして解決してください。

私は、面倒な openssl.cnf の作成をやらずに、適当に検索して出てきた x509 の証明書をクライアント証明書として利用しようとしたらこうなりました。


## 参考

https://qiita.com/3244/items/8c7e6892efdb4e500be9