本当にどうでもいいメモなどをたまに書く程度。

PDNS Manager の API でダイナミック DNS

日付:

前回の記事で PowerDNS のレコードを編集するのに PDNS Manager を導入した。 PDNS Manager は API も備えており API を叩くことでレコードの変更が可能だ。
つまり、ダイナミック DNS を運用可能だ。


詳細はドキュメントの通りだが、要するに

  • パスワードを使った HTTP GET メソッド
  • 公開鍵認証を使った HTTP POST メソッド

と、2種類のやり方がある。

以下、次の想定で書いていく。

  • PowerDNS はマスター master.example.com、スレーブ slave.example.com で動作している。
  • PDNS Manager は https://pdns.example.com/ で動作している。
  • home.example.com をダイナミック DNS として使う。
  • home.exmaple.com のレコード ID は11。

共通部分

  1. PDNS Manager にログイン。
  2. example.com ドメインを選択。
  3. 下部 Add で home.example.com を作る。TTL は標準で86400秒 (24時間) になっているので、600秒程度に短くしておく。
  4. home.example.com のレコード ID を記憶しつつ (本記事中では前述通り11と想定)、右側に3個並んだアイコンの右側、鍵アイコンをクリック。

GET メソッド

  1. Add credential | Key | Password の Password をクリック、右側に入力欄が出るので入力して Save。
  2. https://pdns.example.com/api/v1/remote/ip にアクセスすると自分の IP アドレスが (JSON で) 表示される。
  3. https://pdns.example.com/api/v1/remote/updatepw?record=11&password=(パスワード)&content=(IP アドレス) にアクセスすると home.example.com のレコードが更新される。

cron に登録するシェルスクリプトはこんなところかな。

wget を使う場合:

#!/bin/sh
ARECORD=`dig @master.example.com home.example.com a +short`
CURRENT=`wget -q -O - https://pdns.example.com/api/v1/remote/ip | jq -r .ip`
if [ $ARECORD = $CURRENT ]; then
	echo "The A record does not need to be updated."
else
	echo "Updating the A record..."
	wget -q -O - "https://pdns.example.com/api/v1/remote/updatepw?record=11&password=パスワード&content=${CURRENT}"
fi

curl を使う場合:

#!/bin/sh
ARECORD=`dig @master.example.com home.example.com a +short`
CURRENT=`curl -s https://pdns.example.com/api/v1/remote/ip | jq -r .ip`
if [ $ARECORD = $CURRENT ]; then
	echo "The A record does not need to be updated."
else
	echo "Updating the A record..."
	curl -s "https://pdns.example.com/api/v1/remote/updatepw?record=11&password=パスワード&content=${CURRENT}"
fi

jq 入れたくない人は CURRENT= の行を以下のように変更。
API は {"ip":"203.0.113.9"} といった形式でデータを返すので、awk でセパレーターをダブルクォーテーションにすれば4番目のフィールドが IP アドレス。

CURRENT=`wget -q -O - https://pdns.example.com/api/v1/remote/ip | awk -F \" '{print $4}'`
CURRENT=`curl -s https://pdns.example.com/api/v1/remote/ip | awk -F \" '{print $4}'`

あるいは以下のような、単純に IP アドレスのみを返してくれるところを wget なり curl なりする。

ただ、せっかく API で更新するんだから、同じ API で IP アドレスを取得した方が正しいとは思う。


POST メソッド

PDNS Manager 作者の GitHub にある PDNS Manager Client を使うことになる。
作者の説明では git clone しろとあるが、pdns-client と pdns-keygen という2個の bash スクリプトを配置するだけなので手動でやってもいいと思う。
鍵ファイルなどは内蔵しないので、/usr/local/bin/ に入れてパーミッション 755 で問題ない。

  1. pdns-keygen を実行。pdns.private.pem、pdns.public.pem が生成される。
  2. Add credential | Key | Password の Key をクリック、右側に入力欄が出るので pdns.public.pem の内容を貼り付けて Save。
  3. pdns-client -s https://pdns.example.com/ -i 11 -c (IPアドレス) を実行。

cron に登録するシェルスクリプトはこんなところ。
pdns-client 内で curl、jq を使っているので、その流儀に合わせた。

#!/bin/sh
ARECORD=`dig @master.example.com home.example.com a +short`
CURRENT=`curl -s https://pdns.example.com/api/v1/remote/ip | jq -r .ip`
if [ $ARECORD = $CURRENT ]; then
	echo "The A record does not need to be update."
else
	echo "Updating the A record..."
	pdns-client -s https://pdns.example.com/ -i 11 -c ${CURRENT}
fi

Basic 認証を使っている場合

私がまさに該当する。
Basic 認証と PDNS Manager で二重の認証を必要とすることで、より侵入しにくくする、という考え方。

GET メソッドの場合、さほど悩む必要はない。

  • wget なら --user=ユーザー --password=パスワード をつければいいだけ。
  • curl なら --basic --user ユーザー:パスワード をつければいいだけ。

問題は POST メソッドで、pdns-client が実行する curl にパラメーターを渡す必要があるわけだが、pdns-client 内に認証情報をハードコードしたくない。
というわけで pdns-client を改造した。
GitHub で fork して改造したのを公開しているので、そちらをどうぞ。

使い方はこんな感じ。
「-b ID:パスワード」で認証情報を渡す。

pdns-client -s https://pdns.example.com/ -i 11 -c 203.0.113.9 -b ID:パスワード

PowerDNS で DNS コンテンツサーバー構築。バックエンドは MariaDB でレプリケーション

日付:

ぶっちゃけ「ALIAS レコードが使いたい」というだけの理由で、PowerDNS に乗り換えた。
だが本気で乗り換えてみれば、PDNS Manager でレコード編集は楽だし、バックエンドを MariaDB にしてレプリケーションさせればゾーンの増減時にスレーブでの作業が不要になってさらに楽。
ただし構築は NSD と比べて手間がかかる。


マスターが master.example.com (192.0.2.1)、スレーブが slave.example.com (198.51.100.1) という構成を想定して書いていく。
DNS 的なゾーン転送 (AXFR) をするわけではないのだが、一応。

マスターはメモリ 2GB で nginx 等も動作しており、スレーブはメモリ 512MB で DNS のみを担当という想定。想定というか実際そうなのだけれども。

ALIAS レコードを使うので、127.0.0.1:53 に PowerDNS Recursor を動かす。

手順は Debian でも Ubuntu でもほぼ同じ。実際に私のスレーブのうち1台は Ubuntu である。
(ただし /etc/mysql/my.cnf だけなぜか Ubuntu だとないので、Debian からコピーした)


MariaDB、PowerDNS の導入

マスター、スレーブで共通の作業。

MariaDB は Debian 収録物ではなく MariaDB Repositories で導入する。
ただ add-apt-repository は好みじゃないので手順を少し変更。

sudo apt install dirmngr
sudo apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xF1656F24C74CD1D8
echo 'deb http://ftp.yz.yamagata-u.ac.jp/pub/dbms/mariadb/repo/10.3/debian stretch main' | sudo tee /etc/apt/sources.list.d/mariadb.list
sudo apt update
sudo apt install mariadb-server

PowerDNS も Debian 収録物ではなく PowerDNS repositories で導入する。

wget https://repo.powerdns.com/FD380FBB-pub.asc -O - | sudo apt-key add -
echo 'deb http://repo.powerdns.com/debian stretch-rec-41 main' | sudo tee /etc/apt/sources.list.d/pdns.list
echo 'deb http://repo.powerdns.com/debian stretch-auth-41 main' | sudo tee -a /etc/apt/sources.list.d/pdns.list
sudo apt update
sudo apt install pdns-recursor pdns-server pdns-backend-mysql

pdns-server が起動失敗するが、この時点ではそれで問題ない。
標準では pdns-server が *:53 を listen するのだが、127.0.0.1:53 を先に pdns-recursor が listen しているため、pdns-server が listen できないのだ。


MariaDB のインスタンスを分ける

マスターでは DNS 以外の仕事も担当しており、MariaDB は PowerDNS のみを収容しているわけではない。
しかし MariaDB の REPLICATION SLAVE 権限は特定のデータベースのみを対象とする権限ではなくグローバル権限なので、スレーブがマスターのデータベースすべてをレプリケーションする権限を持ってしまう (権限を持った上で、スレーブ側の設定でレプリケーションするデータベースを絞る、という形になる)。
これはセキュリティ上好ましくないので、PowerDNS 専用の MariaDB インスタンスを別に立ち上げる。

MariaDB を複数インスタンス起動する手段として、ちょっと古い記事だと mysqld_multi を使う説明が多いのだが、mariadb.service の代わりに mariadb@.service を使うという手段がある。
systemctl start mariadb@HOGE すると、mysqld --defaults-file=/etc/mysql/conf.d/myHOGE.cnf が実行される仕組み。この場合 /etc/mysql/my.cnf は読み込まれない。
なので /etc/mysql/my.cnf から末尾の !include-dir を除去したものを /etc/mysql/conf.d/mymariadb.cnf に置く。!include-dir /etc/mysql/conf.d なので除去しないと無限ループ。
!include の方は残してもいいが、PowerDNS 専用インスタンスはすべて mypdns.cnf に記述するので、!include も除去して mymariadb.cnf だけで済ませる方が統一感があるように思われる。

PowerDNS 専用インスタンスは次の設定とする。

  • ソケット /var/run/mysql/mysqld_pdns.sock
  • PID /var/run/mysql/mysqld_pdns.pid
  • ポート 3307
  • データディレクトリ /var/lib/mysql_pdns
  • バイナリログ /var/log/mysql/mariadb_pdns_bin
  • server-id 1
sudo systemctl stop mariadb
sudo systemctl disable mariadb
sed -e '/^!include/d' /etc/mysql/my.cnf | sudo tee /etc/mysql/conf.d/mymariadb.cnf
sed -e '/^!include/d' -e 's/\/var\/lib\/mysql/\/var\/lib\/mysql_pdns/g' -e 's/mysqld.\(sock\|pid\)/mysqld_pdns.\1/g' -e 's/3306/3307/g' 's/#\(server-id\)/\1/' -e 's/mariadb-bin/mariadb_pdns_bin/g' /etc/mysql/my.cnf | sudo tee /etc/mysql/conf.d/mypdns.cnf
sudo mysql_install_db --datadir=/var/lib/mysql_pdns
sudo systemctl enable mariadb@mariadb mariadb@pdns
sudo systemctl start mariadb@mariadb mariadb@pdns

PDNS Manager を使うので、空のデータベース pdns を作っておく。
同時にユーザー pdns@localhost、レプリケーション用ユーザー pdnsrepl@localhost も作る。

root のパスワードが空なので、これもパスワードを設定しておく。
mariadb を導入した時点で root のパスワードをつけたじゃないかって? それは mariadb@mariadb における root のパスワードで、mariadb@pdns における root のパスワードではない。

PowerDNS 慣れしている人はここでテーブルを作りそうになると思うが、PDNS Manager がテーブルを作るので、空のまま。

sudo mysql -P 3307 -h 127.0.0.1
CREATE DATABASE pdns;
GRANT USAGE ON *.* TO pdns@localhost IDENTIFIED BY 'パスワード';
GRANT ALL ON pdns.* TO pdns@localhost;
GRANT REPLICATION SLAVE ON *.* TO pdnsrepl@localhost IDENTIFIED BY 'パスワード';
UPDATE mysql.user SET password=PASSWORD('パスワード') WHERE user='root';
FLUSH PRIVILEGES;
\q

スレーブでもデータベース pdns、ユーザー pdns@localhost を作っておく。

sudo mysql -p
CREATE DATABASE pdns;
GRANT USAGE ON *.* TO pdns@localhost IDENTIFIED BY 'パスワード';
GRANT ALL ON pdns.* TO pdns@localhost;
\q

PDNS Manager の導入

マスターでの作業。

PDNS Manager を入れる。
公式サイトの説明が今時珍しく Apache での説明しか書かれていないので、nginx での設定を書いておく (PHP は php-fpm を使っている)。

server {
	server_name pdns.example.com;

	root /var/www/html/backend/public;
	index index.html index.php;

	location / {
		root /var/www/html/frontend;
		try_files $uri $uri/ /index.html;
	}

	location /api {
		try_files $uri $uri/ /index.php;
	}

	location ~ [^/]\.php(/|$) {
		if ($request_uri ~* "/api(.*)") {
			set $req $1;
		}
		fastcgi_pass unix:/run/php/php7.3-fpm.sock;
		fastcgi_split_path_info ^(/api)(/.*)$;
		fastcgi_index index.php;
		fastcgi_param SCRIPT_FILENAME $request_filename;
		fastcgi_param PATH_INFO $fastcgi_path_info;
		fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
		fastcgi_param REQUEST_URI $req;
	}
}

https://pdns.example.com/setup をブラウザーで開き、初期設定を行う。
基本的に Getting Started の通りだし、ぶっちゃけ画面見ればどこに何を入力すべきか一目瞭然だが、注意点が一つ。

Database の Host は localhost ではなく 127.0.0.1 とする

127.0.0.1 を指定しないと mariadb@pdns ではなく標準の方 (mariadb@mariadb) につながってしまうため。
コマンドラインで mysql -P 3307 -h 127.0.0.1 としていたのも同じ理由による。

ドメインの追加は、Add domain | MASTER | NATIVE | SLAVE とある中で、NATIVE で行う。
PowerDNS でゾーン転送 (AXFR) するのではなく、バックエンドである MariaDB を使って同期するためだ。


ssh port forwarding の設定

MariaDB は標準の設定ファイルでは localhost しか listen しないし、これを開放するのはセキュリティ上好ましくない。
なので、スレーブからマスターに ssh port forwarding で接続できるよう設定する。
レプリケーション用のシステムアカウントを作り、autossh で接続を維持する。

マスター、スレーブで共通の作業:

sudo useradd -r -s /usr/sbin/nologin -d /etc/powerdns/pdnsrepl pdnsrepl
sudo mkdir /etc/powerdns/pdnsrepl
sudo chown pdnsrepl:pdnsrepl /etc/powerdns/pdnsrepl
sudo chmod 700 /etc/powerdns/pdnsrepl
sudo -u pdnsrepl mkdir /etc/powerdns/pdnsrepl/.ssh
sudo -u pdnsrepl chmod 700 /etc/powerdns/pdnsrepl/.ssh

スレーブでの作業:

sudo -u pdnsrepl ssh-keygen -t ed25519
sudo apt install autossh

スレーブの /etc/powerdns/pdnsrepl/.ssh/id_25519.pub の内容を、マスターの /etc/powerdns/pdnsrepl/.ssh/authorized_keys に貼り付け。
もちろん authorized_keys は 600 にする。

スレーブで /etc/systemd/system/autossh-pdnsrepl.service を作成。

[Unit]
Description=AutoSSH port forwarding for PowerDNS DB replication

[Service]
ExecStart=/usr/bin/sudo -u pdnsrepl /usr/bin/autossh -f -M 0 -N -L 3307:127.0.0.1:3307 -o "ServerAliveInterval 45" -o "ServerAliveCountMax 2" pdnsrepl@master.example.com
ExecStop=/usr/bin/killall -KILL autossh
Type=forking

[Install]
WantedBy=multi-user.target

スレーブでの作業:

sudo -u pdnsrepl ssh pdnsrepl@master.example.com
sudo systemctl enable autossh-pdnsrepl
sudo systemctl start autossh-pdnsrepl

これで、スレーブで 127.0.0.1:3307 に接続するとマスターの 127.0.0.1:3307 につながるようになった。


レプリケーションの設定

マスターでの作業:

sudo mysql -P 3307 -h 127.0.0.1 -p
FLUSH TABLES WITH READ LOCK;
\q
sudo mysqldump  -P 3307 -h 127.0.0.1 -p pdns --lock-all-tables > pdns.db
sudo mysql -P 3307 -h 127.0.0.1 -p
UNLOCK TABLES;
SHOW MASTER STATUS;
\q

SHOW MASTER STATUS; で表示されたファイル名とポジションをメモりつつ、できた pdns.db をスレーブにコピーする。

スレーブでの作業:

sudo mysql pdns -p < pdns.db
sudo systemctl stop mariadb

/etc/mysql/mariadb.conf.d/slave.cnf を作成。
マスターでは my.cnf をベースに編集したファイルを mariadb@.service で直接読み込ませていたが、スレーブでは MariaDB インスタンスを分けず mariadb.service で起動するため、最小限の追加で済ませる。

[mysqld]
server_id = 2
read_only
replicate_do_db = pdns

スレーブでの作業:

sudo systemctl start mariadb
sudo mysql -p
CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=3307, MASTER_USER='pdnsrepl', MASTER_PASSWORD='(マスターで pdnsrepl@localhost につけたパスワード)', MASTER_LOG_FILE='(マスターの SHOW MASTER STATUS; で表示されたファイル名)', MASTER_LOG_POS=(マスターの SHOW MASTER STATUS; で表示されたポジションの数字);
START SLAVE;
\q

PowerDNS の設定

/etc/powerdns/pdns.d/mysql.conf を作成。

マスター:

launch+=gmysql
gmysql-dbname=pdns
gmysql-user=pdns
gmysql-password=パスワード
gmysql-socket=/var/run/mysqld/mysqld_pdns.sock

スレーブ:

launch+=gmysql
gmysql-dbname=pdns
gmysql-user=pdns
gmysql-password=パスワード
gmysql-socket=/var/run/mysqld/mysqld.sock

/etc/powerdns/pdns.conf を編集。
変更点は以下の通り。

  • expand-alias=yes
  • local-address は外側 IP アドレス (本記事の想定ではマスター 192.0.2.1、スレーブ 198.51.100.1) に設定する。
  • IPv6 がない場合、local-ipv6= にする。
  • resolver=127.0.0.1

pdns-server を起動。

sudo systemctl start pdns

完了。

NURO の詐欺みたいなタイトルのチラシ

日付:
タグ:

NURO の詐欺みたいなタイトルのチラシが入っていた。

ご不在連絡書

ご不在連絡書
何か自分にとって重要なものかと思って見るよね。
で、結局ただの spam なわけだ。

こういう詐欺みたいなタイトルは So-net 公認なのか?
少なくとも私の NURO に対する印象はかなり悪くなったぞ。
So-net はクソみたいな代理店をきっちり教育しなさい。

ちなみにこれが入ってた日、私は不在じゃなかった。
リアルアーツ株式会社、嘘ばっかじゃん。


先日 NHK が来たので「地デジもワンセグも映る機器がない」と言ったら「ご購入されましたらご連絡をお願いします」と帰ったくせに不在票入れてきたり、最近この手の嘘が流行ってるんだろうか。

アットウィキの anti-adblock を uBlock Origin で対策

日付:

uBlock Origin を使っているのだが、たまたま見たアットウィキ (atwiki、@wiki) の anti-adblock が全面モーダル表示であんまりなので対策。

uBlock Origin の My フィルターに、このどちらかを入れる。

atwiki.jp##div:has(#modal-window)
atwiki.jp##div:matches-css(position: fixed)

Adblock Plus を使っている場合はこうかな? ただし私は使っていないのでこれでいけるか不明。

atwiki.jp##div:-abp-has(#modal-window)
atwiki.jp##div:-abp-properties(position: fixed)

Togetter のまとめでは div の ID がランダムになったので対策不可能とされている。

しかし、そのランダムな ID を持つ div の子要素に #modal-window (「広告が表示されていません」ダイアログ) があるので、子要素で引っかければ良い。
それが :has() だ。

あるいは、全面モーダル表示は基本的に position: fixed を使うので、そこで引っかける。
:matches-css() だ。

私は :matches-css() を使っている。もし ID が変更されても、position: fixed は変わらないと思われるので。

手間いらず株式会社からの Apple を騙るフィッシング spam

日付:
タグ:

もう2回目なので晒し者にしておく。

手間いらず株式会社 (東証マザーズ2477) から Apple を騙るフィッシング spam が届いている。

Received: from earth.temanasi.jp (www.temanasi.jp. [211.125.61.96])
        by mx.google.com with ESMTP id p203si1134417ywg.170.2019.01.17.04.22.39
        for <musashi@araki.jp>;
        Thu, 17 Jan 2019 04:22:39 -0800 (PST)
Received-SPF: pass (google.com: domain of apache@temanasi.jp designates 211.125.61.96 as permitted sender) client-ip=211.125.61.96;
Authentication-Results: mx.google.com;
       spf=pass (google.com: domain of apache@temanasi.jp designates 211.125.61.96 as permitted sender) smtp.mailfrom=apache@temanasi.jp
Received: by earth.temanasi.jp (Postfix, from userid 48) id 6895BE9800D; Thu, 17 Jan 2019 21:22:38 +0900 (JST)
To: musashi@araki.jp
Subject: [情報] Apple IDがロックされています。あなたの最近のアクセスアカウントにいくつか問題がありました。
X-PHP-Originating-Script: 48:s-box.php
MIME-Version: 1.0
Content-type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
Mailer: Mailer
Message-ID: <1547727748-7e1400de641e46f3bba097170b5ece37-9197ec103d780b6779cb78eec6afce31-zkhl@www.temanasi.jp>
From: "ブラウザ上のApple Phone" <n0replay.supportliveOXXX2DEECDDVHJ80support@www.temanasi.jp>
Date: Thu, 17 Jan 2019 21:22:38 +0900 (JST)

以前も SpamCop で上流の abuse@web.ad.jp に通報したと記憶しているのだが、再発しているので HN506JP と ST11510JP も通報先に追加しておいた。


(2019/03/04 追記)

コメント欄としてつけている Disqus に本日このようなコメントが投稿された。

投稿者: ゆー

本文:
手間いらず株式会社のドメインはtemairazu.comですが、そのメールヘッダはtemanasi.jpになってますよ?完全に誤認情報では?

コメント返信でも反論しているが、記事中でもまとめておく。

まず、temanasi.jp の登録者名である「比較.com株式会社」 (JPRS WHOIS) は、手間いらず株式会社の2017年10月以前の商号である。
これは手間いらず株式会社の沿革ページに明記されている。

2017年10月 「比較.com株式会社」を「手間いらず株式会社」に商号変更

次に IP アドレスの話。

  • temairazu.com の IP アドレスは 211.125.61.95 である。
  • temanasi.jp の IP アドレスは 211.125.61.96 である。

両 IP アドレスが属する CIDR ブロック 211.125.61.64/26 は「比較.com株式会社」名義である (JPNIC Whois Gateway)。

以上から誤認ではなく、手間いらず株式会社から送信されたものであることは間違いないと考えている。
さすがにこの程度のことは調べた上でないと、他社を名指しで批判できない。まして相手は上場企業なのだから、ろくに確認もせず言いがかりをつけたとあっては「風説の流布」などで私の手が後ろに回る。

また、211.125.61.64/26 の管理者連絡窓口、技術連絡担当者に指定されている SK14915JP (JPNIC Whois Gateway) の電話番号は、手間いらず株式会社の会社概要ページに大代表として記載されている電話番号と一致している (これは Jun11 氏のコメントで気づいた。御礼申し上げる)。


手間いらず株式会社から送信された Apple を騙るフィッシング spam については、他の方のブログで2018年12月14日送信分2018年12月18日送信分が紹介されている。 私が本記事に掲載したものから約1ヶ月前のものだ。

私は「手間いらず株式会社が主体的に Apple を騙るフィッシング spam をばらまいた」とまでは考えていない。踏み台にされただけだろう。
しかし、これは ASP 事業者たる手間いらず株式会社が spam の踏み台にされる状況を1ヶ月以上も放置していた、ということを意味する。
仮にもプロが何やってんの? という感想しか持てない。

Debian stretch で Misskey インスタンスを立ててみた

日付:

今年 ActivityPub 対応して Mastodon 等とも互換性ができた Misskey インスタンスを立ててみた。

ドキュメントは非常にさらっと書かれていて、Mastodon や Pleroma のようにリバースプロキシを使うのかどうかさえよくわからない。
そのあたりは設定ファイルに書かれていて、インスタンスを立てるような者は必ず見るはずなのでドキュメントには書いていないということかな。


まず

  • Nginx
  • build-essential
  • Node.js
  • MongoDB
  • Redis

を入れる。
ここでは Nginx、Node.js、MongoDB について Debian 収録物ではなく各上流のリポジトリから入れているが、特に MongoDB は 3.6 以上が要求されるため、Debian 収録物では古い点に注意。

% wget -O - https://nginx.org/keys/nginx_signing.key | sudo apt-key add -
% echo 'deb http://nginx.org/packages/debian/ stretch nginx' | sudo tee /etc/apt/sources.list.d/nginx.list
% wget -O - https://deb.nodesource.com/setup_10.x | sudo -E bash -
% sudo apt install dirmngr
% sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4
% echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/4.0 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list
% sudo apt update
% sudo apt install nginx build-essential nodejs mongodb-org redis-server
% sudo systemctl enable mongod
% sudo systemctl start mongod

dirmngr は MongoDB のアーカイブキーをインポートするのに必要。


Misskey インストール。ドキュメント通り。

% su misskey
% cd
% git clone -b master git://github.com/syuilo/misskey.git
% cd misskey
% git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)
% npm install

Mongo まわり。ドキュメント通り。

% sudo mongo
> use misskey
> db.users.save({dummy:"dummy"})
> db.createUser({user:"misskey", pwd:"(略)", roles:[{role:"readWrite", db:"misskey"}]})

設定ファイルを書いてビルド。基本的にドキュメント通り。

% cp .config/example.yml .config/default.yml
% nano .config/default.yml
% npm run build

nginx でリバースプロキシ。

/etc/nginx/conf.d/example.com.conf
(nginx 公式リポジトリからインストールした場合の配置。Debian の流儀とは異なっている)

server {
	server_name example.com;
	listen 80;
	listen [::]:80;
	if ($https != on) {
		return 301 https://$host$request_uri;
	}

	listen 443 ssl http2;
	listen [::]:443 ssl http2;
	ssl_certificate (略);
	ssl_certificate_key (略);

	root /home/example/www;
	index index.html index.htm;
	location / {
		try_files $uri @proxy;
	}
	location @proxy {
		proxy_pass_header Server;
		proxy_pass http://localhost:5000;
		proxy_buffering off;
		proxy_redirect off;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		tcp_nodelay on;
	}

	access_log /home/example/log/access;
	error_log /home/example/log/error warn;
}

重要なのは location @proxy ブロックの proxy_http_version、proxy_set_header の計3行で、要するに WebSocket。これがないとログイン後いつまでたっても右下に「再接続中...」が表示され、投稿もできない。


Misskey は更新頻度が非常に高いので、更新用スクリプトを書いた。
update-misskey.sh

#!/usr/bin/env bash
function update_misskey() {
	git fetch
	git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)
	npm install
	NODE_ENV=production npm run build
	popd
	exit 0
}

pushd ~/misskey/
git checkout .
if [ $# -eq 1 ] && [ $1 = '-f' ]; then
	echo "Forced updating Misskey..."
	update_misskey
fi

MISSKEY_CURRENT_VERSION=`git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1`
git fetch
MISSKEY_NEW_VERSION=`git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1`
if [ $MISSKEY_CURRENT_VERSION = $MISSKEY_NEW_VERSION ]; then
	echo "Misskey is not updated."
	popd
	exit 1
else
	echo "Updating Misskey..."
	update_misskey
fi

$? に更新したら0、すでに最新で更新しなかったら1が返るので、こんな感じのを root で cron に仕込んでおけば楽かも。

#!/bin/sh
sudo -u misskey ~misskey/update-misskey.sh
if [ $? -eq 0 ]; then
	systemctl restart misskey
fi

nginx で月別ログローテーション

日付:

今回は nginx でのログローテーションについて。

logrotate を使う場合

たぶん最も標準的な構成が logrotate を使うものだと思う。
実際、nginx を公式 APT リポジトリからインストールした場合の /etc/logrotate.d/nginx は次の内容になっている。

/var/log/nginx/*.log {
	daily
	missingok
	rotate 52
	compress
	delaycompress
	notifempty
	create 640 nginx adm
	sharedscripts
	postrotate
		if [ -f /var/run/nginx.pid ]; then
			kill -USR1 `cat /var/run/nginx.pid`
		fi
	endscript
}

postrotate で nginx に USR1 シグナルを送るのが要で、そうしないとファイルハンドルを握ったままの nginx が旧ログに書き込み続けてしまう。

このやり方の場合、monthly にすれば「1ヶ月分のログ」でローテーションされるが、「2018年7月分のログ」という形にはならない。

nginx 自体でできるもの (アクセスログのみ)

Frederic Cambus 氏の記事によると、nginx 0.9.6 以降で $time_iso8601 変数が使えて、アクセスログの指定 (access_log) に流用できるそうだ。

if ($time_iso8601 ~ "^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})") {}
access_log /var/log/nginx/access-$year$month.log

$time_iso8601 には ISO 8601 形式で現在の日付と時刻が入っている。「2018-07-25T20:32:01+09:00」みたいな感じ。
nginx は Perl 互換正規表現 (PCRE) が使えるので名前付きキャプチャでマッチすると、以降で $year、$month、$day 変数に値が入る。これを access_log で使えば nginx が月別ログを出力してくれる、という仕掛け。
ただしこの変数は error_log には使えないので、エラーログには他の手段を使う必要がある。

cronolog を使う場合

Apache で月別ログにする場合、cronolog が広く採用されていると思う。
Apache では普通にパイプが使えるので、下記のようにしておけば月別にログができた。

	ErrorLog	"| /usr/bin/cronolog --period=months /home/example/log/error-%Y%m.log"
	CustomLog	"| /usr/bin/cronolog --period=months /home/example/log/access-%Y%m.log" vhost_combined

nginx では素でパイプが使えるわけではないので、mkfifo を使う。

FIFO を作る。
/etc/nginx/mkfifo.sh

#!/usr/bin/env bash
declare -A LOGS=(
	["/var/log/nginx/access"]="nginx"
	["/var/log/nginx/error"]="nginx"
)

for LOG in ${!LOGS[@]}; do
	if ! [ -p $LOG ]; then
		rm -f $LOG
		mkfifo $LOG
		chown ${LOGS[$LOG]} $LOG
	fi
done

/etc/nginx/start_cronolog.sh

#!/usr/bin/env bash
declare -A LOGS=(
	["/var/log/nginx/access"]="nginx"
	["/var/log/nginx/error"]="nginx"
)

for LOG in ${!LOGS[@]}; do
	sudo -b -u ${LOGS[$LOG]} sh -c "cat $LOG | cronolog --period=months -S $LOG.log $LOG.%Y%m"
done

ログの指定は ["ログファイル"]="ファイルオーナー" という形式。

/etc/systemd/system/nginx_cronolog.service

[Unit]
Description=cronolog for nginx
Before=nginx.service

[Service]
Type=forking
ExecStart=/etc/nginx/start_cronolog.sh

[Install]
WantedBy=multi-user.target

nginx_cronolog が開始済みじゃないと nginx は動かない。

レンタルサーバー (共有ホスティング) のように、バーチャルホストのログをユーザーディレクトリ下に出力する場合は、FIFO のオーナーを nginx、グループを当該ユーザーにして、パーミッション 640 にする。
Apache と違い、ログの出力は root 権限ではなく nginx 権限で行われるので、FIFO は nginx で書き込み可能でなくてはならない。

シェルスクリプトで bash を使ったのは連想配列を使いたいからで、別に私は bashism の徒ではない。普段使ってるのは zsh だし。

nginx で SSL Server Test 満点の設定 (2018/07/24)

日付:
タグ:

HiFormance の VPS をぼちぼち実運用に供してみる準備編。

ずっと Apache を (というよりは Apache の ITK MPM を) 愛用してきたのだが、

  • Apache 2.4.27 以降で mod_http2 が prefork MPM で動作しなくなる
  • Debian stretch 収録の Apache は 2.4.25 だが、mod_http2 のパッチバックポートにより 2.4.25 でも mod_http2 が prefork MPM で動作しなくなった
  • (ITK MPM は prefork MPM に依存している)

こんな感じで ITK MPM に未来がなくなってきてしまった。

というわけでまだ実運用に供していない環境で nginx を扱ってみることにした。


せっかくなので SSL Server Test で満点をとれる設定を目指してみたい。

まず nginx 自体を Debian 収録バージョンではなく、nginx 公式のリポジトリで mainline を入れる。

% echo 'deb http://nginx.org/packages/debian/ stretch nginx' | sudo tee /etc/apt/sources.list.d/nginx.list
% wget https://nginx.org/packages/keys/nginx_signing.key -O - | sudo apt-key add -
% sudo apt update
% sudo apt install nginx

Let's Encrypt の証明書を RSA 4096 bit で作る。

% sudo certbot certonly --agree-tos -n --rsa-key-size 4096 --webroot -w /var/www/html -w webmaster@example.com -d example.com

nginx の SSL まわりの設定。
いつも通りだが、Mozilla SSL Configuration Generator を基本にする。
nginx 1.15.1、OpenSSL 1.1.0f、Modern 設定。

結論からいくと、下記の設定で満点になる。

server {
	listen 443 ssl http2;
	listen [::]:443 ssl http2;
	root /var/www/html;
	ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
	ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
	ssl_session_timeout 1d;
	ssl_session_cache shared:SSL:50m;
	ssl_session_tickets off;
	ssl_dhparam /etc/nginx/dhparam.pem;
	ssl_ecdh_curve secp384r1;
	ssl_protocols TLSv1.2;
	ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256';
	ssl_prefer_server_ciphers on;
	add_header Strict-Transport-Security max-age=15768000;
	ssl_stapling on;
	ssl_stapling_verify on;
	resolver 127.0.0.1 valid=300s;
}

(resolver が 127.0.0.1 なのは、私はそこに Unbound を動かしているため)

変更点は次のとおり。

  • ssl_ecdh_curve secp384r1;
  • ssl_ciphers から ECDHE-RSA-AES128-GCM-SHA256、ECDHE-RSA-AES128-SHA256 を除去。

ただしこの設定では、Android 5/6、Windows Phone 8.1 の IE11 からはアクセスできなくなっている。


Mozilla SSL Configuration Generator の出力では ssl_ciphers はこうなっていた。

ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';

この設定だと SSL Server Test は A+ 100/100/100/90 で、Cipher Strength だけ満点が取れず90点。
Android 4.x 以降でアクセスできる、まずまずまともな対応範囲といえる。

  • ECDHE-RSA-AES128-GCM-SHA256 を削ると、Android 5.x/6.x が NG になる。
  • ECDHE-RSA-AES128-SHA256 も削ると Cipher Strength が満点になるが、Windows Phone 8.1 の IE11 が NG になる。

メモ

  • Mozilla SSL Configuration Generator で Modern 設定の ssl_ciphers はもう全部 ECDHE-* なので、DH 鍵を 4096 bit で生成して ssl_dhparam を書いても意味ないっぽい。
  • ssl_ecdh_curve を指定しないと secp256r1 や x25519 の 256 bit (RSA 3072 bit 相当) になり、Key Exchange が90点になる。
  • 欲張って ssl_ecdh_curve secp521r1; なんてやると Chrome でアクセスできなくなる (Chrome というか BoringSSL が2015年に P-521 対応を削除したため)。ssl_ecdh_curve secp521r1:secp384r1; なら OK。

結論

古い環境の切り捨てがスコアに直結するとはいえ、さすがに Android 5.x/6.x はまだ合計 20% 以上のシェアがあって切り捨てられない。
副作用を考えると、結局 ssl_ciphers は Mozilla SSL Configuration Generator の通りにして A+ 100/100/100/90 で我慢するのが落としどころという感じ。

なお、当サイトは Netlify 収容のため、そこまでカリカリに詰めた設定にはなっていない。そもそも証明書からして RSA 2048 bit だし、スコアは A+ 100/100/90/90 である。たはは。

メモ置き場を Hugo に移行した

日付:

これまでこのメモ置き場は WordPress で運用してきたが、本体もプラグインもしょっちゅうアップデートがあって面倒になったこともあり、Hugo に移行することにした。


wordpress-to-hugo-exporter を使えば楽ができそうだと思ったが、案外そうでもなかった。

  • 動作させるのに PHP の zip extension が必要
    extension=zip.so これは結構動作環境を限定すると思う。
  • 出力される .md の frontmatter が YAML。TOML なら良かったのに。しかたないので全部直した。
  • 出力される markdown が汚い。
  • さらに当サイトでは電力料金比較表があり、これは WordPress の固定ページとして作ったので、PHP であり静的 HTML ではない。
    しかたないのでこれは Hugo 用の .md (と言いつつ中身はほとんど HTML) を出力するように改造した。
    PHP CLI で実行すれば .md ができる。

Hugo で WordPress 的な年月日別アーカイブ

WordPress では パーマリンク設定を /%year%/%monthnum%/%day%/%postname%/ にしていた。
この場合、親ディレクトリ (/2018/07/10/) に日別アーカイブ、さらに遡れば月別、年別アーカイブがあった。
この機能は Hugo にはないので、泥臭い形で実装した。

  • 記事は content/post/ 下に
  • content/archive_yearly/ に年別アーカイブ用ダミーファイルを配置
  • content/archive_monthly/ に月別アーカイブ用ダミーファイルを配置
  • content/archive_daily/ に日別アーカイブ用ダミーファイルを配置

config.tomlでパーマリンク設定。
キモは post と archive_* のパーマリンク構造を合わせること。

[permalinks]
post = "/:year/:month/:day/:slug/"
archive_yearly = "/:year/"
archive_monthly = "/:year/:month/"
archive_daily = "/:year/:month/:day/"

次に各アーカイブのテンプレート。私はテーマ下に入れているが、まあ適宜よしなに。

年別
themes/example/layouts/archive_yearly/single.html

{{define "main"}}
{{$archive_year := .Date.Year}}
<h1>{{$archive_year}}年</h1>
<ul>
{{range (where .Site.Pages "Section" "post").ByDate}}
{{if eq .Date.Year $archive_year}}
<li>{{printf "%04d/%02d/%02d" .Date.Year .Date.Month .Date.Day}}<br /><a href="{{.Permalink}}">{{.Title}}</a></li>
{{end}}
{{end}}
</ul>
{{end}}

月別
themes/example/layouts/archive_monthly/single.html

{{define "main"}}
{{$archive_year := .Date.Year}}
{{$archive_month := .Date.Month}}
<h1>{{$archive_year}}年{{printf "%02d" $archive_month}}月</h1>
<ul>
{{range (where .Site.Pages "Section" "post").ByDate}}
{{if eq .Date.Year $archive_year}}{{if eq .Date.Month $archive_month}}
<li>{{printf "%04d/%02d/%02d" .Date.Year .Date.Month .Date.Day}}<br /><a href="{{.Permalink}}">{{.Title}}</a></li>
{{end}}{{end}}
{{end}}
</ul>
{{end}}

日別
themes/example/layouts/archive_daily/single.html

{{define "main"}}
{{$archive_year := .Date.Year}}
{{$archive_month := .Date.Month}}
{{$archive_day := .Date.Day}}
<h1>{{$archive_year}}年{{printf "%02d" $archive_month}}月{{$archive_day}}日</h1>
<ul>
{{range (where .Site.Pages "Section" "post").ByDate}}
{{if eq .Date.Year $archive_year}}{{if eq .Date.Month $archive_month}}{{if eq .Date.Day $archive_day}}
<li><a href="{{.Permalink}}">{{.Title}}</a></li>
{{end}}{{end}}{{end}}
{{end}}
</ul>
{{end}}

これで archive_daily/2018-07-10.md があれば /2018/07/10/index.html に2018年07月10日の記事一覧が出力される。

このダミーファイルも手作業でいちいち作ってたら面倒でかなわないので、自動化する。

content/post/*.md を読んで、

date = "2018-07-10T00:00:00+09:00"

みたいな行を見つけたら、

  • content/archive_yearly/2018.md
  • content/archive_monthly/2018-07.md
  • content/archive_daily/2018-07-10.md

をそれぞれ作成。というもの。

content-src/archives.php

<?php
// make archives

$dir = dirname(dirname(__FILE__)) . '/content/post/';
$archive_yearly = dirname(dirname(__FILE__)) . '/content/archive_yearly';
$archive_monthly = dirname(dirname(__FILE__)) . '/content/archive_monthly';
$archive_daily = dirname(dirname(__FILE__)) . '/content/archive_daily';

cleandir($archive_yearly);
cleandir($archive_monthly);
cleandir($archive_daily);

$dh = opendir($dir);
while(($file = readdir($dh)) != false) {
	if(preg_match('/\.md$/', $file) === 1) {
		$matches = array();
		$str = file_get_contents(sprintf('%s/%s', $dir, $file));
		$array = explode("\n", $str);
		for($i = 0; $i < count($array); $i++) {
			if(preg_match('/date\s*=\s*"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T/', $str, $matches) === 1) {
				file_put_contents(sprintf('%s/%04d.md', $archive_yearly, $matches['year']), sprintf("+++\ndate = \"%04d-01-01T00:00:00+09:00\"\ntitle = \"%04d年\"\n+++\n%04d", $matches['year'], $matches['year'], $matches['year']));
				file_put_contents(sprintf('%s/%04d-%02d.md', $archive_monthly ,$matches['year'], $matches['month']), sprintf("+++\ndate = \"%04d-%02d-01T00:00:00+09:00\"\ntitle = \"%04d年%02d月\"\n+++\n%04d-%02d", $matches['year'], $matches['month'], $matches['year'], $matches['month'], $matches['year'], $matches['month']));
				file_put_contents(sprintf('%s/%04d-%02d-%02d.md', $archive_daily , $matches['year'], $matches['month'], $matches['day']), sprintf("+++\ndate = \"%04d-%02d-%02dT00:00:00+09:00\"\ntitle = \"%04d年%02d月%02d日\"\n+++\n%04d-%02d-%02d", $matches['year'], $matches['month'], $matches['day'], $matches['year'], $matches['month'], $matches['day'], $matches['year'], $matches['month'], $matches['day']));
				break;
			}
		}
	}
}
closedir($dh);



function cleandir($d) {
	if(file_exists($d)) {
		$dh = opendir($d);
		while(($file = readdir($dh)) != false) {
			if(preg_match('/\.md$/', $file) === 1) {
				unlink($d . '/' . $file);
			}
		}
	}
	else {
		mkdir($d);
	}
}

(ぶっちゃけ CI 使う場合 cleandir() は必要ないが、手元でビルドして public/ をアップロードするケースも考えられるので一応……)

php content-src/archives.php

すると、各アーカイブ用ダミーファイルが生成される。
ここで PHP を使っているのは、すでに電力料金比較表の生成で PHP を使っているから。どうせ1個使ってるんだから PHP に揃えてしまえと。

Hugo を使う場合ほとんどの人が CI を使うだろうし、Netlify ではビルドで PHP を使えるので、まあまあ使い物になるのではないか。

Netlify のビルドコマンド:

php content-src/archives.php; hugo

WordPress では年月日別アーカイブで本文を表示しているが、その点だけは追従していない。
日付とタイトルがあってリンクされればそれで用は済むと思うので。

当サイトは記事数が少ないので問題にならないが、記事数が多い場合は年別アーカイブぐらいにはページネーションを入れた方がいいかも知れない。


Hugo でこういうアーカイブを作る場合、普通はアーカイブ用のタクソノミーを作って list.html で出力するのが大半だと思うが、それでは WordPress 同様の /YYYY/MM/DD/ ではなく /archives/YYYY/MM/DD/ のようなパスになってしまう。
既存サイトを Hugo に移行する場合、パーマリンク構造を変化させずに移行することが重要だと思われるので、年月日別アーカイブのページを single.html で post のパーマリンク構造内に出力する形にした。

電力料金比較表を更新 (2018/06/27)

日付:

実際のページ更新は後日で、今日の時点ではまだローカルでデータを更新しただけ。

修正点:

  • 料金計算テーブルを各社の最新情報に更新。
  • 本サイトが WordPress から Hugo に移行するため、静的 HTML を出力するように変更。
  • 同じ理由で表示する契約容量の変更方法をフォームからタブに変更。
  • 当該契約容量でサービスを提供していない (または新規契約ができない) 事業者について、従来「提供なし」と表示していたが、事業者ごと表示しないことにした。