セキュリティ
SSHポート22を閉鎖し、代わりに何を使用するか
I close SSH port 22 (and what I use instead) (michelebologna.net)
要約
この記事では、SSHポート22をインターネットから隠蔽し、セキュリティを強化する方法について解説しています。従来のポートノッキングの弱点を克服するため、fwknopとSingle Packet Authorization(SPA)を利用し、認証されたパケットのみがSSHポートへの一時的なアクセスを許可する仕組みを導入します。これにより、SSHデーモンは認証なしでは到達不能になり、スキャナーからの応答もフィルタリングされた状態になります。
全文翻訳
ほとんどのSSH強化ガイドは、キーのみの認証とfail2banで終わります。これらは役立ちますが、マシンがパブリックIPを持っている場合、ポート22は依然としてインターネットに開かれています。すべての自動スキャナーがそれをプローブし、応答を得ることができます:SSHバージョン文字列、バナー、何かリスニングしている証拠です。
ログのノイズだけでも迷惑です:キーのみの認証であっても、失敗した試行は毎日積み重なります。しかし、より大きな問題は露出です。OpenSSHでゼロデイ脆弱性が発見された場合、ポート22が開いているすべてのサーバーは、パッチを適用する時間ができる前にターゲットになります。これは以前にも起こりました。
SSHデーモン自体が到達不能になることを望んでいました:バナーなし、バージョン文字列なし、nmapが機能するための何もなし。レート制限されているとか、非標準ポートの後ろにあるだけでなく、実際にキーを持っていない限り接続できないようにすることです。
アイデア:ポートノッキング、そしてなぜそれが単独では不十分なのか
ポートノッキングは、この問題に対する元の方法です。アイデアはシンプルです:サーバーは閉じたポートへの特定の接続試行シーケンスを監視します。クライアントがポート7000、次に8000、次に9000に接続すると、サーバーはそのパターンを認識し、一時的にポート22のような実際のポートを開きます。
これは機能しますが、実際の弱点があります:ノックシーケンスはクリアで送信されます。あなたのトラフィックを監視している人は誰でもシーケンスをキャプチャして再生できます。認証はなく、隠蔽だけです。
fwknopは、Single Packet Authorization(SPA)でこれを解決します。ノックシーケンスの代わりに、HMACで暗号化および暗号学的に署名された単一のUDPパケットを送信します。サーバーは、正しいキーを持っている誰かからのパケットであることを検証できる場合にのみポート22を開きます。
Single Packet Authorizationの仕組み
SSHがまったくアクセス可能になる前に、サーバーに1つの暗号化されたUDPパケットを送信します。fwknopdはパケットを検証し、設定可能な時間枠(私は120秒を使用します)の間、あなたのソースIPのみにポート22を開くファイアウォールルールを一時的に挿入します。そのウィンドウの後、ルールは自動的に削除されます。すでに接続している場合、ルールが消える前に接続が確立されたため、セッションは維持されます。
スキャナーの観点からは、ポート22は応答しません。nmapはそれをフィルタリングされた状態として報告します。これは、サイレントにドロップされたパケットと同じ状態です。バナーなし、RSTなし、SSHがそこでリスニングしていることの確認なし。
SPAはUDPを使用するため、パケットは転送中に失われる可能性があります。SSHが接続でハングした場合、再度fwknop -n server1を実行して再試行してください。ステートレスなので、再送信は安全です。
左:ポートスキャナーは応答を得られません。右:有効なSPAパケットは、クライアントのIPに対して120秒間ポート22を開き、その後閉じます。
SPAパケット自体はHMAC-SHA512で認証され、暗号化されています。キャプチャされたパケットを再生しても機能しません:パケットにはタイムスタンプと一度だけ使用できるシーケンスカウンターが含まれています。fwknopdは両方をチェックし、いずれかが失敗した場合はサイレントにパケットをドロップします。
キーの生成
2つのキーが必要です:暗号化キーとHMACキー。暗号化キーはSPAパケットの内容を保護します。HMACキーはそれを認証します。これらは異なる目的を果たし、分離されるべきです。同じキーを両方に使用すると、スキームのセキュリティプロパティが弱まります。
キー生成は一度限りのステップであり、両方のサイドを設定する前に出力が必要です。
fwknop --key-gen
これは次のようなものを出力します。
KEY_BASE64: <long-base64-string>
HMAC_KEY_BASE64: <long-base64-string>
両方のキーを後で取得できる場所に保存してください:サーバー(Ansible vault経由)とすべてのクライアントマシンで必要になります。私はそれらをAnsible vaultにvault_fwknop_spa_keyとvault_fwknop_hmac_keyとして保存し、Bitwardenにもshared/fwknop_spa_keyとshared/fwknop_hmac_keyとしてミラーリングしています。2つのストアは異なるツールにサービスを提供します:Ansibleはサーバーサイドをデプロイするときに独自のvaultから読み取り、chezmoiはクライアントマシンで〜/.fwknoprcをポピュレートするときにBitwardenにクエリします。同じキー、2つのエントリーポイント。
1つのキーペアですべてのホストをカバーできます。各ホストは、同じ共有キーを指す同じfwknopd設定を実行します。ホストごとのキーが必要な場合は、アクセスファイルで別々のスタンザを使用できますが、1つの共有ペアはローテーションが簡単です。
Ansibleによるサーバーサイドのデプロイ
これに対する私のAnsibleロールはfwknop_serverです。これはfwknop-serverをインストールし、サーバー設定とアクセスファイルをデプロイし、UFWルールを調整します。
タスクファイルのキー部分:
- name: Install fwknop-server
apt:
name: fwknop-server
state: present
- name: Deploy fwknopd.conf
template:
src: fwknopd.conf.j2
dest: /etc/fwknop/fwknopd.conf
owner: root
group: root
mode: "0600"
- name: Deploy access.conf
template:
src: access.conf.j2
dest: /etc/fwknop/access.conf
owner: root
group: root
mode: "0600"
no_log: true
- name: Remove public SSH UFW rule
ufw:
rule: limit
port: "{{ fwknop_ssh_port | default(22) }}"
proto: tcp
delete: yes
- name: Allow fwknop SPA port (UDP)
ufw:
rule: allow
port: "{{ fwknop_spa_port }}"
proto: udp
公開SSHのUFWルールは、fwknopがデプロイされた後に削除されます。その時点から、iptablesは有効なSPAパケットが到着したときにのみポート22を開きます。fwknopdは、UFWの管理レイヤーの下にiptablesルールを直接挿入することでこれを実行するため、一時的なルールはufw statusからは見えません。
デフォルト:
fwknop_spa_port: 62201
fwknop_access_timeout: 120 # seconds
fwknop_ssh_port: "{{ ssh_port | default(22) }}"
fwknop_access_timeout は access.conf.j2 に FW_ACCESS_TIMEOUT 値として流れ込み、これは fwknopd に有効な SPA パケット到着後、ファイアウォールルールをどれだけ長く開いたままにするかを伝えます。(ACCESS_EXPIRE は異なるディレクティブです:これはスタンザが SPA パケットの受け入れを完全に停止する時期を設定します。)
クライアントの設定
クライアントサイドは〜/.fwknoprcで、chezmoiが管理し、適用時にBitwardenからキーをポピュレートします。ファイルにはホストごとのスタンザがあります。
[default]
SPA_SERVER_PROTO udp
HMAC_DIGEST_TYPE SHA512
USE_HMAC y
ALLOW_IP resolve
ACCESS tcp/22
KEY_BASE64 {{ (bitwarden "item" "shared/fwknop_spa_key").fields.value }}
HMAC_KEY_BASE64 {{ (bitwarden "item" "shared/fwknop_hmac_key").fields.value }}
[server1]
SPA_SERVER 203.0.113.10
SPA_SERVER_PORT 62201
[server2]
SPA_SERVER server2.example.com
SPA_SERVER_PORT 62201
ALLOW_IP resolve は fwknop に現在のパブリックIPを自動検出するように指示します。キーはbase64エンコードされており、上記の生成ステップから取得されます。
〜/.ssh/configへの配線
ノックは、〜/.ssh/configの各ホストにProxyCommandを追加することで完全に透過的にすることができます。これが設定されると、ssh server1は機能します:SPAパケットが送信され、ポートが開き、SSHが接続します。
Host server1
HostName 203.0.113.10
ProxyCommand fwknop -n server1; sleep 2; nc %h %p
Host server2
HostName server2.example.com
ProxyCommand fwknop -n server2; sleep 2; nc %h %p
fwknop -n server1 は〜/.fwknoprcから[server1]スタンザを選択して、SPAパケットをどこに送信するかを知ります。sleep 2 は、nc(netcat)が実際のTCP接続を開く前に、fwknopdがiptablesを更新する時間を与えます。
Ansibleへの配線
新しいマシンの初期プロビジョニングはプレーンSSHを使用します:fwknopはまだインストールされていないため、接続は直接です。fwknop_serverロールが実行され、UFWルールが削除されると、それ以降のすべてのAnsible実行は最初にノックする必要があります。
私はこれをgroup_vars/linux/vars.yamlでansible_ssh_common_argsを設定することで処理します:
ansible_ssh_common_args: >-
-o ProxyCommand="fwknop -n %h; sleep 2; nc %h %p"
-o StrictHostKeyChecking=accept-new
-o ConnectTimeout=10
%h はインベントリホスト名であり、〜/.fwknoprcのスタンザ名と一致します。最初のプロビジョニング実行後、すべての後続のプレイはこのパスを自動的に通過します。
Tailscaleを並列アクセスパスとして
fwknopと並行して、Tailscaleインターフェース(tailscale0)は常にノックを必要とせずにSSHを許可します。これはfwknopが壊れたときのフォールバックではありません。独立して存在する別のアクセスパスです。
実際には、1つのシナリオをうまくカバーしています:設定ミスでパブリックインターフェースがロックアウトされた場合、Tailscaleに接続されたどのデバイスでもテールネット経由でマシンに到達できます。
# tailscaleロールによって追加されたUFWルール
ufw allow in on tailscale0 to any port 22