WinRM 证书认证
WinRM 证书认证是一种使用 X.509 证书而不是用户名和密码来验证到 Windows 主机的认证方法。
与基于 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 私钥,因此我们需要使用 OpenSSL 将 cert.pfx
文件转换为 PEM 私钥
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()
如果证书是自签名的,或者是由主机不信任的 CA 颁发的,您需要将 CA 证书导入受信任的根存储。由于我们的示例使用的是自签名证书,因此我们将导入该证书作为受信任的 CA,但在生产环境中,您需要导入签署证书的 CA。
$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
是证书 SAN 条目中 userPrincipalName
的值。Issuer
是颁发我们证书的 CA 证书的指纹。Credential
是我们要将证书映射到的本地用户的用户名和密码。
使用 Ansible
以下 Ansible 剧本可用于创建本地用户并将提供的证书映射到用于证书认证。它需要命名为 username
和 cert_pem
变量设置为要创建的用户的名称和生成公钥 PEM 文件的路径。此剧本预计 cert_pem
是一个自签名证书,如果使用由 CA 颁发的证书,您需要对其进行编辑,以便它将该证书复制过来并将其导入到 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
}