ふらふらエンジニアのメモ帳

セキュリティおべんきょちゅ( ˘ω˘ )スヤァ…

AnsibleでCisco FTDを操作する

Ciscoが提供する次世代ファイアウォール Firepower Threat Defence(FTD)を操作するためのAnsibleモジュールftd_configurationを使って、REST API経由でネットワークオブジェクトやアクセスルールの追加を行ってみました。

FTD API概要

FTDの設定変更方法としては、CLIやWeb GUIに加え、バージョン6.2.3からREST APIもサポートされています。GUIREST APIの場合は、管理ツール経由で操作する形になります。

FTDの管理ツールは以下の3種類があります。
1. Firepower Management Center (FMC)
複数のデバイスを一括管理可能。大規模環境向き。(ただし2や3との共存は不可。)
2. Firepower Device Manager (FDM)
単一のデバイスを”On Box(同一筐体上)”で管理。中~小規模環境向き。
3. Cisco Defense Orchestrator (CDO)
クラウドベースで複数のデバイスを管理。

これらの内、ftd_xxxモジュールは、2つめのFDM経由で操作するモジュールになります。
(1や3のAnsibleモジュールは、2019年11月時点でリリースされていません。)

f:id:tsukino_netwkr:20191117001155p:plain
FDM画面

API経由で操作できる項目は以下の通りです。

  • Interface Configuration (incl. BVI, Sub-interfaces)
  • Routing (static routing only)
  • VPN (Site-to-site and Remote Access)
  • System Settings (Interface, NTP, Hostname, etc.)
  • Policy Objects, such as Networks, Ports, Security Zones, Applications, URLs, Geolocation
  • Policies (SSL, Identity, Security Intel, Blacklists, NAT, Access, Intrusion)
  • Talos updates
  • Smart Licensing
  • Backup Restore
  • Troubleshooting

参考URL: DevNet Learning Labs - Firepower Threat Defense API

FTD Ansibleモジュール概要

FTD用のモジュールは、執筆時点で以下の4つがあります。

  1. ftd_configuration
    REST API経由でFTDを操作するモジュール
  2. ftd_file_download
    HTTP(S)経由でFTDから各種ファイル(pending changes, disk files, certificates, troubleshoot reports, and backup)をダウンロードするモジュール
  3. ftd_file_upload
    HTTP(S)経由でFTDへ各種ファイルをアップロードするモジュール
  4. ftd_install
    ROMMONイメージやFTD pkgイメージをインストールするモジュール

一見少なく見えますが、1つ目のftd_configurationモジュールで、利用可能なAPIをすべてAnsibleから実行できるようです。具体的には、operationオプションで、各設定項目名の頭に以下を付けることで、CRUDに近い形で操作できます。

  • get - fetches a object by its ID (e.g., getNetworkObject);
  • getList - fetches a list of objects matching given criteria (e.g., getNetworkObjectList). For example, it can be used to find an object by name or other attribute when its ID is not known;
  • add - creates a new object (e.g., addNetworkObject);
  • edit - updates an existing object (e.g., editNetworkObject). ID of the existing object is a mandatory attribute for this operation type;
  • delete - deletes an existing object by its ID (e.g., deleteNetworkObject);
  • upsert - creates a new object if it does not exist, or updates it when the object already exists (e.g., upsertNetworkObject). By default, upsert operation looks for an object by its name, but the filtering criteria can be adjusted. See Upsert operation for more details;

この中でupsertは、存在しない場合新規作成(Create)したり、既に存在する場合に更新(Update)できるもので、メンテナンス時は使い勝手が良さそうです。 詳細は、Cisco DevNet - FTD AnsibleのUser GuideやOperationsを参照願います。

その他、Playbookを作る上で参考になりそうなサイトを記載しておきます。

  1. Ansible Network modules - Ftd
    Ansibleの公式ドキュメント。まず初めに目を通す。
  2. GitHub - FTD Ansible / samples
    Playbookサンプルが豊富。どのバージョンで動くかもまとめられていて便利。
  3. FDM - API Explorer https://<FDMのIPアドレス>/#/api-explorer
    REST APIの詳細を確認可能。

用意した環境

  • FTD:FTDv 6.4.0 VMware install package
    VMware Workstation Player 15.5.0上に構築
    ※ URLベースのアクセスルールを設定する場合は、URL Licenseの有効化が必要
  • Ansible:2.9.0

Cisco DevNet Sandbox「FirePower Threat Defense REST API (6.5)」だと、ネットワークオブジェクトの作成は出来るものの、アクセスルールの作成がエラーで出来ませんでした。

Inventoryファイル

以下の通り作成しました。

  • inventory_ftd_vm
[ftd]
firepower ansible_host=192.168.100.45

[ftd:vars]
ansible_network_os=ftd
ansible_user=admin
ansible_password=<FDMのパスワード>
ansible_httpapi_use_ssl=yes
ansible_httpapi_validate_certs=no

Playbook① ポリシー作成

  1. ホストオブジェクトの作成
    1つ目のタスクで、operationオプションupsertNetworkObjectを指定し、IPアドレス192.168.2.1に紐づくネットワークオブジェクトAnsible-network-hostを作成しています。
    2つ目のタスクで、operationオプションgetNetworkObjectを指定し、objId(オブジェクトID)をキーに情報取得を行い、3つ目のタスクで結果を表示しています。
  2. アクセスルールの作成
    後半は、operationオプションupsertAccessRuleで、Ansible-network-hostからの通信を許可するアクセスルールを設定し、同様に結果を表示しています。

playbook_ftd_acl_add2.yml

---

- hosts: ftd
  gather_facts: no
  connection: httpapi

  tasks:
    - name: Create a network object
      ftd_configuration:
        operation: upsertNetworkObject
        data:
          name: Ansible-network-host
          description: From Ansible with love
          subType: HOST
          value: 192.168.2.1
          dnsResolution: IPV4_AND_IPV6
          type: networkobject
        register_as: hostNetwork

    - name: Get network object
      ftd_configuration:
        operation: getNetworkObject
        path_params:
          objId: "{{ hostNetwork['id'] }}"
        register_as: network_obj

    - name: Display network object
      debug:
        msg: "{{ network_obj }}"

    - name: Create an access rule allowing traffic from Ansible-network-host
      ftd_configuration:
        operation: upsertAccessRule
        data:
          name: Allow_Ansible_Traffic
          type: accessrule
          sourceNetworks:
            - "{{ hostNetwork }}"
          ruleAction: PERMIT
          eventLogAction: LOG_BOTH
        path_params:
          parentId: default
        register_as: accessRule

    - name: Get access rule
      ftd_configuration:
        operation: getAccessRule
        path_params:
          parentId: default
          objId: "{{ accessRule['id'] }}"
        register_as: access_rule

    - name: Display access rule
      debug:
        msg: "{{ access_rule }}"

実行結果① ポリシー作成

changed=2となり、問題なくオブジェクトとアクセスルールが設定されました。
ちなみに、同じPlaybookをもう一回実行すると、changed=0になりました。また、operationaddNetworkObjectaddAccessRuleとした場合も同様の結果だったため、冪等性は担保されているようです。

$ ansible-playbook -i inventory_ftd_vm playbook_ftd_acl_add2.yml

PLAY [ftd] **********************************************************************************************************************************

TASK [Create a network object] **************************************************************************************************************
changed: [firepower]

TASK [Get network object] *******************************************************************************************************************
ok: [firepower]

TASK [Display network object] ***************************************************************************************************************
ok: [firepower] => {
    "msg": {
        "description": "From Ansible with love",
        "dnsResolution": "IPV4_AND_IPV6",
        "id": "c9b4138b-087c-11ea-948b-9bbf3850c347",
        "isSystemDefined": false,
        "links": {
            "self": "https://192.168.100.45/api/fdm/v3/object/networks/c9b4138b-087c-11ea-948b-9bbf3850c347"
        },
        "name": "Ansible-network-host",
        "subType": "HOST",
        "type": "networkobject",
        "value": "192.168.2.1",
        "version": "hs4yv7xe7yk34"
    }
}

TASK [Create an access rule allowing traffic from Ansible-network-host] *********************************************************************
changed: [firepower]

TASK [Get access rule] **********************************************************************************************************************
ok: [firepower]

TASK [Display access rule] ******************************************************************************************************************
ok: [firepower] => {
    "msg": {
        "destinationNetworks": [],
        "destinationPorts": [],
        "destinationZones": [],
        "embeddedAppFilter": null,
        "eventLogAction": "LOG_BOTH",
        "filePolicy": null,
        "id": "cc1ef90e-087c-11ea-948b-5f82967c1861",
        "identitySources": [],
        "intrusionPolicy": null,
        "links": {
            "self": "https://192.168.100.45/api/fdm/v3/policy/accesspolicies/default/accessrules/cc1ef90e-087c-11ea-948b-5f82967c1861"
        },
        "logFiles": false,
        "name": "Allow_Ansible_Traffic",
        "ruleAction": "PERMIT",
        "ruleId": 268435462,
        "sourceNetworks": [
            {
                "id": "c9b4138b-087c-11ea-948b-9bbf3850c347",
                "name": "Ansible-network-host",
                "type": "networkobject",
                "version": "hs4yv7xe7yk34"
            }
        ],
        "sourcePorts": [],
        "sourceZones": [],
        "syslogServer": null,
        "type": "accessrule",
        "urlFilter": null,
        "users": [],
        "version": "jk3l2ohh5ku2v"
    }
}

PLAY RECAP **********************************************************************************************************************************
firepower                  : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

FDMのWeb GUI画面でも、問題なく設定されている事が分かります。

f:id:tsukino_netwkr:20191116234720p:plain
FDM画面(ネットワークオブジェクト設定)
f:id:tsukino_netwkr:20191116234817p:plain
FDM画面(ポリシー設定)

Playbook② デプロイ

この段階では、設定がFTDに反映されておらず、反映するにはデプロイが必要です。

f:id:tsukino_netwkr:20191116233805p:plain
FDM画面(Deploy前)

以下のPlaybookでデプロイを行います。

  • inventory_ftd_deploy.yml
---

- hosts: ftd
  gather_facts: no
  connection: httpapi

  tasks:
    - name: Fetch pending changes
      ftd_configuration:
        operation: getBaseEntityDiffList
        register_as: pending_changes

    - name: Complete playbook when nothing to deploy
      meta: end_play
      when: pending_changes | length == 0

    - name: Start deployment
      ftd_configuration:
        operation: addDeployment
        register_as: deployment_job

    - name: Poll deployment status until the job is finished
      ftd_configuration:
        operation: getDeployment
        path_params:
          objId: '{{ deployment_job.id }}'
        register_as: deployment_status
      until: deployment_status.endTime != -1
      retries: 100
      delay: 3

    - name: Stop the playbook if the deployment failed
      fail:
        msg: 'Deployment failed. Status: {{ deployment_status.statusMessages }}'
      when: deployment_status.state != 'DEPLOYED'

出力結果② デプロイ

$ ansible-playbook -i inventory_ftd_vm playbook_ftd_deploy.yml

PLAY [ftd] **********************************************************************************************************************************

TASK [Fetch pending changes] ****************************************************************************************************************
ok: [firepower]

TASK [Start deployment] *********************************************************************************************************************
changed: [firepower]

TASK [Poll deployment status until the job is finished] *************************************************************************************
FAILED - RETRYING: Poll deployment status until the job is finished (100 retries left).
FAILED - RETRYING: Poll deployment status until the job is finished (99 retries left).
FAILED - RETRYING: Poll deployment status until the job is finished (98 retries left).
FAILED - RETRYING: Poll deployment status until the job is finished (97 retries left).
FAILED - RETRYING: Poll deployment status until the job is finished (96 retries left).
FAILED - RETRYING: Poll deployment status until the job is finished (95 retries left).
FAILED - RETRYING: Poll deployment status until the job is finished (94 retries left).
FAILED - RETRYING: Poll deployment status until the job is finished (93 retries left).
ok: [firepower]

TASK [Stop the playbook if the deployment failed] *******************************************************************************************
skipping: [firepower]

PLAY RECAP **********************************************************************************************************************************
firepower                  : ok=3    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0  

FDM画面でも、デプロイに成功し、保留中の変更がない事が確認できます。

f:id:tsukino_netwkr:20191116234611p:plain
FDM画面(Deploy後)

最後にFTDのCLI画面で、設定が反映されたことを確認します。

> show running-config
: Saved

:
: Serial Number: XXXXXXXX
: Hardware:   NGFWv, 8192 MB RAM, CPU Lynnfield 1991 MHz, 1 CPU (4 cores)
:
NGFW Version 6.4.0
!
hostname firepower
enable password ***** encrypted
strong-encryption-disable
names
no mac-address auto

!
interface GigabitEthernet0/0
 nameif outside
 cts manual
  propagate sgt preserve-untag
  policy static sgt disabled trusted
 security-level 0
 ip address 192.168.100.46 255.255.255.0
!
interface GigabitEthernet0/1
 nameif inside
 cts manual
  propagate sgt preserve-untag
  policy static sgt disabled trusted
 security-level 0
 ip address 10.1.1.2 255.255.255.0
!
(中略)
object network any-ipv4
 subnet 0.0.0.0 0.0.0.0
object network any-ipv6
 subnet ::/0
object network OutsideIPv4Gateway
 host 192.168.100.1
object network OutsideIPv4DefaultRoute
 subnet 0.0.0.0 0.0.0.0
object network Ansible-network-host
 host 192.168.2.1
object-group service |acSvcg-268435457
 service-object ip
object-group service |acSvcg-268435462
 service-object ip
access-list NGFW_ONBOX_ACL remark rule-id 268435457: ACCESS POLICY: NGFW_Access_Policy
access-list NGFW_ONBOX_ACL remark rule-id 268435457: L5 RULE: Inside_Outside_Rule
access-list NGFW_ONBOX_ACL advanced trust object-group |acSvcg-268435457 ifc inside any ifc outside any rule-id 268435457 event-log both
access-list NGFW_ONBOX_ACL remark rule-id 268435462: ACCESS POLICY: NGFW_Access_Policy
access-list NGFW_ONBOX_ACL remark rule-id 268435462: L5 RULE: Allow_Ansible_Traffic
access-list NGFW_ONBOX_ACL advanced permit object-group |acSvcg-268435462 object Ansible-network-host any rule-id 268435462 event-log both
access-list NGFW_ONBOX_ACL remark rule-id 1: ACCESS POLICY: NGFW_Access_Policy
access-list NGFW_ONBOX_ACL remark rule-id 1: L5 RULE: DefaultActionRule
access-list NGFW_ONBOX_ACL advanced deny ip any any rule-id 1
(中略)

最後に

今回試した範囲では、FTD 6.4.0では正しく設定が行われ、冪等性も問題ありませんでした。
まだ触り始めたばかりで不明点が多いので、引き続き勉強していきたいと思います。