使用Ansible通过证书认证连接和控制位于Azure上的Windows Server 2019

首先

最近有机会使用 Ansible 并触摸 Windows Server,所以为了备忘起见,我决定写一篇文章。具体来说,我将使用 Azure Cloudshell 上的 Ansible 通过 WinRM(通过https:5986端口)连接到 Azure 上的 Windows Server 2019,并介绍如何使用 win_shell 模块执行主机名和简单的 PowerShell。此外,我将使用证书作为 WinRM 的身份验证方法。

本文所述的执行结果等内容仅限于撰写时点(2022年3月16日)的信息。
为了方便起见,在验证目的下作为执行环境,我们使用了Azure Cloudshell。
在使用之前,请务必注意安全要求等。

前提条件

    • コントロール対象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. 运行环境

$ 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.预先准备

在事前准备阶段,我们将进行以下操作。

    在Windows Server(受控虛擬機)上進行各種設定,以創建用於將憑證映射到本地用戶的認證證書。

3.1. 创建用于将证书映射到本地用户的证明书,以进行认证。

首先,在Azure Cloud Shell中,根据Ansible官方文档的参考,使用OpenSSL创建用于证书认证的证书对。

请注意,在映射目标中将用户名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
$ 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(受控制的虚拟机)上进行各种设置。

下一步,您可以使用RDP等方式,连接到Azure上的Windows Server,并进行以下设置。

    1. 启用WinRM

 

    1. 关联证书和本地账户

 

    启用WinRM的证书认证

3.2.1. 激活WinRM等

根据 Ansible 官方文档,在 Windows Server 上的 Powershell(以下简称 Powershell,必须以管理员模式运行)中运行 ConfigureRemotingForAnsible.ps1,以使其能通过 https(端口:5896)进行 WinRM 连接可用。

$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,并在桌面上解压ZIP文件。

C:\Users\azureuser\Desktop\pem\へ証明書のペア(cert_key.pem,cert.pem)を配置

接下来,运行以下 PowerShell 命令,对证书对进行导入,并进行与本地帐户的映射。(Ansible 官方文档)

请替换目标映射用户名azureuser和密码P@ssword1234为实际环境中的凭证。
$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
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 的证书认证。

最后,通过PowerShell启用WinRM的证书认证并确认WinRM的监听器和配置情况。至此,Windows Server的准备工作完成。

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

winrm enumerate winrm/config/Listener

winrm get winrm/config
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及其他示例代码

本次验证中使用的Playbook等如下所示。

通过WinRM(使用https:5986端口)连接到Windows Server 2019,在认证方法上,使用证书作为库存文件,并利用win_shell模块执行主机名和简单的powershell命令的示例。

请注意,将YOUR_VM_IP_ADDRESS(Windows VM的全局IP地址)和YOUR_CERT_FULL_PATH(例如:/home/yourname/.ssh/等)替换为适合您环境的正确数值。
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
- hosts: win
  gather_facts: false
  tasks:
        - win_shell: "{{ item }}"
          with_items:
            - 'hostname'
            - |
              $date = Get-date
              $date
          register: command_results
        - debug: var=command_results.results
[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的示例结果。

$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/

最后

今回は、本当に久しぶりの Ansible ということもあり、公式ドキュメントや、過去に書いた自分の記事等を参照しながらと、手探りでの検証となりました。。。

また、以前よりも、情報が増えたとはいえ、未だに Ansible * Windows の事例は、やはり少ないな〜という印象でした。

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

请遵循 Ansible 官方文档中所述的认证选项,证书认证仅适用于本地用户,无法用于域用户。
    その他、Azure Cloudshell 上で Ansible がデフォルトで利用可能なのは便利でした。

以上就是。