Colorless Green Ideas

インフラエンジニアとして勉強したこと

Ansibleでufwによるファイアウォール構築を試す

要約

  • Ubuntu Server 22.04 LTSに標準でインストールされているファイアウォール管理ツールであるufwを試した。
  • 検証にはAnsibleを用い、community.general.ufwを通して動作を確認した。
  • ポートや送信元/宛先に基づくパケットフィルタくらいのシンプルな用途であれば十分に使えそうに感じられた。
  • community.general.ufwモジュールの各種パラメータにおいて、いくつかのパラメータ間に依存関係があるのでその点は注意が必要。

はじめに

勉強のためにufwを使ってLinuxベースのファイアウォール構築を試しました。

ufwのuは"Uncomplicated"の略であり、その名の通り素のiptablesなどと比べて直感的なインターフェースを提供しています。

★参考:ufw in Launchpad

ufwは、iptablesでは可能な細やかな制御(パケットの特定フィールドの内容に基づくフィルタルール設定、カスタムチェーンの作成など)ができない反面、使いやすさに重きを置いているようです。

調べる前はホスト型ファイアウォール専用という先入観がありましたが、実際はそんなことはなく、後述の通り転送パケットに対してもフィルタの設定は可能です。

ポートや送信元/宛先を指定して許可/拒否するくらいの、アプライアンスを導入するまでもないファイアウォール構築であれば丁度良いツールかもしれません。

検証

Ansibleのcommunity.general.ufwを使い検証しました。

★参考:community.general.ufw module – Manage firewall with UFW — Ansible Documentation

具体的には以下のようなシナリオを想定しています。

  • ネットワーク型ファイアウォールとして構築
  • 当該ファイアウォールは4つのインターフェースを持ち、それぞれglobal、dmz、internal、managementの4つのセグメントに接続されている
  • 各セグメントとネットワークアドレスの対応関係は下記の通り
    • global: 192.168.201.0/24
    • dmz: 192.168.202.0/24
    • internal: 192.168.203.0/24
    • management: 10.0.2.0/24
  • 設定するルールは下記の通り

上記に基づき、下記のようなPlaybookとvarsファイルを作成しました。

Playbook

---
- name: Set Default Deny Policy
  become: true
  community.general.ufw:
    direction: "{{ item }}"
    policy: deny
  loop:
    - incoming
    - routed

- name: Set Allow Rule To Incoming
  become: true
  community.general.ufw:
    rule: allow
    comment: "{{ item.comment }}"
    from_ip: "{{ item.from_ip }}"
    interface_in: "{{ item.if_in }}"
    port: "{{ item.port }}"
  loop: "{{ fw_allow_incoming_list }}"

- name: Set Allow Rule To Routing
  become: true
  community.general.ufw:
    rule: allow
    comment: "{{ item.comment }}"
    route: true
    from_ip: "{{ item.from_ip }}"
    interface_in: "{{ item.if_in }}"
    to_ip: "{{ item.to_ip }}"
    interface_out: "{{ item.if_out }}"
    port: "{{ item.port }}"
  loop: "{{ fw_allow_routing_list }}"

- name: Set UFW ON
  become: true
  community.general.ufw:
    state: enabled

上記のPlaybookについて簡単に説明します。

まず"Set Default Deny Policy"で、ファイアウォール自身への接続および転送パケットのフィルタリングのポリシーをDeny(原則許可しない)にしています。

続けて"Set Allow Rule To Incoming"で、ファイアウォール自身への接続を許可するためのルールを設定しています。

ルールの設定には送信元IPアドレス、入力インタフェース、宛先ポートの3つを指定しています。

また"comment"のパラメータにより、そのルールの意図を記録できるようにしています。

"Set Allow Rule To Routing"で、ファイアウォールのパケットの転送を許可するためのルールを設定しています。

ルールの設定には送信元IPアドレス、入力インタフェース、宛先IPアドレス、出力インタフェース、宛先ポートの5つを指定しています。

最後に"Set UFW ON"により、ufwを有効化しています。

このufwの有効化は、ポリシーをDenyにしてから許可するルールを設定する前に実行してしまうと、対象のファイアウォールへの通信が一切できなくなってしまうため注意が必要です。

このPlaybookに対して、下記のように変数を設定することで、想定シナリオに合ったルールを設定します。

varsファイル

---
fw_allow_incoming_list:
  - comment: "management -> self: SSH"
    from_ip: 10.0.2.0/24
    if_in: enp0s3
    port: 22

fw_allow_routing_list:
  - comment: "global -> dmz: HTTP"
    from_ip: 192.168.201.0/24
    if_in: enp0s8
    to_ip: 192.168.202.0/24
    if_out: enp0s9
    port: 80
  - comment: "global -> dmz: HTTPS"
    from_ip: 192.168.201.0/24
    if_in: enp0s8
    to_ip: 192.168.202.0/24
    if_out: enp0s9
    port: 443
  - comment: "internal -> global: SSH"
    from_ip: 192.168.203.0/24
    if_in: enp0s8
    to_ip: 192.168.201.0/24
    if_out: enp0s9
    port: 22
  - comment: "internal -> global: HTTP"
    from_ip: 192.168.203.0/24
    if_in: enp0s8
    to_ip: 192.168.201.0/24
    if_out: enp0s9
    port: 80
  - comment: "internal -> global: HTTPS"
    from_ip: 192.168.203.0/24
    if_in: enp0s8
    to_ip: 192.168.201.0/24
    if_out: enp0s9
    port: 443
  - comment: "internal -> dmz: SSH"
    from_ip: 192.168.203.0/24
    if_in: enp0s8
    to_ip: 192.168.202.0/24
    if_out: enp0s9
    port: 22
  - comment: "internal -> dmz: HTTP"
    from_ip: 192.168.203.0/24
    if_in: enp0s8
    to_ip: 192.168.202.0/24
    if_out: enp0s9
    port: 80
  - comment: "internal -> dmz: HTTPS"
    from_ip: 192.168.203.0/24
    if_in: enp0s8
    to_ip: 192.168.202.0/24
    if_out: enp0s9
    port: 443

Ansibleの実行結果は下記の通りです。

実行ログ

TASK [fw : Install Require Packages] *******************************************
ok: [extfw0101]

TASK [fw : Set Kernel Parameter] ***********************************************
ok: [extfw0101]

TASK [fw : Set Default Deny Policy] ********************************************
ok: [extfw0101] => (item=incoming)
ok: [extfw0101] => (item=routed)

TASK [fw : Set Allow Rule To Incoming] *****************************************
changed: [extfw0101] => (item={'comment': 'management -> self: SSH', 'from_ip': '10.0.2.0/24', 'if_in': 'enp0s3', 'port': 22})

TASK [fw : Set Allow Rule To Routing] ******************************************
changed: [extfw0101] => (item={'comment': 'global -> dmz: HTTP', 'from_ip': '192.168.201.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.202.0/24', 'if_out': 'enp0s9', 'port': 80})
changed: [extfw0101] => (item={'comment': 'global -> dmz: HTTPS', 'from_ip': '192.168.201.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.202.0/24', 'if_out': 'enp0s9', 'port': 443})
changed: [extfw0101] => (item={'comment': 'internal -> global: SSH', 'from_ip': '192.168.203.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.201.0/24', 'if_out': 'enp0s9', 'port': 22})
changed: [extfw0101] => (item={'comment': 'internal -> global: HTTP', 'from_ip': '192.168.203.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.201.0/24', 'if_out': 'enp0s9', 'port': 80})
changed: [extfw0101] => (item={'comment': 'internal -> global: HTTPS', 'from_ip': '192.168.203.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.201.0/24', 'if_out': 'enp0s9', 'port': 443})
changed: [extfw0101] => (item={'comment': 'internal -> dmz: SSH', 'from_ip': '192.168.203.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.202.0/24', 'if_out': 'enp0s9', 'port': 22})
changed: [extfw0101] => (item={'comment': 'internal -> dmz: HTTP', 'from_ip': '192.168.203.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.202.0/24', 'if_out': 'enp0s9', 'port': 80})
changed: [extfw0101] => (item={'comment': 'internal -> dmz: HTTPS', 'from_ip': '192.168.203.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.202.0/24', 'if_out': 'enp0s9', 'port': 443})

TASK [fw : Set UFW ON] *********************************************************
changed: [extfw0101]

PLAY RECAP *********************************************************************
extfw0101                  : ok=7    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

対象のファイアウォールのマシンにログインし、ufwのルールのリストを表示させた結果は下記の通りです。

vagrant@extfw0101:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22 on enp0s3               ALLOW IN    10.0.2.0/24                # management -> self: SSH

192.168.202.0/24 80 on enp0s9 ALLOW FWD   192.168.201.0/24 on enp0s8 # global -> dmz: HTTP
192.168.202.0/24 443 on enp0s9 ALLOW FWD   192.168.201.0/24 on enp0s8 # global -> dmz: HTTPS
192.168.201.0/24 22 on enp0s9 ALLOW FWD   192.168.203.0/24 on enp0s8 # internal -> global: SSH
192.168.201.0/24 80 on enp0s9 ALLOW FWD   192.168.203.0/24 on enp0s8 # internal -> global: HTTP
192.168.201.0/24 443 on enp0s9 ALLOW FWD   192.168.203.0/24 on enp0s8 # internal -> global: HTTPS
192.168.202.0/24 22 on enp0s9 ALLOW FWD   192.168.203.0/24 on enp0s8 # internal -> dmz: SSH
192.168.202.0/24 80 on enp0s9 ALLOW FWD   192.168.203.0/24 on enp0s8 # internal -> dmz: HTTP
192.168.202.0/24 443 on enp0s9 ALLOW FWD   192.168.203.0/24 on enp0s8 # internal -> dmz: HTTPS

意図した通りにルールがセットされていることが分かります。

所感

上記の通りcommunity.general.ufwでシンプルにファイアウォールの設定ができますが、試した際にいくつか注意するべきと感じたポイントがありました。

パラメータ間の依存関係

公式ドキュメントに記載の通り、複数同時に設定できないパラメータがあります。

"interface"が"interface_in"および"interface_out"と同時に使えない、というのは分かりやすいのですが、"direction"が"interface_in"および"interface_out"と同時に使えない、というのはやや直感的ではないかもしれません。

また"interface_in"と"interface_out"が設定されており、かつ"route"が設定されていない場合には下記のようなエラーが出力されます。

TASK [fw : Set Allow Rule To Routing] ******************************************
failed: [extfw0101] (item={'comment': 'global -> dmz: HTTP', 'from_ip': '192.168.201.0/24', 'if_in': 'enp0s8', 'to_ip': '192.168.202.0/24', 'if_out': 'enp0s9', 'port': 80}) => {"ansible_loop_var": "item", "changed": false, "item": {"comment": "global -> dmz: HTTP", "from_ip": "192.168.201.0/24", "if_in": "enp0s8", "if_out": "enp0s9", "port": 80, "to_ip": "192.168.202.0/24"}, "msg": "Only route rules can combine interface_in and interface_out"}

"Only route rules can combine interface_in and interface_out"というメッセージを見て「routeパラメータは使ってないはずなのにどういうことだ?」と一瞬混乱しましたが、該当箇所のコードを見て理解しました。

★参考:community.general/plugins/modules/ufw.py at main · ansible-collections/community.general · GitHub

要するに「"route"のパラメータ指定が無い、かつ、"interface_in"のパラメータ指定がある、かつ、"interface_out"のパラメータ指定がある」の場合にエラーになるということなので、後者2つの条件のみがtrueになった場合でも"Only route rules~" のエラーが出るということですね。