はじめに
これはConoHa Advent Calendar 2025 18日目の記事です。
今年、私が関わっているプロジェクトではさまざまな部署や所属のメンバーが課題を共有するためのRedmineをインターネット上に立ち上げないかという話が出たことがありました。となると、どうやってセキュリティを確保するのかという心配をしなければなりませんが、mod_authn_otpというモジュールを使用すればApacheレベルでワンタイムパスワード(OTP)認証をすることができそうだということがわかり、やるならその方向性かなあとぼんやりと思いながら構えていました。
結局それはなかったことになったのですが、いつ同じような話が出てくるかわかりません。備えあれば憂いなし、この機会にその方法について学んでおこうと思います。
というわけで本記事では、ConoHa VPS上に架空の超巨大インターネットカンパニー 美雲ホールディングス株式会社の経営管理システムを構築するというシチュエーションでモックアップを設置し、その経営陣のみアクセスできるようワンタイムパスワード認証を設定する、という演習を行ないます。そして、この演習を通してmod_authn_otpの使い方について学びます(私が)。
モックアップを作る
この演習を始めるにあたってまず必要なものはなにか? モックアップを設置すると決めたのだからそれが必要です。というわけで作ります。GUIツールキットにはFlutterを採用しました。もっとウェッブケーっぽいやつを使えやという声も聞こえてきそうですが、私がデスクトップ畑の人間なのでしょうがありません。
というわけで勢いよくガシガシと作りました。ページは2つ、ログインページとホームページです。ログインページは以下のようになりました。
中央にユーザーとパスワードのテキストフィールド、それにログインボタンがあるというおやくそくのやつです。美雲ホールディングスのCEO cmikumoさんの情報が入力された状態でログインボタンが押されるとホームページに遷移するようハードコードされています。
ホームページは以下のようになりました。
左ペインには経営管理システムってこんな感じなのかなーと想像で作ったリストタイルが並んでおり、タップするとなんか中二っぽい数値をランダムに生成し、ダイアログで表示して超巨大インターネットカンパニーっぷりを誇示するようにハードコードされています。
そして右ペインには清楚かわいい美雲このはちゃん(CEOのcmikumoさんとの関係性は不明)がランダムに表示され、クリックするたびランダムに更新されるようになっています。超巨大インターネットカンパニーなのですから、このくらいの遊び心は当然要求されてくるのだろうと認識しています。
動作確認フェーズはWindowsとかのデスクトップ向けビルドでさくさくやってしまってよいでしょう。なんとなく動いて自己満足できたらウェブ向けにビルドします。のちほどサーバーに持っていくことを考慮してアーカイブ化し、準備完了です。
$ flutter build web
$ cd build
$ tar -zcvf web.tar.gz web
サーバーを立てる
では、下ごしらえができたところで実際にサーバーを立てていきましょう。今回はVPS 1GB Ubuntu 24.04でサーバーを作成します。今のConoHaにはセキュリティグループの概念があり、使いたいプロトコルが通るように設定してあげないといけませんが、この設定はサーバー作成のタイミングでできることに今回気づいたので、ここでやってしまうのがよいでしょう。
そういえば、長期割引のサービス名がまとめトクという名前になっているようですね。3ヶ月分契約して本記事の公開後もしばらくはアクセスできるように、しかし期限が来たら自動的に削除されるようにします。
例によってsudoができる一般ユーザーを作り、HTTP、HTTPS、SSH、Moshが通るようファイアーウォールを開け、公開鍵認証でSSHが使えるように設定します。私はスマートフォンから接続することが多いのでMoshも必須ですが、そうでない人はそのへんは無視してください。
すみませんこのあたりは早足でいきますので、不明なことがありましたらぐぐったりLLMったりしてうまく切り抜けてください... というわけで、SSHなりMoshなりで接続できるようになりましたか? できたらオッケーです。
DNSの設定
このあとLet's EncryptでTLS証明書を取得してHTTPSサーバーを設定していきますが、それには独自ドメインでの運用が必須となります。私は個人サイト用に持っているドメインがあるので、それにサブドメインを付けてmikumohd.gonypage.jpとしてアクセスできるようにDNSを設定しました。
超巨大インターネットカンパニーのはずなのに弱小個人サイトのサブドメインなのはいいの? という疑問もありますが、まあ細かいことは気にしてはいけません...
Apacheの設定
では前半戦の山場、Apacheの設定をやっていきましょう。まずはインストールします。
$ sudo apt install apache2
ウェブブラウザーからHTTPでサーバーにアクセスして、Apache2 Default Pageが見えることを確認します。
続いてCertbotを設定してLet's Encryptから証明書をもらいます。最近のUbuntuではaptでインストールできるようでいい時代になりましたねえ。
$ sudo apt install certbot
$ sudo apt install python3-certbot-apache
$ sudo certbot --apache
sudo certbot --apacheを実行するとメールアドレスの入力、規約への同意、Electronic Frontier Foundationへのメールアドレス共有有無の返答、ドメイン名の入力を求められます。それらを済ませると自動的に設定してくれます。
Successfully deployed certificate for mikumohd.gonypage.jp to /etc/apache2/sites-available/000-default-le-ssl.conf
Congratulations! You have successfully enabled HTTPS on https://mikumohd.gonypage.jp
念のためApacheを再起動します。
$ sudo systemctl restart apache2.service
今度はHTTPSでサーバーにアクセスしてみましょう。HTTPSでもApache2 Default Pageが見えたらオッケーです。証明書の更新も自動でやってくれるよう設定済みのようです。
$ systemctl status certbot.timer
● certbot.timer - Run certbot twice daily
Loaded: loaded (/usr/lib/systemd/system/certbot.timer; enabled; preset: enabled)
Active: active (waiting) since Sun 2025-11-16 16:00:53 JST; 5 days ago
Trigger: Sat 2025-11-22 20:31:13 JST; 10h left
Triggers: ● certbot.service
いやあ世の中便利になった。
さてこれでHTTPSの設定が完了しましたが、今回は超巨大インターネットカンパニーの経営管理システムというシチュエーションでの演習であるため、HTTPではアクセスできないようにしてしまいます。/etc/apache2/sites-enabled/000-default.confを削除してこのステップはおしまいです。
モックアップの設置
現時点ではApache2 Default Pageが/var/www/htmlに置いてあり、ウェブサーバーにアクセスした際には当然それが見えています。これをモックアップに差し替えて前半戦の仕上げとしましょう。上で作ったモックアップのアーカイブをscpでサーバーのホームディレクトリにコピーしてから差し替えていきます。
$ sudo mv html html-orig
$ sudo cp ~/web.tar.gz .
$ sudo tar -zxvf web.tar.gz
$ sudo mv web html
もう一度ウェブブラウザーでサーバーにアクセスし、モックアップが動くことを確認しましょう。動きましたね! よかったよかった。
これで前半戦は終わりです。お疲れさまでした! お好きな飲み物とおやつでブレイクでもいかがですか?
TOTPについて予習する
さてここから後半戦ですが、まずはウォーミングアップを。これから設定していくTOTPについて、それに必要な程度のあっさり味な予習をしておきましょう。
TOTPとはTime-based One-Time Passwordの略で、ワンタイムパスワードのうち特に時間情報を使用するものです。その仕組みについてですが、まず事前にサーバーとクライアントでシークレットを共有しておきます。RFC 6238ではあるシークレットと時間からワンタイムパスワードを生成する方法が定義されており、サーバーとクライアントの両者がそれに準拠しており、シークレットと時間が同一であれば、ワンタイムパスワードが一致して認証が成功するはず、とだいたいこんな感じです。ワンタイムパスワードは有効期間が短いため、万が一漏洩してもリスクが低いとされています(ゼロではありません)。シークレットの長さとしてはRFC 4226で160ビットが推奨されています。
本記事ではこのシークレットをPythonのインタラクティブモードで生成します。secretsモジュールのtoken_bytes()関数で任意の長さで暗号学的に安全な乱数のバイト列を生成でき、1バイトは8ビットなので、20バイト分生成すれば160ビットのシークレットが得られるということになります。
得られたシークレットはbytes型で、hex()メソッドで16進数表記、base64モジュールのb32encode()関数でBase32表現にそれぞれ変換できます。後述しますが16進数表記はサーバー側の設定時に、Base32表現はクライアントへの通知時に使います。以下にその例を示します。
$ python3
Python 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import secrets
>>> s = secrets.token_bytes(20)
>>> s
b's\rP\xbc\xd6.\x83h\xe3\n\xc4\xab\x93\xc5\xdc\xba\x90\xe6\xec\x85'
>>> s.hex()
'730d50bcd62e8368e30ac4ab93c5dcba90e6ec85'
>>> import base64
>>> base64.b32encode(s)
b'OMGVBPGWF2BWRYYKYSVZHRO4XKION3EF'
mod_authn_otpの導入
ではApacheにmod_authn_otpを導入して設定していきましょう。しかし、ここで出鼻をくじくようですがUbuntuにmod_authn_otpのパッケージはないようです... なので野良ビルドをやっていきます。やってやるぜ!
まずは開発環境をインストールします。
$ sudo apt install gcc build-essential automake apache2-dev apche2-ssl-dev pkgconf
ソースコードを取ってきます。
$ jj git clone https://github.com/archiecobbs/mod-authn-otp.git
ビルドとインストールをします。
$ cd mod-authn-otp
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install
/etc/apache2/apache2.confに以下のように追加し、モジュールをロードします。
LoadModule authn_otp_module /usr/lib/apache2/modules/mod_authn_otp.so
設定を確認し問題なさそうならApacheを再起動します。
$ sudo apache2ctl configtest
$ sudo systemctl restart apache2.service
無事入ってくれたように見えますね。では、次のステップで設定していきましょう。
mod_authn_otpの設定
次に、mod_authn_otpの設定をしていきましょう。まずはApacheから。/etc/apache2/sites-enabled/000-default-ls-ssl.confに以下のように追加します。
<Location />
AuthType basic
AuthName "Enter the password to view the information provided by Mikumo Holdings, Inc."
AuthBasicProvider OTP
Require valid-user
OTPAuthUsersFile /var/www/otp/users
OTPAuthLogoutOnIPChange On
</Location>
ロケーション/以下でOTPによるBasic認証を有効にします。有効なユーザーすべてのアクセスを許可、ユーザーファイルは/var/www/otp/usersにあり、IPアドレスが変わった場合はログアウトします。
続いて/var/www/otp/usersにユーザーファイルを作成します。一般的なTOTP(30秒間隔、6桁)、ユーザー名はcmikumo、PINはなし、そしてシークレットの16進表記とスペース区切りで記入します。
HOTP/T30 cmikumo - 730d50bcd62e8368e30ac4ab93c5dcba90e6ec85
ここでハマりポイントです。Apacheはユーザーファイルと同じ場所にロックファイルを作成します。つまり、この場合でいうとotpディレクトリはwww-dataユーザーが書き込めるよう設定されていないといけません。また、ユーザーファイルもApacheが変更するようなので、それもwww-dataユーザーが書き込めるようにしておいたほうがよいでしょう。
ここまでできたら設定を確認し、問題なさそうならApacheを再起動します。
$ sudo apache2ctl configtest
$ sudo systemctl restart apache2.service
続いてクライアント側でワンタイムパスワードを生成できるようにします。そのためにはotpauth URIというものを作る必要があります。今回は以下のようになります。
otpauth://totp/MikumoHD:cmikumo?secret=OMGVBPGWF2BWRYYKYSVZHRO4XKION3EF&?issuer=MikumoHD
分解するとotpauthスキーマではじまり、totp、発行者とユーザー名を組み合わせたラベル、シークレットのBase32表現、発行者となります。
そしてこの文字列をQRコード化すると、よく見かけるワンタイムパスワード生成用QRコードが出来上がります。それでは、QRコードもPythonのインタラクティブモードで作ってみましょう。まずはPyQRCodeモジュールをインストールします。
$ sudo apt install python3-pyqrcode
続いてこのモジュールをインタラクティブモードで叩いてみます。
$ python3
Python 3.12.3 (main, Nov 6 2025, 13:44:16) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyqrcode
>>> c = pyqrcode.create("otpauth://totp/MikumoHD:cmikumo?secret=OMGVBPGWF2BWRYYKYSVZHRO4XKION3EF&?issuer=MikumoHD")
>>> c.png("cmikumo.png", scale=8)
これで以下のようなQRコードができました。
このQRコードをGoogle Authenticator (Android、iOS)などのワンタイムパスワード生成アプリに読み取らせてみましょう。ラベル MikumoHD: cmikumoのワンタイムパスワードが生成されていれば成功です(スクリーンショットを貼ろうと思ったけどプロテクトされているみたいだったので断念)。
ではいよいよ、ウェブサーバーにアクセスしてみます。ウェブブラウザーにパスワードを要求されたら、ユーザーにcmikumo、パスワードにアプリが生成したワンタイムパスワードを入力してモックアップが表示されるか確認しましょう。表示されましたか? されていればこのステップも終わりです。
ModSecurityの設定
ここまででmod_authn_otpの設定としては完了しています。しかし、超巨大インターネットカンパニー 美雲ホールディングスの経営管理システムとして見た場合、総当り攻撃への対策も必要になってくるのではないでしょうか。そこでModSecurityを導入し、認証失敗が連続したIPアドレスをブロックすることで企業秘密を守りましょう。
幸いなことにModSecurityのほうはaptで入ります。出力を見るとインストール後に自動的に有効になっていたようで、a2enmodは不要だったかもしれません。
$ sudo apt install libapache2-mod-security2
$ sudo a2enmod security2
設定ファイルは/etc/modsecurityにあります。modsecurity.conf-recommendedというファイルがあるので、それをリネームし、たたき台にして編集していきましょう。
$ sudo mv modsecurity.conf-recommended modsecurity.conf
SecRuleEngineディレクティブはModSecurityの動作を設定します。初期値はDetectionOnlyで検出してログ出力だけするようですが、Onに変更して実際に動作させます。
SecRuleEngine On
続いてModSecurityが入出力に使用するディレクトリです。/var/www/以下にディレクトリを作成してそこを指定します。このディレクトリは当然www-dataユーザーが読み書きできるように設定されている必要があります。
SecTmpDir /var/www/modsecurity
SecDataDir /var/www/modsecurity
ここでまたハマりポイント。これらはデフォルトでは/tmpに設定されており普通に考えればそのままで動きそうなものなんですが、どうがんばってもファイル出力がうまく行えないようでした。結局原因はわからなかったのでモヤモヤは残りますが、とりあえず私の場合は/var/www以下に変更することで対策できました...
ではいよいよ、連続で認証に失敗したIPアドレスをブロックするルールを書いていきましょう。今回は以下のようになりました。
SecAction \
"id:10001, \
phase:1, \
pass, \
nolog, \
initcol:ip=%{REMOTE_ADDR}"
SecRule IP:BLOCKED "@eq 1" \
"id:10002, \
phase:1, \
drop, \
log, \
msg:'This IP address is blocked due to consecutive authentication failures'"
SecRule RESPONSE_STATUS "@streq 401" \
"id:10003, \
phase:5, \
pass, \
nolog, \
setvar:ip.failed=+1, \
expirevar:ip.failed=300"
SecRule IP:FAILED "@ge 10" \
"id:10004, \
phase:5 \
pass, \
log, \
setvar:ip.blocked=1, \
expirevar:ip.blocked=600"
順次解説します。最初のSecActionディレクティブではリモートアドレスごとに変数を確保しています。ここに失敗回数やブロック中などの情報を保持していく感じですね。
次のSecRuleディレクティブでは変数 ip.blockedが1であるか調べ、その場合はパケットをドロップしてログに記録します。
その次のSecRuleディレクティブではレスポンスステータスが401であるか調べ、その場合は変数 ip.failedを1加算します。なおこの変数は300秒で失効します。
最後のSecRuleディレクティブでは変数 ip.failedが10以上であるかをしらべ、その場合は変数 ip.blockedを1にします。なおこの変数は600秒で失効します。
これらのルールの組み合わせにより、5分間に10回401 Unauthorizedを発生させたIPアドレスは10分間ブロックされます。なお、認証中にウェブブラウザーがfaviconを取りに行ったりするのも401を発生させるので、実際の認証試行可能回数は10回より少なくなると思われます。10分というブロック時間は演習用のもので、実際の業務に応用する場合は数時間というオーダーにしておいたほうがよいかもしれません。
ここでまた設定を確認し、問題なさそうならApacheを再起動しましょう。
$ sudo apache2ctl configtest
$ sudo systemctl restart apache2.service
これで設定はすべて完了です。がんばったえらい!
動作確認
では最後に通しでの動作確認をしてみましょう。本記事で演習として作成したモックアップは実際に公開されており、ConoHa VPSの長期割引が終了して自動削除されるまではどなたでもアクセスすることができます。
上でワンタイムパスワード生成の準備をしていない方は、Google Authenricatorなどのワンタイムパスワード生成アプリに以下のQRコード(上と同じものです)を読み取らせ、ワンタイムパスワードを作れるようにしてください。
その後、以下のURLからアクセスしてください。
ログイン情報は以下の通りです。
- ワンタイム認証ユーザー名: cmikumo
- ワンタイムパスワード: (アプリが生成したもの)
- モックアップログイン時ユーザー名: cmikumo
- モックアップログイン時パスワード: iuK7mohsh1
モックアップが動作していることを確認できましたでしょうか? たぶん動いていると思います。動いてなかったらどうしよう...
おわりに
大変長くなってしまいましたが、ようやく締めの言葉です。
本記事では架空の超巨大インターネットカンパニー 美雲ホールディングス株式会社の経営管理システムというシチュエーションでモックアップを設置し、それにワンタイムパスワードを設定することでmod_authn_otpの使い方を学びました。その環境にはConoHa VPSを使用することで実際の動作をみなさんに確認していただける状態になっており、また契約期間終了後には自動削除されるということも実現できているはずです。
今回の反省としてはちょっと内容を詰め込みすぎたなあというのがありますね... こんなことをやろう、きっと面白いぞと思いついたのはいいのですが、それがどれくらいの文章量になるかの見積もりが甘かった。次回はもっとおきがるおてがるに読んでもらえるものを目指したいと思います。
ともあれ、最後まで読んでいただき、本当にありがとうございました!


