「とりあえずパスワードは8桁以上」で満足していませんか?――PAMによる鉄壁の認証要塞化術
現場でインシデント対応をしていると、いまだに「パスワードの複雑性」をアプリ側のバリデーションだけで制御しているシステムに出くわす。だが、断言しよう。それは「玄関のドアは頑丈だが、壁が段ボールでできている家」と同じだ。
アプリの脆弱性やOSのコマンドインジェクションを突かれた際、攻撃者が最初に行うのは、/etc/shadow のハッシュ値をオフラインでクラックするか、あるいはSSH経由での総当たり攻撃(ブルートフォース)だ。OSレベルで認証を縛り上げていないサーバーは、プロの攻撃者からすれば「ただの通過点」に過ぎない。
今日は、Linuxエンジニアが最低限叩き込んでおくべき PAM (Pluggable Authentication Modules) を使った防御の「急所」を解説する。
—
1. なぜ「アプリのバリデーション」だけでは足りないのか?
例えば、PythonのWebアプリでパスワードポリシーを強制しても、攻撃者はWeb画面を介さず、直接SSHや su コマンドで侵入を試みる。このとき、OS側の認証モジュールであるPAMが正しく設定されていないと、いくらでも試行錯誤ができてしまう。
攻撃者は hydra や medusa といったツールを使い、辞書攻撃で数百万通りのパスワードを1分足らずで試す。この「試行回数」を物理的に制限し、さらに「辞書に載っているような安易なパスワード」をOSレベルで弾く。これが要塞化の第一歩だ。
—
2. 実装:pam_pwquality で「パスワードの知能」を縛る
まずは、ユーザーが「password123」のような無能なパスワードを設定できないようにする。RHEL系やUbuntu系では pam_pwquality が標準的だ。
/etc/security/pwquality.conf を開き、以下の設定を適用してほしい。
# /etc/security/pwquality.conf
# パスワードの最小長
minlen = 14
# 大文字、小文字、数字、記号のうち、含めるべきクラスの最小数
dcredit = -1
ucredit = -1
lcredit = -1
ocredit = -1
# 辞書攻撃対策:過去のパスワードとの共通部分の制限
minclass = 4
# ユーザー名を含めることを禁止
reject_username = 1
# 辞書ファイルを使って、よくある単語を拒否
dictcheck = 1
ポイント: minlen = 14 としているのは、昨今のGPUを用いたパスワードクラッキング速度を考慮した現実的なラインだ。12桁以下は、もはや「鍵がない」のと同義だと思っていい。
—
3. 実装:faillock で「総当たり攻撃」を物理的に遮断する
次に、認証失敗を繰り返すIPやアカウントを一定時間凍結する。かつて使われていた pam_tally2 はすでに非推奨だ。現代のLinuxでは pam_faillock を使うのが正解だ。
設定ファイル /etc/pam.d/common-auth (Debian/Ubuntu系の場合)の冒頭に以下を追記する。
# 3回失敗したら10分間ロックする
auth required pam_faillock.so preauth silent audit deny=3 unlock_time=600
auth [success=1 default=bad] pam_unix.so
auth [default=die] pam_faillock.so authfail audit deny=3 unlock_time=600
auth sufficient pam_faillock.so authsucc audit deny=3 unlock_time=600
現場の知見:
この設定で最も重要なのは silent オプションだ。これを入れないと、攻撃者に「あと何回でロックされるか」という情報を与えてしまう。また、audit を付与することで、不正アクセスの試行が /var/log/auth.log に確実に記録されるようになる。これを監視ツール(Fail2BanやDatadog等)に流し込み、攻撃者のIPをファイアウォール(iptables/nftables)で即座にBANするループを組むのが、プロの運用の定石だ。
—
4. 開発者が知るべき「アプリ側の実装」
インフラ担当がPAMを固めても、アプリ側の登録フォームで脆弱なパスワードを許容していては意味がない。バックエンド(Python/FastAPI)で実装する場合の、正規表現によるバリデーション例を挙げておく。
import re
def is_strong_password(password: str) -> bool:
"""
PAMのポリシーと整合性を取るためのバックエンドバリデーション
"""
if len(password) < 14:
return False
# 大文字、小文字、数字、記号を最低1つずつ含むかチェック
pattern = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{14,}$"
return bool(re.match(pattern, password))
# 開発時のTips:
# パスワードは必ずハッシュ化(bcryptやArgon2)すること。
# 生のパスワードをログに出力するような愚行は、コードレビューで即却下だ。
—
最後に:セキュリティは「多層」で戦う
ここまで設定しても、SSHのポートを22番のまま開けっ放しにしているなら、それはまだ甘い。公開鍵認証への完全移行と、パスワード認証自体の無効化(/etc/ssh/sshd_config で PasswordAuthentication no)をセットで行うこと。
セキュリティとは、単なる設定作業ではない。「攻撃者がどれだけ面倒だと感じるか」という心理戦だ。今回紹介したPAM設定は、その心理戦において、攻撃者のコストを跳ね上げる極めて効率の良い防御策になる。
今日、帰る前にサーバーの auth.log を見てほしい。見慣れないIPが何千回もの試行を繰り返しているなら、それは君のサーバーがすでに狙われている証拠だ。今すぐ設定を見直し、要塞を強固にしてほしい。
コメント