1つのアドレス・ポートを SSH (over SSL) と HTTPS で使う


やりたいこと

以前調べた時に、stunnel を使えば SSH over SSL が出来ることは分かった。
stunnel で SSH over SSL する (forward proxy 経由も可)

しかし今度は HTTPS も同じサーバで使いたくなった。
アドレスを分けてそれぞれで tcp/443 を listen させれば当然できるが、
追加で IP アドレスを調達するのは面倒だったりお金がかかったりする。
サーバ側の都合としてはポートを分けてもいいのだが、クライアント側の環境が特定ポート宛てしか通信できない場合は困る。

そこで、1つの IP アドレス・ポート番号 のペアで対応する方法を検討した。

解決方法

1. Name based Virtual Host で対応する?

簡単に名前ベースな Virtual Host を HTTPS で使うなら、
多分サーバ証明書に Subject Alternative Name を入れたりワイルドカード証明書を使った上で Host ヘッダを見れば出来るはず。

しかし今回は複数の HTTPS サーバではなく、SSH のために単なる TCP proxy としても動いてもらわないと困る。SSH クライアントは当然 Host ヘッダは送ってこないので、この方法は合っていない。

2. SNI (Server Name Indication) で対応する

SNI は TLS の拡張で、Client Handshake の時にサーバ名を通知するものだ。
クライアント・サーバともに対応している必要があるが、TLS だけで完結しているので上位レイヤは任意のプロトコルが使用可能だ。
詳細は RFC 6066 を参照。

あとは TCP proxy 的な動きをして、かつ SNI をさばける人 を探して設定すればよい。

サーバ設定

最初は nginx が ssl を listen して特定の名前の時にバックエンドに TCP proxy すればよいかと思ったが、
nginx_tcp_proxy_module との組み合わせでは SNI ができないようだった。
(あまり詳しく調べていないが、それらしい設定ができなかった)

ググると stunnel も SNI をサポートしているらしい。
Changelog を見ると 4.37 で Client-side SNI が
4.38 で Server-side SNI がそれぞれ実装されたようで、stunnel を使うことにした。

サーバは CentOS 6 を使っており EPEL のパッケージだと古いので、ソースからインストールする。

インストール
# cd /usr/local/src
# wget --no-check-certificate https://www.stunnel.org/downloads/stunnel-5.03.tar.gz
# tar xf stunnel-5.03.tar.gz
# cd stunnel-5.03
# ./configure
# make
# make install

インストールの方は特に問題なかったが、設定にはおまじないが必要だった。

(NGな例)SNI対応サーバ設定
sslVersion = TLSv1
chroot = /var/run/stunnel
setuid = nobody
setgid = nobody
pid = /stunnel.pid
output = /var/log/stunnel/stunnel.log
cert = /etc/stunnel/server.crt
key = /etc/stunnel/server.key
CAfile = /etc/stunnel/ca.crt

[SNI]
accept  = 443

[www]
sni = SNI:www.example.com
connect = 80

[sshd]
sni = SNI:ssh.example.com
connect = 22
TIMEOUTclose = 0
stunnel実行時のエラー
# /usr/local/bin/stunnel /usr/local/etc/stunnel.conf
<< 省略 >>
[!] Service [SNI]: Each service must define two endpoints

オプション解析で endpoint の定義数チェックに引っかかる。

src/options.c
    if(cmd==CMD_END) {
        if(new_service_options.next) { /* daemon mode checks */
            if(endpoints!=2)
                return "Each service must define two endpoints";

endpoint としてカウントされる connect や exec を書いておけば、(多分使われないが) エラーを回避して動いた。

(OKな例)SNI対応サーバ設定
sslVersion = TLSv1
chroot = /var/run/stunnel
setuid = nobody
setgid = nobody
pid = /stunnel.pid
output = /var/log/stunnel/stunnel.log
cert = /etc/stunnel/server.crt
key = /etc/stunnel/server.key
CAfile = /etc/stunnel/ca.crt

[SNI]
accept  = 443
; connect で適当なポートを書いたら動いた
connect = 0
; connect の代わりに exec で適当なものを書いても動いた
;exec = /bin/true

[www]
sni = SNI:www.example.com
connect = 80

[sshd]
sni = SNI:ssh.example.com
connect = 22
TIMEOUTclose = 0

これで tcp/80 で nginx を、tcp/22 で sshd を動かしておけば OK

証明書の検証をちゃんとしたいなら、Subject Alternative Name に設定したり CN をワイルドカード (*.example.com) にする。

クライアント認証をやる場合は 以前の設定 を参考にする。

クライアント設定

クライアント側の cygwin の方は以前インストールしたものが 5.01 だったので、パッケージの stunnel を使う。

SNI対応クライアント設定
debug = info
output = /var/log/stunnel.log
CAfile = /etc/stunnel/ca.crt
options = NO_SSLv2

[SNI-test]
client = yes
accept = 9999
connect = ssh.example.com:443
sni = ssh.example.com

これでクライアントから自分の tcp/9999 に ssh すれば ssh.example.com にログインできるし、
ブラウザで https://www.example.com/ にアクセスすれば Web ページが返ってくる。

forward proxy を通したりクライアント認証をやる場合は 以前の設定 を参考にする。