おい、ちょっと手を止めてくれ。インフラやWebアプリの構築、日々の運用ご苦労さん。
俺たちはこれまで数々のインシデント現場を踏んできた。ランサムウェアで身代金を要求された企業、リスト型攻撃で一瞬にして顧客データベースを抜かれたECサイト……その原因のほとんどは、驚くほど「基本の欠落」にあった。
特に、いまだに「とりあえず8文字以上、記号を1つ混ぜる」「90日ごとにパスワードを定期変更させる」なんていう、昭和の遺物のようなパスワードポリシーをドヤ顔で設定している現場を見ると、頭を抱えたくなる。攻撃者からすれば、そんなルールは笑いながら突破できる踏み台にすぎない。
今回は、現代のサイバー攻撃(ブルートフォース、クレデンシャルスタッフィング、辞書攻撃)の現実を踏まえ、IAM(アイデンティティおよびアクセス管理)におけるパスワードポリシーの強化とアカウントロックアウトの実装について、現場のプロとして本質的な話をしよう。
—
1. 攻撃者が笑う「脆弱なポリシー」と現実の脅威
NIST(アメリカ国立標準技術研究所)のガイドライン改定から何年も経っているというのに、いまだに「定期的なパスワード変更」を強要するシステムが多い。
定期変更を義務付けると何が起きるか? ユーザーは「P@ssword2023!」「P@ssword2024!」のように、末尾の数字や記号を変えるだけの予測可能なインクリメントを行うか、メモ帳や付箋にパスワードを書き残すようになる。結果として、セキュリティは確実に低下する。
さらに、攻撃者は人間が想像するような文字列なんて使わない。数千万件単位の「過去に流出したパスワードリスト(辞書)」を保持しており、API経由で秒間何千回ものリクエストを送り込んでくる(クレデンシャルスタッフィング)。
ここで重要になるのは、文字数の強制と「辞書に載っている既知の脆弱なパスワードの排除」、そして「機械的なブルートフォースを防ぐアカウントロックアウト」の3点だ。
—
2. インフラ・OSレベルでの設定(Linux / PAMモジュール)
まずは、Linuxサーバーのコンソールを叩くインフラエンジニア向けの話だ。
サーバーへのSSHログインや、OS上のローカルアカウントのパスワードポリシーは、Pluggable Authentication Modules (PAM) の pam_pwquality と pam_faillock を使って鉄壁に固める。
/etc/security/pwquality.conf を開き、以下のように設定してくれ。これが現場の最低ラインだ。
# /etc/security/pwquality.conf の設定例
# 最低文字数は12文字以上(できれば16文字以上を推奨)
minlen = 12
# 英大文字、英小文字、数字、記号の4種類のうち、最低3種類を含める
minclass = 3
# 連続する同じ文字の最大数(例: "aaa" のような入力を防ぐ)
maxrepeat = 3
# ユーザー名を含む文字列をパスワードに使用することを禁止
dictcheck = 1
次に、ブルートフォース攻撃を完全に無力化するためのアカウントロックアウト設定だ。/etc/security/faillock.conf(またはシステムによっては /etc/pam.d/system-auth)を調整する。
# /etc/security/faillock.conf の設定例
# 連続で3回認証に失敗したらアカウントをロックする
deny = 3
# 失敗をカウントする猶予時間(この秒数以内に連続失敗するとカウント)
fail_interval = 900
# アカウントがロックされる時間(1800秒 = 30分間ロック)
unlock_time = 1800
# rootアカウントもロックの対象にする(※極めて重要。踏み台にされるのを防ぐ)
even_deny_root
この設定を入れておけば、スクリプトを使った総当たり攻撃は数回で完全に遮断される。攻撃者はログの海に沈むことになる。
—
3. Webアプリ・クラウドIAMにおける実装とコード例
次は、自社開発のWebアプリケーションや、クラウド(AWS IAMや独自認証基盤)でパスワードバリデーションを実装する際のコードだ。
単に「文字数が足りているか」を見るだけではなく、「過去に漏洩した既知のパスワードリストに含まれていないか」をチェックするのが現代のWebセキュリティの常識だ。
以下に、Python(Flask / FastAPIなど)を想定した、セキュアなパスワードバリデーションとアカウントロックアウト(Redis使用)のロジックを示す。
import re
import redis
from passlib.context import CryptContext
# パスワードハッシュ化の設定(Argon2idを使用。bcryptより強固)
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
# 失敗試行回数を管理するRedisクライアント
r = redis.Redis(host='localhost', port=6379, db=0)
# 既知の脆弱なパスワードのブラックリスト(実運用ではHaveIBeenPwned APIやローカルDBを使用)
WEAK_PASSWORDS = {"password123", "admin12345", "P@ssw0rd", "Letmein123!"}
def validate_password_policy(password: str) -> tuple[bool, str]:
"""
パスワードポリシーの検証を行う
"""
if len(password) < 12:
return False, "パスワードは12文字以上である必要があります。"
if not re.search(r"[A-Z]", password):
return False, "英大文字を少なくとも1文字含めてください。"
if not re.search(r"[a-z]", password):
return False, "英小文字を少なくとも1文字含めてください。"
if not re.search(r"\d", password):
return False, "数字を少なくとも1文字含めてください。"
if not re.search(r"[ !@#$%^&*(),.?\":{}|<>]", password):
return False, "記号を少なくとも1文字含めてください。"
if password in WEAK_PASSWORDS:
return False, "このパスワードは推測されやすいため使用できません。"
return True, "OK"
def check_account_lockout(username: str) -> bool:
"""
アカウントがロックされているかチェックする
"""
lock_key = f"lockout:{username}"
if r.exists(lock_key):
return True # ロック中
return False
def record_login_failure(username: str):
"""
ログイン失敗回数を記録し、閾値を超えたらロックする
"""
fail_key = f"fail_count:{username}"
lock_key = f"lockout:{username}"
# 失敗回数をインクリメント(有効期限は15分)
failures = r.incr(fail_key)
if failures == 1:
r.expire(fail_key, 900)
# 5回失敗で30分間ロック
if failures >= 5:
r.setex(lock_key, 1800, "locked")
r.delete(fail_key) # カウントはリセット
このコードでは、ハッシュアルゴリズムに古臭い MD5 や SHA-256 ではなく、GPUによる総当たりに強い Argon2id を採用している。また、Redisを使ってログイン失敗回数を厳格にトラッキングし、DDoSやブルートフォースの踏み台になるのを防いでいる。
—
4. チーフからの教訓:運用で絶対に忘れてはならないこと
設定ファイルを書き換え、コードをデプロイしたら終わり、ではない。ここからがセキュリティエンジニアの腕の見せ所だ。
1. エラーメッセージで親切心を出さない
ログイン失敗時に「ユーザーが存在しません」や「パスワードが間違っています」と親切に分かせてはいけない。攻撃者にアカウントの存在を教えることになる。「メールアドレスまたはパスワードが正しくありません」と必ず統一すること。
2. 多要素認証(MFA / 2FA)の強制
どれほど強固なパスワードポリシーを設定しても、フィッシングやキーロガーの前には無力化されることがある。IAM設計の究極のゴールは「パスワードに依存しない認証(FIDO2 / WebAuthn、あるいは強固なMFAの義務化)」だ。パスワードはあくまで「最後の砦の、そのまた補助」くらいに捉えるのが、現代のゼロトラストアーキテクチャの思想である。
セキュリティは「面倒くさい」と切り捨てられた瞬間から綻びが生じる。
後輩のエンジニアたちよ、自分が書いたコード、自分が構築したインフラが、明日深夜のインシデントアラートを引き起こさないよう、今日紹介した基準を確実に実装に落とし込んでくれ。頼んだぞ。
コメント