LoginSignup
5
1

More than 1 year has passed since last update.

Azure の Public IP Ranges から IOS の ACL 設定を生成

Last updated at Posted at 2022-12-14

本記事は Microsoft Azure Tech Advent Calendar 2022 の 15 日目の記事です。

Azure では、Azure 上で利用され得る Public IP (Global IP) の範囲を公開しています。
今回は上記の Public IP 範囲をもとに Cisco IOS の ACL 設定を自動生成する Python スクリプトを紹介します。

ネットワーク自動化や Azure SDK for Python をいじる際のチュートリアルとしても手頃な題材かと思うので、これらに興味のある方も是非ご覧ください。

はじめに

Azure では、以下のサイトで Azure 上で利用され得る Public IP の範囲を公開しています。
Azure IP Ranges and Service Tags – Public Cloud

Public IP は サービスタグ 毎に公開されており、例えば AzureActiveDirectory サービスタグであれば 2022/12/13 時点で以下の様な形式で IP 範囲が記載されています。

json
    {
      "name": "AzureActiveDirectory",
      "id": "AzureActiveDirectory",
      "properties": {
        "changeNumber": 13,
        "region": "",
        "regionId": 0,
        "platform": "Azure",
        "systemService": "AzureAD",
        "addressPrefixes": [
          "13.64.151.161/32",
          "13.66.141.64/27",
          "13.67.9.224/27",
        ---一部省略---
          "2603:1047:1::/48",
          "2603:1056:2000::/48",
          "2603:1057:2::/48"
        ],
        "networkFeatures": [
          "API",
          "NSG",
          "UDR",
          "FW",
          "VSE"
        ]
      }
    },

時々、上記の IP 範囲をベースに通信制御を適用したいといった要件を耳にします。

Azure 上であれば NSG や Azure Firewall によって通信制御を自動化出来ますが、オンプレミス上の機器はそうもいきません。

本記事の目的は、上記の Public IP 範囲から以下の様な Cisco IOS の ACL 設定を生成することです。

Config
ip access-list extended IPv4AzureActiveDirectoryRanges
 10 permit ip any 13.64.151.161 0.0.0.0
 11 permit ip any 13.66.141.64 0.0.0.31
 12 permit ip any 13.67.9.224 0.0.0.31  …続く

ipv6 access-list IPv6AzureActiveDirectoryRanges
 sequence 10 permit ip any 2603:1047:1::/48
 sequence 11 permit ip any 2603:1056:2000::/48
 sequence 12 permit ip any 2603:1057:2::/48 …続く

コードサンプル

以下の GitHub にアップロードしました。
https://github.com/atusi0628/pub_cisco_automation_with_python

本記事では上記のコードを順を追って解説していきます。
また、上記コードはあくまでサンプルですので、利用は自己責任でお願いします。

ACL 設定自動生成の全体像

image.png

以下に各ステップ毎に重要なコンポーネントを記載します。

  1. リクエストを送信し、Public IP 範囲を取得

    • Azure SDK for Python -> API を叩く操作を簡素化するために利用。
    • Service Principal -> API を叩く際の認証に利用。
    • Service Tag Discovery API -> Public IP 範囲の取得に利用。
  2. IOS の ACL 設定を生成

    • Jinja2 -> ACL 設定の生成に利用。
  3. ACL 設定を投入

    • Netmiko -> IOS への SSH & 設定投入に利用。

1. リクエストを送信し、Public IP 範囲を取得

リクエストを送信し、Public IP 範囲を取得するために以下のコードを利用します。

Python
from azure.identity import ClientSecretCredential
from azure.mgmt.network import NetworkManagementClient

# Credentials for Azure
SUBSCRIPTION_ID = "<Your subscription id>"
TENANT_ID = "<Your AAD tenant id>"
CLIENT_ID = "<Your app id obtain from service principal>"
SECRET = "<Your app secret obtain from service principal>"

credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, SECRET)
network_client = NetworkManagementClient(credential, SUBSCRIPTION_ID)
result = network_client.service_tags.list('japaneast')

Azure SDK for Python について

Azure SDK for Python は、 Python から Azure リソースの管理やプロビジョニングを行うための SDK です。
Python 用 Azure ライブラリ (SDK) を使用する

Azure の REST API を利用する際の認証処理や API を叩く処理を容易に実装できます。
今回はこの SDK を利用して、後述する Service Principal による認証や、Service Tag Discovery API を叩く処理を実装します。

Service Principal について

Azure の REST API を叩く際、主に以下の 4 つの認証方法が用意されており、今回は Service Principal による認証を利用します。
Azure Identity client library for Python

  • Authenticate Azure hosted applications
    -> 環境変数やVaultから資格情報を読み取って認証/認可する方式

  • Authenticate service principal
    -> サービスプリンシパルの情報から認証/認可する方式

  • Authenticate users
    -> ブラウザやデバイスコードによって認証/認可する方法

  • Authenticate via development tools
    -> Azure CLI や VSCode を介して認証/認可する方法

Service Principal とは

Service Principal は、 Azure リソース (API) にアクセスするアプリケーションを識別をし、認証やアクセス制御を行うための仕組みです。
Azure CLI で Azure サービス プリンシパルを作成する

パスワードによる認証と証明書による認証を選択可能ですが、今回は手軽なパスワード認証を採用します。
上記ドキュメントにも記載がありますが、以下の通り認証情報を発行します。

Command line
// login
az login

// Create Service Principal
az ad sp create-for-rbac --name pyApp
{
  "appId": “xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”,
  "displayName": “pyApp",
  "password": “yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy”,
  "tenant": “zzzzzzzz-zzzz-zzzz-zzzzz-zzzzzzzzzzzz”
}

// Assign Contoributor Role
az role assignment create --assignee <appId> --role Contributor

Service Principal を用いて認証するコード

事前に以下の認証用のライブラリをインストールしておきます。

Command line
pip install azure-identity
Python
from azure.identity import ClientSecretCredential
from azure.mgmt.network import NetworkManagementClient

# Credentials for Azure
SUBSCRIPTION_ID = "<Your subscription id>"
TENANT_ID = "<Your AAD tenant id>"
CLIENT_ID = "<Your app id obtain from service principal>"
SECRET = "<Your app secret obtain from service principal>"

credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, SECRET)

先ほど生成した認証情報を ClientSecretCredential の引数に設定することで認証が実施され、戻り値として API 操作用のクレデンシャルが渡されます。

今後はこのクレデンシャルを利用して API を叩きます。

上記の認証処理については下記ドキュメントが非常に分かりやすいです。
オンプレミスでホストされている Python アプリから Azure リソースに対して認証する

Service Tag Discovery API について

Service Tag Discovery API は、前述した Azure の Public IP 範囲を取得するための API です。
Service Tags - List

Service Tag Discovery API を叩くコード

Azure の REST API はサービス毎に API が分かれており、Service Tag Discovery API を利用するには以下のライブラリを事前にインストールします。

Command line
pip install azure-mgmt-network
Python
from azure.identity import ClientSecretCredential
from azure.mgmt.network import NetworkManagementClient

# Issue credential
credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, SECRET)
# Authntication 
network_client = NetworkManagementClient(credential, SUBSCRIPTION_ID)
# Get Public IP ranges from Service Tag Discovery API
result = network_client.service_tags.list('japaneast')

先ほど取得したクレデンシャルと自身のサブスクリプション ID を指定し、認証を行います。

引数にリージョン('japaneast')を指定していますが、API の仕様上指定が必須なだけで、取得できる Public IP 範囲をリージョンに限定するようなものではありません。

取得した Public IP 範囲の中身を確認してみる

Python
result = network_client.service_tags.list('japaneast')
target_name = "AzureActiveDirectory"
for item in result.values:
    if item.name == target_tag_name:
        address_prefixes = item.properties.address_prefixes
        break
print(address_prefixes)

サービスタグ AzureActiveDirectory に含まれる address_prefixes を抽出しています。
以下の通り、IPv4 と IPv6 アドレスが混在したリストが応答されます。

json
[
    '13.64.151.161/32', 
    '13.66.141.64/27', 
    '13.67.9.224/27',
    ---一部省略---
    '2603:1006:2000::/48', 
    '2603:1007:200::/48', 
    '2603:1016:1400::/48',
]

2. IOS の ACL 設定を生成

Python のテンプレートエンジンである Jinja2 を利用し、ACL 設定を生成するコードを書きます。

Python
import ipaddress
import jinja2

# Target service tag name
target_tag_name = "<Target service tag name>"

# Create lists used to generate config file
ipv4_sequence = 10
ipv6_sequence = 10
ipv4_list = []
ipv6_list = []

for item in address_prefixes:
    address_object = ipaddress.ip_network(item)

    # Create list for IPv4 ACL config
    if address_object.version == 4:
        # Get IP address
        address = str(address_object.network_address)
        # Get wildcard mask
        wildcard = str(address_object.hostmask)
        ipv4_list.append({'sequence':ipv4_sequence, 'address': address, 'wildcard': wildcard})
        ipv4_sequence += 1
    # Create list for IPv6 ACL config
    else:
        # Get IP address and subnet mask
        address_mask = str(address_object)
        ipv6_list.append({'sequence': ipv6_sequence, 'address_mask': address_mask})
        ipv6_sequence += 1

# Generate config from ipv4_list
with open('ios_acl_ipv4.conf', 'w') as f:
    env = jinja2.Environment(loader=jinja2.FileSystemLoader('./templates/'))
    template = env.get_template('ios_acl_ipv4.j2')
    result = template.render(ipv4_list=ipv4_list, target_tag_name=target_tag_name)
    f.write(result)

# Generate config from ipv6_list
with open('ios_acl_ipv6.conf', 'w') as f:
    env = jinja2.Environment(loader=jinja2.FileSystemLoader('./templates/'))
    template = env.get_template('ios_acl_ipv6.j2')
    result = template.render(ipv6_list=ipv6_list, target_tag_name=target_tag_name)
    f.write(result)
Jinja2 template - ios_acl_ipv4.j2
no ip access-list extended IPv4{{ target_tag_name }}Ranges
ip access-list extended IPv4{{ target_tag_name }}Ranges
{%- for item in ipv4_list %}
 {{ item.sequence }} permit ip any {{ item.address }} {{ item.wildcard }}
{%- endfor -%}
Jinja2 template - ios_acl_ipv6.j2
no ipv6 access-list IPv6{{ target_tag_name }}Ranges
ipv6 access-list IPv6{{ target_tag_name }}Ranges
{%- for item in ipv6_list %}
 sequence {{ item.sequence }} permit ip any {{ item.address_mask }}
{%- endfor -%}

ipaddress ライブラリによる IP アドレスの操作

ipadress ライブラリを使うと、引き渡した IP アドレスが IPv4 or IPv6 かの判定や、サブネットマスクの抽出、ワイルドカードマスクの計算等を容易に行えます。
ipaddressモジュールの紹介

先ほど確認したように、Service Tag Discovery API からは IPv4 と IPv6 アドレスが混在したリストが応答されるのに加え、Cisco IOS の IPv4 ACL 設定はワイルドカードマスクの計算が必要なため、本ライブラリを使用します。

事前に ipaddress ライブラリをインストールしておきます。

pip3 install ipaddress
Python
import ipaddress

# Target service tag name
target_tag_name = "<Target service tag name>"

# Create lists used to generate config file
ipv4_sequence = 10
ipv6_sequence = 10
ipv4_list = []
ipv6_list = []

for item in address_prefixes:
    address_object = ipaddress.ip_network(item)

    # Create list for IPv4 ACL config
    if address_object.version == 4:
        # Get IP address
        address = str(address_object.network_address)
        # Get wildcard mask
        wildcard = str(address_object.hostmask)
        ipv4_list.append({'sequence':ipv4_sequence, 'address': address, 'wildcard': wildcard})
        ipv4_sequence += 1
    # Create list for IPv6 ACL config
    else:
        # Get IP address and subnet mask
        address_mask = str(address_object)
        ipv6_list.append({'sequence': ipv6_sequence, 'address_mask': address_mask})
        ipv6_sequence += 1

以下の部分で IPv4 or IPv6 アドレスの判定を行っています。

if address_object.version == 4:
else:

IPv4 ACL の場合

IPv4 の場合、以下の部分でシーケンス番号、IP アドレス、ワイルドカードマスクを取得しています。

# Get IP address
address = str(address_object.network_address)
# Get wildcard mask
wildcard = str(address_object.hostmask)
ipv4_list.append({'sequence':ipv4_sequence, 'address': address, 'wildcard': wildcard})
ipv4_sequence += 1

例えば、AzureActiveDirectory サービスタグを指定して、最初に 13.64.151.161/32 という IP 範囲が引き渡された場合、各変数には以下のような値が入ります。

  • target_tag_name = AzureActiveDirectory
  • sequece = 10
  • address = 12.64.151.161
  • wildcard = 0.0.0.0

将来的に上記の値は、以下のように Cisco IOS のコンフィグに落とし込みます。

ip access-list extended IPv4AzureActiveDirectoryRanges
 10 permit ip any 13.64.151.161 0.0.0.0

IPv6 ACL の場合

IPv6 の場合、以下の部分でシーケンス番号、Prefix (IP アドレス + サブネットマスク)を取得しています。

Python
# Get IP address and subnet mask
address_mask = str(address_object)
ipv6_list.append({'sequence': ipv6_sequence, 'address_mask': address_mask})
ipv6_sequence += 1

例えば、AzureActiveDirectory サービスタグを指定して、最初に 13.64.151.161/32 という IP 範囲が引き渡された場合、各変数には以下のような値が入ります。

  • target_tag_name = AzureActiveDirectory
  • sequece = 10
  • address_mask = 2603:1006:2000::/48

将来的に上記の値は、以下のように Cisco IOS のコンフィグに落とし込みます。

ipv6 access-list IPv6AzureActiveDirectoryRanges
 sequence 10 permit ip any 2603:1006:2000::/48

Jinja2 とは

Python で利用可能なテンプレートエンジンで、Python コードから変数等を受け取り、それをもとに動的にファイルを生成することが出来ます。
Template Designer Documentation

ざっくりですが、主な構文は以下の通りです。

  • {% %} -> Statement (for 文や if 文を埋め込む際に使用)
  • {{ }} -> Expressions (変数を展開する際に使用)
  • {# #} -> Comments (コメントを記述する際に使用)

以下の通り、事前に jinja2 ライブラリをインストールしておきます。

pip3 install jinja2

IPv4 ACL 設定を生成

Python
# Generate config from ipv4_list
with open('ios_acl_ipv4.conf', 'w') as f:
    env = jinja2.Environment(loader=jinja2.FileSystemLoader('./templates/'))
    template = env.get_template('ios_acl_ipv4.j2')
    result = template.render(ipv4_list=ipv4_list, target_tag_name=target_tag_name)
    f.write(result)

以下の部分でコンフィグファイル生成に使用する Jinja2 テンプレートを指定しています。

env = jinja2.Environment(loader=jinja2.FileSystemLoader('./templates/'))
template = env.get_template('ios_acl_ipv4.j2')

以下の部分で変数を引き渡してレンダリングをしています。
ipv4_list は 先ほど生成した IPv4 ACL 設定用の辞書を引き渡し、target_tag_name には Public IP 範囲を取得したサービスタグ名が入り、ACL 名の設定に利用します。

result = template.render(ipv4_list=ipv4_list, target_tag_name=target_tag_name)

テンプレートは以下の様になっており、ACL 名および ACL の各エントリを、引き渡した変数に応じて動的に生成できます。

Jinja2 template - ios_acl_ipv4.j2
no ip access-list extended IPv4{{ target_tag_name }}Ranges
ip access-list extended IPv4{{ target_tag_name }}Ranges
{%- for item in ipv4_list %}
 {{ item.sequence }} permit ip any {{ item.address }} {{ item.wildcard }}
{%- endfor -%}

例えば、 AzureActiveDirectory サービスタグを指定していた場合は、以下のようなコンフィグファイルが出力されます。

no ip access-list extended IPv4AzureActiveDirectoryRanges
ip access-list extended IPv4AzureActiveDirectoryRanges
 10 permit ip any 13.64.151.161 0.0.0.0
 11 permit ip any 13.66.141.64 0.0.0.31
 12 permit ip any 13.67.9.224 0.0.0.31 ...続く

※ 先頭に no ip access-list を入れているのは、2 回目 3 回目を実行した際に ACL 設定を競合させないためです。
※ 非常に雑なので、実運用で使う際は既存設定と新規設定の diff を取り、差分だけを反映させるようなロジックを組んだほうが良いと思います。

IPv6 ACL 設定を生成

Python
# Generate config from ipv6_list
with open('ios_acl_ipv6.conf', 'w') as f:
    env = jinja2.Environment(loader=jinja2.FileSystemLoader('./templates/'))
    template = env.get_template('ios_acl_ipv6.j2')
    result = template.render(ipv6_list=ipv6_list, target_tag_name=target_tag_name)
    f.write(result)

以下の部分でコンフィグファイル生成に使用する Jinja2 テンプレートを指定しています。

env = jinja2.Environment(loader=jinja2.FileSystemLoader('./templates/'))
template = env.get_template('ios_acl_ipv6.j2')

以下の部分で変数を引き渡してレンダリングをしています。
ipv6_list は 先ほど生成した IPv4 ACL 設定用の辞書を引き渡し、target_tag_name には Public IP 範囲を取得したサービスタグ名が入り、ACL 名の設定に利用します。

result = template.render(ipv6_list=ipv6_list, target_tag_name=target_tag_name)

テンプレートは以下の様になっており、ACL 名および ACL の各エントリを、引き渡した変数に応じて動的に生成できます。

Jinja2 template - ios_acl_ipv6.j2
no ipv6 access-list IPv6{{ target_tag_name }}Ranges
ipv6 access-list IPv6{{ target_tag_name }}Ranges
{%- for item in ipv6_list %}
 sequence {{ item.sequence }} permit ip any {{ item.address_mask }}
{%- endfor -%}

例えば、 AzureActiveDirectory サービスタグを指定していた場合は、以下のようなコンフィグファイルが出力されます。

no ipv6 access-list IPv6AzureActiveDirectoryRanges
ipv6 access-list IPv6AzureActiveDirectoryRanges
 sequence 10 permit ip any 2603:1006:2000::/48
 sequence 11 permit ip any 2603:1007:200::/48
 sequence 12 permit ip any 2603:1016:1400::/48 ...続く

3. ACL 設定を投入

Cisco IOS への設定投入には Python の Netmiko ライブラリを利用します。

Netmiko とは

Netmiko は、Python からネットワーク機器への SSH/Telnet 接続や、 コマンド/コンフィグ 投入等の操作を容易に行うためのライブラリです。
https://github.com/ktbyers/netmiko

設定の投入方法はいくつかありますが、今回はコンフィグファイルを一括で投入する方法を採用します。

  • send_command … コマンドを1つ送信
  • send_config_set … リスト内のコマンドを先頭から送信
  • send_config_from_file … configファイルの内容を先頭から送信
  • send_command_timing … 指定の時間コマンドのアウトプットを待機

以下のライブラリを事前にインストールしておきます。

pip3 install netmiko

Netmiko による SSH 接続 + コンフィグ投入

Python
from netmiko import ConnectHandler

# IOS information and credential
cisco_ios = {
        'device_type': 'cisco_ios',
        'host': '<Your management ip>',
        'username': '<Your username>',
        'password': '<Your password>',
        'port': 22
        }

# Connect IOS
ssh = ConnectHandler(**cisco_ios)
ssh.enable()

# Send config
ssh.send_config_from_file('ios_acl_ipv4.conf')
ssh.send_config_from_file('ios_acl_ipv6.conf')

# Close connection
ssh.disconnect()

以下の部分で認証情報を定義し、SSH 接続及び特権モードへの昇格を行っています。
認証情報の形式については、下記にサンプルがあります。
https://github.com/ktbyers/netmiko#create-a-dictionary-representing-the-device

# IOS information and credential
cisco_ios = {
        'device_type': 'cisco_ios',
        'host': '<Your management ip>',
        'username': '<Your username>',
        'password': '<Your password>',
        'port': 22
        }

# Connect IOS
ssh = ConnectHandler(**cisco_ios)
ssh.enable()

以下の部分で先ほど生成したコンフィグファイルを投入し、 SSH 接続を切断しています。

# Send config
ssh.send_config_from_file('ios_acl_ipv4.conf')
ssh.send_config_from_file('ios_acl_ipv6.conf')

# Close connection
ssh.disconnect()

IOS 側でコンフィグが投入されていることを確認

以下の通り、設定が反映されています。

show running-config | section ip access-list

ip access-list extended IPv4AzureActiveDirectoryRanges
 10 permit ip any host 13.64.151.161
 11 permit ip any 13.66.141.64 0.0.0.31
 12 permit ip any 13.67.9.224 0.0.0.31
 13 permit ip any 13.69.66.160 0.0.0.31
 14 permit ip any 13.69.229.96 0.0.0.31
 15 permit ip any 13.70.73.32 0.0.0.31 ...続く


show running-config | section ipv6 access-list
ipv6 access-list IPv6AzureActiveDirectoryRanges
 sequence 10 permit ipv6 any 2603:1006:2000::/48
 sequence 11 permit ipv6 any 2603:1007:200::/48
 sequence 12 permit ipv6 any 2603:1016:1400::/48
 sequence 13 permit ipv6 any 2603:1017::/48
 sequence 14 permit ipv6 any 2603:1026:3000::/48
 sequence 15 permit ipv6 any 2603:1027:1::/48 ...続く

まとめ

Azure の Public IP アドレス範囲を基に、オンプレミスの機器で通信制御を行う構成を時々耳にするので、今回はそれを自動化するスクリプトを紹介してみました。

紹介したスクリプトは冪等性の確保やエラー処理が甘いので、もし利用する際は運用に耐えうるカスタマイズをしてもらえればと思います。

ネットワーク自動化や Azure SDK for Python をいじる際のチュートリアルとしても手頃な題材かと思うので、これらに興味のある方も是非お試しください!

参考情報

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1