てんこ

ブログ名は愛猫(てん)の愛称です。中身は個人のIT系学習記録です。

AnsibleでFortigateのポリシー情報をGETしてみた

別記事でも書きましたが、現状ではfortios用のモジュールに、GETのモジュールは無いように思えます。

なので、パケットキャプチャで取得したHTTPの情報から、uriモジュールを利用してGETリクエストが発行できるPlaybookを書いてみました。

Ansible関連の実践記事一覧はこちら


目的

fortios系モジュールには無いと思われる、現在の情報取得をPlaybookで実現するため

環境

  • OS: CentOS Linux release 7.7.1908 (Core)
  • Ansible : 2.9.0
  • Python: 2.7.6 / 3.6.8
  • Fortios: 6.0.6

やってみたいと思ったこと

現在のPolicyの情報を、json形式で取得したい

作成したPlaybook

ユーザ名やらパスワードやらは、inventoryだったりgroup_varsだったりに書いています。

---
- hosts: fortigate  
  gather_facts: false
  connection: local # httpapiでも利用可能でした。
  vars:
    host : "{{ ansible_host }}"

  tasks:
  - name: Login
    uri:
      url: https://{{ host }}/logincheck
      validate_certs: no
      method : "POST"
      force_basic_auth: yes
      body_format: "form-urlencoded"
      url_username: "{{ ansible_user }}"
      url_password: "{{ ansible_password }}"
      body:
        - [ username, "{{ ansible_user }}" ]
        - [ secretkey, "{{ ansible_password }}" ]
        - [ ajax, "1" ]
    register: http_response
    
  - name: debug
    debug:
      var: http_response

  - name: GET Policy
    uri:
      url: https://{{ host }}/api/v2/cmdb/firewall/policy
      validate_certs: no
      method: "GET"
      headers:
        Cookie: "{{ http_response.set_cookie }}"
      return_content: yes
    register: get_policy

  - name: debug2
    debug:
      var: get_policy.json
      

結果(debug2の部分)

  fw01 ok: {
    "changed": false, 
    "get_policy.json": {
        "build": 272, 
        "http_method": "GET", 
        "http_status": 200, 
        "name": "policy", 
        "path": "firewall", 
        "results": [
            {
                "action": "accept", 
(略)
                "dstaddr": [
                    {
                        "name": "all", 
                        "q_origin_key": "all"
                    }
                ], 
                "dstaddr-negate": "disable", 
                "dstintf": [
                    {
                        "name": "wan1", 
                        "q_origin_key": "wan1"
                    }
                ], 
(略)
                "ippool": "disable", 
                "ips-sensor": "", 
                "label": "", 
                "learning-mode": "disable", 
                "logtraffic": "disable", 
                "logtraffic-start": "disable", 
                "match-vip": "disable", 
                "name": "", 
                "nat": "disable", 
(略)
                "policyid": 1, 
                "poolname": [], 
                "profile-group": "", 
                "profile-protocol-options": "default", 
                "profile-type": "single", 
                "q_origin_key": 1, 
                "radius-mac-auth-bypass": "disable", 
                "redirect-url": "", 
                "replacemsg-override-group": "", 
                "rsso": "disable", 
                "rtp-addr": [], 
                "rtp-nat": "disable", 
                "scan-botnet-connections": "disable", 
                "schedule": "always", 
                "schedule-timeout": "disable", 
                "send-deny-packet": "disable", 
                "service": [
                    {
                        "name": "SSH", 
                        "q_origin_key": "SSH"
                    }
                ], 
                "service-negate": "disable", 
                "session-ttl": 0, 
                "spamfilter-profile": "", 
                "srcaddr": [
                    {
                        "name": "all", 
                        "q_origin_key": "all"
                    }
                ], 
                "srcaddr-negate": "disable", 
                "srcintf": [
                    {
                        "name": "internal", 
                        "q_origin_key": "internal"
                    }
                ], 
(略)
                "status": "enable", 
(略)
            }, 
            {
(略)
                "policyid": 2, 
(略)            }
        ], 
        "revision": "80.0.0.661485498.1572955273", 
        "serial": "FGT6xxxxxxxxxxxx", 
        "status": "success", 
        "vdom": "root", 
        "version": "v6.0.6"
    }
}

書き忘れ

VDOM指定を書き忘れました…これでも取れるんですが、VDOM指定したい場合どうすればいいか、調べてまた修正します。

どうやら、2個目のurlの末尾に、?vdom=<vdom_name>とつけると対象のVDOM指定ができるようです。

感想

思いのほか、手軽に現在のポリシー情報のGETが実現できました。

やってることとしては、キャプチャを取った結果に従って、単純に実装するだけなのでそれほど難しいことはやっていないかと思いますが、これでAPIのエンドポイントさえわかれば、簡単に情報が取得できそうです。

ここでは結局、コネクションプラグインはlocalを使っているので、httpapiよりもfortiosapiのほうが一式で考えると相性がいいのかな…?

と、httpapi使えない頃の自分は考えていましたが、httpapiでも問題なく利用可能でした。 そうすると、やはりhttpapi側に統一していくべきですね…

AnsibleでFortigateのhttpapi利用がうまくいっていない話→Python3に上げたら解消!

AnsibleでFortigateをREST APIで操作する方法は、以下の2種類が存在します。

  • fortiosapi (Legacy Mode)
  • httpapi (今後の推奨)

私の環境では、前者のfortiosapiは今のところ問題なく動作しますが、後者のhttpapiはうまく動いていません。

[2019/11/10追記] Python3にあげることで解消しました!!

いったん棚上げしますが、調べている最中の備忘録です。

Ansible関連の実践記事一覧はこちら


目的

自身の環境で、httpapi経由でFortigateが操作できない原因を明らかにするため

環境

  • FortiOS : 6.0.6
  • Ansible実行環境のOS: Windows 10 WSL2上のUbuntu 18.04.3
  • 上記環境内のAnsible : 2.9.0
  • 上記環境内のPython: 2.7.15+
(ansible29) ansible@ENVY360:~/ansible-study$ ansible --version
ansible 2.9.0
  config file = /home/ansible/ansible-study/ansible.cfg
  configured module search path = [u'/home/ansible/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/ansible/ansible29/local/lib/python2.7/site-packages/ansible
  executable location = /home/ansible/ansible29/bin/ansible
  python version = 2.7.15+ (default, Oct  7 2019, 17:39:04) [GCC 7.4.0]

httpapiを利用すると発生するエラー

以下のようなエラーが発生してfailedになります。-vvvつけた結果も、見てもさっぱり…(T_T)

An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.module_utils.connection.ConnectionError: addinfourl instance has no attribute 'getheaders' fw01 failed | msg: MODULE FAILURE See stdout/stderr for the exact error

fortios系モジュールのAPI利用手順

fortiosapi側

リファレンスがありませんので実測からですが、以下のような流れのようです。

No. 方向 内容
1. [Ansible->FGT] /logincheck に対してPOSTで認証情報を投げる
2. [Ansible<-FGT] 上記リクエストに対して、問題ない場合にはSet-Cookieccsrftokenの値が返される
3. [Ansible->FGT] 2.で返されたHTTPヘッダを付けた状態でAPIが利用可能かの確認のため、GET /api/v2/cmdb/system/status?global=1 というリクエストを投げる(これは一例。処理をしようとするエンドポイントに合わせてこの値は変化する)
4. [Ansible<-FGT] 上記リクエストに対して、問題ない場合には200 OKのレスポンスが返る
5. [Ansible->FGT] 3.と同様、2.で返されたヘッダを付与した状態で、目的のエンドポイントに対してGET/PUTメソッドでリクエストを投げる。(fortios_facts以外はおそらくPUT)
6. [Ansible<-FGT] 5.のリクエストに対し、処理を行った結果をレスポンスで返す
7. [Ansible->FGT] 処理が完了したら、FIN/ACKでコネクションを切断する

上記のような動作のため、同一のコネクションを使いまわしています。

httpapi側

fortiosapiとは異なり、常にコネクションを切断→再接続していました。終了の処理もより丁寧になっています。

No. 方向 内容
1. [Ansible->FGT] /logincheck に対してPOSTで認証情報を投げる
2. [Ansible<-FGT] 上記リクエストに対して、問題ない場合にはSet-Cookieccsrftokenの値が返される
3. [Ansible->FGT] FIN/ACKでコネクションを切断する。
4. [Ansible->FGT] 2.で返されたHTTPヘッダを付けた状態でAPIが利用可能かの確認のため、GET /api/v2/cmdb/system/status?global=1 というリクエストを投げる(これは一例。処理をしようとするエンドポイントに合わせてこの値は変化する)
5. [Ansible<-FGT] 上記リクエストに対して、問題ない場合には200 OKのレスポンスが返る
6. [Ansible->FGT] FIN/ACKでコネクションを切断する。
7. [Ansible->FGT] 4.と同様、2.で返されたヘッダを付与した状態で、目的のエンドポイントに対してGET/PUTメソッドでリクエストを投げる。(fortios_facts以外はおそらくPUT)
8. [Ansible<-FGT] 7.のリクエストに対し、処理を行った結果をレスポンスで返す
9. [Ansible->FGT] FIN/ACKでコネクションを切断する。
10. [Ansible->FGT] /logout に対してPOSTで認証情報を付与したリクエスト投げる
11. [Ansible<-FGT] 10.のリクエストに対し、処理を行った結果をレスポンスで返す
12. [Ansible->FGT] FIN/ACKでコネクションを切断する。

共通していること

Fortigateのモジュールを触っていると、成功する場合は必ずOKが1以上返ってくるのですが、3.の時点でOK=1がカウントされているのだと思います。(ソースを確認したわけではありませんが)。

おそらくは以下のようなカウント方法では無いかと思います。

【ポイント】 APIへのリクエストに対し、成功するか否か

【カウント先判定】 GETメソッドの場合はOK、PUT(POSTもあるのか?)メソッドの場合はChanged

個人的には上「現在の値が何で、リクエストは実行するべきか?」という、冪等性を担保するための仕組み(GETメソッドでの現状確認)が欲しいなあと思うところではありますが、現状は上記のような動作となるため、設定変更系のモジュールについては冪等性はなさそうです。

httpapiではエラーがどの箇所で発生していたか

上述のhttpapiのうち、1.は問題なく動作しているのですが、4.のリクエストが出ず、上述した10.のlogoutに対して切断を行う動作となっています。

そのため、4.で必要なCookie情報をヘッダに付与する、という部分でエラーが発生していると理解しています。そうなると、基本的にはAnsibleホスト側の問題です。

切り分け

OS環境を変えてとりあえずやってみる

もしかしたらPythonのバージョンとかが影響するのかなあとなんもかんがえずにCentOSを構築。

  • 切り分け環境のOS: CentOS Linux release 7.7.1908 (Core)
  • 上記環境内のAnsible : 2.9.0
  • 上記環境内のPython: 2.7.5

CentOS側の環境

(ansible29) [ansible@ansible ansible-playbook]$ ansible --version
ansible 2.9.0
  config file = /home/ansible/ansible-playbook/ansible.cfg
  configured module search path = [u'/home/ansible/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/ansible/ansible29/lib/python2.7/site-packages/ansible
  executable location = /home/ansible/ansible29/bin/ansible
  python version = 2.7.5 (default, Aug  7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

→失敗。

リクエストそのものの違いを見てみる

共通して成功している1.の/logincheckへのPOSTリクエストをキャプチャし、キャプチャデータから各々のリクエストの差を比較してみました。

違いとして見えるのは以下の通り。

カテゴリ fortiosapi httpapi 備考
User-Agent python-requests 2.22 Python-urllib2.7
Connection keepalive close httpapiも1.1でリクエストはしているが
Content-Type 定義なし application/x-www-form-urlencoded
Authenticationヘッダ なし Basic認証 dataとしてはfortiosapiも同じ値を渡している

Pythonのバージョンを上げてやってみる[2019/11/10追記]

よこちさんの環境はPython3という情報をゲットしました。

まあ、サポート終了考えれば、3を使っていて当然ですよね…

python3を以下の手順でインストールし、venvを作成から実施。

$ sudo yum install python3

$ python3 -V
Python 3.6.8

$ python3 -m venv p3-ansible29

$ source p3-ansible29/bin/activate

$ python -V
Python 3.6.8

python3の環境で実行してみる。

(p3-ansible29) [ansible@ansible kiriwake]$ ansible-playbook forti_getfacts_httpapi.yml 
Executing playbook forti_getfacts_httpapi.yml

- fortios on hosts: fortios -
Get Facts....
  fortios01 ok
Debug...
  fortios01 ok: {
    "changed": false,
    "getfact_result": {
        "ansible_facts": {
            "ansible_net_gather_network_resources": [],
            "ansible_net_gather_subset": [],
            "ansible_network_resources": {
                "system_status_select": {
                    "action": "select",
                    "build": 272,
                    "http_method": "GET",
                    "name": "status",
                    "path": "system",
                    "results": {},
                    "serial": "FGT60D4615093671",
                    "status": "success",
                    "vdom": "root",
                    "version": "v6.0.6"
                }
            },
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": false,
        "failed": false
    }
}

- Play recap -
  fortios01                  : ok=2    changed=0    unreachable=0    failed=0    rescued=0    ignored=0   
(p3-ansible29) [ansible@ansible kiriwake]$ 

キタ━━━━(゚∀゚)━━━━!!

結論

Python3にあげることで問題なく利用ができるようになりました。

よこちさん、大変ご迷惑をおかけいたしました…

以下に、棚上げ時点での弱音を残しておきますが、実際にどんな差が出てPython2系だとだめなのかはちょっと気になります。すごーーーく時間がある時に調べられたら調べてみたいと思います。

■↓ここから棚上げ時↓■

ここから先行くのはUser-Agentで定義されている各々のライブラリを確認する必要があるのかなあと思い、python不勉強なのでいったん棚上げします。

もしかすると、よこちさんの環境とはFortiOSの環境も異なるので、そのあたりの差分も影響しているのかもしれません。

ぐぐぐ…悔しいが、前に進みます…

どなたか、何か気が付いた点があれば、Twitterなりなんなりでコメントいただけると泣いて喜びます…

AnsibleでFortigateの情報取得モジュール(fortios_facts)を試してみた。

AnsibleでのFortigate操作について、fortios_factsモジュールの出力について書いておきます。

Ansible関連の実践記事一覧はこちら


目的

唯一の情報取得用のモジュールであるfortios_factsが出力できる内容を確認するため

環境

  • ansible: 2.9.0
  • Fortios: 6.0.6
  • ansibleのコネクションプラグイン: local(fortiosapiライブラリ利用)

サンプルPlaybook

別のエントリにも書いてありますが、以下のように記載します。

---
- hosts: localhost 
  gather_facts: false
  vars:
    host: "10.254.254.254"
    username: "admin"
    password: "password"
    vdom: "root"
    ssl_verify: "no"

  tasks:
  - name: Get Facts.
    fortios_facts:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
      vdom: "{{ vdom }}"
      ssl_verify: "{{ ssl_verify }}"
      https: "yes"
      gather_subset:
        - fact: 'system_status_select' #ここに欲しい要素の情報を書く
    register: getfact_result

  - name: Debug
    debug: 
      var: getfact_result

fortios_factsで利用可能な要素

利用可能な要素とその概要は以下の通りです。

要素名 概要
system_current-admins_select 管理者ユーザがログインしたイベントのログ
system_firmware_select 利用可能なファームウェアの表示【未確認】
system_fortimanager_status fortimanagerとの接続ステータス【未確認】
system_ha-checksums_select HAのConfigチェックサム状態の表示【未確認】
system_interface_select インターフェースの状態やカウンタの表示
system_status_select 用途不明… 【未確認】
system_time_select FW本体の時刻情報の取得

なにかしら変な値を入れたりすると、以下のようなエラーメッセージが表示され、利用できるのが以下の要素だというのがわかります。(公式のモジュール群説明にも、ソースにも書いてあります)

TASK [Get Facts.] ***********************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "Subset must be one of [system_current-admins_select, system_firmware_select, system_fortimanager_status, system_ha-checksums_select, system_interface_select, system_status_select, system_time_select], got system_ha_select"}

system_current-admins_select

以下のように、unixtimeでイベントの発生タイミングとログインユーザ、現在のログイン状態などが表示されます。

実際のログはクリックして展開

ok: [localhost] => {
    "getfact_result": {
        "ansible_facts": {
            "ansible_net_gather_network_resources": [], 
            "ansible_net_gather_subset": [], 
            "ansible_network_resources": {
                "system_current-admins_select": {
                    "action": "select", 
                    "build": 272, 
                    "http_method": "GET", 
                    "name": "current-admins", 
                    "path": "system", 
                    "results": [
                        {
                            "admin": "admin", 
                            "disconnect_enabled": true, 
                            "id": 31, 
                            "is_current": false, 
                            "method": "https", 
                            "profile": "super_admin", 
                            "srcaddr": "10.254.254.1", 
                            "time": 1572681512
                        }, 
                        {
                            "admin": "admin", 
                            "disconnect_enabled": true, 
                            "id": 43, 
                            "is_current": false, 
                            "method": "http", 
                            "profile": "super_admin", 
                            "srcaddr": "10.254.254.1", 
                            "time": 1572682842
                        }, 
(略)
                    ], 
                    "serial": "FGTxxxxxxxxxxxxxxx", 
                    "status": "success", 
                    "vdom": "root", 
                    "version": "v6.0.6"
                }
            }
        }, 
        "changed": false, 
        "failed": false
    }
}

system_firmware_select

インターネットから切り離して検証しているのですが、おそらくavailableというところに、オンラインでアップデート可能なファームの情報が出てくるのだと思います。

【12/22追記】別記事で実際にオンラインにしてファーム情報を取得してきましたので、そちらもご参照ください!

実際のログはクリックして展開

(冒頭は省略)
"system_firmware_select": {
    "action": "select", 
    "build": 272, 
    "http_method": "GET", 
    "name": "firmware", 
    "path": "system", 
    "results": {
        "available": [], 
        "current": {
            "branch-point": 272, 
            "build": 272, 
            "id": "current", 
            "major": 6, 
            "minor": 0, 
            "name": "FortiOS", 
            "notes": "http://docs.fortinet.com/d/fortios-6.0.6-release-notes/download", 
            "patch": 6, 
            "platform-id": "FGT60D", 
            "release-type": "GA", 
            "source": "current", 
            "version": "v6.0.6"
        }
    }, 
    "serial": "FGxxxxxxxxxxxxxxxx", 
    "status": "success", 
    "vdom": "root", 
    "version": "v6.0.6"
}

system_fortimanager_status

fortimanager使ってないのでどんな情報が出るのが正しいのかわかりません…

実際のログはクリックして展開

"system_fortimanager_status": {
    "action": "status", 
    "build": 272, 
    "http_method": "GET", 
    "name": "fortimanager", 
    "path": "system", 
    "results": {},  # ここに何か出てくるはず…
    "serial": "FGxxxxxxxxxxxxx", 
    "status": "success", 
    "vdom": "root", 
    "version": "v6.0.6"
}, 

system_ha-checksums_select

おそらく、HAを組んでいればHAを組んだ対抗同士の設定情報のリビジョン番号などが出るのかなと思いますが、1台しかないので確認できず…

【12/22追記】取得してみました。各パーティション毎の、自分自身と対抗側のチェックサムを表示してくれるようですね。

実際のログはクリックして展開

"system_ha-checksums_select": {
    "action": "select",
    "build": 303,
    "http_method": "GET",
    "name": "ha-checksums",
    "path": "system",
    "results": [
        {
            "checksum": {
                "all": "8d840039e887d0ae71395107a396adb7",
                "global": "2bdb9a96e1e3afe9870b15d4339c20b7",
                "root": "a0f21470df97822589f0a2801dd9c915"
            },
            "is_manage_master": 1,
            "is_root_master": 1,
            "serial_no": "FGT60*******"
        },
        {
            "checksum": {
                "all": "8d840039e887d0ae71395107a396adb7",
                "global": "2bdb9a96e1e3afe9870b15d4339c20b7",
                "root": "a0f21470df97822589f0a2801dd9c915"
            },
            "is_manage_master": 0,
            "is_root_master": 0,
            "serial_no": "FGT60*******"
        }
    ],
    "revision": "149586697367309",
    "serial": "FGT60D******", #アクセスした側のホストのSerial
    "status": "success",
    "vdom": "root",
    "version": "v6.0.8"
}

system_interface_select

インターフェースのカウンタなどが表示されます。

…あれ?今気が付きましたが、スイッチモードになってるinternalのは表示されてませんね。インターフェース分離すれば個別のインターフェースになって表示されるんでしょうか…

実際のログはクリックして展開

"system_interface_select": {
    "action": "select", 
    "build": 272, 
    "http_method": "GET", 
    "name": "interface", 
    "path": "system", 
    "results": {
        "dmz": {
            "alias": "", 
            "duplex": 1, 
            "id": "dmz", 
            "ip": "10.10.10.1", 
            "link": true, 
            "mac": "00:00:00:00:00:00", 
            "mask": 24, 
            "name": "dmz", 
            "rx_bytes": 3112840, 
            "rx_errors": 0, 
            "rx_packets": 5276, 
            "speed": 1000.0, 
            "tx_bytes": 240, 
            "tx_errors": 0, 
            "tx_packets": 4
        }, 
        "modem": {
(略)
        }, 
        "wan1": {
(略)
        }, 
        "wan2": {
(略)
        }
    }, 
    "revision": "1572684445.19228", 
    "serial": "FGTxxxxxxxxxxxxxxxxxxx", 
    "status": "success", 
    "vdom": "root", 
    "version": "v6.0.6"
}, 

system_status_select

何が出るんだろう…

【12/22追記】HA組んでも表示されるものは変わりませんでした。うーん?

実際のログはクリックして展開

"system_status_select": {
    "action": "select", 
    "build": 272, 
    "http_method": "GET", 
    "name": "status", 
    "path": "system", 
    "results": {},  #ここになにかでるはず…
    "serial": "FGxxxxx", 
    "status": "success", 
    "vdom": "root", 
    "version": "v6.0.6"
}, 

system_time_select

本体の時刻の情報がUnixtime形式で取得できるようです。

実際のログはクリックして展開

"system_time_select": {
    "action": "select", 
    "build": 272, 
    "http_method": "GET", 
    "name": "time", 
    "path": "system", 
    "results": {
        "time": 1572684445
    }, 
    "serial": "FGTxxxxxx", 
    "status": "success", 
    "vdom": "root", 
    "version": "v6.0.6"
}

他の使いかた

複数要素を同時に出力させ、フィルターをかけることで該当する要素(ポート指定だけとか)もできるようです。

2019/11/6時点では未確認です。

感想

これだけしか取れないか‥‥という感想です(T_T)

現在の設定情報(アドレスオブジェクトやユーザオブジェクト、ポリシーなど)が取れないかと期待もしたのですが…

「現状の設定を確認」→「想定通りでなければ設定」のような処理を実装しようと思うと、fortiosモジュール単体では実現できず、自分で処理をくみ上げる必要がある、ということだと理解しました。

むう、実用化への道のりは長い…か…?

AnsibleでFortigateのポリシーを設定してみた(その2)

前回に引き続き、AnsibleによるFortigateのポリシー設定のお話です。

だんだん「普段だとエラーチェックで弾かれる設定をAPIで叩いたのち、再起動してどうなるかを試す」のが楽しくなってきています。

Ansible関連の実践記事一覧はこちら


現在のポリシー設定情報の取得

色々とポリシー系のモジュールを眺めてみましたが、基本的にfortios用の現在のモジュールには、「設定変更」用のモジュールしか存在せず、「設定取得」用のモジュールはなさそうです…

唯一の設定取得用のfortios_configモジュールはpyFGなので今のところ動かせてません…

FortigateのAPIそのものはGETメソッドには対応しており、例えばログインした状態で以下のようにURLにアクセスすることで、指定されたエンドポイントの情報をJSON形式で取得することが可能なようです。

■ポリシー全体の情報を取得
htttp[s]://<fortigate_ip_address>/api/v2/cmdb/firewall/policy

■単一のポリシー情報を取得
htttp[s]://<fortigate_ip_address>/api/v2/cmdb/firewall/policy/1
■出力例
{
  "http_method":"GET",
  "revision":"39.0.0.661485498.1572955273",
  "results":[
    {
      "q_origin_key":1,
      "policyid":1,
      "name":"",
      "uuid":"ee645184-fec9-51e9-d467-0b6fb1a24d6d",
      "srcintf":[
        {
          "q_origin_key":"internal",
          "name":"internal"
        }
      ],
      "dstintf":[
        {
          "q_origin_key":"wan1",
          "name":"wan1"
        }
      ],
      "srcaddr":[
        {
          "q_origin_key":"all",
          "name":"all"
        }
      ],
      "dstaddr":[
        {
          "q_origin_key":"all",
          "name":"all"
        }
      ],
(略)
      "action":"accept",
      "send-deny-packet":"disable",
      "firewall-session-dirty":"check-all",
      "status":"enable",
      "schedule":"always",
      "schedule-timeout":"disable",
      "service":[
        {
          "q_origin_key":"HTTP",
          "name":"HTTP"
        },
        {
          "q_origin_key":"HTTPS",
          "name":"HTTPS"
        },
        {
          "q_origin_key":"DNS",
          "name":"DNS"
        }
      ],
(略)

ポリシーの並び順

Policy IDには関係なく、上から順に登録されるようです。

ポリシーの順序の入れ替え

モジュールは見当たりませんでした。 CLIだとこれだけなんですけどね…

ポリシー要素の複数オブジェクトの指定

モジュールのリファレンスにも「list形式」とあったので、以下のように宣言したところ、問題なく複数サービス指定のポリシーになりました。(前回と共通する部分は省略)

        service:
          - name: "HTTP"
          - name: "HTTPS"
          - name: "DNS"

ポリシーのEnable/Disableの切り替え

ポリシーの削除と同じように、指定したPolicy IDだけを宣言し、status: "disable"にすることで実施可能でした。

  tasks:
  - name: Configure Firewall Policy (Disable)
    fortios_firewall_policy:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
      vdom: "{{ vdom }}"
      ssl_verify: "{{ ssl_verify }}"
      state: "present"
      firewall_policy:
        policyid: "1"
        status: "disable"  #有効化するときはstatus: "enable"

↓実行結果

初期定義以外のオブジェクトが使えるか

当たり前ですが、宣言されてさえいれば利用が可能でした。↑の画像の「HOGE_TCP999」はカスタムサービスオブジェクトです。

「名無しポリシー許可」無効状態での名前無しポリシーの設定

Fortigateのポリシーには、いつからか初期設定で名前の設定が必須になりました。これを無効化(=名無しポリシーを許可)できるスイッチが「Allow Unnamed Policies」の有効化です。

名無しポリシー禁止の状態で、Ansible のPlaybook(前回のものと同じ)を実行してみましたが、特にエラーもなく正常終了しました。

また、その状態で再起動も試してみましたが、特に設定が消えるようなことはありませんでした。

やはり、CLI/WebUIでエラーチェックがかかる設定も、REST APIだと設定可能なようです。要注意ですね…

感想

モジュールリストを眺めていて薄々感じてはいましたが、最初のほうにも書いたようにfortios用のモジュールは「設定変更」用のものしかなく、一部の例外(fortios_facts)を除いては「情報取得」用のものはなさそうです。

これまで、もくもく会で実施してきたコンテンツや環境は、どれだけ恵まれた状態だったのかを痛感しました…

また、一部分だけをSSH用のコマンドを実行する形でPlaybook化して実装する、などしたときにも、fortiosapiが利用するlocalコネクションプラグインではつらそうだなあと感じました。

いきなりモジュールを試すのもいいですが、地力をつけるためにも色々と基礎から勉強をせねばな、と改めて感じました。

AnsibleでFortigateのポリシーを設定してみた(その1)

前回に引き続き、AnsibleでFortigateの設定をしてみました。

なかなか素敵なハマりをしたので、ご紹介しつつ。

入れ替え(move)とか並び順とかはその2を別で書こうと思いますので、まずはスタンダードなところから。

■間違い探し

みなさんは、以下の2つのポリシーの違いは判りますか…?

Ansible関連の実践記事一覧はこちら


目的

AnsibleでFortigateのFirewallポリシーを設定できるか試すため

環境

  • ansible: 2.9.0
  • Fortios: 6.0.6
  • ansibleのコネクションプラグイン: local(fortiosapiライブラリ利用)

実践

Firewallポリシーの追加・削除に利用するモジュール

ansible2.8にリリースされた、fortios_firewall_policyモジュールを利用します。

モジュールの公式リファレンスはこちら

ansible2.9からstateなどが加わっているようです。むしろ、これまでは削除ってどうしてたんだろう…

ポリシーの追加

以下のようなPlaybookでポリシーの追加が可能です。

---
- hosts: localhost 
  gather_facts: false
  vars:
    host: "10.254.254.254"
    username: "admin"
    password: "password"
    vdom: "root"
    ssl_verify: "False"

  tasks:
  - name: Configure Firewall Policy(add).
    fortios_firewall_policy:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
      vdom: "{{ vdom }}"
      ssl_verify: "{{ ssl_verify }}"
      state: "present"
      firewall_policy:
        policyid: "1"
        srcintf:
          - name: "internal"
        dstintf:
          - name: "wan1"
        srcaddr: 
          - name: "all"
        dstaddr: 
          - name: "all"
        service:
          - name: "ALL"
        schedule: "always"
        logtraffic: "disable"
        fsso: "disable"
        action: "accept"
        status: "enable"

ポリシーの削除

stateをabsentにして、policyidだけ書けば削除が可能でした。

---
- hosts: localhost 
  gather_facts: false
  vars:
    host: "10.254.254.254"
    username: "admin"
    password: "password"
    vdom: "root"
    ssl_verify: "False"

  tasks:
  - name: Configure Firewall Policy (Delete)
    fortios_firewall_policy:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
      vdom: "{{ vdom }}"
      ssl_verify: "{{ ssl_verify }}"
      state: "absent"
      firewall_policy:
        policyid: "1"

ポリシーの編集

編集したいポリシーのpolicyidを指定することで、上書きすることが可能でした。

---
- hosts: localhost 
  gather_facts: false
  vars:
    host: "10.254.254.254"
    username: "admin"
    password: "password"
    vdom: "root"
    ssl_verify: "False"

  tasks:
  - name: Configure Firewall Policy (modify).
    fortios_firewall_policy:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
      vdom: "{{ vdom }}"
      ssl_verify: "{{ ssl_verify }}"
      state: "present"
      firewall_policy:
        policyid: "1"
        srcintf:
          - name: "internal"
        dstintf:
          - name: "wan1"
        srcaddr: 
          - name: "all"
        dstaddr: 
          - name: "all"
        service:
          - name: "SSH" # もともとは”ALL"
        schedule: "always"
        logtraffic: "disable"
        fsso: "disable"
        action: "accept"
        status: "enable"

↓変更後のポリシー

冪等性があるか?

全く同じPlaybookを2回実行しても、changed=1になってしまいました…

とはいえ、結果的には同じ内容になるので、changedになるのを知ってさえいれば問題なさそうです。

わかったこと

想像通りな感じで、ポリシーについては、それほど支障なく追加・削除・編集はできそうです。

また、連続でPlaybook書いてると、やっぱりhttpapi側に切り替えたくなってきました…長い…

httpapiで操作する環境についてのブログをよこちさんが書いてくださったので、そちらも参考にしてください!

tekunabe.hatenablog.jp

ただ、公式なこのモジュールの説明見ても、httpapi使えるとは書いてないんですよね。修正漏れなのか、本当に使えないのか…httpapiの環境が動いたらやってみます。

気を付けていただきたいこと

ansibleによるFortigateの操作は、原則としてFortigateのREST APIを利用しており、これはCLIとは異なります。

なので、しっかりと動作試験を行ったうえで利用してください。やってはいけない例を1つご紹介します。

やってはいけないPlaybook

以下のようなPlaybookを実行したとします。

---
- hosts: localhost 
  gather_facts: false
  vars:
    host: "10.254.254.254"
    username: "admin"
    password: "password"
    vdom: "root"
    ssl_verify: "False"

  tasks:
  - name: Configure Firewall Policy Ng Pattern (Removed at Reboot).
    fortios_firewall_policy:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
      vdom: "{{ vdom }}"
      ssl_verify: "{{ ssl_verify }}"
      state: "present"
      firewall_policy:
        policyid: "1"
        srcintf:
          - name: "internal"
        dstintf:
          - name: "wan1"
        srcaddr: 
          - name: "all"
        dstaddr: 
          - name: "all"
        service:
          - name: "ALL"
        action: "accept"
        status: "enable"

↓実行結果はok,changedで成功した時と同じような感じ。

-vつけてもこんなかんじ。ステータスコードは200でSuccess。

このときのWebUIの画面(これが冒頭で出した、NGなポリシーのもの)

この状態で再起動するとどうなるか?

以下のようなログが出ます。

再起動するとポリシーはどうなるか?

消えます…

Configからも消え去ります。

なぜこうなるのか?

REST APIを使うことで、CLIでカバーされているエラーチェックが動かないことが主な要因のようです。

この状態のConfigは以下のようになります。

config firewall policy
    edit 1
        set uuid ee645184-fec9-51e9-d467-0b6fb1a24d6d
        set srcintf "internal"
        set dstintf "wan1"
        set srcaddr "all"
        set dstaddr "all"
        set action accept
        set service "ALL"
    next
end

キーとなるのはなんなのか?

あまり細かくは試していませんが、NGなPlaybookの中に「schedule: "always"」だけ加えたら、再起動後も設定は維持されていました。

CLIだとどうなるか?

CLIで同じように、「schedule」の指定を抜いた状態でendしようとすると、怒られました。

この状態でポリシーは動作するのか?

pingレベルでは動いてしまいました…(1.1.1.254は暫定で付与したwan1のIP)

やってはいけないことをやってみて

アンチパターンは貴重な財産ですね。

今回のは、CLIで普段設定する人からすると当たり前かもしれませんが、Playbookを手抜きしていた結果、見つけることができました。

今後もこういうのを見つけたらどんどん載せていきたいと思います。