fail2banでWordPressを守る 2019年1月

以前にfail2banでWordPressを守るというのを書いたが、それの新しい版。
fail2banというのはUNIX系のOSで利用できるツールで、ログファイルを監視して不正なログイン試行が行われた・繰り返されたなどを検知するとその不正ログインを行ったIPアドレスをファイアウォールレベルで遮断するもの。WordPressもウェブサイトのオーナーやユーザーがログインして使用するものなのでログを監視して不正なログイン元を遮断するという仕組みは利用したいところ。ただし、いろいろ面倒。
「がとらぼ」ではWordPressのWP fail2banプラグインを使っていた。機能が絞られていてシンプル簡単だったので。
そのWordPressのWP fail2banプラグインが、ここ最近バージョンアップを重ねて機能が肥大した挙句、有料ライセンスの機能拡張が誕生し「そんなの求めてないよ」というものの塊になってしまった。

WP fail2banプラグイン 1
管理画面で見るWP fail2ban。機能は増えに増えて、もうよくわからないものに。なのに、無料版は管理画面では何もできない?

WP fail2banプラグイン 2
有料ライセンスは意外と高い。そして不必要な付加価値を付けすぎ。以前からの機能は引き続き無料で利用できるようだが、こうなると使い続けたくはなくなるよね。

しばらく愛用していたWP fail2banだが、見切りをつけることに。

で、ウェブサーバのアクセスログから認証時のエラーコード200を見つけてログイン失敗として検知するという以前の記事のやり方に戻そうかとも思ったけど、エラーコードを監視というのもどうだかなぁと思うし、それでは進歩がゼロだわね。

WordPress側に追加

WordPressのフック機能を使ってログイン失敗だけを専用ログに吐き出して、そのログをfail2banに監視させたら良くね?つまり、以前のシンプルだった頃のWP fail2banプラグインのお仕事を自分で書いてみようと。簡単なものなのでプラグインを新しく作るのではなくテーマのfunctions.phpにちょこっと書く。

使用中のテーマのfunctions.php (最後辺りにでも追記)
1
2
3
4
5
6
7
8
add_action('wp_login_failed', 'login_failure_log');
function login_failure_log($intruder) {
    $msg = date('[Y-m-d H:i:s T]') ." login failure from " . $_SERVER['REMOTE_ADDR'] . " for $intruder\n";
    $authlog = "/var/log/wp_auth_failure.log";
    $log_append = fopen($authlog, "a");
    fwrite($log_append, $msg);
    fclose($log_append);
}

4行目が出力されるログファイルだが、存在しない状態では出力エラーになる。ファイルを作成してパーミッションをウェブサーバのユーザーが書き込める状態にする。

ちなみに1行目のwp_login_failedをwp_loginに書き換えればログイン成功の記録を採る機能に変わる。(もちろん、関数名やメッセージは書き換えてやらないとログイン成功なのか失敗なのかわからなくなる。)
失敗・成功の両方を書いておけば認証記録になる。

ログファイルの作成とファイルオーナー変更の実行例(自分の環境に読み替えて)
# touch /var/log/wp_auth_failure.log
# chown www:www /var/log/wp_auth_failure.log
これで、WordPressにログインしようとして失敗すると作成したログファイルに下のように記録される
[2019-01-25 06:25:17 UTC] login failure from 192.168.4.211 for userhoge

サーバによって出力される時刻が日本時間かUTCかわからないのでタイムゾーンを時刻の後に出力するようにしている。 上の例ではUTCなので9時間足したものが日本時間になる。このようにタイムゾーンが書いてあればfail2banの側で時刻を認識するので後で時刻を変換するとかは要らないはず。 他はアクセス元のIPアドレスとログインしようとしたユーザー名程度の簡単なログ。

fail2banの設定

メインの設定ファイルとなるjail.localに設定追加。(これは以前の記事と同様)
/usr/local/etc/fail2ban/jail.local (追記または変更)
1
2
3
4
5
6
[wordpress-auth]
enabled  = true
filter   = wordpress-auth
logpath  = /var/log/wp_auth_failure.log
timepattern = %Y-%b-%d %H:%M:%S %Z
maxretry = 2

4行目は先に作成したログファイルを指定する。

フィルターファイルを作成する。

/usr/local/etc/fail2ban/filter.d/wordpress-auth.conf (新規作成)
1
2
3
4
[Definition]                                                                    
failregex = login failure from <HOST> for

ignoreregex =

検知しなければならない文字列とその中で遮断対象のIPアドレス (fail2banでは<HOST>と書く)が何処に記入されるかというのをfailregexに書く。つまり(既存のor 追記された)ログの中から login failure from IPアドレス for を見つけたらjail.localで指定された処理を行う。
「指定された処理」というのは上の設定例では書いていないが、[wordpress-auth]でbanactionが指定されていなければ[default]で指定されているbanactionが実行される。

設定ができたらfailban-regexを実行して意図したとおりにログファイルから不正アクセスレコードが検知されるか確認する。

$ fail2ban-regex /var/log/wp_auth_failure.log /usr/local/etc/fail2ban/filter.d/wordpress-auth.conf

Running tests
=============

Use   failregex filter file : wordpress-auth, basedir: /usr/local/etc/fail2ban
Use      datepattern : Default Detectors
Use         log file : /var/log/wp_auth_failure.log
Use         encoding : UTF-8


Results
=======

Failregex: 5 total
|-  #) [# of hits] regular expression
|   1) [5] login failure from <HOST> for
`-

Ignoreregex: 0 total

Date template hits:
|- [# of hits] date format
|  [5] {^LN-BEG}ExYear(?P<_sep>[-/.])Month(?P=_sep)Day(?:T|  ?)24hour:Minute:Second(?:[.,]Microseconds)?(?:\s*Zone offset)?
`-

Lines: 5 lines, 0 ignored, 5 matched, 0 missed
[processed in 0.00 sec]

今回はログ中の該当する5行がただしく認識できていた。ログ中の該当数と認識されている数が違えばNG。

ファイアウォールの設定はこの記事では省略。
ファイアウォールにFreeBSD/OpenBSDのpfを使っているならfail2ban (0.10系)をファイアウォールpfとの組み合わせで使うを参照下さい。

ファイアウォールの準備ができたらfail2banを再起動するか再読込するか。

FreeBSDでfail2banの設定再読込
# service fail2ban reload

WordPressのAMPプラグインを使う 2019年1月 後編

この記事の前編

AMPのJSライブラリ

AMPプラグインではどのモードでも最低限の https://cdn.ampproject.org/v0.js は自動的に読み込まれるようになっている。他にPairedモードでは https://cdn.ampproject.org/v0/amp-form-0.1.js も読み込まれるよう。(使用している非AMPテーマにフォームが含まれてる場合には自動的に含まれるのかな?)
ClassicモードではAMPライブラリを必要に応じて追加する。AMPの場合はライブラリはAMPのヘッダで読み込んで、それを使用するタグ(+設定などのJSON)をbody内に書くというのがお約束のよう。なのでAMPのライブラリはテンプレートのhtml-start.phpに書くことになるかと。

追加例
1
2
3
4
5
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
<script async custom-element="amp-auto-ads" src="https://cdn.ampproject.org/v0/amp-auto-ads-0.1.js"></script>
<script async custom-element="amp-list" src="https://cdn.ampproject.org/v0/amp-list-0.1.js"></script>
<script async custom-template="amp-mustache" src="https://cdn.ampproject.org/v0/amp-mustache-0.2.js"></script>

次に<body></body>内のどこかにタグや設定用のJSONを書く。

AdSense用タグ (広告ユニット用コード)

利用条件: ヘッダでamp-ad-0.1.jsを読むこと
1
2
3
4
5
6
7
8
<amp-ad width="100vw" height=320
  type="adsense"
  data-ad-client="ca-pub-1234567891234567"
  data-ad-slot="1234567890"
  data-auto-format="rspv"
  data-full-width>
    <div overflow></div>
</amp-ad>

広告を貼りたい場所に挿入する。data-ad-clientとdata-ad-slotの値は自分に与えられている値に書き換える。 上の例はGoogleの推奨コードだが必要に応じて変更する。

AdSense用タグ (自動広告用コード)

利用条件: ヘッダでamp-auto-ads-0.1.jsを読むこと
<amp-auto-ads type="adsense" data-ad-client="ca-pub-0000000000000000"></amp-auto-ads>

<body>直下あたりに置けば良い。data-ad-clientは自分に与えられている値に書き換える。

以下2件はアクセス解析用のトラッキングコード。
よく、AMPにしたらPVが減ったなどと言う人がいるが、これはAMPページ(AMPキャッシュ)をGoogleだのの(自分のサーバではない)他所のサーバから閲覧されたときにそのアクセスを知ることができないままにしているから。AMPプラグインには一応analytics用の機能が付いているのでそれを利用するか自分でAMP用テンプレートにハードコーティングするか。一応、よく使われるGoogle Analytics用と、(マイナーな)自前サーバでのアクセス解析としてmatomo用の例を挙げる。

Google Analytics

利用条件: ヘッダでamp-analytics-0.1.jsを読むこと
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<amp-analytics type="googleanalytics" id="analytics1">
<script type="application/json">
{
  "vars": {
    "account": "UA-00000000-0"        ←自分用のUA番号に書き換える
  },
  "triggers": {
    "trackPageview": {
      "on": "visible",
      "request": "pageview"
    }
  }
}
</script>
</amp-analytics>

アクセス解析Matomo (旧Piwik)

利用条件: ヘッダでamp-analytics-0.1.jsを読むこと
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<amp-analytics>
<script type="application/json">
{
    "triggers": {
        "trackPageview": {
            "on": "visible",
            "request": "pageview"
        }
    },
    "requests": {
        "base": "https://matomo.example.com/matomo.php?idsite=1&rec=1&action_name=${title}&url=${sourceUrl}&rand=${random}&apiv=1&urlref=${documentReferrer}&res=${screenWidth}x${screenHeight}&lang=${browserLanguage}&gt_ms=${serverResponseTime}&cs=${documentCharset}&_cvar={\"1\":[\"errorName\",\"${errorName}\"],\"2\":[\"errorMessage\",\"${errorMessage}\"]}",
        "pageview": "${base}"
    }
}
</script>
</amp-analytics>

11行目のURL(ドメイン部分)はmatomoサーバを指定する。以前はpiwik.phpを指定したところも現在はmatomo.phpを指定するようになっている。idsiteの数値はmatomoの計測対象ウェブサイト用のトラッキングコードに含まれるsetSiteIdの値に合わせる。


AMPプラグイン 7
matomoのアクセスログでAMPページにアクセスしたレコードの詳細を見るとカスタム変数としてerrorNameとerrorMessageが表示される。(画像の赤枠部分)これは下から6行目の記述によるもので、\"errorName\"と\"errorMessage\" (${error***}じゃない方)を書き換えるとその書き換えた文字列がmatomoのアクセスレコードの詳細に表示されるが、今の所「がとらぼ」の中の人がmatomoのカスタム変数の使い方と意味がよくわかっていないのでスミマセン。

リスト
WordPressなどのCMSには必ずあるといって良い「最新の投稿○件」という機能。登録された逆順に最新○件をリスト表示するものだが、AMPの場合は他所のサーバにキャッシュされるという特性上、キャッシュされた時点の「最新の投稿○件」が次にキャッシュされるまでそのまま表示され続けることになる。2月1日にキャッシュされたとしたらそのときの最新記事、それが仮に1月15日から1月31日までに登録された10件とすると、4月1日に次にキャッシュされて表示されるようになるまで2ヶ月間はそれがリスト表示され続ける。これだと3月31日に閲覧した人が最新記事を1月31日だと認識し2月1日以降の記事の存在を知ることができない可能性がある。少なくとも「最新の投稿○件」リストだけはリアルタイムで最新のものを取得して表示したい。そこで使うのがamp-list。
最近のWordPressはREST APIが標準で利用可能。https://example.com/wp-json/wp/v2/posts?per_page=○ (example.comは"自分の"WordPressのドメインに読み替え)というURLで最新記事○件のリストを取得できるので、それをAMPページを表示するときにリアルタイムで嵌め込むイメージ。

利用条件: ヘッダでamp-list-0.1.jsとamp-mustache-0.2.jsを読むこと
1
2
3
4
5
6
7
<amp-list width="auto" height="360" layout="fixed-height" src="https://example.com/wp-json/wp/v2/posts?per_page=20" items=".">
    <template type="amp-mustache">
        <li>
            <a href="{{link}}">{{title.rendered}}</a>
        </li>
    </template>
</amp-list>

height(高さ)を指定する場合はper_pageで指定する表示記事数と矛盾しないこと。つまりheightを低い値で指定するとper_pageで大きな値を指定しても表示数が少なくなる。その反対も。

GoogleやCloudflareのようなAMPキャッシュを閲覧される場合、「最近の投稿」のリストはキャッシュでない別オリジンつまり元のサイトから取得されることになるが、当然違うオリジンのコンテンツのミックスということになるのでCORSが絡んでくる。で、下手にウェブサーバでCORS関係の設定をしているとハマることになったのでメモ。WordPress REST APIとCORS

WordPressのウィジェットの扱い

WordPressでは通常ページ用に表示するウィジェットは管理画面で登録・変更・削除できるが、それにより例えばサイドバー用の「ウィジェットのセット」として登録されてしまう。つまり、「検索・最新の投稿・カテゴリ・タグクラウド」のように登録するとそれがウィジェットのセットになってしまうので、例えば上に書いた「最新の投稿」は個別対応したいとか、あるウィジェットはJavascriptを使ってるのでAMPページでは外したいというときに困ることがある。
テンプレートに個別にウィジェットを登録すれば良いので以下。

通常(非AMP)のテンプレートであれば、例えばサイドバー用ウィジェットのセットなら次のように書けばサイドバー用のウィジェットのセットがそこに表示される。
<?php dynamic_sidebar( 'sidebar-1' ); ?>

ウィジェット個別に表示するのであれば、例えばタグクラウドウィジットなら次。
<?php the_widget( 'WP_Widget_Tag_Cloud' ); ?>

これをAMP用テンプレートのサイドバー(になる部分)の中などに書く。

サイドバーとメニュー

通常(非AMP)のテンプレートを真似たサイドバーでも良いが、AMP JSライブラリで最近のスマホアプリのサイドバーやメニュー的な表示も簡単にできるのでそれを利用するというのもあり。

AMP JSライブラリをヘッダ内で読み込む
<script async custom-element="amp-sidebar" src="https://cdn.ampproject.org/v0/amp-sidebar-0.1.js"></script>

さらにbody内に以下を書く

サイドバーが最も単純なリスト(固定)だけという例
1
2
3
4
5
6
7
<amp-sidebar id="sidebar1" layout="nodisplay" side="right">
  <ul>
    <li>Nav item 1</li>
    <li>Nav item 2</li>
    <li>Nav item 3</li>
  </ul>
</amp-sidebar>

もちろん、固定リストの代わりにWordPressの関数なども好きに置ける。

また、amp-sidebarではサイドバーだけでなくナビゲーションメニューなども作れる。ハンバーガーメニューもこれでできる。
WordPressではamp-sidebarと<?php wp_nav_menu( array( 'theme_location' => 'primary' ) ); ?>とスタイルシートを組み合わせて使うことになると思う。
AMPプラグインの新しいバージョンで旧版のテンプレートに存在したナビゲーションメニューが外されたのはこれを使えということかしら?

AMPプラグイン 8
もちろん、通常テーマと同じく<?php wp_nav_menu( array( 'theme_location' => 'primary' ) ); ?>+スタイルシートのようなナビゲーションメニューそのままも可。
赤枠の部分が追加されたナビゲーションメニュー。ただし、まだ対応するスタイルシートを書いていないので通常の<ul>+</li>の見た目のままになっている。

直前にも書いたが、ウィジェットやサイドバー、ナビゲーションメニューはHTMLタグを書いただけではデザインとしては何もないただのリストが表示されるだけな筈なので対応するスタイルシートは別途style.phpに追記のこと。

その他

AMPでは勝手Javascriptなどは書いてはいけないのだが、amp-bindを使えばユーザーの操作に応じて動作を変えるようなことが(少し?)可能になる。何のために勝手Javascriptを禁止にしたんだっけ?何したいのかイマイチ不明。
これは今のところは使いたいと思っていないので全然知らない。

AMPプラグインを使用すると、通常のHTMLタグをAMP-HTMLタグに変換するあたりは自動で、さらにJavascriptなどAMPとしてエラーになるものは(手動対応を含めて)解決されるのでGoogleなどのAMPキャッシュとして登録して貰える。(エラーのあるAMPページはインデックスに登録してもらえない。)
ただし、AMPページとして表示する際に通常ページを表示する際には存在した筈のJavascriptやCSSの一部が取り除かれるので何処か表示に異常があってもおかしくない。エラー回避のために除去されたものが、位置表示を制御するものであれば表示位置が変であったり、表示内容を制御するものであれば正しくない表示或いは表示されないなどが発生する。その制御が行われるのが条件によるものであれば異常が発生したり発生しなかったり。これがAMPプラグインのNative,Pairedモードではちょっと困りもの。
なので、個人的には専用のAMP用テンプレートを自分で如何ようにもできるAMPプラグインのClassicモードが良いと思っている。Classicモードならば、(テーマ部分はもともとAMP用に作っている筈だから)記事の内容だけ不適切なタグなどを使わなければAMPプラグインによる自動変換による挙動(の不審さ)を心配することはなくて済む筈。
今後もClassicモードが存続し続けてくれるか不明で怖いけど。

AMPプラグイン 9
AMPプラグインのClassicモード用のテンプレートファイルに書き足して「がとらぼ」の通常(非AMP)テーマを真似たAMP用テーマを作ってみた。そのAMPページをPCのブラウザで表示してみたのが上の画像。
まだ通常ページと比べると足りてない部分があるけどそれは作り込んでいないだけ。
とにかく、AMPページだからとか、AMPプラグインのClassicモードだからといってデザインが大幅に貧弱になるということはないと書きたかった。

AMP関係のドキュメント

関連記事:
Up