Hexo server をよりセキュアに

Hexo server への直接アクセスを禁止して、Web サーバ H2O を「SSL/TLS あり Basic 認証ありの reverse proxy」として動かして、その H2O 経由で Hexo server にアクセスするようにします。

まえがき

静的サイトジェネレータ (Static Site Generator: SSG) のひとつである Hexo には、”Hexo server” という便利な機能があります。Hexo で構築しているブログを更新するとき、Web サーバに deploy せずにプレビューできる機能です。

コマンドラインで “hexo server -p 8888“ とか実行するだけで Hexo 内蔵の HTTP サーバ機能が起動します。手元の Web ブラウザでポート番号を指定してアクセスすれば、更新後のブログのプレビューが見られます。ブログの原稿(Markdown ファイル)を更新すると、Hexo server がそれをリアルタイムに検知して自動的にコンテンツに反映してくれるので、ブラウザでリロードするだけで結果をチェックできます。

Hexo server の難点は、アクセス制限がかけられないことです。クラウド上の公開サーバで実行すると、インターネットに向けてコンテンツが公開されてしまうのです。プレビュー用なのに。もちろん、ポート番号を誰も使ってなさそうな大きな番号にするとか、アクセス可能な IP アドレスを iptables で制限するとか、緩和策はあります。でも、なんかイマイチ。そもそも、Hexo server は本格的な Web サーバとして作られているわけではないので、セキュリティ的にも不安があります(ちゃんと調べてないので実は安全なのかもしれませんが)。

そこで思いついたのが、「HTTPS サーバを Basic 認識ありの reverse proxy として使って Hexo server にアクセスする」という方法です。「いまさら Basic 認証?」と思われるかもしれませんが、これが意外と使えるのです。

  • すべての Web ブラウザが対応している。
  • SSL/TLS で通信路を保護すれば認証情報も漏れない。
  • サーバ側の設定が簡単(セッション管理とか不要)。

ポイントは、初回アクセスから常に HTTPS を使うことです。間違っても HTTP でアクセスしようとしてはいけません。認証情報が漏れてしまう危険があります。サーバ側の設定で、Basic 認証より先に HTTPS へのリダイレクトをすれば(HTTP リクエストに対して 401 じゃなくて 301 を返せば)大丈夫かもしれませんが、ブラウザの実装に依存しそうな気もするので、自信なし。サーバ側で HSTS を設定しておくか、80番ポートを閉じておくのが安心かも。

Hexo の設定(_config.yml ファイルの url: フィールドや root: フィールド)を変えたくないので、ブラウザからアクセスする際に指定する URL の path 部(スラッシュで始まる部分)として任意のパスを許容する必要があります。したがって、通常の HTTPS サーバとして使っているホスト名 (FQDN) とポート番号 (443/tcp) の少なくともどちらか一方を変えた URL を、その Hexo server 専用として用意する必要があります。ホスト名を変える場合は、使用する HTTP サーバが “virtual host” 機能 (SNI) に対応している必要があります。ポート番号を変える場合は、使用する HTTP サーバが受信ポートごとに異なるアクションを設定できる機能をもっている必要があります。ホスト名を変えると SSL 証明書を取り直さないといけなかったりして面倒なので、今回はポート番号を変えることにします。

設定例

1
2
3
FQDN : www.example.com
HTTPS 待ち受けポート : 8888/tcp
Hexo server 待ち受けポート : 9999/tcp

これまで Hexo server で使っていたポートがあるなら、それをそのまま HTTPS 待ち受けポートに流用するのが良いかもしれません。

ファイアウォールの設定

これまで Hexo server で使っていたポートをそのまま流用する場合、ファイアウォールの設定は変更不要です。

新たに HTTPS 待ち受けポートを用意する場合は、ファイアウォールでそのポートでの受信を許可します:

1
$ sudo ufw 8888 allow

Hexo server の待ち受けポートは loopback アクセスのみ可能にしてください。外部からアクセス可能にしてはいけません。せっかくのセキュリティ強化が無意味になってしまいます。もし外部からアクセス可能な設定になっていたら、設定を変更してください:

1
$ sudo ufw 9999 deny

Web サーバ H2O の設定

HTTPS サーバの設定と同じ設定にして、Listen するポート番号を 8888/tcp にします。

h2o.conf の記述例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
host:
"www.example.com:8888":
listen:
port: 8888
ssl:
certificate-file: path/to/server.crt
key-file: path/to/server.key
paths:
"/":
mruby.handler: |
lambda do |env|
require "htpasswd.rb"
Htpasswd.new("/path/to/htpasswd", "Hexo server at 9999/tcp").call(env)
end
proxy.reverse.url: "http://127.0.0.1:9999/"

注意点として、H2O で reverse proxy する接続先のホスト名としては、localhost ではなく 127.0.0.1 を指定してください。理由は、Hexo server がデフォルトでは IPv4 接続しか受け付けないからです。OS (Linux) の設定にも依存するのかもしれませんが、H2O で接続先を http://localhost:9999/ のようにすると、コネクションごとに IPv4 で接続したり IPv6 で接続したりするようで、接続エラーになったりならなかったりします。H2O の error-log を見ると、一部のリクエストが “connection failed” となっていることが確認できます。ちなみに、H2O で接続先を http://[::1]:9999/ とかにして IPv6 接続を強制すると、100% 接続エラーになります。

error-log の例:

1
2
[lib/core/proxy.c] in request:/:connection failed
[lib/core/proxy.c] in request:/favicon.ico:connection failed

なお、Hexo server は、デフォルトでは 0.0.0.0 を Listen します(参考:heso-server/index.js:10)。つまり、サーバ上のすべてのインターフェースで IPv4 接続を受け付けます。IPv6 接続は受け付けません。Hexo server のヘルプ(下記)には “Bind to all IP address by default” と書かれていますが、実際には IPv4 限定です。Hexo server 起動時に明示的にコマンドラインオプションで -i ::1 と指定すれば、IPv6 でのローカルループバック接続のみ受け付けるようになります(つまり、Hexo server 自体は IPv6 に対応しています)。もちろん、同様に -i 127.0.0.1 と指定すれば、IPv4 でのローカルループバック接続のみ受け付けるようになります。そして、同様に -i :: と指定すると、IPv4 と IPv6 のどちらでも受け付けるようになりました(netstat コマンドで確認すると IPv6 しか Listen していないように見えるのですが、curl コマンドなどで試すと IPv4 でも IPv6 でも接続できました)。試した環境は Ubuntu 16.04.2 LTS (x86_64) と Hexo 3.3.7 の組み合わせです。

デフォルトで IPv6 接続も受け付けるようにするパッチを作る・・・・作りたい。そのうち。

参考)Hexo server のヘルプ:

1
2
3
4
5
6
7
8
9
10
11
12
$ hexo server --help
Usage: hexo server

Description:
Start the server and watch for file changes.

Options:
-i, --ip Override the default server IP. Bind to all IP address by default.
-l, --log [format] Enable logger. Override log format.
-o, --open Immediately open the server url in your default web browser.
-p, --port Override the default port.
-s, --static Only serve static files.

参考)デフォルト:

1
2
3
4
$ hexo server -p 9999 >log.txt 2>&1 &
$ netstat -antu | grep ':9999'
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN
$

参考)IPv4 も IPv6 も接続可能なケース:

1
2
3
4
$ hexo server -i :: -p 9999 >log.txt 2>&1 &
$ netstat -antu | grep ':9999'
tcp6 0 0 :::9999 :::* LISTEN
$

参考)試した環境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ hexo -V
hexo: 3.3.7
hexo-cli: 1.0.3
os: Linux 4.4.0-64-generic linux x64
http_parser: 2.7.0
node: 8.1.2
v8: 5.8.283.41
uv: 1.12.0
zlib: 1.2.11
ares: 1.10.1-DEV
modules: 57
openssl: 1.0.2l
icu: 59.1
unicode: 9.0
cldr: 31.0.1
tz: 2017b
$
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$

あと、H2O が reverse proxy 接続する際に IPv4 接続と IPv6 接続を混在させてしまう理由は未調査です。H2O で getaddrinfo(3) あたりを呼んでいるコードを追えばわかりそうな気もしますが、未着手です。ちなみに、/etc/hosts には 127.0.0.1 localhost::1 localhost の両方が書いてあります。

Hexo server の設定

Hexo の設定(_config.yml ファイル)を変更する必要はありません。

Hexo server を起動するときに、IP address とポート番号を指定してあげるだけです:

1
$ /usr/bin/nice -19 /usr/bin/ionice -c 3 hexo server -i 127.0.0.1 -p 9999 --debug

最後に確認

Web サーバに HTTPS 接続できることを確認します。手元のブラウザで https://www.example.com:8888/ にアクセスしてみてください。Basic 認証が要求され、ユーザ名とパスワードを正しく入力すると、Hexo のコンテンツが表示されるはずです。無事にアクセスできたら、Web サーバのログにエラーが記録されていないことを確認してください。

念のため、Hexo server に直接アクセスできないことを確認します。手元のブラウザで(Hexo server を実行しているサーバ以外から) http://www.example.com:9999/ にアクセスすると、ファイアウォールに弾かれて接続エラーになるハズです。なお、Hexo server を実行しているサーバ上で curl コマンドなどでアクセスすると、おそらく成功してしまいます(ファイアウォールはローカルループバック接続をデフォルトで許可していることが多いので)。

システム起動時にプログラムを自動実行させたい

昨日、iPhone から VPN 接続ができなくなってしまいました。調べてみると、SoftEther VPN Server を動かしている VPS が勝手に再起動してました。原因は不明。

先日記事にしたように、SoftEther VPN Server は一般ユーザ権限で「ユーザモード」で動かしています。当然ながら /etc/rc スクリプトなど置いてないので、サーバがリブートしてしまうと SoftEther VPN Server は落ちたままです。

不便なのでちょっと調べてみたところ、cron に「システム起動時に自動的にプログラムを実行する」という機能があることを知りました。

この機能を使えば、一般ユーザでもシステム起動時に自動的に SoftEther VPN Server を実行できます。

cron にこんな機能があるのは知りませんでした。便利ですね!

待ち受けポートごとに H2O を動かす

うちのサーバでは、デバッグ用に複数のポートをこっそり開けて H2O で待ち受けているのですが、デバッグ用なのでそのポート数は増減します。また、デバッグ用に H2O で配信したいコンテンツも変化します。そのときどきで、作成中の HTML/CSS/JavaScript ファイルが置かれたディレクトリだったり、デバッグ中の CGI プログラムだったり、UNIX Socket だったりします。

※ポートスキャンしないでくださいね!

H2O は複数のポートで待ち受けることができます。しかし、H2O は起動時にポートを bind するらしく、設定ファイル h2o.conflisten: ディレクティブを追加してリロードさせても(H2O プロセスに kill -HUP しても)、新しく追加したポートは bind してくれません。

そこで、待ち受けポートごとに専用の H2O インスタンスを動かすことにしました。

なお、この記事は、先に投稿した記事「H2O を専用のユーザで運用したい」の設定を前提としています。

H2O 実行専用ユーザを作る

待ち受けポートごとに専用のユーザを作成します。ログイン不可&ホーム無しです。とりあえず、「h2o-PORTNUMBER」という名前にしておきます:

1
2
3
$ sudo adduser --home /noexistent --shell /bin/false --no-create-home --disabled-password --disabled-login h2o-443
$ sudo adduser --home /noexistent --shell /bin/false --no-create-home --disabled-password --disabled-login h2o-80
$ sudo adduser --home /noexistent --shell /bin/false --no-create-home --disabled-password --disabled-login h2o-22222

これらのユーザを、グループ h2o-runner に参加させておきます:

1
2
3
$ sudo adduser h2o-443 h2o-runner
$ sudo adduser h2o-80 h2o-runner
$ sudo adduser h2o-22222 h2o-runner

また、H2O 管理専用ユーザ h2o-manager を各ユーザのグループに参加させておきます:

1
2
3
$ sudo adduser h2o-manager h2o-443
$ sudo adduser h2o-manager h2o-80
$ sudo adduser h2o-manager h2o-22222

実際に H2O でコンテンツを配信するのは HTTPS(443番ポート)だけなので、ユーザ h2o-443 をグループ www01 に参加させておきます:

1
$ sudo adduser h2o-443 www01

sudo の設定

先の記事「H2O を専用のユーザで運用したい 」で、すでに、H2O 管理専用ユーザ h2o-manager には、グループ h2o-runner に属するユーザに sudo する権限が与えられているので、変更は不要です。

H2O ログディレクトリの作成

H2O 実行用ディレクトリに H2O インスタンスごとのログディレクトリを作成して、グループ権限で書き込みできるようにしておきます:

1
2
3
4
5
6
7
8
9
$ sudo -u h2o-manager -i
$ cd ~h2o-manager/run
$ for PORT in 443 80 22222; do
NAME=h2o-$PORT
TARGET=./$NAME.logs
mkdir $TARGET
chgrp $NAME $TARGET
chmod 770 $TARGET
done

H2O 設定ファイルの作成

H2O インスタンスごとに作成します。

コンテンツの配信は h2o-443.conf で行い、h2o-80.conf は HTTPS へのリダイレクトだけにします。h2o-22222.conf は今回はデバッグ用に internal redirect (reverse proxy) としています。

h2o-manager/run/h2o-443.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
error-log: "| rotatelogs -l /home/h2o-manager/run/h2o-443.logs/error-log.%Y%m%d 604800"
pid-file: /home/h2o-manager/run/h2o-443.logs/pid

listen:
port: 443
ssl:
certificate-file: /home/h2o-manager/run/cert/www01.example.com/server.crt
key-file: /home/h2o-manager/run/cert/www01.example.com/server.key
dh-file: /home/h2o-manager/run/cert/www01.example.com/dhparam.pem

hosts:
"sentinel.example.com:0":
paths:
"/":
file.dir: /dev/null
access-log: "| rotatelogs -l /home/h2o-manager/run/h2o-443.logs/access-log.sentinel.%Y%m%d 604800"

"www01.example.com:80":
paths:
"/":
file.dir: /home/www01/htdocs
access-log: "| rotatelogs -l /home/h2o-manager/run/h2o-443.logs/access-log.www01.%Y%m%d 604800"

h2o-manager/run/h2o-80.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
error-log: "| rotatelogs -l /home/h2o-manager/run/h2o-80.logs/error-log.%Y%m%d 604800"
pid-file: /home/h2o-manager/run/h2o-80.logs/pid

listen:
port: 80

hosts:
"sentinel.example.com:0":
paths:
"/":
file.dir: /dev/null
access-log: "| rotatelogs -l /home/h2o-manager/run/h2o-80.logs/access-log.sentinel.%Y%m%d 604800"

"www01.example.com:80":
paths:
"/":
redirect:
url: "https://www01.example.com/"
status: 301
access-log: "| rotatelogs -l /home/h2o-manager/run/h2o-80.logs/access-log.www01.%Y%m%d 604800"

h2o-manager/run/h2o-22222.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
error-log: "| rotatelogs -l /home/h2o-manager/run/h2o-22222.logs/error-log.%Y%m%d 604800"
pid-file: /home/h2o-manager/run/h2o-22222.logs/pid

listen:
port: 22222

hosts:
"sentinel.example.com:0":
paths:
"/":
file.dir: /dev/null
access-log: "| rotatelogs -l /home/h2o-manager/run/h2o-22222.logs/access-log.sentinel.%Y%m%d 604800"

"www01.example.com:22222":
paths:
"/":
proxy.reverse.url: "http://localhost:33333/"
access-log: "| rotatelogs -l /home/h2o-manager/run/h2o-22222.logs/access-log.www01.%Y%m%d 604800"

これらのファイルはグループ権限でのみ読み出せるようにしておきます:

1
2
3
4
5
6
7
8
$ sudo -u h2o-manager -i
$ cd ~h2o-manager/run
$ for PORT in 443 80 22222; do
NAME=h2o-$PORT
TARGET=./$NAME.conf
chgrp $NAME $TARGET
chmod 640 $TARGET
done

H2O 起動スクリプトの作成

先の記事「H2O を専用のユーザで運用したい 」と同じ内容のものを作って、環境変数 RUNNERLOGS_DIRCONF_FILE の値だけ変えれば OK です。

H2O の起動/停止/再起動/リロード

先の記事「H2O を専用のユーザで運用したい」と同様です。

H2O を専用のユーザで運用したい

Web サーバ H2O を「よりセキュアに」運用したいと思って、ちょっと試してみたお話です。H2O 専用のユーザを4個作って役割分担します。

まえがき

今回のお話の目標:

  • 万一、H2O プロセスが乗っ取られても、被害を最小限に抑えたい。
  • 関連ディレクトリ/ファイルへのアクセス権は必要最小限にしたい。
  • 使用する H2O のバージョンを簡単に切り替えられるようにしたい。

この目標を実現するための設定のポイント:

  • H2O プロセスのユーザはログイン不可かつホームディレクトリなし。
  • H2O プロセスは H2O 管理専用ユーザが sudo で起動する。
  • H2O 実行ファイル一式をビルドして保持する専用ユーザを用意する。
  • H2O で配信するコンテンツデータを保持する専用ユーザを用意する。

なお、「H2O プロセス専用のユーザで H2O プロセスを起動する」のは、sudo を使わずに H2O 実行ファイルに setuid することでも可能です。今回、sudo を使った理由は、以下のようなものです:

  • 複数の H2O インスタンスを動かしたいとき(これは改めて記事を書きます)、それぞれの H2O 専用ユーザごとに H2O 実行ファイル一式を配置すると管理が煩雑になる。
  • バージョンアップなどで H2O をビルドして配置するたびに root 権限で setuid するのが面倒(自動化するには結局 sudo が要る)。
  • H2O プロセスにシグナルを送る(kill コマンドを使う)ためのプログラムを作るのが面倒(シェルスクリプトへの setuid は避けたい)。sudo なら sudo 設定ファイルで /bin/kill の実行を許可しておくだけで済む。

なお、H2O には、root 権限で起動された場合に指定されたユーザに切り替えてから動作する、という機能があります。今回、この機能を使わずに sudo を使った理由は、以下のようなものです:

  • たとえ一瞬であっても H2O に root 権限を与えたくない。
  • perlbrew の類いを有効にするために環境変数 PATH を H2O プロセスに引き継ぐ方法がわからない。

専用ユーザの作成

  • 「H2O 管理専用ユーザ」をひとつ新規作成する。たとえば h2o-manager とか。
  • 「H2O ビルド専用ユーザ」をひとつ新規作成する。たとえば h2o-builder とか。
  • 「H2O 実行専用ユーザ」をひとつ新規作成する。たとえば h2o-runner とか。
  • 「H2O 配信データ専用ユーザ」を新規作成する。たとえば www01 とか。

なお、「H2O 配信データ専用ユーザ」は複数作って使い分けることも可能です。たとえば、SNI を使った VirtualHost 運用をする場合に、ホスト名(ドメイン名)ごとに専用ユーザを作るとか。今回はユーザ www01 ひとつだけで説明しますが、各ユーザに同様の設定をすれば OK です。

具体的な作業は以下のようになります:

1
2
3
4
$ sudo adduser --disabled-password h2o-builder
$ sudo adduser --disabled-password h2o-manager
$ sudo adduser --home /noexistent --shell /bin/false --no-create-home --disabled-password --disabled-login h2o-runner
$ sudo adduser --shell /bin/false --disabled-password www01

セキュリティ上の注意点として、これらのユーザには root 権限への sudo を許可してはいけません。具体的には、グループ sudo に参加させてはいけません。

H2O 配信データ専用ユーザの設定

ホームディレクトリ配下に適当なディレクトリを作って、H2O で配信したいコンテンツデータ(ファイル/ディレクトリ)を置くだけです。たとえば、~www01/htdocs/ とかに index.html とかを置きます。

よりセキュアな運用をしたいなら、ユーザ h2o-runner をグループ www01 に追加して、コンテンツディレクトリのパーミッションを 750 に制限します:

1
2
$ sudo adduser h2o-runner www01
$ sudo -u www01 chmod -R o-rwx ~www01/htdocs

逆に、特定のユーザだけにコンテンツの変更を許可したい場合、それらのユーザをグループ www01 に追加して、コンテンツディレクトリのパーミッションを 775 にします:

1
2
3
$ sudo adduser user77 www01
$ sudo adduser user88 www01
$ sudo -u www01 chmod -R g+w ~www01/htdocs

ただし、後者のようにユーザが直接ファイルを触れる運用は、個人的にはオススメできません。コンテンツデータを VCS(git とか)で管理して、ユーザ www01 が ~www01/htdocs に checkout するのが良いと思います。checkout する作業をスクリプト化して、そのスクリプトだけ特定のユーザが sudo -u www01 で実行できるようにしておけば運用は簡単です。

なお、上記の両方を組み合わせることも可能です(ユーザ h2o-runner をグループ www01 に追加して chmod g+w すればよい)。しかしながら、H2O プロセスにコンテンツディレクトリへの書き込み権限を与えることになるので、オススメしません。

H2O ビルド専用ユーザの設定

ただ単に、GitHub のリポジトリを clone/pull して、ビルドして、格納するだけです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sudo -u h2o-builder -i
$ cd /home/h2o-builder
$ mkdir deploy
$ mkdir build
$ cd build
$ git clone https://github.com/h2o/h2o.git
$ cd h2o
$ git tag
$ cp -a . ../20170614a.tag-v2.2.2
$ cd ../20170614a.tag-v2.2.2
$ git checkout v2.2.2
$ time nice -19 ionice -c 3 cmake -DCMAKE_INSTALL_PREFIX=/home/h2o-builder/deploy/h2o_20170614a.tag-v2.2.2 -DWITH_BUNDLED_SSL=on -DWITH_MRUBY=on .
$ time nice -19 ionice -c 3 make
$ time nice -19 ionice -c 3 make install

更新作業を自動化するシェルスクリプトを作って、cron で定期的に実行させておくと便利です。

あと、デフォルトで「安定版」を使えるように、stable という名前でシンボリックリンクを作っておくのもオススメです。このシンボリックリンクの張り替えは手動で良いと思います(一週間くらい様子を見てからとか)。

1
2
3
$ cd ~h2o-builder/deploy
$ rm -f stable
$ ln -s 20170614a.tag-v2.2.2 stable

H2O 管理専用ユーザの設定

まず、管理専用ユーザ h2o-manager をグループ h2o-runner に追加しておきます:

1
$ sudo adduser h2o-manager h2o-runner

H2O 実行用のディレクトリを作って、グループ h2o-runner だけが読み出せるようにします:

1
2
3
4
5
6
$ sudo -u h2o-manager -i
$ cd ~h2o-manager
$ mkdir run
$ cd run
$ chgrp h2o-runner .
$ chmod 750 .

また、ログファイルと pid ファイルを置くディレクトリを作って、グループ h2o-runner に書き込み権限を与えます:

1
2
3
$ mkdir logs
$ chgrp h2o-runner logs
$ chmod 770 logs

次に、H2O 設定ファイル /home/h2o-manager/run/h2o.conf を作成します:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
error-log: "| rotatelogs -l /home/h2o-manager/run/logs/error-log.%Y%m%d 604800"
pid-file: /home/h2o-manager/run/logs/pid

listen:
port: 80

hosts:
"sentinel.example.com:0":
paths:
"/":
file.dir: /dev/null
access-log: "| rotatelogs -l /home/h2o-manager/run/logs/access-log.sentinel.%Y%m%d 604800"

"www01.example.com:80":
paths:
"/":
file.dir: /home/www01/htdocs
access-log: "| rotatelogs -l /home/h2o-manager/run/logs/access-log.www01.%Y%m%d 604800"

念のため、最後にもう一度 chgrp と chmod を実行しておきます:

1
2
$ chgrp -R h2o-runner ~h2o-manager/run
$ chmod -R o-rwx ~h2o-manager/run

sudo の設定

H2O 管理専用ユーザ h2o-manager が、H2O 実行専用ユーザ(グループ h2o-runner に所属しているユーザ)に sudo できるように設定します:

1
$ sudo visudo --strict -f /etc/sudoers.d/h2o
1
2
3
4
5
6
Runas_Alias H2O_RUNNERS = %h2o-runner, !h2o-manager
Cmnd_Alias H2O_COMMANDS = /bin/kill, /usr/bin/test, /home/h2o-builder/deploy/*/bin/h2o

Defaults>H2O_RUNNERS !authenticate

h2o-manager ALL = (H2O_RUNNERS) H2O_COMMANDS

H2O 起動スクリプトを作成

~h2o-manager/run/run.sh とかを作ります。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/bin/sh

set -e
set -x

H2O_ROOT="${H2O_ROOT:-/home/h2o-builder/deploy/stable}"
LOGS_DIR="/home/h2o-manager/run/logs"
CONF_FILE="/home/h2o-manager/run/h2o.conf"
PID_FILE="$LOGS_DIR/pid"
CMD="$H2O_ROOT/bin/h2o"
RUNNER=h2o-runner

: '======================================================================'

SELF="$0"
expr "x$SELF" : '^x/' >/dev/null || SELF="./$SELF"

PREDICT="/usr/bin/nice -n 10"
test "$RUNNER" = "$USER" || PREDICT="$PREDICT sudo -u $RUNNER"

cd "`dirname \"$SELF\"`"

get_existent_pid() {
test ! -z "$PID_FILE" || {
echo "ERROR: BUG: invaild PID_FILE : $PID_FILE" 1>&2
return 99
}
test -e $PID_FILE || return 0
test -r $PID_FILE || {
echo "ERROR: can not read $PID_FILE (permission error)" 1>&2
return 99
}
local STATUS
STATUS=
local PID
PID="`cat $PID_FILE`" || STATUS="$?"
test -z "$STATUS" || {
echo "ERROR: can not read $PID_FILE (status=$STATUS)" 1>&2
return 99
}
expr "x$PID" : '^x[1-9][0-9]*$' >/dev/null || {
echo "ERROR: detect unexpected contents in $PID_FILE : $PID" 1>&2
return 99
}
ps --no-headers --quick-pid $PID -o comm,user,cmd |
while read ARG1 ARG2 ARG3 ARG4 ARG5 ARGS; do
COMM=perl
test "x$ARG1" = "x$COMM" || return 0
test "x$ARG2" = "x$RUNNER" || {
echo "ERROR: the owner of existent process (PID:$PID) is not $RUNNER : $ARG2" 1>&2
return 3
}
test "x$ARG3" = "xperl" -a "x$ARG4" = "x-x" -a "x$ARG5" = "x$H2O_ROOT/share/h2o/start_server" || {
echo "WARNING: unexpected executable name of existent process (PID:$PID) : $ARG3 $ARG4 $ARG5" 1>&2
}
echo $PID
done
return 0
}

PID=`get_existent_pid` || exit $?

start_process() {
if [ ! -z "$PID" ]; then
echo "ERROR: h2o is already running" 1>&2
exit 2
fi

chgrp -R $RUNNER $LOGS_DIR 2>/dev/null || true
chmod -R g+w $LOGS_DIR 2>/dev/null || true
$PREDICT test -d $LOGS_DIR
$PREDICT test -r $LOGS_DIR
$PREDICT test -x $LOGS_DIR
$PREDICT test -w $LOGS_DIR

$PREDICT test -r $CONF_FILE
$PREDICT test -f $CMD
$PREDICT test -r $CMD
$PREDICT test -x $CMD
export H2O_ROOT
umask 007
$PREDICT $CMD -c $CONF_FILE -t
$PREDICT $CMD -c $CONF_FILE -m daemon
}

send_signal() {
local SIGNAL
SIGNAL="$1"
test "x$SIGNAL" = 'x-TERM' -o "x$SIGNAL" = 'x-HUP'
if [ -z "$PID" ]; then
echo "ERROR: h2o is not running" 1>&2
exit 2
fi
$PREDICT /bin/kill $SIGNAL $PID
}

case "$1" in
start)
start_process
;;
stop)
send_signal -TERM
wait $PID || echo "WARNING: status=$?" 1>&2
;;
reload)
send_signal -HUP
;;
restart)
"./`basename \"$0\"`" stop
"./`basename \"$0\"`" start
;;
*)
echo "Usage: `basename \"$SELF\"` (start|stop|reload|restart)" 1>&2
exit 1
;;
esac
: 'Completed'

ポイントは、環境変数 H2O_ROOT を設定して export することです。これを忘れると H2O が関連ファイルを見つけられずにエラーになります。環境変数 PATH は特に変更する必要はありません。

あと、sudo コマンドには -E オプションは付けません。付けるとエラーになります。これは、上記の /etc/sudoers.d/h2o の中で setenvDefaults ディレクティブを記述してしていないためです。そもそも、!reset_envDefaults ディレクティブを記述しているので、-E オプションは不要(無意味)なのです。

H2O の起動

H2O 管理専用ユーザ h2o-manager として、上記の起動スクリプトを実行するだけです:

1
$ sudo -u h2o-manager ~h2o-manager/run/run.sh start

正常に起動できれば、サブディレクトリ /home/h2o-manager/run/logs/ にログファイルと pid ファイルが生成されているハズです。あと、netstat -antu を実行すれば、待ち受けポート(80/tcp とか 443/tcp とか)を listen しているのを確認できるハズです。

H2O の停止

実行すると kill -TERM します。

1
$ sudo -u h2o-manager ~h2o-manager/run/run.sh stop

H2O の再起動

H2O が listen するポートを変えたいときは、reload ではダメなので restart します。

1
$ sudo -u h2o-manager ~h2o-manager/run/run.sh restart

H2O 設定ファイルのリロード

H2O 設定ファイルを更新したときには reload します。実行すると kill -HUP します。

1
$ sudo -u h2o-manager ~h2o-manager/run/run.sh reload

Linux でシステムデフォルトのエディタを変えたい

Ubuntu 16.04 のシステムデフォルトのエディタは nano です。Seaoak は nano の使い方がわからないので、vi を使いたい。しかし、毎回 EDITOR=vi を付けて sudo するのが面倒です。

以下のコマンドを実行すると、インタラクティブに変更できます:

1
$ sudo update-update-alternatives --config editor

ちなみに、Ubuntu 16.04 には素の vi は入っておらず、実体は /usr/bin/vim.tiny でした。

sudo を使いこなすためのメモ

細かい制御が可能なので、su じゃなくて sudo を使いましょう。

sudo 設定のお約束

  • sudo の設定ファイルは /etc/sudoers ですが、/etc/sudoers.d/ 内に置いたファイルもファイル名順に読み込まれます。
  • これらの設定ファイルをテキストエディタで直接編集してはいけません。文法ミスとかすると二度と sudo が使えなくなり、詰みます。必ず visudo コマンド経由で編集してください。編集終了時に自動的に編集結果をチェックして、文法ミスなどがあれば編集結果の反映をブロックしてくれます。
  • メインの設定ファイル /etc/sudoers を編集したいときは、sudo visudo --strict を実行します。
  • システムデフォルトのテキストエディタが nano とかになっている場合に vi を使いたければ、sudo EDITOR=vi visudo --strict とすれば OK です。
  • /etc/sudoers.d/ 内の設定ファイルを新規作成/編集するときは、sudo visudo --strict -f /etc/sudoers.d/foobar みたいにします。
  • 基本的に /etc/sudoers は触らず、/etc/sudoers.d/ に個別設定ファイルを作ることをオススメします。
  • ただし、/etc/sudoers の中で「editor 設定の無効化」だけはしておいたほうが良いみたいです。 ⇒ 参考記事
  • /etc/sudoers の中にある #includedir /etc/sudoers.d という行の行頭のシャープ記号 (#) は、「コメントアウト」の意味ではないので削除してはいけません。
  • 設定ファイルの変更は即時反映されます。システムの再起動などは不要です。
  • 設定ファイルの中では各種 Alias を使うと楽です。

なお、/etc/sudoers に追加しておいたほうが良いのは、次の2行:

1
2
Defaults    !env_editor
Defaults editor=/usr/bin/vi

環境変数(特に PATH)を引き継ぎたい

perlbrew とか nvm とか pyenv とか便利ですよね?

まぁ、最近流行の Docker を使えば話は簡単なのかもしれませんが、古い Linux カーネルの環境だと Docker が使えなかったりするので、perlbrew などの需要は今後もあると思います。仮想化方式が OpenVZ の VPS では Linux カーネルが更新できない、という話もありますし。

Ubuntu 16.04 LTS のデフォルト設定では、sudo 時に環境変数 PATH がリセットされてしまいます。したがって、ファイル先頭に #!/usr/bin/env perl などと書かれた Perl スクリプトを sudo で起動すると、perlbrew は無視されてしまい、システムデフォルトの perl(/usr/bin/perl とか)が使われます。この挙動は /etc/sudoers の中の Defaults ディレクティブで reset_envsecure_path が指定されているためです。sudo コマンドには -E オプションがありますが、これらの Defaults 設定が有効な限り、環境変数 PATH には効きません。

特権ユーザ root に sudo する場合、環境変数のリセットはセキュリティ的に妥当です。しかし、ある目的のために「特定のプログラムを特定の一般ユーザの権限で」sudo したいだけなら、環境変数を引き継ぐことも許容できると思います。sudo の設定を変える(ルールを追加する)ことで、これを実現できます。

たとえば、「ユーザ foo-runner の権限でプログラム /home/foo-builder/bin/foo を実行する」ことをユーザ bar に許可する場合、以下のような内容のファイルを /etc/sudoers.d/ 内に置きます:

1
2
3
4
5
6
7
8
Runas_Alias FOO_RUNNERS = %foo-runner, !foo
Cmnd_Alias FOO_COMMANDS = /bin/kill, /usr/bin/test, /home/foo-builder/deploy/*/bin/foo

Defaults>FOO_RUNNERS !env_reset
Defaults>FOO_RUNNERS !secure_path
Defaults>FOO_RUNNERS !authenticate

foo ALL = (FOO_RUNNERS) FOO_COMMANDS

Defaults ディレクティブを使ってシステムデフォルトの設定を限定的に上書きしているのがポイントです。あと、/bin/kill の実行を許可しておかないと、起動したプロセスを制御できなくなってしまうので、ご注意ください。test コマンドについても許可しておくのがオススメです(後述)。

ファイル名はわかりやすく /etc/sudoers.d/foo とかにしておきましょう。前述のとおり、このファイルを作成/編集するときは、必ず visudo コマンドを使いましょう(具体的には sudo visudo --strict -f /etc/sudoers.d/foo と実行しましょう)。ファイルのパーミッション等は visudo が適切に設定してくれます。

sudo 実行用シェルスクリプトの例

関連ディレクトリ/ファイルへのアクセス権を確認してから起動するのがオススメです。上述の設定ファイル例で test コマンドの実行を許可しておいたのは、このため。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/bin/sh

set -e
set -x

RUNNER=foo-runner
FOO_ROOT=/home/foo-builder/deploy/stable

CMD=$FOO_ROOT/bin/foo
CONF=./conf/foo.conf
LOGS_DIR=./logs
PID_FILE=$LOGS_DIR/pid
DATA_DIR=./data
PREDICT="/usr/bin/nice -10"

if [ -z "$RUNNER" ]; then
RUNNER=$USER
fi
if [ "$RUNNER" != "$USER" ]; then
PREDICT="$PREDICT sudo -u $RUNNER"
fi

if expr "x$0" : '^x/' >/dev/null; then
cd "`dirname \"$0\"`"
else
cd "`dirname \"./$0\"`"
fi

get_existent_pid() {
test ! -z "$PID_FILE" || {
echo "ERROR: BUG: invaild PID_FILE : $PID_FILE" 1>&2
return 99
}
test -e $PID_FILE || return 0
test -r $PID_FILE || {
echo "ERROR: can not read $PID_FILE (permission error)" 1>&2
return 99
}
local STATUS
STATUS=
local PID
PID="`cat $PID_FILE`" || STATUS="$?"
test -z "$STATUS" || {
echo "ERROR: can not read $PID_FILE (status=$STATUS)" 1>&2
return 99
}
expr "x$PID" : '^x[1-9][0-9]*$' >/dev/null || {
echo "ERROR: detect unexpected contents in $PID_FILE : $PID" 1>&2
return 99
}
ps --no-headers --quick-pid $PID -o comm,user,cmd |
while read ARG1 ARG2 ARG3 ARG4 ARG5 ARGS; do
COMM=perl
test "x$ARG1" = "x$COMM" || return 0
test "x$ARG2" = "x$RUNNER" || {
echo "ERROR: the owner of existent process (PID:$PID) is not $RUNNER : $ARG2" 1>&2
return 3
}
test "x$ARG3" = "xperl" -a "x$ARG4" = "x-x" -a "x$ARG5" = "x$FOO_ROOT/share/foo/start_server" || {
echo "WARNING: unexpected executable name of existent process (PID:$PID) : $ARG3 $ARG4 $ARG5" 1>&2
}
echo $PID
done
return 0
}

PID=`get_existent_pid` || exit $?

start_process() {
if [ ! -z "$PID" ]; then
echo "ERROR: foo is already running" 1>&2
exit 2
fi
for DIR in $DATA_DIR $LOGS_DIR; do
chgrp -R $RUNNER $DIR 2>/dev/null || true
chmod -R g+w $DIR 2>/dev/null || true
$PREDICT test -d $DIR
$PREDICT test -r $DIR
$PREDICT test -x $DIR
$PREDICT test -w $DIR
done
$PREDICT test -r $CONF
$PREDICT test -f $CMD
$PREDICT test -r $CMD
$PREDICT test -x $CMD
export FOO_ROOT
umask 007
$PREDICT $CMD -c $CONF -t
$PREDICT $CMD -c $CONF -m daemon
}

send_signal() {
local SIGNAL
SIGNAL="$1"
test "x$SIGNAL" = 'x-TERM' -o "x$SIGNAL" = 'x-HUP'
if [ -z "$PID" ]; then
echo "ERROR: foo is not running" 1>&2
exit 2
fi
$PREDICT /bin/kill $SIGNAL $PID
}

case "$1" in
start)
start_process
;;
stop)
send_signal -TERM
wait $PID || echo "WARNING: status=$?" 1>&2
;;
reload)
send_signal -HUP
;;
restart)
"./`basename \"$0\"`" stop
"./`basename \"$0\"`" start
;;
*)
echo "Usage: $0 (start|stop|reload|restart)" 1>&2
exit 1
;;
esac

参考記事

Linux でのアカウント操作メモ

普通のユーザ foo の新規作成

1
$ sudo adduser foo

ユーザ名と同じ名前のグループ foo が自動的に作成されます。

名前だけのユーザ foo を新規作成

nobody みたいなユーザを作れます。ログイン不可&ホームディレクトリ無しです。

1
$ sudo adduser --home /noexistent --shell /bin/false --no-create-home --disabled-password --disabled-login foo

ユーザ名と同じ名前のグループ foo が自動的に作成されます。

パスワードを無効化(su と sudo のみ可能に)

普通に作ったユーザのパスワードを後から無効化できます。

1
$ sudo usermod --expiredate 1 --lock foo

--expiredate オプションによりアカウントへのログインができなくなります。公開鍵証明書認証による ssh リモートログインもできなくなります。また、cron も実行できなくなるのでご注意。

--lock オプションにより /etc/shadow のパスワードフィールドの先頭にエクスクラメーションマーク (!) が付与されます。これが「パスワード無効」を意味しています。

既存ユーザのログインシェルを無効化(su も不可に)

1
$ sudo usermod --shell /bin/false foo

ただし、sudo -u USERNAME -i でインタラクティブなシェルが使えてしまうので注意。

既存ユーザのログインを再び有効化

1
$ sudo usermod --expiredate '' --shell /bin/bash foo

パスワードのロック状態については変更されません。公開鍵証明書認証による ssh リモートログインや su は可能になります。

名前だけのグループを新規作成

1
$ sudo addgroup foo

既存ユーザ foo を既存グループ bar に参加させる

1
$ sudo adduser foo bar

ちなみに、usermod -G はダメらしいです。すでに参加しているグループから抜けてしまいます。usermod -a -G なら OK です。

VPS に VPN サーバをたてて iPhone からアクセス(第4回)

レンタル VPS で VPN サーバを動かして iPhone からのネットアクセスをセキュアにする(ついでに自宅 LAN にリモートアクセスできるようにする)というお話の第4回です。今回は iPhone から SoftEther VPN Server に OpenVPN 接続するお話です。

前回の記事で、iPhone から L2TP/IPsec 接続できるようになりましたが、iPhone の回線が無線 LAN (Wi-Fi) と 4G LTE の間で切り替わったりすると、VPN 接続が切れてしまうことがありました。そのまま気づかずに iPhone を使ってしまうと、暗号化されていない状態で通信してしまいます。ちょっと調べてみると、iPhone の OpenVPN クライアントアプリは自動的に再接続してくれたりするみたいなので、試してみることにしました。

OpenVPN とは

OpenVPN は、オープンソースの VPN ソフトウェアです。マルチプラットフォーム対応で、安全性も高いようです。

サーバ側の設定

SoftEther VPN Server には OpenVPN クライアントからの接続を受け付ける機能があるので、それを利用します。OpenVPN クライアント設定用ファイルを自動生成してくれるので、とても簡単です。

OpenVPN ではデフォルトで UDP 1194番ポートを使用しますが、SoftEther VPN Server はすべてのリスナーポートで OpenVPN クライアントからの接続を受けられるので、今回は TCP 443番ポートをそのまま使用します。UDP 1194番ポートは無効化します。

Windows PC で「SoftEther VPN サーバー管理マネージャ」を使って設定する場合は、OpenVPN 機能を有効にして、UDP ポート番号の欄を空欄にします。また、「OpenVPN クライアント用のサンプル設定ファイルを生成」ボタンを押して zip ファイルをダウンロードしておきます。

vpncmd を使って設定する場合は、公式マニュアルの OpenVpnEnable コマンドの説明を参照して、以下のように行います。

1
2
VPN Server>OpenVpnEnable yes /PORTS:none
VPN Server>OpenVpnMakeConfig vpn33_openvpn_config_20170510d.zip

カレントディレクトリにクライアント設定用ファイル(zip ファイル)ができているはずです。

クライアント設定用ファイルの作成

上記の手順で得られた zip ファイルを展開すると、Readme ファイルの他に、拡張子 .opvn のファイルが2個できるはずです。このうち、***_openvpn_remote_access_l3.opvn のほうを使います。

1
2
3
4
vpn33_openvpn_remote_access_l3.ovpn
vpn33_openvpn_site_to_site_bridge_l2.ovpn
readme.pdf
readme.txt

まず、念のため、ファイルのコピーを作成して、わかりやすい名前を付けておきます(拡張子は変えないでください)。たぶんファイル名には日本語を使わないほうが安全だと思います。このコピーのほうを使います。ファイル名は仮に vpn33_hub01.opvn とします。

ファイル vpn33_hub01.opvn をテキストエディタで開いて編集します:

  • プロトコルを「udp」から「tcp」に変更。
  • 接続先を IP アドレスから「vpn33.example.com」に変更。
  • ポート番号を「1194」から「443」に変更。

編集後に diff をとると以下のようになります:

1
2
3
4
5
6
7
8
9
10
$ diff vpn33_openvpn_remote_access_l3.ovpn vpn33_hub01.ovpn
41c41
< proto udp
---
> proto tcp
62c62
< remote 192.0.2.33 1194
---
> remote vpn33.example.com 443
$

iPhone に OpenVPN クライアントアプリをインストール

iOS 用の OpenVPN クライアントアプリ「OpenVPN Connect」を App Store からインストールします。無料です。

インストールしたら、「設定」アプリで「OpenVPN」を選択して、設定を行います。

なお、この設定画面で「Connect via」欄を「Wi-Fi only」に変更すると、Wi-Fi と 4G LTE の自動切り替えがうまくいかないことがありました(Wi-Fi の電波が届かなくなっても 4G LTE に切り替わらず通信できない状態になる)。原因がわからないので、とりあえず「Any network」に設定しています。

クライアント設定用ファイルを iPhone に転送

作成しておいたクライアント設定用ファイル vpn33_hub01.opvn を、iTunes 経由で iPhone に転送します。

  1. iTunes で iPhone 画面を開く。
  2. ウインドウ左側に縦に並んでいるメニュー項目から「App」を選択する。
  3. iPhone のホーム画面のスクリーンショットが並んでいるフレームが右側に表示されるので、下方向にスクロールする。
  4. 「ファイル共有」のところにアプリが並んでいるので、その中から「OpenVPN」を選択する。
  5. 右側にある「ファイルを追加」ボタンを押して、開いたダイアログでクライアント設定用ファイルを選択する。
  6. iPhone と同期を行う。 ←これは不要かも?

OpenVPN クライアントアプリで接続設定する

iPhone で作業します。

  1. すでに VPN 接続中の場合は切断する(設定を削除する必要はありません)。
  2. OpenVPN クライアントアプリを起動する。
  3. 「新しい profile があるよ」と言われるのでインポートを選択する。
  4. Profile 設定画面になる。
  5. ユーザ名として「yamada@hub01」と入力する。
  6. パスワードとして仮想 HUB に設定した「K8nhrGJHHe98tCck4NGA」を入力する。
  7. Save を ON にする。
  8. 「Disconnected」欄のすぐ下のスイッチを ON にすると接続開始。
  9. 「Connected」と表示されれば接続成功。
  10. 画面左上の電波状況アイコンの隣に「VPN」アイコンが表示されていることを確認する。

これで VPN を経由して普通にインターネットにアクセスできるはずです。Safari や Twitter や LINE などが使えることを確認してください。

サーバ側のログを確認

L2TP/IPsec 接続のときと同様、vpnserver のログファイルが更新されているはずです。

1
$ less server_log/vpn_20170510.log

iPhone から OpenVPN でアクセスしたログが表示されるはずです。仮想 HUB「hub01」にユーザ名「yamada」で接続しているはずです。

1
2
3
4
5
6
7
8
9
10
11
2017-05-10 12:58:04.862 OpenVPN モジュール: OpenVPN サーバーモジュールを起動しました。
2017-05-10 12:58:05.127 [HUB "hub01"] コネクション "CID-4723" (IP アドレス 203.0.113.111, ホスト名 203.0.113.111.example.com, ポート番号 61449, クライアント名 "OpenVPN Client", バージョン 4.22 ビルド 9634) が仮想 HUB への接続を試行しています。提示している認証方法は "外部サーバー認証" でユーザー名は "yamada" です。
2017-05-10 12:58:05.127 [HUB "hub01"] コネクション "CID-4723": ユーザー "yamada" として正しく認証されました。
2017-05-10 12:58:05.127 [HUB "hub01"] コネクション "CID-4723": 新しいセッション "SID-yamada-[OPENVPN_L3]-1455" が作成されました。(IP アドレス 203.0.113.111, ポート番号 61449, 物理レイヤのプロトコル: "Legacy VPN - OPENVPN_L3")
2017-05-10 12:58:05.127 [HUB "hub01"] セッション "SID-YAMADA-[OPENVPN_L3]-1455": パラメータが設定されました。最大 TCP コネクション数 1, 暗号化の使用 はい, 圧縮の使用 いいえ, 半二重通信の使用 いいえ, タイムアウト 20 秒
2017-05-10 12:58:05.127 [HUB "hub01"] セッション "SID-YAMADA-[OPENVPN_L3]-1455": VPN Client の詳細: (クライアント製品名 "OpenVPN Client", クライアントバージョン 422, クライアントビルド番号 9634, サーバー製品名 "SoftEther VPN Server (64 bit) (Open Source)", サーバーバージョン 422, サーバービルド番号 9634, クライアント OS 名 "OpenVPN Client", クライアント OS バージョン "-", クライアントプロダクト ID "-", クライアントホスト名 "", クライアント IP アドレス "203.0.113.111", クライアントポート番号 61449, サーバーホスト名 "192.0.2.33", サーバー IP アドレス "192.0.2.33", サーバーポート番号 443, プロキシホスト名 "", プロキシ IP アドレス "0.0.0.0", プロキシポート番号 0, 仮想 HUB 名 "hub01", クライアントユニーク ID "12345678901234567890ABCDEF123456")
2017-05-10 12:58:05.906 [HUB "hub01"] SecureNAT: DHCP エントリ 1596 が作成されました。MAC アドレス: 12-34-56-78-90-AB, IP アドレス: 192.168.233.10, ホスト名: , 有効期限: 7200 秒
2017-05-10 12:58:05.906 [HUB "hub01"] セッション "SID-SECURENAT-1": このセッション上のホスト "00-AC-F5-3B-F5-5B" (192.168.233.1) の DHCP サーバーは、別のセッション "SID-YAMADA-[OPENVPN_L3]-1455" 上のホスト "12-34-56-78-90-AB" に対して新しい IP アドレス 192.168.233.10 を割り当てました。
2017-05-10 12:58:05.906 OpenVPN セッション 1 (203.0.113.111:61449 -> 192.0.2.33:443) チャネル 0: チャネルが確立状態になりました。
2017-05-10 12:58:05.906 OpenVPN セッション 1 (203.0.113.111:61449 -> 192.0.2.33:443) チャネル 0: クライアントの IP アドレスおよびその他の IP ネットワーク情報の設定が完了しました。クライアント IP アドレス: 192.168.233.10, サブネットマスク: 255.255.255.0, デフォルトゲートウェイ: 192.168.233.1, DNS サーバー 1: 192.168.233.1, DNS サーバー 2: , WINS サーバー 1: , WINS サーバー 2:
2017-05-10 12:58:05.906 OpenVPN セッション 1 (203.0.113.111:61449 -> 192.0.2.33:443) チャネル 0: 応答オプション文字列の全文: "PUSH_REPLY,ping 3,ping-restart 10,ifconfig 192.168.233.10 192.168.233.14,dhcp-option DNS 192.168.233.1,route-gateway 192.168.233.14,redirect-gateway def1"

VPS に VPN サーバをたてて iPhone からアクセス(第3回)

レンタル VPS で VPN サーバを動かして iPhone からのネットアクセスをセキュアにする(ついでに自宅 LAN にリモートアクセスできるようにする)というお話の第3回です。今回は iPhone から SoftEther VPN Server に L2TP/IPsec で接続するお話です。

まずは公式マニュアル

サーバ側の設定

第2回の記事にも書きましたが、サーバ環境は次のとおりです:

1
2
3
4
Interface: vnet1
IPv4: 192.0.2.33/24
IPv6: 2001:db8::2:33/64
FQDN: vpn33.example.com

まず、ファイアウォールに穴をあけます。UDP 500番ポートと、UDP 4500番ポートで、待ち受けできるようにします。

1
2
3
4
5
6
7
$ sudo iptables -A INPUT -i vnet1 -d '192.0.2.33' -p udp --dport 500 -j ACCEPT
$ sudo iptables -A INPUT -i vnet1 -d '192.0.2.33' -p udp --dport 4500 -j ACCEPT
$ sudo iptables -L -vn
$ sudo ip6tables -A INPUT -i vnet1 -d '2001:db8::2:33' -p udp --dport 500 -j ACCEPT
$ sudo ip6tables -A INPUT -i vnet1 -d '2001:db8::2:33' -p udp --dport 4500 -j ACCEPT
$ sudo ip6tables -L -vn
$ sudo /etc/init.d/iptables-persistent save

次に、SoftEther VPN Server で L2TP/IPsec 接続を有効化します。

Windows PC 上の「SoftEther VPN サーバー管理マネージャ」でサーバに接続して設定する場合は、次のスクリーンショットのように設定します:

もちろん、vpncmd コマンドを使えばコマンドライン上でも設定できます:

1
2
$ ./vpncmd localhost:443 /SERVER
VPN Server>IPsecEnable /L2TP:yes /L2TPRAW:no /ETHERIP:no /PSK:Ppz6o5x9J /DEFAULTHUB:hub01

なお、PSK (Pre-Shared-Key) には35文字くらいのランダムな英数字を指定するのが良さそうです。とある解説記事によれば「30~40文字で英数字のみ」が推奨らしいです。ただし、SoftEther VPN 公式マニュアルの IPsecEnable コマンドの説明には「Google Android 4.0 にはバグがあり、PSK の文字数が 10 文字を超えた場合は VPN 通信に失敗することがあります。そのため、PSK の文字数は 9 文字以下にすることを推奨します。」と書かれているので、古い Android を使いたい人は9文字推奨です(上記の例はこれ)。

参考:

iPhone 側の設定

iOS 10.3.1 で試しました。

iOS 標準機能で L2TP/IPsec 接続が可能です。アプリのインストールは不要です。VPN 接続の ON/OFF も、標準の「設定」アプリでできます。

  1. 「設定」アプリを起動。
  2. 「VPN」を選択。
  3. 「VPN構成を追加…」を選択。
    1. 「タイプ」は「L2TP」を選択。
    2. 「説明」欄は「hub01@vpn33」とか適当に入力。
    3. 「サーバ」欄には「vpn33.example.com」と入力。
    4. 「アカウント」欄には「yamada@hub01」と入力。
    5. 「RSA SecureID」は OFF のまま。
    6. 「パスワード」欄には「K8nhrGJHHe98tCck4NGA」と入力。
    7. 「シークレット」欄には PSK 「Ppz6o5x9J」を入力。
    8. 「すべての信号を送信」は ON のまま。
    9. 「プロキシ」は「OFF」のまま。
    10. 右上の「完了」を押す。
  4. 「VPN」画面で、追加した構成を押す(チェックマークが付く)。
  5. 「状況」欄が「未接続」となっているでタッチ。
  6. 「状況」欄が「接続しています…」となる。
  7. VPN 接続に成功すると「状況」欄が「接続中」になる。
  8. 画面左上の電波状況アイコンの隣に「VPN」アイコンが表示されていれば完了。

これで VPN を経由して普通にインターネットにアクセスできるはずです。Safari や Twitter や LINE などが使えることを確認してください。

サーバ側のログを確認

vpnserver を実行したディレクトリにログファイル用のディレクトリが自動的に作られているはずです。デフォルトでは、自動的に定期的に新しいログファイルに切り替わっていきます。ログファイルは単なる UTF-8 のテキストファイルです。SoftEther VPN Server の言語設定が「日本語」だと、ログも日本語になります。

1
$ less server_log/vpn_20170510.log

先ほど iPhone からアクセスしたログが表示されるはずです。仮想 HUB「hub01」にユーザ名「yamada」で接続しているはずです。

1
2
3
2017-05-10 12:05:57.268 [HUB "hub01"] コネクション "CID-4" (IP アドレス 203.0.113.111, ホスト名 203.0.113.111.example.com, ポート番号 1701, クライアント名 "L2TP VPN Client", バージョン 4.20 ビルド 9608) が仮想 HUB への接続を試行しています。提示している認証方法は "外部サーバー認証" でユーザー名は "yamada" です。
2017-05-10 12:05:57.268 [HUB "hub01"] コネクション "CID-4": ユーザー "yamada" として正しく認証されました。
2017-05-10 12:05:57.268 [HUB "hub01"] コネクション "CID-4": 新しいセッション "SID-YAMADA-[L2TP]-2" が作成されました。(IP アドレス 203.0.113.111, ポート番号 1701, 物理レイヤのプロトコル: "Legacy VPN - L2TP")

VPS に VPN サーバをたてて iPhone からアクセス(第2回)

レンタル VPS で VPN サーバを動かして iPhone からのネットアクセスをセキュアにする(ついでに自宅 LAN にリモートアクセスできるようにする)というお話の第2回です。今回は SoftEther VPN Server の導入編です。

SoftEther VPN とは

SoftEther VPN は、オープンソースで高機能な VPN ソフトウェアです。独自の VPN 通信方式を実装していて、様々なネットワーク構成に柔軟に対応しています。また、独自の Windows/Linux クライアントはもちろん、L2TP/IPsec や MS-SSTP や OpenVPN など、多くのクライアントからの接続が可能です。

SoftEther VPN を選んだ理由

  • Layer 2 VPN なので LAN 専用のホーム機器にもリモートアクセス可能。
  • ファイアウォール貫通機能が異様に強い。
  • クラウド上の Linux VPS (Ubuntu 14.04 LTS) で VPN サーバを動かせる。
    • プログラムとして x86_64 Linux(カーネルが古い)で動作可能。
    • VPS が設置されているネットワークに迷惑をかけない。
      (構築した VPN がサーバの実ネットワークから切り離されている)
  • iPhone からの接続が可能。
    • iOS 標準機能の L2TP/IPsec が使える。
    • L2TP/IPsec がダメでも OpenVPN が使える。
  • 自宅の Windows PC から接続可能。
  • 自宅の Windows PC をブリッジとして自宅 LAN にリモートアクセス可能。
  • 将来的に Raspberry Pi を買えばブリッジとして使える。
  • VPN サーバをユーザモードで動かせる。 ★すごくうれしい★
  • NAT 機能と DHCP サーバ機能を内蔵していて簡単に使える。
  • 待ち受けポート番号を変更できる(デフォルトは避けたい)。
  • Windows GUI アプリからでも SSH コマンドラインからでも設定・管理が可能。
  • オープンソースなので好きに改造できる。
  • 日本語のマニュアルが充実している。
  • 国産のプロダクトであり、応援したい。

ちなみに、ちょっと気になる点もあります:

  • TLS 1.2 に対応していない(TLS 1.0 のみ)。
  • 使用できる暗号スイートが古い(弱い)。
  • SHA-1 しか選べない(SHA-256 にしたい)。
  • 証明書の鍵が RSA 2048bit までしか選べない(弱い)。
  • 証明書の SAN フィールドに対応していない(廃止された CN フィールドを見てる)。
  • 日本語のログを吐く(検索や加工がしにくい)。
  • テストスクリプトが公開されていない(改造してもテストできない)。

SoftEther VPN Server のダウンロード

ダウンロードするアーカイブファイルの URL は公式サイト参照:
https://ja.softether.org/5-download

最新版 (beta) と安定版 (RTM) が異なる場合は、お好きな方を選んでください。

表示されたリンクの URL をコピペしてダウンロードします:

1
2
3
4
5
$ cd softether
$ curl -O http://jp.softether-download.com/files/softether/v4.20-9608-rtm-2016.04.17-tree/Linux/SoftEther_VPN_Server/64bit_-_Intel_x64_or_AMD64/softether-vpnserver-v4.20-9608-rtm-2016.04.17-linux-x64-64bit.tar.gz
$ mkdir softether-vpnserver-v4.20-9608-rtm-2016.04.17-linux-x64-64bit
$ cd softether-vpnserver-v4.20-9608-rtm-2016.04.17-linux-x64-64bit
$ tar xvfz ../softether-vpnserver-v4.20-9608-rtm-2016.04.17-linux-x64-64bit.tar.gz

セキュリティ関係のソフトウェアなのに配布サーバが HTTPS 化されてないのが残念。不安な人は GitHub からソースファイルをダウンロードしてビルドしましょう。

今回のサーバ環境

今回のインストール先は、クラウド上のレンタル VPS です。

1
2
3
4
5
仮想化方式 : OpenVZ (Linux カーネルが更新できない!)
CPU : Intel Xeon
Memory : 2GB
OS : Ubuntu 14.04 LTS (x86_64)
グローバル IP アドレス : 固定で2個(IPv6 ありの仮想インタフェース)

2個あるグローバル IP アドレスは、いずれも FQDN で DNS lookup できます。ただし、CNAME レコードで DNS に登録しているため、逆引きはその FQDN とは異なります。今回は、2個あるグローバル IP アドレスのうち、「default route でない方」を SoftEther VPN Server で使用します。文中では、仮に、以下の値を使用します:

1
2
3
4
Interface: vnet1
IPv4: 192.0.2.33/24
IPv6: 2001:db8::2:33/64
FQDN: vpn33.example.com

なお、ほんとうは Docker で SoftEther VPN Server を動かしてみたかったのですが、Linux カーネルが古くて Docker は使えませんでした。上記のとおり仮想化方式が OpenVZ なので、カーネルの更新ができないのです。残念!

SoftEther VPN Server のインストール

まず、公式マニュアルを参照:

今回は SoftEther VPN Server をユーザモードで動かすので、公式マニュアルの「7.3.6 VPN Server の配置」以降の作業はやりません(make を実行するだけです)。

1
2
$ cd vpnserver
$ nice -19 make

無事に make できたら、実行に必要なファイルだけ手動でコピーします。

1
2
3
$ mkdir ../../bin
$ cp -p hamcore.se2 vpncmd vpnserver ../../bin
$ cd ../../bin

ここでひとつ、おまじないが必要です。

SoftEther VPN Server はデフォルトで TCP 443番ポート(https プロトコル用のポート)で接続を待ち受けます(bind します)。また、iPhone からの接続に L2TP/IPsec を使う場合、UDP 500番ポートと UDP 4500番ポートも bind します。Linux(というか UNIX 全般)では1024番未満のポートを bind するためには「特権」(capabilities) が必要です。iptables で1024番以上のポートにリダイレクトする、という抜け道もありますが、SoftEther VPN Server で L2TP/IPsec の待ち受けポート番号を変える方法がわからないので、今回は使えません(参考記事:「一般ユーザ権限で特権ポートを bind したい」)。Docker を使えればポートマッピングでなんとかなったかもしれませんが。

今回は、Linux の “File Capabilities” 機能を利用して、実行ファイル vpnserver に CAP_NET_BIND_SERVICE 権限を付与します。これにより、root 権限がなくても、この実行ファイルを実行したときに限り、1024番未満のポートを bind できるようになります。ついでに、念のため、ディレクトリ丸ごと Owner を別のユーザに変更して、実行ファイルを書き換えられないようにしておきます。

1
2
3
4
$ sudo chmod -R a-w .
$ sudo chown -R nobody .
$ sudo chgrp -R nogroup .
$ sudo setcap CAP_NET_BIND_SERVICE+eip vpnserver

もし、setcap コマンドが無いと怒られたら、libcap2-bin パッケージをインストールしてください。CentOS などの RPM 系では libcap パッケージらしいです。(参考ページ

1
$ sudo apt-get install libcap2-bin

最後に、vpnserver を実行するディレクトリ(この配下にログファイルなどが作られる)に移動して、上記ファイルへのシンボリックリンクを作成します。

1
2
3
4
5
6
$ mkdir ../run
$ cd ../run
$ rm -f hamcore.se2 vpncmd vpnserver
$ ln -s ../bin/hamcore.se2
$ ln -s ../bin/vpncmd
$ ln -s ../bin/vpnserver

初期状態では SoftEther VPN Server の管理者パスワードが設定されていないので、まだファイアウォール (iptables) に穴を開けてはいけません。ローカル・ループバック通信は iptables でデフォルトで許可されているはずなので、次に説明する初期設定は可能なはずです。

SoftEther VPN Server の初期設定

上記の公式マニュアルでは、vpnserver を起動してから「できるだけ早く」VPN サーバー管理マネージャで接続して管理者パスワードを変更するように、と書かれていますが、セキュリティ関係のソフトウェアでそんな危ういことをしてはいけません。

まず、vpnserver が初期状態で bind するすべてのポートを、ファイアウォール (iptables と ip6tables) がブロックしていることを確認します。

  • 443/tcp
  • 992/tcp
  • 1194/tcp
  • 5555/tcp

次に、vpnserver を起動します。起動するとすぐにコマンドプロンプトに戻ってきます。

1
$ date && nice -10 ./vpnserver start

続いて、vpncmd を実行して初期設定を開始します。

1
$ ./vpncmd localhost:443 /SERVER
  1. 管理者パスワードを設定する。
  2. デフォルトのリスナーポート「992番」を Disable する。
  3. デフォルトのリスナーポート「1194番」を Disable する。
  4. デフォルトのリスナーポート「5555番」を Disable する。
  5. SSL 通信で使用する暗号化アルゴリズムを一番強い「DHE-RSA-AES128-SHA」に変更する。
  6. 「インターネット接続の維持機能」を Disable する。
  7. 「VPN Azure 中継サービス」を利用しないようにする。
  8. デフォルトで存在している仮想 HUB「DEFAULT」を削除する。
  9. ランダムに初期化されたサーバ証明書をファイルとして保存しておく。
  10. SoftEther VPN プロジェクトが提供している Dynamic DNS (DDNS) サービスを利用しないようにする。

最後の項目だけは vpncmd では設定できません。それ以外の項目を vpncmd 上で設定します。

1
2
3
4
5
6
7
8
9
10
11
12
VPN Server>ServerPasswordSet
VPN Server>ListenerList
VPN Server>ListenerDisable 992
VPN Server>ListenerDisable 1194
VPN Server>ListenerDisable 5555
VPN Server>ServerCipherGet
VPN Server>ServerCipherSet DHE-RSA-AES128-SHA
VPN Server>KeepDisable
VPN Server>VpnAzureSetEnable no
VPN Server>HubDelete DEFAULT
VPN Server>ServerCertGet ./cert.pem
VPN Server>exit

なお、今回は接続性を優先して待ち受けポートを443番(HTTPS と同じ)にしていますが、セキュリティを優先するなら別のポートにしたほうが良いです。1024番以上のポートを使えば、前述の「おまじない」(Capabilities 付与)も不要になります(ただし iPhone から L2TP/IPsec 接続する場合は UDP 500番ポートを使うので「おまじない」必須)。

vpncmd が終了したら、vpnserver も停止させます。

1
$ ./vpnserver stop

続いて、設定ファイル vpn_server.config をテキストエディタで開いて、直接編集します。公式マニュアルの “DynamicDnsGetHostname” の項を参照して、Dynamic DNS を無効化します。

最後に、設定ファイル vpn_server.config を別の機器(USB メモリとか)にバックアップしておきます。ただし、このファイルには SoftEther VPN Server の秘密鍵が含まれているので、取り扱いには注意しましょう。

ファイアウォールに穴を開ける

TCP 443番ポートのみ待ち受け可能にします。

1
2
3
4
5
$ sudo iptables -A INPUT -i vnet1 -d '192.0.2.33' -p tcp --dport 443 -j ACCEPT
$ sudo iptables -L -vn
$ sudo ip6tables -A INPUT -i vnet1 -d '2001:db8::2:33 ' -p tcp --dport 443 -j ACCEPT
$ sudo ip6tables -L -vn
$ sudo /etc/init.d/iptables-persistent save

もし、iptables-persistent が無いと怒られたら、iptables-persistent パッケージをインストールしてください。

1
$ sudo apt-get install iptables-persistent

vpnserver を起動して、ちゃんと TCP 443番ポートを listen しているか netstat コマンドで確認しておきます。

1
2
$ date && nice -10 ./vpnserver start
$ netstat -antu

仮想 HUB の新規作成

iPhone からネットアクセスする際に使う仮想 HUB を新規作成します。

  • 仮想 HUB の名前は hub01 とする。
  • iPhone 側のユーザ名は yamada とする。
  • ユーザ yamada のパスワードは K8nhrGJHHe98tCck4NGA とする(乱数生成)。
  • 仮想 HUB 管理パスワードは不要なので設定しない。
  • SecureNAT 機能を有効化する(デフォルトで DHCP サーバ機能も有効になる)。
  • 仮想 HUB で使用するサブネットは 192.168.223.0/24 とする(他とかぶらなさそうなプライベートアドレスを適当に選ぶ)。

実際の作業は vpncmd で行います。もちろん、Windows PC の「SoftEther VPN サーバー管理マネージャ」でも同様の設定が可能です。

1
$ ./vpncmd localhost:443 /SERVER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
VPN Server>HubCreate hub01
VPN Server>Hub hub01
VPN Server>SetEnumDeny
VPN Server>UserCreate yamada /GROUP:none /REALNAME:none /NOTE:none
VPN Server>UserPasswordSet yamada
K8nhrGJHHe98tCck4NGA
VPN Server>SecureNatStatusGet
VPN Server>SecureNatHostGet
VPN Server>SecureNatHostSet /MAC:none /IP:192.168.223.1 /MASK:255.255.255.0
VPN Server>NatGet
VPN Server>DhcpGet
VPN Server>DhcpSet /START:192.168.223.10 /END:192.168.223.200 /MASK:255.255.255.0 /EXPIRE:86400 /GW:192.168.223.1 /DNS:192.168.223.1 /DNS2:none /DOMAIN:none /LOG:yes
VPN Server>SecureNatEnable
VPN Server>SecureNatStatusGet

iPhone から接続するときには、ユーザ名として「yamada@hub01 」を、パスワードとして「K8nhrGJHHe98tCck4NGA」を、それぞれ使用することになります。

なお、仮想 HUB を新規に作成したときは、「SoftEther VPN サーバー管理マネージャ」の仮想 HUB のプロパティ設定ウインドウでチェックボックス「匿名ユーザーに対してこの仮想 HUB を列挙しない」に必ずチェックを入れましょう。あるいは、vpncmd で仮想 HUB 設定に遷移して SetEnumDeny コマンドを実行しても同じです(上記の設定例でも実行しています)。

より安全に運用するには

  1. Windows PC 上の GUI アプリ「SoftEther VPN サーバー管理マネージャ」を使わずに vpncmd だけで管理できるなら、管理者ログインを VPN サーバ内部からのみに制限できます。リモートから SoftEther VPN Server に直接接続して設定することが不可能になるので、より安全になります。もちろん、SoftEther VPN Server を動かしているサーバに SSH ログインできれば、vpncmd を使ってリモートから管理できます。

    具体的には、SoftEther VPN Server のリモート管理接続元 IP アドレスとして 127.0.0.1::1 だけを許可します。設定方法は公式マニュアルの「3.3.18 IP アドレスによるリモート管理接続元の制限」を参照してください。

  2. 万が一に備えて、vpnserver を実行するユーザを新たに作って SoftEther VPN Server 専用のユーザにすることをオススメします。

    SoftEther VPN Server のダウンロードや make などは普段使っているユーザで行い、生成された実行ファイル vpnserver とライブラリファイル hamcore.se2 を実行用ユーザのホームディレクトリ配下にコピーすれば、vpnserver を実行できます。実行用ユーザに GitHub などへのアクセス権を与える必要はありません。

    管理コマンド vpncmd は誰でも(vpnserver を実行しているユーザ以外でも)使えるので、vpnserver 実行用ユーザはそれこそ「ログインできないユーザ」(ログインシェルが /bin/false/usr/sbin/nologin になっているユーザ)でも問題ありません。というか、ログインできないように設定することをオススメします。

    vpnserver 実行用ユーザは、sudo できないユーザにしておきましょう。具体的な条件は /etc/sudoers の設定に依存しますが、デフォルトならば「グループ sudo に属さないユーザ」にしておけば大丈夫です。

    ユーザ root が存在するシステムでは、vpnserver 実行ユーザは root への su ができないユーザにしておきましょう。たとえば、グループ wheel に属しているユーザは避けましょう。