SSH パスワード総当り攻撃を防御する

# はじめに

組織外からアクセス可能な SSH サーバを運用していると、うんざりするほどパスワード総当り攻撃がやってきます。例えば、`/var/log/auth.log` に以下のような記録が残っていれば、root ユーザに対して、パスワード候補を試行し失敗していることが分かります。

```text
2018-10-22T09:11:22+09:00 fs0 sshd[27071]: Failed password for root from 79.8.94.58 port 49538 ssh2
```

適切なパスワードを設定していれば、こういうパスワード総当り攻撃が成功する可能性はかなり低いのですが、それでも可能性がゼロではないのは嫌なものなので、以下のどちらかの防御対策は必ず行っておくようにしましょう。

# パスワード認証によるログインを禁止する

各ユーザの `~/.ssh/authorized_keys` に公開鍵を登録し、その公開鍵によって認証されたユーザのログインのみを許可するようにします。こうすれば、秘密鍵が漏洩しない限りは安全ですので、簡単かつお勧めの設定です。具体的には、`/etc/ssh/sshd_config` に以下のように設定して下さい。

```conf:/etc/ssh/sshd_config
ChallengeResponseAuthentication no
PasswordAuthentication no

Match Address 192.0.2.0/24
    PasswordAuthentication yes
```

ただ、全てのネットワークからパスワード認証を禁止してしまうと、秘密鍵を喪失した場合に回復することが困難になりますので、ごく一部の信用できるネットワークからだけは、パスワード認証を許可しておきます。上記の例では、192.0.2.0/24 に属するホストからはパスワード認証を許可しています。許可する範囲は適宜に変更して下さい。

その上で、sshd を再起動しておきます。

```shell-session
sudo service sshd restart
```

## LDAP サーバ上の公開鍵を参照する

管理対象のホストとユーザが多くなってくると、公開鍵を各ホストに配置する作業が面倒になってきます。その場合は、LDAP サーバに公開鍵を保管しておくことができます。本稿では、LDAP サーバ側の設定は完了しているものとしますので、適当なウェブページを参照して下さい。クライアント側では、まず、以下のようなスクリプトを用意します。

```sh:/usr/lib/openssh/ldap_authkey.sh
#!/bin/sh

ldapurl="ldaps://ldap.example.net/"
basedn="ou=users,dc=example,dc=net"
binddn="cn=ssh,dc=example,dc=net"
bindpw="********"

if [ -n "${1}" ]; then
    /usr/bin/ldapsearch -o ldif-wrap=no -x -D "${binddn}" -w "${bindpw}" -H "${ldapurl}" -b "${basedn}" '(uid='"${1}"')' sshPublicKey | /bin/sed -n 's/^sshPublicKey: //p'
fi
```

その上で、このスクリプトで公開鍵が取得できることを確認しておきます。

```shell-session
$ /usr/lib/openssh/ldap_authkey.sh ユーザ名
ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX alice@example.net
```

正常に公開鍵が取得できることを確認したら、以下のように `/etc/ssh/sshd_config` に設定して、sshd を再起動してください。

```conf:/etc/ssh/sshd_config
AuthorizedKeysCommand /usr/lib/openssh/ldap_authkey.sh
AuthorizedKeysCommandUser nobody
```

なお、Debian では、この種の独自スクリプトは `/usr/local/` 配下に置くことが推奨されています。筆者も、当初は `/usr/local/sbin/` 配下に設置したのですが、以下のようなエラーになりました。

```text:/var/log/auth.log
May 13 20:07:37 example sshd[13175]: error: Unsafe AuthorizedKeysCommand "/usr/local/sbin/ldap_authkey.sh": bad ownership or modes for directory /usr/local/sbin
```

これは `/usr/local/` や `/usr/local/sbin/` が root 以外も書き込めることが原因で起きています。

```shell-session
$ ls -ld /usr/local
drwxrwsr-x 10 root staff 4096  5月 13 12:30 /usr/local/
```

この問題をどのように修正するのが正しいのかを調べる時間がなかったので、`/usr/lib/openssh/` 配下にインストールして誤魔化してあります。正しい方法をご存知の方は教えて頂けないでしょうか。

# パスワード認証の失敗回数を制限する

どうしてもパスワード認証によるログイン回数が禁止できない場合、ある IP アドレスからのパスワード認証が何回か連続で失敗した場合には、当該 IP アドレスをブラックリストに登録して、ログインを禁止するという方法があります。パスワード辞書を用いた総当り攻撃の場合、実際に攻撃が成功するまでに何回か失敗してしまうことが普通ですので、この対策でもかなり確率を下げることができます。

ただし、お勧めは **パスワード認証によるログインを禁止する** 方法です。

この方法を利用する場合は、いくつかの選択肢がありますが、筆者は pam_shield を使っています[^reason]。Debian GNU/Linux の場合は、以下のコマンドで必要ソフトウェアをインストールできます。

[^reason]: パスワード認証の失敗回数を制限するには、pam_shield 以外に SSHGuard や DenyHosts などの幾つかの選択肢があります。これらのソフトウェアは `/var/log/auth.log` に含まれるログイン失敗記録を監視するという実装になっているため、syslog の書式をカスタマイズするとログイン失敗の検出に失敗する可能性がありますし、sshd とは別に監視用のプロセスが動くのも無駄だと思います。それに対して、pam_shield は PAM プラグインの形で sshd に組み込まれて、必要なデータは全て直接に取得されますから、余計な監視用プロセスやログ解析処理は不要となり、効率的です。

```shell-session
sudo apt install libpam-shield
```

設定ファイル `/etc/security/shield.conf` をチェックして、以下の2行に相当する行が存在することを確認しておきます。第1行は、全てのユーザをチェック対象とするという意味で、第2行は、192.0.2.0/24 に属するホストは信用し、そのホストからのパスワード認証失敗はカウントしないという意味です。信用するホストのネットワークは適宜変更して下さい。

```config:/etc/security/shield.conf
block all-users
allow 192.0.2.0/24
```

その上で、sshd を再起動しておきます。

```shell-session
sudo service sshd restart
```

正常に動作しているか確認するには、設定したマシンに ssh で接続して、故意にパスワード認証に失敗してみて下さい。その時、`/var/log/auth.log` に以下のようなログが出現すれば、pam-shield は正常に設定されています。

```text
Oct 22 00:07:01 fs0 PAM-shield[6462]: logging debug info
Oct 22 00:07:01 fs0 PAM-shield[6462]: allowing from 192.0.2.0/255.255.255.0
Oct 22 00:07:01 fs0 PAM-shield[6462]: done reading config file, 0 errors
Oct 22 00:07:01 fs0 PAM-shield[6462]: user admin
Oct 22 00:07:01 fs0 PAM-shield[6462]: remotehost 95.216.161.22
Oct 22 00:07:01 fs0 PAM-shield[6462]: remoteip 95.216.161.22
Oct 22 00:07:01 fs0 PAM-shield[6462]: putting new record in db
```

繰り返し失敗していると、以下のようなログが出現してブラックリストに追加されたことが分かります。

```text
Oct 22 00:12:36 fs0 PAM-shield[6645]: remoteip 82.44.255.66
Oct 22 00:12:36 fs0 PAM-shield[6645]: 10 times from 82.44.255.66
Oct 22 00:12:36 fs0 PAM-shield[6645]: running command 'add 82.44.255.66'
Oct 22 00:12:36 fs0 shield-trigger[6648]: blocking 82.44.255.66
```

## パスワード認証の失敗回数の制限が回避されている可能性

[2022/6/9追記]
筆者の環境では、2022年6月の4日間に744箇所から32166件の攻撃がありましたが、実際にパスワード認証の失敗回数制限により接続が禁止されたIPアドレスは57箇所に留まっていました。

```sh
$ grep remoteip /var/log/auth.log | wc -l
32166
$ grep remoteip /var/log/auth.log | awk '{print $7}' | sort | uniq | wc -l
744
$ grep shield-trigger /var/log/auth.log | grep -v unblocking | wc -l
57
```

以上から、最近の攻撃者は、複数のIPアドレスから分担してパスワード試行することにより、パスワード認証の失敗回数制限を回避している可能性があります。特別な事情が無い限り **パスワード認証によるログインを禁止する** べきと考えられます。