Ansible から Azure 上 の Windows Server 2019 へ Certificate 認証で接続してコントロールする

## 0.はじめに
かなり久しぶりに Ansible を利用して Windows Server を触る機会がありましたので、備忘も兼ねて記事にします。
具体的には、Azure Cloudshell 上 の Ansible より、Azure 上の Windows Server 2019 へ WinRM（ https:5986 port ）経由で接続し、[win_shell モジュール](https://docs.ansible.com/ansible/2.9_ja/modules/win_shell_module.html)を利用してホスト名や、簡易な powershell を実行するまでを紹介します。
また、WinRM の認証方法として、[Certificate](https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_winrm.html#id4) を利用します。

:::note info
本記事の実行結果等は執筆時点（2022年3月16日）での内容となります
:::

:::note warn
検証用途での実行環境として便宜上、Azure Cloudshell を利用しています。
利用にあたっては、セキュリティ要件等十分にご留意ください。
:::

## 1.前提条件
+ コントロール対象VM等は以下の通りAzure上に作成済
    + Windows (Windows Server 2019 Datacenter)
    + パブリック IP アドレス付与
    + ユーザーは`azureuser`、パスワードは`P@ssword1234`
    + Azure NSG（ network security group ）にて Ansible 実行環境より、対象 VM へ TCP：5986 の接続を許可
        + `curl ifconfig.io` 等で送信元のグローバル IP を取得し、事前に接続可能なように設定
+ Ansible  及び OpenSSL 実行環境は、Azure Cloudshell を利用
    + Ansible や、pywinrm 等のインストールについては、この記事では触れません
    + WinRM の認証方法として、Certificate を利用

## 2.実行環境
```bash
$ ansible --version
ansible 2.10.2
  config file = None
  configured module search path = ['/home/takuya/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/ansible/lib/python3.7/site-packages/ansible
  executable location = /opt/ansible/bin/ansible
  python version = 3.7.3 (default, Jul 25 2020, 13:03:44) [GCC 8.3.0]
$ 
$ openssl version
OpenSSL 1.1.1d  10 Sep 2019
$ 
```

## 3.事前準備
事前準備として、以下を実施します。
1. Certificate 認証で、ローカルユーザーにマッピングする為の証明書の作成
1. Windows Server（コントロール対象 VM ）側での各種設定

### 3.1.Certificate認証で、ローカルユーザーにマッピングする為の証明書の作成
はじめに、Azure Cloudshell 上より、[Ansible 公式ドキュメント](https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_winrm.html#id5)を参考に
 OpenSSL を利用して Certificate 認証で利用する証明書のペアを作成します。

:::note warn
注意
マッピング対象のユーザー名`azureuser`は実環境のものへ置き換えてください
:::

```
# Set the name of the local user that will have the key mapped to
USERNAME="azureuser"

cat > openssl.conf << EOL
distinguished_name = req_distinguished_name
[req_distinguished_name]
[v3_req_client]
extendedKeyUsage = clientAuth
subjectAltName = otherName:1.3.6.1.4.1.311.20.2.3;UTF8:$USERNAME@localhost
EOL

export OPENSSL_CONF=openssl.conf
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -out cert.pem -outform PEM -keyout cert_key.pem -subj "/CN=$USERNAME" -extensions v3_req_client
rm openssl.conf
# 証明書のペア(`cert_key.pem`,`cert.pem`)が作成されていることを確認
ls -altr
# 次の Windows Server 側でのマッピングで利用するため、pem ファイルをZIPで圧縮して、Cloudshell からダウンロードしておく
zip pem.zip *.pem
```

```bash:"実行結果（例）"
$ USERNAME="azureuser"
$ 
$ cat > openssl.conf << EOL
> distinguished_name = req_distinguished_name
> [req_distinguished_name]
> [v3_req_client]
> extendedKeyUsage = clientAuth
> subjectAltName = otherName:1.3.6.1.4.1.311.20.2.3;UTF8:$USERNAME@localhost
> EOL
$ 
$ export OPENSSL_CONF=openssl.conf
takuya@Azure:~/wwwwork$ openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -out cert.pem -outform PEM -keyout cert_key.pem -subj "/CN=$USERNAME" -extensions v3_req_client
Generating a RSA private key
...............+++++
............+++++
writing new private key to 'cert_key.pem'
-----
$ rm openssl.conf
$ ls -altr
total 16
drwxr-xr-x 39 takuya takuya 4096 Mar 16 03:29 ..
-rw-r--r--  1 takuya takuya 1099 Mar 16 03:38 cert.pem
-rw-------  1 takuya takuya 1708 Mar 16 03:38 cert_key.pem
drwxr-xr-x  2 takuya takuya 4096 Mar 16 03:38 .
$ zip pem.zip *.pem
  adding: cert_key.pem (deflated 23%)
  adding: cert.pem (deflated 25%)
$
```

### 3.2.Windows Server（コントロール対象VM）側での各種設定
次に Azure 上の Windows Server へ RDP 等で接続し以下を設定します。
1. WinRM等の有効化
1. 証明書とローカルアカウントの紐付け
1. WinRMのcertificate認証を有効化

#### 3.2.1. WinRM等の有効化
[Ansible 公式ドキュメント](https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_setup.html#id3)
に則り、https（ port：5896 ）で WinRM 接続可能なように、Windows Server上 の Powershell(以降 Powershell は全て `Run as administrator`: 管理者モードで起動) より Ansible 設定用`ConfigureRemotingForAnsible.ps1`を実行します。

```powershell
$url = "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"

(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)

powershell.exe -ExecutionPolicy ByPass -File $file

winrm enumerate winrm/config/Listener
```

#### 3.2.2. 証明書とローカルアカウントの紐付け
続いて、作成した証明書ペア ZIP ファイル`pem.zip`を Windows Server へアップロードし、Desktop 上で ZIP ファイルを解凍しておきます。
+ `C:\Users\azureuser\Desktop\pem\`へ証明書のペア(`cert_key.pem`,`cert.pem`)を配置

次に、以下 powershell を実行し、証明書ペアのインポート、ローカルアカウントとのマッピングを行います。([Ansible 公式ドキュメント](https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_winrm.html#id6))

:::note warn
注意
マッピング対象のユーザー名`azureuser`とパスワード`P@ssword1234`は実環境のものへ置き換えてください
:::

```powershell
$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
$cert.Import("C:\Users\azureuser\Desktop\pem\cert.pem")
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]::Root
$store_location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
$store.Open("MaxAllowed")
$store.Add($cert)
$store.Close()

$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
$cert.Import("C:\Users\azureuser\Desktop\pem\cert.pem")
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople
$store_location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
$store.Open("MaxAllowed")
$store.Add($cert)
$store.Close()

$username = "azureuser"
$password = ConvertTo-SecureString -String "P@ssword1234" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password

$thumbprint = (Get-ChildItem -Path cert:\LocalMachine\root | Where-Object { $_.Subject -eq "CN=$username" }).Thumbprint
New-Item -Path WSMan:\localhost\ClientCertificate `
     -Subject "$username@localhost" `
     -URI * `
     -Issuer $thumbprint `
     -Credential $credential `
     -Force
```

```powershell:"実行結果（例）"
PS C:\Users\azureuser> $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
>> $cert.Import("C:\Users\azureuser\Desktop\pem\cert.pem")
>> $store_name = [System.Security.Cryptography.X509Certificates.StoreName]::Root
>> $store_location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
>> $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
>> $store.Open("MaxAllowed")
>> $store.Add($cert)
>> $store.Close()
>>
PS C:\Users\azureuser>
PS C:\Users\azureuser>
>> $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
>> $cert.Import("C:\Users\azureuser\Desktop\pem\cert.pem")
>> $store_name = [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople
>> $store_location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
>> $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
>> $store.Open("MaxAllowed")
>> $store.Add($cert)
>> $store.Close()
PS C:\Users\azureuser>
PS C:\Users\azureuser> $username = "azureuser"
>> $password = ConvertTo-SecureString -String "P@ssword1234" -AsPlainText -Force
>> $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password
PS C:\Users\azureuser>
PS C:\Users\azureuser> $thumbprint = (Get-ChildItem -Path cert:\LocalMachine\root | Where-Object { $_.Subject -eq "CN=$username" }).Thumbprint
>> New-Item -Path WSMan:\localhost\ClientCertificate `
>>      -Subject "$username@localhost" `
>>      -URI * `
>>      -Issuer $thumbprint `
>>      -Credential $credential `
>>      -Force
>>


   WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\ClientCertificate

Type            Keys                                Name
----            ----                                ----
Container       {URI=*, Issuer=F6652B7E0DD09FB81... ClientCertificate_1870013346


```

#### 3.2.3. WinRM の Certificate 認証を有効化
最後に powershell で WinRM の Certificate 認証を有効化し、WinRM の Listener 及び設定状況を確認します。
以上で Windows Server 側の準備が整いました。

```powershell
winrm set winrm/config/service/Auth '@{Certificate="true"}'

winrm enumerate winrm/config/Listener

winrm get winrm/config
```

```powershell:"実行結果（例）"
PS C:\Users\azureuser> winrm set winrm/config/service/Auth '@{Certificate="true"}'
Auth
    Basic = true
    Kerberos = true
    Negotiate = true
    Certificate = true
    CredSSP = false
    CbtHardeningLevel = Relaxed

PS C:\Users\azureuser> winrm enumerate winrm/config/Listener
Listener
    Address = *
    Transport = HTTP
    Port = 5985
    Hostname
    Enabled = true
    URLPrefix = wsman
    CertificateThumbprint
    ListeningOn = 127.0.0.1, 172.18.0.4, ::1, fe80::10de:125d:29e4:eb5a%6

Listener
    Address = *
    Transport = HTTPS
    Port = 5986
    Hostname = win001
    Enabled = true
    URLPrefix = wsman
    CertificateThumbprint = 624371D7DBFE6B80FB902C379155B84E2AD2FBCA
    ListeningOn = 127.0.0.1, 172.18.0.4, ::1, fe80::10de:125d:29e4:eb5a%6

PS C:\Users\azureuser> winrm get winrm/config
Config
    MaxEnvelopeSizekb = 500
    MaxTimeoutms = 60000
    MaxBatchItems = 32000
    MaxProviderRequests = 4294967295
    Client
        NetworkDelayms = 5000
        URLPrefix = wsman
        AllowUnencrypted = false
        Auth
            Basic = true
            Digest = true
            Kerberos = true
            Negotiate = true
            Certificate = true
            CredSSP = false
        DefaultPorts
            HTTP = 5985
            HTTPS = 5986
        TrustedHosts
    Service
        RootSDDL = O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;IU)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)
        MaxConcurrentOperations = 4294967295
        MaxConcurrentOperationsPerUser = 1500
        EnumerationTimeoutms = 240000
        MaxConnections = 300
        MaxPacketRetrievalTimeSeconds = 120
        AllowUnencrypted = false
        Auth
            Basic = true
            Kerberos = true
            Negotiate = true
            Certificate = true
            CredSSP = false
            CbtHardeningLevel = Relaxed
        DefaultPorts
            HTTP = 5985
            HTTPS = 5986
        IPv4Filter = *
        IPv6Filter = *
        EnableCompatibilityHttpListener = false
        EnableCompatibilityHttpsListener = false
        CertificateThumbprint
        AllowRemoteAccess = true
    Winrs
        AllowRemoteShellAccess = true
        IdleTimeout = 7200000
        MaxConcurrentUsers = 2147483647
        MaxShellRunTime = 2147483647
        MaxProcessesPerShell = 2147483647
        MaxMemoryPerShellMB = 2147483647
        MaxShellsPerUser = 2147483647

PS C:\Users\azureuser>
```

## 4.Ansible Playbook 等Sample コード
今回の検証で利用する Playbook 等は以下の通りです。

Windows Server 2019 へ WinRM（ https:5986 port ）経由で接続し
認証方法として、Certificate を利用するインベントリファイルと
[win_shell モジュール](https://docs.ansible.com/ansible/2.9_ja/modules/win_shell_module.html)を利用してホスト名や、簡易な powershell を実行するサンプルです。

:::note warn
注意
`YOUR_VM_IP_ADDRESS`(Windows 対象 VM のグローバルIPアドレス)及び`YOUR_CERT_FULL_PATH`（例： /home/yourname/.ssh/等）については、適宜自分の環境へ置き換えてください。
:::

``` inventory.yml 
win:
  hosts:
    win001:
      ansible_host: YOUR_VM_IP_ADDRESS
  vars:
    ansible_winrm_server_cert_validation: ignore
    ansible_connection: winrm
# Below are the settings for the certificate
    ansible_winrm_cert_pem: /YOUR_CERT_FULL_PATH/cert.pem
    ansible_winrm_cert_key_pem: /YOUR_CERT_FULL_PATH/cert_key.pem
    ansible_winrm_transport: certificate
# Below are the settings for the ntlm
#    ansible_winrm_transport: ntlm
#    ansible_user: YOUR_VM_USERNAME
#    ansible_password: YOUR_VM_PASSWORD
#    ansible_winrm_scheme: https
#    ansible_port: 5986
```

```  demo-win.yml
- hosts: win
  gather_facts: false
  tasks:
        - win_shell: "{{ item }}"
          with_items:
            - 'hostname'
            - |
              $date = Get-date
              $date
          register: command_results
        - debug: var=command_results.results
```

``` ansible.cfg 
[defaults]
host_key_checking=false
validate_certs=False
log_path=./ansible.log
```

## 5.実行結果（例）
Azure Cloudshell 側でサンプルコードの準備と作成済み証明書のペア('cert_key.pem','cert.pem')を、Ansible のインベントリファイル`inventory.yml`へ記載した`YOUR_CERT_FULL_PATH`へ(例： /home/yourname/.ssh/等)配置します。

Cloudshell より `ansible-playbook -i inventory.yml demo-win.yml`を実行した結果の例は以下のようになります。 

```bash:”実行結果（例）"
$ansible-playbook -i inventory.yml demo-win.yml 

PLAY [win] **************************************************************************************************************************************************************************************

TASK [win_shell] ********************************************************************************************************************************************************************************
changed: [win001] => (item=hostname)
changed: [win001] => (item=$date = Get-date
$date
)

TASK [debug] ************************************************************************************************************************************************************************************
ok: [win001] => {
    "command_results.results": [
        {
            "ansible_loop_var": "item",
            "changed": true,
            "cmd": "hostname",
            "delta": "0:00:00.421907",
            "end": "2022-03-17 02:11:15.905737",
            "failed": false,
            "item": "hostname",
            "rc": 0,
            "start": "2022-03-17 02:11:15.483829",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "win001\r\n",
            "stdout_lines": [
                "win001"
            ]
        },
        {
            "ansible_loop_var": "item",
            "changed": true,
            "cmd": "$date = Get-date\n$date",
            "delta": "0:00:00.421894",
            "end": "2022-03-17 02:11:18.123249",
            "failed": false,
            "item": "$date = Get-date\n$date\n",
            "rc": 0,
            "start": "2022-03-17 02:11:17.701354",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "\r\nThursday, March 17, 2022 2:11:18 AM\r\n\r\n\r\n",
            "stdout_lines": [
                "",
                "Thursday, March 17, 2022 2:11:18 AM",
                "",
                ""
            ]
        }
    ]
}

PLAY RECAP **************************************************************************************************************************************************************************************
win001                     : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

$
```
## 6.参考ドキュメント
以下参考ドキュメント
+ https://docs.ansible.com/ansible/2.9_ja/installation_guide/intro_installation.html
+ https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_setup.html
+ https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_winrm.html
+ https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_usage.html
+ https://docs.microsoft.com/ja-jp/troubleshoot/windows-client/system-management-components/errors-when-you-run-winrm-commands
+ https://docs.microsoft.com/ja-jp/azure/developer/ansible/

## 7.さいごに
+ 今回は、本当に久しぶりの Ansible ということもあり、公式ドキュメントや、過去に書いた自分の記事等を参照しながらと、手探りでの検証となりました。。。
+ また、以前よりも、情報が増えたとはいえ、未だに Ansible * Windows の事例は、やはり少ないな〜という印象でした。

+ なお、今回初めて、Windows Server への Certificate 認証を試してみて
事前準備として証明書ペアの作成と、その後のローカルユーザーとのペアリング等、慣れないと分かりづらい部分も多く（公式ドキュメントの通りではありますが...）、敷居が高く感じましたが、設定後は、Linuxと同様に、インベントリファイルへ証明書の配置場所のみの記載で良い（ユーザー名やパスワードの記載が不要）というのは、コード管理等のセキュリティ上の観点からも良さそうと感じました。

:::note warn
注意
[Ansible 公式ドキュメントの認証オプション](https://docs.ansible.com/ansible/2.9_ja/user_guide/windows_winrm.html#id2)へ記載の通り、
Certificate 認証については、ローカルユーザーのみ対応となっており、ドメインユーザーでの利用はできません。
:::

+ その他、Azure Cloudshell 上で Ansible がデフォルトで利用可能なのは便利でした。


以上です。