Amazon Linux 2 で SSH を保護する

# 概要

Amazon Linux 2 で (比較的) 安全に SSH を利用するための方法例です。
以下の 3 パターンについて記述します。

- SSH 利用者が固定 IP アドレスを持っている。
    - → A. セキュリティグループに SSH 利用者の IP アドレスを設定しておく。
- SSH 利用者が固定 IP アドレスを持っていない。
    - → B. SSH を利用するときだけセキュリティグループを更新する。
    - → C. iptables+fail2ban で SSH への接続を制限する。

安全性は A >> B >> C >> (何もしない) です。
出来る限り固定 IP アドレスを利用しましょう。

# 前提 1. SSH の基本設定

SSH は

- プロトコルは SSH-2 のみ (そもそも、もう SSH-1 は設定できなくなりました)
- root ではログインできないようにしておく (Amazon Linux 2 ではこれがデフォルト)
- パスワードによるログインもできないようにしておく (同上)

です。
デフォルトから変えてなければ、このあたりは問題ありません。

# 前提 2. SSH のポートを変更する。

全てのパターンにおいて、 SSH のポートは変更しておきましょう。
これだけで攻撃を受ける回数が極端に減ります。

- ネット上では 2222 や 10022 に変更する例が散見されますが、それらのポートも攻撃されるので、 49152～65535 (動的・私用ポート) の範囲でランダムに決めるのがいいと思います。
    - 参考) [AWS EC2へのSSHに対する攻撃を分析してみた (2015年4月〜6月)  | ある研究者の手記](http://m-mizutani.github.io/blog/2015/10/24/aws-ec2-ssh/)
        - 222, 2222, 22222 8022 22000 10022 など 22 以外も攻撃を受けています。

今回は例として 51921 ポートに変更します。 

```bash
# /etc/ssh/sshd_config から Port の設定を削除する。
sudo sed -ie '/^Port /d' /etc/ssh/sshd_config

# そして追記。
sudo bash -c 'echo Port 51921 >> /etc/ssh/sshd_config'

# SSH (sshd) の再起動。
sudo systemctl restart sshd
```

51921 ポートに接続できるよう、セキュリティグループを更新するのを忘れずに。

# A. セキュリティグループに SSH 利用者の IP アドレスを設定しておく。

SSH 利用者が固定 IP アドレスを持っている場合、この方法が最も簡単で安全です。(絶対に安全とは言ってない)
それ以上書くことはないので、 IP アドレスを固定する方法をいくつか列挙します。

- [インターリンク マイIP](https://www.interlink.or.jp/service/myip/) を利用する。
    - もっともシンプルで簡単です。
    - ソフトイーサ版 (月額 1,296 円) をお勧めします。
        - 通常版のほうは (私の環境だけかもしれませんが) 頻繁に接続できない状態になりました。
    - 1 IP アドレスごとに 1 端末でしか利用できないため、利用者の数だけ用意することになります。
- (既に固定 IP アドレスのインターネット回線を契約済みの場合) ルータでリモートアクセス VPN を設定する。
    - 外部からそのルータにログインし、ネットワークを利用できる仕組みです。
    - ルータによって設定方法が異なります。
        - 法人向けのルータであれば、だいたいはできると思います。
        - 例) [ゼロから始める! ヤマハルータでつくるVPN(7) VPNルータ「RTX1210」でリモートアクセスVPNを設定する方法 | マイナビニュース](https://news.mynavi.jp/article/vpn_yamaha-7/)
    - 1 IP アドレスを複数人で同時に利用できます。
    - セキュリティ的な設定が別途必要です。
        - 1 アカウントごとに 1 端末でしか利用できないようにする、一定時間経過で自動的にログアウトするようにする、 LAN にアクセスできないようにするなど。
        - EC2 のセキュリティを確保するためにルータのセキュリティを確保する……という話になります。
- EC2 などのサーバに VPN サーバを立てる。
    - 例) [AWSにSoftEther VPNServerで簡単にVPN接続しよう ｜ Developers.IO](https://dev.classmethod.jp/cloud/aws/rk-2018-02-12-softethervpn/)
    - ルータでやる場合と同様、セキュリティ的な設定をする必要があります。
    - (EC2 でやるのであれば) 当然 EC2 の料金 (特にネットワーク従量料金) が発生するので、必要なときだけ VPN を利用するようにすることが重要です。
        - VPN 利用したままネットで映画を見たりすると、ネットワーク料金が酷いことになります。
        - ネットワーク料金が固定である場合が多い国内クラウド系のサーバならいいかもしれません。

手間を惜しまないのであれば、ルータによるリモートアクセス VPN の利用をお勧めします。
マイ IP は簡単ですが、そのマイ IP を使う人がいなくなる等 IP アドレスに変更が必要になった場合、その IP アドレスが設定されているセキュリティグループを全て修正しなければならないためです（＝見落とし、漏れが発生しやすい）。
CloudFormation や Terraform で機械的に管理していれば楽かもしれません。

# B. SSH を利用するときだけセキュリティグループを更新する。

SSH を利用する前に何らかの方法でセキュリティグループに自分の IP アドレスを追加し、利用が終わったら削除する、という方法です。
手動でこれをやるのはお勧めできませんが、ある程度自動化する方法がいくつかあります。

- [[AWS][CLI] EC2にSSHするときだけ、security groupに自分のアドレスを追加する ｜ Developers.IO](https://dev.classmethod.jp/etc/ssh_with_adding_ingress_rule/)
     - ssh コマンドをラップし、その前後でセキュリティグループに自分の IP アドレスを追加/削除するようにする方法。
     - 仕組み上、利用者にセキュリティグループの編集権限を与える必要がある。
     - 異常終了などの理由でセキュリティグループに IP アドレスが残ってしまうケースがあるため、定期的にセキュリティグループをクリアする処理が別途必要。
         - 定期実行される Lambda を作成するのがシンプル。
- [API GatewayからEC2のSecurity Groupを更新する - Qiita](https://qiita.com/sumikawa@github/items/996bd59996be293c6afa)
     - アクセスすると、アクセス元の IP アドレスをセキュリティグループに追加する URL を API Gateway + Lambda で作成する。
     - やはり定期的にセキュリティグループをクリアする必要がある。
     - セキュリティグループを管理する超簡易的な WEB サービスを作る、みたいな話。
         - どこまで手間を掛けるか次第。
         - ユーザ管理機能を持たせて、ユーザごとにどのセキュリティグループに追加するかなどを統一的に管理できると便利そう。

# C. iptables+fail2ban で SSH への接続を制限する。

Amazon Linux 2 の SSH 関連の設定で、以下の設定を実施し、安全性を確保します。

- セキュリティグループでは、 SSH のポートにアクセスできるのは IPv4 のみ (0.0.0.0/0) にしておく。(IPv6 は許可しない)
    - せめて設定量を減らしたい……。
    - そもそも EC2 を IPv6 でアクセスできるように作成していることも殆どないかと思いますが。
- iptables と fail2ban を導入し、攻撃と思われるアクセスの IP アドレスを自動でブロックするようにする。
- (可能なら) iptables で特定の国の IP アドレスだけを許可するようにする。

なお、この設定は、***マシンのパフォーマンスに影響を与えます***。(特に iptables が)
AWS でやるならば、可能な限り固定 IP アドレスとセキュリティグループを利用しましょう。

## 1. iptables ipset fail2ban をインストール、設定する。

- **iptables** は、マシン内で発生する通信に対して、条件に応じて通信を許可/不許可したり、別のところへ転送するツールです。
    - この IP アドレスからの接続は許可、この IP アドレスからの接続は不許可、といったようなことができます。
- **ipset** は、 iptables の補助ツールです。沢山の IP アドレスを効率的に管理できます。
- **fail2ban** は、条件に応じて自動的にアクセス拒否を行うツールです。

```bash
# fail2ban は Amazon Linux 2 標準のリポジトリにはないので、 EPEL リポジトリを追加する。
sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm

# インストール。
# 結果の通知に使うため、 whois コマンド (jwhois@epel) もインストールする。
sudo yum -y install iptables-services ipset-service fail2ban-server fail2ban-sendmail jwhois
```

## 2. ipset を設定、起動する。

```bash
# ipset 終了時、 ipset の状態を保存するようにしておく。
sudo sed -ie '/^IPSET_SAVE_ON_STOP=/d' /etc/sysconfig/ipset-config
sudo bash -c 'echo IPSET_SAVE_ON_STOP=yes >> /etc/sysconfig/ipset-config'

# 起動する。
sudo systemctl start ipset

# 自動起動を設定する。
sudo systemctl enable ipset
```

## 2. iptables を設定する。

```bash
cat << 'EOT' | sudo sh -c 'cat - > /etc/sysconfig/iptables'
*filter
# 受信: 基本的に不許可。
:INPUT DROP [0:0]
# フォワーディング: 基本的に不許可。
:FORWARD DROP [0:0]
# 送信: 基本的に許可。
:OUTPUT ACCEPT [0:0]
# SSH が利用するチェーン。
:INPUT_SSHD - [0:0]
# fail2ban が利用するチェーン。
:FW_F2B_SSHD - [0:0]
# 特定の国だけを許可するチェーン。
:FW_COUNTRY_SSHD - [0:0]

# データを持たないパケットの接続を破棄する。
-A INPUT -p tcp --tcp-flags ALL NONE -j DROP

# SYN flood 攻撃と思われる接続を破棄する。
-A INPUT -p tcp ! --syn -m state --state NEW -j DROP

# ステルススキャンと思われる接続を破棄する。
-A INPUT -p tcp --tcp-flags ALL ALL -j DROP

# 既に確立している接続に関するものは許可。
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# 自身の通信は全て許可。
-A INPUT -i lo -j ACCEPT

# multicast を破棄。
-A INPUT -s 224.0.0.0/4 -j DROP

# broadcast を破棄。
-A INPUT -d 255.255.255.255 -j DROP

# ポート 51921 (SSH) への新規のアクセスは INPUT_SSHD チェーンへ移動。
-A INPUT -p tcp -m state --syn --state NEW --dport 51921 -j INPUT_SSHD

# 必要ならば、 http (80) https (443) なども開放する。
#-A INPUT -p tcp --dport 80 -j ACCEPT
#-A INPUT -p tcp --dport 443 -j ACCEPT

# 上記にマッチしないものを全て拒否。
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -j REJECT --reject-with icmp-proto-unreachable

### INPUT_SSHD の定義。

# fail2ban のルールはこのタイミングで評価する。
# (国より前、 hashlimit より前)
-A INPUT_SSHD -j FW_F2B_SSHD

# 特定の国だけを許可するルールはこのタイミングで評価する。
# (fail2ban より後、 hashlimit より前)
-A INPUT_SSHD -j FW_COUNTRY_SSHD

# ブルートフォース攻撃対策。 (hashlimit)。
# 説明が非常に難しいですが、だいたい
# - 同一 IP アドレスから (--hashlimit-mode srcip)
# - 2 分間以上の間隔を置かず (--hashlimit-htable-expire 120000)
# - 5 回 (--hashlimit-burst 5)
# - 接続がきたら、
# - それ以降の接続が緩やかに制限される。(緩やかさの程度が --hashlimit 2/m)
# - そうでなければ、許可。
# です。
-A INPUT_SSHD -m hashlimit --hashlimit-name t_sshd --hashlimit-mode srcip --hashlimit-htable-expire 120000 --hashlimit-burst 5 --hashlimit 2/m -j ACCEPT

COMMIT
EOT
```

## 3. iptables を起動する。

まずは

```bash
# iptables を起動し、 60 秒後、 iptables を停止する。
sudo systemctl start iptables && sleep 60 && sudo systemctl stop iptables &
```

で、 60 秒間だけ起動します。
その間に、改めて SSH 接続ができるかどうかを確認します。
大丈夫だったら

```bash
sudo systemctl start iptables
```

で起動します。

## 4. fail2ban を設定する。

```bash
cat << 'EOT' | sudo sh -c 'cat - > /etc/fail2ban/jail.d/sshd.conf'
[sshd]
enabled = true

# SSH のポート。
port = 51921

# iptables で利用するチェーン名。
chain = FW_F2B_SSHD

# 通知先のメールアドレス。
destemail = you@example.com

# sshd-aggressive というフィルタを利用。
# (sshd フィルタと sshd-ddos フィルタを合算したもの)
filter = sshd-aggressive

# 600 秒間に
findtime = 600

# 5 回 SSH ログインに失敗したら、
maxretry = 5

# 3600 秒間
bantime = 3600

# アクセスを遮断し、それをメールで通知する。
banaction=iptables-ipset-proto6
action = %(action_mw)s
EOT
```

## 5. fail2ban を起動する。

iptables 同様、一時的に起動し、 SSH 接続ができることを確認したら、改めて起動します。

```bash
# 60 秒間だけ fail2ban を起動する。
sudo systemctl start fail2ban && sleep 60 && sudo systemctl stop fail2ban &

# 大丈夫だったら改めて起動する。
sudo systemctl start fail2ban
```

## 6. iptables と fail2ban の自動起動を設定する。

これで設定が完了したため、次回以降、マシンが起動したときに自動的に起動するよう設定します。

```bash
sudo systemctl enable iptables
sudo systemctl enable fail2ban
```

## 7. (オプション) 特定の国の IP アドレスのみ許可する。

攻撃元となることが多い国の IP アドレスを全てブロックすることで、攻撃を受ける機会を減らせます。

- 参考) [2016年度 サイバー攻撃白書を公開いたしました。 | クラウド型WAF 攻撃遮断くん](https://www.shadan-kun.com/news/datail20170315.html)
    - 中国 > アメリカ > 日本 > ウクライナ > 韓国 > ロシア > ...

以下は日本の IP アドレスのみ許可する例です。
ある IP アドレスがどの国に割り当てられているかは定期的に変わるため、自動的に毎日更新するようにします。

```bash
cat << 'EOT' | sudo sh -c 'cat - > /etc/cron.daily/update-country-sshd-ipset'
#!/bin/sh

# 許可する国の ISO 国名コード (2文字)。
countries=JP

# 複数の国を許可する場合、 ' で囲み、  \| 区切りで連結する。
# 日本、アメリカ、イギリスを許可するなら、以下の通り。
#countries='JP\|US\|GB'

# ipset で利用する IP セットの名前。
setname=FW_COUNTRY_SSHD

# iptables で利用するチェーンの名前。
chain=FW_COUNTRY_SSHD

# コマンドのパス。
iptables=/usr/sbin/iptables
ipset=/usr/sbin/ipset

### 設定ここまで。以下、実行部。

# 利用する IP セット $setname が存在してなければ、作成する。
$ipset create -exist $setname hash:net

# 新しく一時的な IP セットを作成。
$ipset create -exist $setname-tmp hash:net
$ipset flush $setname-tmp

# 新しい IP セットに、 $countries の IP アドレスを全て追加する。
wget -nv -q -O - http://nami.jp/ipv4bycc/cidr.txt.gz \
  | gunzip -c \
  | sed -n "s/^\($countries\)\t//p" \
  | while read cidr
do
  $ipset add -q $setname-tmp $cidr
done

# 新しい IP セットに 100 件以上の登録があれば、正常に動作したと判断する。
if [ `$ipset list -t $setname-tmp | sed -n 's/^Number of entries: //p'` -gt 100 ]; then
  # 既存の IP セットと内容を入れ替える。
  $ipset swap $setname $setname-tmp

  # 正常に動作した場合のみ、指定した国の IP アドレスにマッチしなかった場合に拒否するルールを追加する。
  $iptables -F $chain
  $iptables -A $chain -m set --match-set $setname src -j ACCEPT
  $iptables -A $chain -j REJECT
fi

# 一時的な IP セットを削除。
$ipset destroy $setname-tmp
EOT

# 作成したコマンドに実行権限を付与。
sudo chmod 700 /etc/cron.daily/update-country-sshd-ipset

# 作成したコマンドを実行する。
sudo /etc/cron.daily/update-country-sshd-ipset
```

## 注意

iptables は出来ることが多く、凝れば凝るだけ強固になる可能性がありますが、それだけ複雑になり得ます。

また、 iptables は起動しているだけで接続ごとに CPU とメモリを消費し、また同時接続数の上限にも注意を払い、監視する必要がでてきます。
参考) [あなたの大量配信サーバ、ip_conntrack溢れていませんか？│株式会社イー・エージェンシー](https://www.e-agency.co.jp/column/20121225.html)

> `raw` テーブルで `NOTRACK` する、 `--state ESTABLISHED` など conntrack に依存する機能を使わない、いっそ conntrack モジュールを無効化などすれば、上記の問題はだいたい解消します。が、より進んだ設定になるため、ここでは記述しません。

本当に、必要なければ使わないに越したことはないので、 ***可能な限り固定 IP アドレスとセキュリティグループを利用しましょう*** 。