REST-API
big-ip
iControl

(習作) 端末からBIG-IPにSSL証明書をインポートするコマンドを作ってみた

はじめに

iControl RESTの勉強の目的で、iControl RESTで次のことを行うPythonスクリプトを作成してみました。

  1. SSL証明書(サーバ証明書・中間証明書)、秘密鍵をBIG-IPにアップロードして
  2. BIG-IPのFile オブジェクトとしてインポートして
  3. 既存のClient SSL Profile内のcert, key, chainを更新する

動作イメージはこのような感じです。

ssl_file_importer.png

要するに、SSL証明書の更新の自動化です。(コマンドを手動実行する必要があるので、オペレーションの簡略化、というのが正確でしょうか。)

注意

実際に利用することを前提としたものではなく「個人がiControl RESTの学習のために作ってみた」ものに過ぎません。動くことは確認しましたが、例えば、どこかの処理でエラーが発生した場合の対処など、実用上必要になるだろう処理が多く抜けていると思います。

もし同様の機能を真面目に作成されたい方は、下記の参考情報のURLを参照するのが良いかと思います。

参考情報・引用元

  • DevCentral: Demystifying iControl REST Part 5: Transferring Files
    • "Demystifying iControl REST"は、F5のスタッフによって作成されたiControl RESTの入門用シリーズです。このPart5では、iControlでファイルを転送する方法が説明されていて、具体例として、SSL証明書、秘密鍵のアップロードが扱われてます。今回、ファイルのアップロードは、DevCentralに記載のコード(_upload メソッド)をそのまま拝借してます。

環境

以下の環境で作成・動作を確認しました。
- クライアント:Linux(Python 3.6.3)
- サーバ(BIG-IP):v12.1.2

概要(スクリプトの使い方)

完成したスクリプトを「bigip_ssl_importer.py」という名前とした場合、次のように実行します。

(bash) $ bigip_ssl_importer.py -b "接続するBIG-IPのホスト名 or IPアドレス" \
  -u "ユーザ名" \
  -p "パスワード" \
  -s "SSL Profile名" \
  -c "サーバ証明書ファイル"(任意) \
  -k "サーバ秘密鍵ファイル"(任意) \
  -i "中間証明書ファイル"(任意)

証明書や秘密鍵ファイルの指定は任意です。ただし、サーバ秘密鍵ファイルを指定するときは、サーバ証明書ファイルも指定しないと動作しないようにしています。

証明書や秘密鍵のフォーマットはPEM形式、パスワードおよびパスフレーズなしを前提としています。

コード説明

ここから下はソースコードです。コードは、ある程度の固まりごとに区切っていますが、基本的に上から全部マージすれば、動作するものとなっています。(一部はDevCentralの記事を参照する必要があります)

ヘッダ

#!/usr/bin/env python
# coding: utf-8

import sys
import os
import requests
import json
import urllib3
import argparse

今回使ったライブラリ。特に説明すべき事項はありません。

ファイルアップロード用メソッド

def _upload(host, creds, fp):

・・・
DevCentralの記事を参照)
・・・

これはDevCentralに記載のコードをそのまま使わせてもらいました。そのため、コード詳細は参考情報のリンク先を参照ください。

iControl RESTでアップロードするファイルは、アップロード時に指定するURLに応じて、2種類の異なるディレクトリに保存されます。(OSまたはHotfixのイメージファイルをアップロードする場合と、それ以外の2種類。)

今回のアップロードファイルは、/var/config/rest/downloads/に保存されます。この時点ではまだファイルがBIG-IPホスト内に転送されただけであり、設定内で扱えるファイルオブジェクトになっていません。

BIG-IPにアップロードされたファイルを、設定にインポートする処理

アップロードしたファイルを、SSLの証明書や秘密鍵のファイルオブジェクトとして登録します。iControl RESTリクエストのためのURLやJSONデータを準備して、リクエストを送信します。

class ssl_file_importer:
    def __init__(self, session, bigip_host):
        self.session = session
        self.payload = {}
        self.payload['name'] = ''
        self.payload['command'] = 'install'
        self.payload['from-local-file'] = ''
        self.url = 'https://%s/mgmt/tm/sys/crypto' % bigip_host

    def import_files(self, files):
        for file_type, file_name in files.items():
            self.payload['name'] = file_name
            self.payload['from-local-file'] = '/var/config/rest/downloads/%s' % file_name

            if (file_type == 'key'):
                self._api_exec('/key')
            else:
                self._api_exec('/cert')

    def _api_exec(self, api_url):
        api_url = self.url + api_url
        res = b.post(api_url, data=json.dumps(self.payload))
        return res

ここでBIG-IPに行なっている処理をtmshコマンドで行なった場合、以下のようなコマンドになります。

(tmos) $ install sys crypto cert (ファイルオブジェクト名) from-local-file /var/config/rest/downloads/(証明書ファイル名)

(tmos) $ install sys crypto key (ファイルオブジェクト名) from-local-file /var/config/rest/downloads/(秘密鍵ファイル名)

上記が、iControl RESTでは次のようなリクエストになります。

メソッド URL(Endpoint) Body(JSON)
POST /mgmt/tm/sys/crypto/cert (下に記載)
POST /mgmt/tm/sys/crypto/key (下に記載)
(cert)
{"name": "ファイルオブジェクト名", 
 "command": "install",
 "from-local-file": "/var/config/rest/downloads/証明書ファイル名"}

(key)
{"name": "ファイルオブジェクト名",
 "command": "install",
 "from-local-file": "/var/config/rest/downloads/秘密鍵ファイル名"}

Client SSL Profileの更新処理

インポートした証明書や秘密鍵を、指定のClient SSL Profileに紐づけます。

class ssl_profile_modifier:
    def __init__(self, session, bigip_host, profile_name):
        self.session = session
        self.profile_name = profile_name
        self.payload = {}
        self.url = 'https://%s/mgmt/tm/ltm/profile/client-ssl' % bigip_host
        res = self._get_cKC()
        self.payload = json.loads(res.text)

    def apply(self, files):
        if 'key' in files:
            self.payload['certKeyChain'][0]['key'] = files['key']
        if 'cert' in files:
            self.payload['certKeyChain'][0]['cert']  = files['cert']
        if 'chain' in files:
            self.payload['certKeyChain'][0]['chain']  = files['chain']

        res = self._api_exec()
        return res

    def _get_cKC(self):
        api_url = '%s/~Common~%s?$select=certKeyChain' % (self.url, self.profile_name)
        res = b.get(api_url)
        return res

    def _api_exec(self):
        api_url = '%s/~Common~%s' % (self.url, self.profile_name)
        res = b.patch(api_url, data=json.dumps(self.payload))
        return res

具体的には、Client SSL Profile内の「certKeyChain」というパラメータ内に、ファイルオブジェクトとしての証明書や秘密鍵を紐づけます。「certKeyChain」とパラメータ名が長いので、以下「cKC」と略します。

ここでBIG-IPに行なっている処理をtmshコマンドであらわすと、以下のようなコマンドになります。

(tmos) $ modify ltm profile client-ssl (profile名) { cert-key-chain replace-all-with { (cKC設定名) { cert (サーバ証明書ファイルオブジェクト) key (サーバ秘密鍵ファイルオブジェクト) chain (中間証明書ファイルオブジェクト) } }

certKeyChainパラメータについての補足事項

BIG-IPの仕様として、一つのclientssl profileに複数のcKC設定を入れることができます(つまり一つのclientssl profileに複数のSSL証明書・秘密鍵設定が紐づけられます)。

今回のスクリプトでは、複数のcKC設定には対応しません。cKC設定は1つのclient ssl profileに一つしか入っていないものを前提としてます。

一つのcKCの中には、cert, key, chain, passphrase 等の設定値があります。部分的な変更(例えば、certとkeyは変更してもchainは変更しないとか)に対応するには、一旦、現在のcKC内のパラメータを取得し、その後、コマンド実行時の引数の有無に従って、必要な箇所のみを更新します。

補足事項、ここまで。


上記の通り、cKC設定自体は複数登録可能な項目なので、iControl RESTでは、cKCの設定は配列で表されます。1つしかない前提なので、配列の[0]を対象に変更を行います。

iControl RESTでは次のようなリクエストになります。(サーバ証明書、サーバ秘密鍵、中間証明書を全て指定した場合)

メソッド URL(Endpoint) Body(JSON)
PACTCH /mgmt/tm/ltm/profile/client-ssl/~Common~(profile名) (下に記載)
{"certKeyChain": [
  {
    "name": "cKC設定名",
    "cert": "サーバ証明書のファイルオブジェクト名",
    "certReference": {
      "link": "(略)"
    }, 
    "chain": "中間証明書のファイルオブジェクト名",
    "chainReference": {
      "link": "(略)"
    },
    "key": "サーバ秘密鍵のファイルオブジェクト名",
    "keyReference": {
      "link": "(略)"
    }
  }
]}

メイン関数(残りの処理)

iControl RESTの動作はほぼ確認できたので、後の必要な処理は全部mainにしてしまいました。

引数の処理

parser = argparse.ArgumentParser()

parser.add_argument('-b', '--bigip', help='set connect big-ip hostname or ip address', required=True)
parser.add_argument('-u', '--user', help='set icontrol user name', required=True)
parser.add_argument('-p', '--password', help='set icontrol user password', required=True)
parser.add_argument('-s', '--sslprofile', help='set ssl profile name', required=True)
parser.add_argument('-c', '--cert', help='set cert file, if required.')
parser.add_argument('-k', '--key', help='set key file, if required.')
parser.add_argument('-i', '--chain', help='set intermediate(chain) file, if required.')

args = parser.parse_args()

cred = (args.user, args.password)
bigip_host    = args.bigip
ssl_profile   = args.sslprofile

files = {}
if (args.cert):  files['cert']  = args.cert
if (args.key):   files['key']   = args.key
if (args.chain): files['chain'] = args.chain

引数の処理にargparseを使い、上から4つ(接続先BIG-IPの指定、ユーザ名とパスワード、SSL Profile名)は指定が必須で、証明書、秘密鍵ファイルについては任意としました。

sessionオブジェクトの初期化

b = requests.session()
b.auth = cred
b.verify = False
b.headers.update({'Content-Type':'application/json'})

iControl RESTのセッションオブジェクトです。

処理の実行

# もしkeyファイルをimportする場合は、必ずcertも同時にimportすること
# certファイルについては、単体でimportしても良い
if 'key' in files and 'cert' not in files:
    sys.stderr.write('Error: Key file should import with Cert file.')
    sys.exit()

# cert file, key fileをiControlでBIG-IPにUploadする
for file in files.values():
    _upload(bigip_host, cred, file)

# UploadしたCertとkeyをBIG-IP内でConfigにImportする
importer = ssl_file_importer(b, bigip_host)
importer.import_files(files)

# ssl profileの設定を変更する
ssl_prof_modifier = ssl_profile_modifier(b, bigip_host, ssl_profile)
ssl_prof_modifier.apply(files)

コードについては以上です。


補足事項(画像について)

今回、最初のイメージを作るのに、BIG-IPのステンシルを以下から利用しました。

askF5: K682: F5 logo, product images, and Visio stencils

今回、これをdraw.ioで読み込ませるのに一番苦労しました。最新機種のiシリーズの画像にしたかったのですが、うまく読み込んでくれず・・・

しかも、努力した割にあまり見た目がよくないのが残念です。

このページは以上です。