(習作) 端末から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](https://qiita-image-store.s3.amazonaws.com/0/157891/68bc41a9-d0f6-1940-2cc2-fd1d1e7a7c0b.png)

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

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

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

# 参考情報・引用元
 - [DevCentral: Demystifying iControl REST Part 5: Transferring Files](https://devcentral.f5.com/articles/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
(bash) $ bigip_ssl_importer.py -b "接続するBIG-IPのホスト名 or IPアドレス" \
  -u "ユーザ名" \
  -p "パスワード" \
  -s "SSL Profile名" \
  -c "サーバ証明書ファイル"（任意） \
  -k "サーバ秘密鍵ファイル"（任意） \
  -i "中間証明書ファイル"（任意）
```

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

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

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

###ヘッダ
```python3
#!/usr/bin/env python
# coding: utf-8

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

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

### ファイルアップロード用メソッド
```python3
def _upload(host, creds, fp):

・・・
（DevCentralの記事を参照）
・・・
```

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

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

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

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

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

```python3
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コマンドで行なった場合、以下のようなコマンドになります。

```text
(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|（下に記載）|

```json
(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に紐づけます。

```python3
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コマンドであらわすと、以下のようなコマンドになります。

```text
(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の設定は配列で表されます。１つしかない前提なので、配列の[0]を対象に変更を行います。

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

|メソッド|URL(Endpoint)|Body(JSON)|
|:----:|:-----------:|:---|
|PACTCH|/mgmt/tm/ltm/profile/client-ssl/~Common~(profile名)|(下に記載）

```json
{"certKeyChain": [
  {
    "name": "cKC設定名",
    "cert": "サーバ証明書のファイルオブジェクト名",
    "certReference": {
      "link": "（略）"
    }, 
    "chain": "中間証明書のファイルオブジェクト名",
    "chainReference": {
      "link": "（略）"
    },
    "key": "サーバ秘密鍵のファイルオブジェクト名",
    "keyReference": {
      "link": "（略）"
    }
  }
]}
```

## メイン関数（残りの処理）
iControl RESTの動作はほぼ確認できたので、後の必要な処理は全部mainにしてしまいました。

### 引数の処理

```python3
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を使い、上から４つ（接続先BIG-IPの指定、ユーザ名とパスワード、SSL Profile名）は指定が必須で、証明書、秘密鍵ファイルについては任意としました。

### sessionオブジェクトの初期化
```python3
b = requests.session()
b.auth = cred
b.verify = False
b.headers.update({'Content-Type':'application/json'})
```

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

### 処理の実行
```python3
# もし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](https://support.f5.com/csp/article/K682)

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

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

このページは以上です。