Ansibleの ONTAPモジュールでSSLクライアント認証を使う

Ansibleの ONTAPモジュールを使うと 色々繰り返しの設定が楽になります。

ただ、モジュールが使うAPIの認証情報=ユーザ名/パスワードはセキュリティが気になるので、変数としてファイルに埋め込んで vaultで暗号化したりしていました。

そんな中、ONTAP モジュール 20.06から SSLの クライアント認証が利用できるようになったので、その設定をしてみたいと思います。

同様の記事が netapp.ioにあります[^1]が、ここでは少し細かく日本語で解説しながらやってみたいと思います。英語とONTAPの知識はバッチリ！という方は、netappp.ioの記事を読んでいただければ良いかと思います。

なお、以下で出てくるサンプルのスクリプトや playbook等については [github](https://github.com/yasuoohno/ontap-ansible-ssl)においてあります。

[^1]: [Authenticating Ansible ONTAP Modules with Certificates Instead of Username/Password](https://netapp.io/2020/06/25/cert4uid-pswd/)

# Update

- 2022/10/06 Typo修正, Org投稿に変更

# 要約

1. CNにユーザ名入れてクライアント認証キーペアを作る
2. ONTAPに、クライアント証明書用CAの公開キーを設定する(記事で書いてるのは自己証明書利用)
3. ONTAPの SVMと管理者アカウントで、クライアント証明書での認証を有効にする
4. PlaybookのTaskで cert_filepath/key_filepathで証明書ファイルを指定して実行する

# 前提

動作を確認したソフトウェアのバージョン等は以下の通りです。

- ONTAP: 9.7
- Ansible Control Machine
    - OS: Debian 10
    - Software: openssl, curl, jq, python3-pip
    - Python: 3.7.3
    - Ansible: 2.10.1  
    - netapp-lib 2020.7.16
    - NetApp ONTAP Collection : 20.10.0

追加で必要になったソフトウェアのインストール

```bash:setup.sh
sudo apt-get install openssl curl jq python3-pip
sudo pip3 install ansible netapp-lib
ansible-galaxy collection install netapp.ontap
```

# 設定

証明書の作成に openssl を使う事もあって Linuxマシンで行っていきます。

準備としては大まかに二つの作業が必要です。

1. クライアント証明書の作成とインストール
2. API 利用時のクライアント証明書の有効化

## 1. クライアント証明書のインストール

クライアント証明書を、ONTAPの管理者ユーザに設定する必要があります。

### 1.1. APIの認証に使うユーザについて

ここで設定するクライアント認証は **Ansibleのモジュールから、ONTAPのAPIを利用するユーザ** の認証方式です。

ONTAPにはクラスタの中でマルチテントを実現する SVM[^svm] という考え方があります。このため APIからの管理操作も SVM=テナントのレベルと、クラスタ全体の操作が可能なレベルの二つがあります。

Ansible のモジュールでログインに使うユーザが、クラスタ管理者であれば、全ての操作が実行できます。SVM管理者であれば SVMの中で許された操作のみとなります[^rbac]。

どちらのレベルを使うにしても、**準備作業にはクラスタ管理者権限が必要となります**。続くONTAP上で行う作業は、クラスタ管理権限を持つ adminユーザで実施してください。

なお、クラスタ管理ユーザ adminの SVM名称は、通常クラスタ名と同じ名前のSVM[^adminSVM]になり、CLIからは `vserver show` で確認できます。

以下の例では ontapsim という Vserver(=SVM) の Type が admin となっており、クラスタ管理SVMである事が分かります。

```console
ontapsim::> vserver show
                               Admin      Operational Root
Vserver     Type    Subtype    State      State       Volume     Aggregate
----------- ------- ---------- ---------- ----------- ---------- ----------
ontapsim    admin   -          -          -           -          -
ontapsim-01 node    -          -          -           -          -
2 entries were displayed.
```

説明の中で色々パラメータが出てきてややこしいので、以下で必要なパラメータを全て変数に設定するファイル(params)を準備してから作業に入りたいと思います。

```bash:params
#
# Ansibleで使う、ONTAP APIの認証ユーザ(クラスタ管理者またはSVM管理者)
#
ANSIBLE_USER=AnsibleでAPIを認証するユーザ名(通常はadminかvsadmin)
ANSIBLE_SVM=Ansibleで管理しようとするSVMの名前/クラスタ全体ならクラスタ管理SVM

#
# 設定に使う ONTAPのクラスタ管理者情報
#
ONTAP_CLUSTER=クラスタ管理SVMの名前
ONTAP_ADMIN=admin
ONTAP_CLUSTER_MGMT_IP=xxx.xxx.xxx.xxx

#
# 証明書のファイル名
#
PUBLICKEY_FILE=$ANSIBLE_SVM-$ANSIBLE_USER.pem
PRIVATEKEY_FILE=$ANSIBLE_SVM-$ANSIBLE_USER.key
```

[^svm]: [SVMの使用目的](http://docs.netapp.com/ontap-9/topic/com.netapp.doc.onc-sm-help-960/GUID-ABC30BB1-222D-4663-B878-52D4C6BFC191.html)

[^rbac]: APIのスコープの他にRBACで細かく設定できたりします [管理者認証とRBAC](http://docs.netapp.com/ontap-9/topic/com.netapp.doc.dot-cm-concepts/GUID-F05CF30C-01B0-442E-88F1-BF875DF8497C.html)

[^adminSVM]: [クラスタ管理サーバとは](https://docs.netapp.com/ontap-9/topic/com.netapp.doc.dot-cm-sag/GUID-2FD612B2-4B6B-4A2A-ABD0-0D096CF3542B.html)

### 1.2 証明書の作成

以下では自己署名の証明書を使います。もしも、クライアント認証用の認証局が別にある場合は、そちらでクライアント証明書を発行して、client-caとして 認証局の公開キーを登録すれば良いかと思います。

ANSIBLE_USER用の自己署名クライアント証明書ですので、Subjectの Common Name(CN) として ANSIBLE_USER を指定します。Subjectで重要なのは Common Name(CN)だけですので、Organization(O)として クラスタの名前やSVM名等を設定しています。その他のオプションは、有効期間 3年=`-days 1095` 、Private Keyの暗号化をしない=`-nodes` です。

```bash:01_make_selfsigned_cert.sh
openssl req -x509 -nodes -days 1095 -newkey rsa:2048 -keyout $PRIVATEKEY_FILE -out $PUBLICKEY_FILE -subj "/O=$ONTAP_CLUSTER/OU=$ANSIBLE_SVM/CN=$ANSIBLE_USER"
```

これで自己署名の証明書が出来ました。

### 1.3 クライアント証明書のインストール

作成が出来たら、公開キーをクライアント認証用に ONTAPにインストールします。

証明書のインストールをCLIからする場合は [security certificate install](https://docs.netapp.com/ontap-9/topic/com.netapp.doc.dot-cm-cmpr-970/security__certificate__install.html) コマンドを使います。

ssh等でクラスタ管理者としてログインした上で、`security certificate install -vserver SVM名 -type client-ca` を実行し、pemファイルの内容を端末に張り付けて最後にEnterを押せばOKです。

```bash:02_install_cert_as_client_ca.sh
ssh $ONTAP_ADMIN@$ONTAP_CLUSTER_MGMT_IP security certificate install -vserver $ANSIBLE_SVM -type client-ca
```

sshではなく、APIで叩くならこんな感じです。

```bash
curl -k -u $ONTAP_ADMIN -X POST "https://$ONTAP_CLUSTER_MGMT_IP/api/security/certificates" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"svm\":{\"name\":\"$ANSIBLE_SVM\"},\"type\":\"client-ca\",\"public_certificate\":\"`cat $PUBLICKEY_FILE`\"}"
```

ここで `type: client-ca` としていますが、この証明書で署名された証明書を、クライアント証明書として信頼する事になります。
この証明書は、この証明書自身で署名(自己署名)されているため、クライアント証明書として有効になるというわけです（やや、ややこしい）

蛇足ですが、`type: client` は、ONTAPにアクセスするときのクライアント認証ではなく、ONTAP自身が https接続を使って別のサーバにアクセスするときに利用するクライアント証明書を設定する事になります。

## 2. SSLのクライアント認証を有効にする

ここでは二つの設定が必要です。

1. ONTAP側のSSL設定で クライアント認証を有効にする
2. 対象ユーザで API及び httpを使うときの認証方法として クライアント証明書を有効にする

CLIでやるなら クラスタ管理者で ssh等を使ってログインしてコマンドを三つ実行します

```bash
# SVMへ https でアクセスした時の、クライアント認証を有効にする
ssh $ONTAP_ADMIN@$ONTAP_CLUSTER_MGMT_IP security ssl modify -vserver $ANSIBLE_SVM -client-enabled true
# SVM上のユーザが、ONTAP API/httpsでアクセスしたときに SSLのクライアント認証を使う設定
ssh $ONTAP_ADMIN@$ONTAP_CLUSTER_MGMT_IP security login create -vserver $ANSIBLE_SVM -user-or-group-name $ANSIBLE_USER -application ontapi -authentication-method cert
ssh $ONTAP_ADMIN@$ONTAP_CLUSTER_MGMT_IP security login create -vserver $ANSIBLE_SVM -user-or-group-name $ANSIBLE_USER -application http   -authentication-method cert
```

APIでやる場合は、３点ほど注意があります。

1. ANSIBLE_SVMの UUIDが必要になるのですが、それを APIで問い合わせてから 実際の実行になります。  
   SVM一覧を入手するためのAPIである、 `GET /svm/svms` APIは DATA SVM(テナント用SVM)のみが対象[^svm_collection_get]です。ANSIBLE_SVM がテナントを対象としていれば特に問題ないのですが、クラスタ管理SVMに対する UUIDを取得する事は このAPIでは できません。この様な専用APIが無い場合には、cli passthrough APIを使う事になります。
2. SVMで クライアント認証を有効にする 専用APIも ONTAP 9.7では存在していませんので、cli passthrough APIを使う事になります。
3. `PATCH /security/accounts/UUID/NAME` APIで渡すアプリケーションと認証方式の組み合わせについてですが、applications全体について上書きとなります。このため、追加のつもりで http/certificateのみを渡すと その他の認証が無効になります（ただ、admin等のユーザについて consoleアクセスのpassword認証等は消せない様になっていますが）。現在のApplicationsを調べて、そのうえで追加する 認証方式を足してAPIを投げる必要があります。

というわけで、APIを叩く スクリプトについては少し大きくなります。

jqコマンド を使っていますので、インストールされていなければ `sudo apt-get install jq` 等でインストールしてください。

```bash
{

# SVMへ https でアクセスした時の、クライアント認証を有効にする
curl "https://$ONTAP_CLUSTER_MGMT_IP/api/private/cli/security/ssl?vserver=$ANSIBLE_SVM" \
    -k \
    -X PATCH \
    -H "accept: application/json" \
    -H "Content-Type: application/json" \
    -u "$ANSIBLE_USER" \
    -d '{ "client-enabled": true }'

echo .

# ANSIBLEがアクセスするSVMのUUIDを取得
ANSIBLE_SVM_UUID=`curl -s -k -u $ONTAP_ADMIN -X GET -H "accept: application/json" "https://$ONTAP_CLUSTER_MGMT_IP/api/private/cli/vserver?vserver=$ANSIBLE_SVM&fields=uuid" | jq -r ".records[0].uuid"`

echo "ANSIBLE SVM UUID = $ANSIBLE_SVM_UUID"

# 現在有効な認証情報について取得
CURRENT_APPS=`curl -s -k -u $ONTAP_ADMIN -X GET -H "accept: application/json" "https://$ONTAP_CLUSTER_MGMT_IP/api/security/accounts/$ANSIBLE_SVM_UUID/$ANSIBLE_USER" | jq -r ".applications"`

echo "Current applications for $ANSIBLE_USER@$ANSIBLE_SVM = "
echo "$CURRENT_APPS" | jq

read -p "Hit any key to continue: " -n 1 -r
echo

# 新しく設定する認証情報について念のため確認メッセージ
NEW_APPS=`echo "$CURRENT_APPS" | jq -r '. + [{ "application": "ontapi", "authentication_methods": ["certificate"] },{ "application": "http", "authentication_methods": ["certificate"] }]'`
echo "New applications for $ANSIBLE_USER@$ANSIBLE_SVM = "
echo "$NEW_APPS" | jq

read -p "Please confirm the applications and hit any key to continue: " -n 1 -r

JSON="{\
    \"applications\": $NEW_APPS
}"

curl "https://$ONTAP_CLUSTER_MGMT_IP/api/security/accounts/$ANSIBLE_SVM_UUID/$ANSIBLE_USER" \
    -k \
    -X PATCH \
    -H "accept: application/json" \
    -H "Content-Type: application/json" \
    -u "$ONTAP_ADMIN" \
    -d "$JSON"

}
```

これで設定は完了です。

[^svm_collection_get]: [GET /svm/svms](https://library.netapp.com/ecmdocs/ECMLP2862544/html/index.html#/svm/svm_collection_get) "Important notes: REST APIs only expose a data SVM as an SVM."

# Playbookの書き方

Playbookの書き換えは 今までユーザ名パスワードで行っていたところを cert/key_filepath に置き換えます。

↓これを↓

```yaml
hostname: Ansible01
username: admin
password: netapp123
https: true
validate_certs: false
```

↓こんな風に↓

```yaml
hostname: Ansible01
cert_filepath: name.pem
key_filepath: name.key
validate_certs: false
```

`https: true` については クライアント認証が有効な場合は必ず利用しますので省略可能です。
cert_filepath/key_filepath については Playbookを置いた場所と同じ場所に証明書ファイルを設置して、ファイル名を設定すればOKでした。

## 設定例

Ansibleから ONTAPへのアクセスに必要な情報を group_vars/ontap.yml として定義します。

```yaml:group_vars/ontap.yml
ansible_host: localhost
ansible_connection: local
ansible_python_interpreter: "/usr/bin/python3"

ontap_hostname: "xxx.xxx.xxx.xxx"
ontap_cert_filepath: "ontap-admin.pem"
ontap_key_filepath: "ontap-admin.key"
ontap_validate_certs: false
```

inventoryの中で ontapグループを定義して必要なホスト（cluster/svm/node等 使う物だけでOK）を全部入れます。

実際のAPIネットワーク接続は (ontap_hostname変数に設定した)モジュールに渡す hostnameに対して行われるため、名前解決等はできない名前でも構いません。

```yaml:inventory.yml
#
# ontap group:
#   all ontap hosts that shared group vars 'group_vars/ontap.yml'
#
ontap:
  hosts:
    cluster:
    svm01:
    node01:
    node02:
```

Playbookの中では 以下の様に varsに ログイン情報をまとめて書き、`&login` としてアンカーを打ちます。

tasksの中では モジュールのパラメータとして login情報を alias `<<: *login_info` を使って呼び出します。

以下のサンプルPlaybookでは、ライセンスを全てインストール(ontap_licenses roleについては後述)した後に、ONTAPの情報を集めて表示しています。

```yaml:cluster.yml
- hosts: cluster
  collections: netapp.ontap
  gather_facts: no
  vars:
    cert_login: &login
      hostname: "{{ ontap_hostname }}"
      cert_filepath: "{{ ontap_cert_filepath }}"
      key_filepath: "{{ ontap_key_filepath }}"
      validate_certs: "{{ ontap_validate_certs }}"

  roles:
    - role: ontap_licenses
      licenses:
        - "xxxxxxxxxxxxxxxxxAAAAAAAAAAA"
        - "xxxxxxxxxxxxxxxxxAAAAAAAAAAA"

  tasks:
    - name: Gather ONTAP facts.
      na_ontap_rest_info:
        <<: *login
        state: info
      register: result

    - debug: var=result
```

ロールの中は aliasを展開できないので共通して `ontap_` とprefixをつけた変数で解決するようにしています。

```yaml:roles/ontap_licenses/tasks/main.yml
- name: ensure ontap licenses
  loop: "{{ licenses }}"
  na_ontap_license:
    state: present
    hostname: "{{ ontap_hostname }}"
    cert_filepath: "{{ ontap_cert_filepath }}"
    key_filepath: "{{ ontap_key_filepath }}"
    validate_certs: "{{ ontap_validate_certs }}"
    license_codes: "{{ item }}"
```

作成したクライアント認証用の.pem/.keyファイルは Playbookと同じフォルダに入れておけばよいです。

[github](https://github.com/yasuoohno/ontap-ansible-ssl)においてある[playbooks](https://github.com/yasuoohno/ontap-ansible-ssl/tree/main/playbooks)の中身は

- group_vars/ontap.yml を適宜変更する
- cluster.ymlのlicensesのライセンスコードを修正するか、roleを削除する
- ontap-admin.pem / ontap-admin.key ファイルを設定したクライアント認証ファイルに置き換える
- run.sh を実行する

で試せます。

# というわけで

SSLのクライアント認証について良く知ってる人であれば client-ca を設定するって話と、cert/key_filepathで 認証情報を渡してやるってだけですが、こんな感じで クライアント認証によるAPI利用が可能になります。