Аутентификация WinRM по сертификату
Аутентификация WinRM по сертификату — это способ аутентификации на узле Windows с помощью сертификатов X.509 вместо имени пользователя и пароля.
По сравнению с аутентификацией на основе ключей SSH у аутентификации по сертификату есть некоторые недостатки:
- сертификат можно привязать только к локальному пользователю Windows, но не к учетной записи домена
- имя пользователя и пароль должны быть привязаны к сертификату; если пароль изменится, сертификат потребуется привязать заново
- администратор узла Windows может получить пароль локального пользователя через привязку сертификата
- Ansible не может использовать зашифрованные закрытые ключи — они должны храниться без шифрования
- Ansible не может использовать сертификаты и закрытые ключи, сохраненные в виде переменных, — они должны быть файлами
Настройка Ansible
При аутентификации по сертификату сертификаты используются в качестве ключей, как и пары ключей SSH. Открытый и закрытый ключи хранятся на управляющем узле Ansible и используются для аутентификации. В следующем примере показаны hostvars, настроенные для аутентификации по сертификату:
# psrp ansible_connection: psrp ansible_psrp_auth: certificate ansible_psrp_certificate_pem: /path/to/certificate/public_key.pem ansible_psrp_certificate_key_pem: /path/to/certificate/private_key.pem # winrm ansible_connection: winrm ansible_winrm_transport: certificate ansible_winrm_cert_pem: /path/to/certificate/public_key.pem ansible_winrm_cert_key_pem: /path/to/certificate/private_key.pem
Аутентификация по сертификату по умолчанию отключена на узле Windows, но ее можно включить, выполнив в PowerShell следующую команду:
Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true
Из-за ограничений используемой Ansible базовой библиотеки Python закрытый ключ нельзя зашифровать.
Примечание
Для включения аутентификации по сертификату при подключении TLS 1.3 требуются Python 3.8+, 3.7.1 или 3.6.7 и пакет Python urllib3>=2.0.7 или новее.
Создание сертификата
Первый шаг для использования аутентификации по сертификату — создание сертификата и закрытого ключа. Сертификат должен иметь следующие свойства:
-
Extended Key Usageдолжен включатьclientAuth (1.3.6.1.5.5.7.3.2) -
Subject Alternative Nameдолжен включать записьotherNameдляuserPrincipalName (1.3.6.1.4.1.311.20.2.3)
Значением userPrincipalName может быть любой текст, но в этом руководстве мы будем использовать значение $USERNAME@localhost, где $USERNAME — имя пользователя, к которому будет привязан сертификат.
Это можно сделать разными способами, например с помощью OpenSSL, PowerShell или служб сертификатов Active Directory. В следующем примере показано, как создать сертификат с помощью OpenSSL:
# Set the username to the name of the user the certificate will be mapped to
USERNAME="local-user"
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
openssl req \
-new \
-sha256 \
-subj "/CN=${USERNAME}" \
-newkey rsa:2048 \
-nodes \
-keyout cert.key \
-out cert.csr \
-config openssl.conf \
-reqexts v3_req_client
openssl x509 \
-req \
-in cert.csr \
-sha256 \
-out cert.pem \
-days 365 \
-extfile openssl.conf \
-extensions v3_req_client \
-key cert.key
rm openssl.conf cert.csr
В следующем примере показано, как создать сертификат с помощью PowerShell:
# Set the username to the name of the user the certificate will be mapped to
$username = 'local-user'
$clientParams = @{
CertStoreLocation = 'Cert:\CurrentUser\My'
NotAfter = (Get-Date).AddYears(1)
Provider = 'Microsoft Software Key Storage Provider'
Subject = "CN=$username"
TextExtension = @("2.5.29.37={text}1.3.6.1.5.5.7.3.2","2.5.29.17={text}upn=$username@localhost")
Type = 'Custom'
}
$cert = New-SelfSignedCertificate @clientParams
$certKeyName = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey(
$cert).Key.UniqueName
# Exports the public cert.pem and key cert.pfx
Set-Content -Path "cert.pem" -Value @(
"-----BEGIN CERTIFICATE-----"
[Convert]::ToBase64String($cert.RawData) -replace ".{64}", "$&`n"
"-----END CERTIFICATE-----"
)
$certPfxBytes = $cert.Export('Pfx', '')
[System.IO.File]::WriteAllBytes("$pwd\cert.pfx", $certPfxBytes)
# Removes the private key and cert from the store after exporting
$keyPath = [System.IO.Path]::Combine($env:AppData, 'Microsoft', 'Crypto', 'Keys', $certKeyName)
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($cert.Thumbprint)" -Force
Remove-Item -LiteralPath $keyPath -Force
Поскольку PowerShell не может создать закрытый ключ PKCS8 PEM, для преобразования файла cert.pfx в закрытый ключ PEM необходимо использовать OpenSSL:
openssl pkcs12 \
-in cert.pfx \
-nocerts \
-nodes \
-passin pass: |
sed -ne '/-BEGIN PRIVATE KEY-/,/-END PRIVATE KEY-/p' > cert.key
Файл cert.pem содержит открытый ключ, а файл cert.key — незашифрованный закрытый ключ. Эти файлы должны быть доступны управляющему узлу Ansible для использования при аутентификации. Закрытый ключ не требуется размещать на узле Windows.
Настройка Windows
После создания открытого и закрытого ключей необходимо импортировать открытый ключ, установить к нему доверие и настроить привязку пользователя на узле Windows. Узлу Windows не требуется доступ к закрытому ключу; для настройки аутентификации по сертификату должен быть доступен только открытый ключ cert.pem.
Импорт сертификата в хранилище сертификатов
Чтобы Windows доверяла сертификату, его необходимо импортировать в хранилище сертификатов LocalMachine\TrustedPeople. Для этого выполните следующую команду:
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("cert.pem")
$store = Get-Item -LiteralPath Cert:\LocalMachine\TrustedPeople
$store.Open('ReadWrite')
$store.Add($cert)
$store.Dispose()
Если сертификат является самоподписанным или выдан центром сертификации, которому узел не доверяет, потребуется импортировать сертификат центра сертификации в хранилище доверенных корневых сертификатов. Поскольку в нашем примере используется самоподписанный сертификат, мы импортируем его как доверенный сертификат центра сертификации. Однако в рабочей среде следует импортировать сертификат центра сертификации, который подписал сертификат.
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("cert.pem")
$store = Get-Item -LiteralPath Cert:\LocalMachine\Root
$store.Open('ReadWrite')
$store.Add($cert)
$store.Dispose()
Привязка сертификата к локальной учетной записи
После импорта сертификата в хранилище LocalMachine\TrustedPeople служба WinRM может создать привязку между сертификатом и локальной учетной записью. Для этого выполните следующую команду:
# Will prompt for the password of the user.
$credential = Get-Credential local-user
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("cert.pem")
$certChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
[void]$certChain.Build($cert)
$caThumbprint = $certChain.ChainElements.Certificate[-1].Thumbprint
$certMapping = @{
Path = 'WSMan:\localhost\ClientCertificate'
Subject = $cert.GetNameInfo('UpnName', $false)
Issuer = $caThumbprint
Credential = $credential
Force = $true
}
New-Item @certMapping
Значение Subject — это значение userPrincipalName в записи SAN сертификата. Значение Issuer — это отпечаток сертификата центра сертификации, который выдал наш сертификат. Значение Credential — имя пользователя и пароль локального пользователя, к которому мы привязываем сертификат.
Использование Ansible
Следующий плейбук Ansible можно использовать для создания локального пользователя и привязки предоставленного сертификата для аутентификации по сертификату. Его необходимо назвать username, а переменной cert_pem задать имя создаваемого пользователя и путь к созданному файлу открытого ключа PEM. Этот плейбук предполагает, что cert_pem — самоподписанный сертификат. Если используется сертификат, выданный центром сертификации, необходимо отредактировать плейбук, чтобы он копировал этот сертификат и импортировал его в хранилище LocalMachine\Root.
- name: Setup WinRM Client Cert Authentication
hosts: windows
gather_facts: false
tasks:
- name: Verify required facts are setup
ansible.builtin.assert:
that:
- cert_pem is defined
- username is defined
- name: Check that the required files are present
ansible.builtin.stat:
path: '{{ cert_pem }}'
delegate_to: localhost
run_once: true
register: local_cert_stat
- name: Fail if cert PEM is not present
ansible.builtin.assert:
that:
- local_cert_stat.stat.exists
- name: Generate local user password
ansible.builtin.set_fact:
user_password: "{{ lookup('ansible.builtin.password', playbook_dir ~ '/user_password', length=15) }}"
- name: Create local user
ansible.windows.win_user:
name: '{{ username }}'
groups:
- Administrators
- Users
update_password: always
password: '{{ user_password }}'
user_cannot_change_password: true
password_never_expires: true
- name: Copy across client certificate
ansible.windows.win_copy:
src: '{{ cert_pem }}'
dest: C:\Windows\TEMP\cert.pem
- name: Import client certificate
ansible.windows.win_certificate_store:
path: C:\Windows\TEMP\cert.pem
state: present
store_location: LocalMachine
store_name: '{{ item }}'
register: client_cert_info
loop:
- Root
- TrustedPeople
- name: Enable WinRM Certificate auth
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$Ansible.Changed = $false
$authPath = 'WSMan:\localhost\Service\Auth\Certificate'
if ((Get-Item -LiteralPath $authPath).Value -ne 'true') {
Set-Item -LiteralPath $authPath -Value true
$Ansible.Changed = $true
}
- name: Setup Client Certificate Mapping
ansible.windows.win_powershell:
parameters:
Thumbprint: '{{ client_cert_info.results[0].thumbprints[0] }}'
sensitive_parameters:
- name: Credential
username: '{{ username }}'
password: '{{ user_password }}'
script: |
param(
[Parameter(Mandatory)]
[PSCredential]
$Credential,
[Parameter(Mandatory)]
[string]
$Thumbprint
)
$ErrorActionPreference = 'Stop'
$Ansible.Changed = $false
$userCert = Get-Item -LiteralPath "Cert:\LocalMachine\TrustedPeople\$Thumbprint"
$subject = $userCert.GetNameInfo('UpnName', $false) # SAN userPrincipalName
$certChain = New-Object -TypeName Security.Cryptography.X509Certificates.X509Chain
[void]$certChain.Build($userCert)
$caThumbprint = $certChain.ChainElements.Certificate[-1].Thumbprint
$mappings = Get-ChildItem -LiteralPath WSMan:\localhost\ClientCertificate |
Where-Object {
$mapping = $_ | Get-Item
"Subject=$subject" -in $mapping.Keys
}
if ($mappings -and "issuer=$($caThumbprint)" -notin $mappings.Keys) {
$null = $mappings | Remove-Item -Force -Recurse
$mappings = $null
$Ansible.Changed = $true
}
if (-not $mappings) {
$certMapping = @{
Path = 'WSMan:\localhost\ClientCertificate'
Subject = $subject
Issuer = $caThumbprint
Credential = $Credential
Force = $true
}
$null = New-Item @certMapping
$Ansible.Changed = $true
}
© 2012–2018 Michael DeHaan
© 2018–2025 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/projects/ansible/latest/os_guide/windows_winrm_certificate.html