Add per-build WinRM cert generation

This adds roles that, similar to add-build-sshkey, create a per-build
WinRM certificate, install it on remote windows nodes, and then switch
to using the certificate in Ansible for authentication.  A second role
is included which can clean up the cert which is useful for static
nodes.

Since winrm certificates must be acessible within the bubblewrap
container, these roles can be used to restrict the system-wide winrm
cert to trusted playbooks while untrusted playbooks will only have access
to the per-build cert (with appropriate configuration of the executor).

Change-Id: I4efe25594c2f543886a000aa02fb0a38683a43cb
This commit is contained in:
James E. Blair 2022-04-09 13:29:41 -07:00
parent 57df8b9d6d
commit 59d7af0e67
8 changed files with 149 additions and 0 deletions

View File

@ -3,6 +3,7 @@ General Purpose Roles
.. zuul:autorole:: add-authorized-keys
.. zuul:autorole:: add-build-sshkey
.. zuul:autorole:: add-build-winrm-cert
.. zuul:autorole:: add-gpgkey
.. zuul:autorole:: add-sshkey
.. zuul:autorole:: bindep
@ -36,6 +37,7 @@ General Purpose Roles
.. zuul:autorole:: prepare-workspace-git
.. zuul:autorole:: prepare-workspace-openshift
.. zuul:autorole:: remove-build-sshkey
.. zuul:autorole:: remove-build-winrm-cert
.. zuul:autorole:: remove-gpgkey
.. zuul:autorole:: remove-sshkey
.. zuul:autorole:: render-diff

View File

@ -0,0 +1,60 @@
Generate and install a build-local WinRM certificate on all Windows hosts
This role is intended to be run on the Zuul Executor at the start of
every job. It generates a self-signed certificate and installs the
certificate on every Windows host in the inventory.
It then updates the host vars for each such host to use the new
certificate. The original certificate used to initially connect to
the host still remains on disk, but once the build-local certificate
is in place, later untrusted playbooks no longer need it to be
provided.
**Role Variables**
.. zuul:rolevar:: build_winrm_cert_credentials
A complex argument expected to be supplied from a Zuul secret.
These are the Windows login credentials for the account to
associate with the certificate.
.. zuul:rolevar:: username
The username of the account.
.. zuul:rolevar:: password
The password of the account.
.. zuul:rolevar:: build_winrm_cert_change_password
:default: ``False``
If this is true, then change the password for the user to the value
supplied before adding the certificate. This is useful if the
initial account password is automatically generated and otherwise
unknown.
.. zuul:rolevar:: zuul_temp_winrm_name
:default: ``{{ zuul.build }}_winrm``
The base name of the certificate file.
.. zuul:rolevar:: zuul_temp_winrm_cert
:default: ``{{ zuul.executor.work_root }}/{{ zuul_temp_winrm_name }}.crt``
File name for the the newly-generated certificate.
.. zuul:rolevar:: zuul_temp_winrm_key
:default: ``{{ zuul.executor.work_root }}/{{ zuul_temp_winrm_name }}.key``
File name for the the newly-generated private key.
.. zuul:rolevar:: zuul_temp_winrm_pfx
:default: ``{{ zuul.executor.work_root }}/{{ zuul_temp_winrm_name }}.pfx``
Executor-local file name for the the exported certificate.
.. zuul:rolevar:: zuul_temp_winrm_remote_tempfile
:default: ``~/appdata/local/temp/{{ zuul_temp_winrm_name }}.pfx``
Remote temporary location for the certificate during import.

View File

@ -0,0 +1 @@
build_winrm_cert_change_password: false

View File

@ -0,0 +1,49 @@
- name: Create temp WinRM cert
command: "openssl req -x509 -newkey rsa:2048 -keyout {{ zuul_temp_winrm_key }} -out {{ zuul_temp_winrm_cert }} -days 365 -nodes -subj '/C=US/ST=California/L=Oakland/O=Company Name/OU=Org/CN={{ zuul.build }}' -addext 'subjectAltName = otherName:1.3.6.1.4.1.311.20.2.3;UTF8:{{ build_winrm_cert_credentials.username }}' -addext 'keyUsage = digitalSignature,keyEncipherment'"
delegate_to: localhost
run_once: true
- name: Export temp WinRM cert
command: "openssl pkcs12 -export -inkey {{ zuul_temp_winrm_key }} -in {{ zuul_temp_winrm_cert }} -out {{ zuul_temp_winrm_pfx }} -passout pass:{{ zuul_temp_winrm_password }}"
delegate_to: localhost
run_once: true
- name: Change password
when: build_winrm_cert_change_password
win_shell: |
net user {{ build_winrm_cert_credentials.username }} "{{ build_winrm_cert_credentials.password }}"
- name: Copy temp WinRM cert
when: ansible_os_family == "Windows"
win_copy:
src: "{{ zuul_temp_winrm_pfx }}"
dest: "{{ zuul_temp_winrm_remote_tempfile }}"
- name: Import temp WinRM cert
when: ansible_os_family == "Windows"
win_shell: |
$cert = Import-PfxCertificate -FilePath {{ zuul_temp_winrm_remote_tempfile }} -CertStoreLocation Cert:\LocalMachine\root -Password (ConvertTo-SecureString -AsPlainText -String "{{ zuul_temp_winrm_password }}" -Force)
rm {{ zuul_temp_winrm_remote_tempfile }}
$password = ConvertTo-SecureString -AsPlainText -String "{{ build_winrm_cert_credentials.password }}" -Force
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist {{ build_winrm_cert_credentials.username }}, $password
New-Item -Path WSMan:\localhost\ClientCertificate -Subject {{ build_winrm_cert_credentials.username }} -URI * -Issuer $($cert.Thumbprint) -Force -Credential $cred
- name: Update WinRM key location
when: ansible_os_family == "Windows"
set_fact:
cacheable: true
ansible_winrm_cert_key_pem: "{{ zuul_temp_winrm_key }}"
ansible_winrm_cert_pem: "{{ zuul_temp_winrm_cert }}"
# These are likely already set to these values, but set them here
# anyway to future-proof against potential changes in the executor
# to support more initial connection methods.
ansible_winrm_transport: certificate
ansible_winrm_server_cert_validation: ignore
- name: Verify we can still connect to all nodes
when: ansible_os_family == "Windows"
win_ping:

View File

@ -0,0 +1,17 @@
- name: Check to see if WinRM cert was already created for this build
stat:
path: "{{ zuul_temp_winrm_key }}"
register: zuul_temp_winrm_key_stat
delegate_to: localhost
run_once: true
failed_when: false
- name: Generate WinRM export password
set_fact:
zuul_temp_winrm_password: "{{ lookup('password', '/dev/null') }}"
no_log: true
when: not zuul_temp_winrm_key_stat.stat.exists
- name: Create a new key in workspace based on build UUID
include_tasks: create-key-and-replace.yaml
when: not zuul_temp_winrm_key_stat.stat.exists

View File

@ -0,0 +1,5 @@
zuul_temp_winrm_name: "{{ zuul.build }}_winrm"
zuul_temp_winrm_cert: "{{ zuul.executor.work_root }}/{{ zuul_temp_winrm_name }}.crt"
zuul_temp_winrm_key: "{{ zuul.executor.work_root }}/{{ zuul_temp_winrm_name }}.key"
zuul_temp_winrm_pfx: "{{ zuul.executor.work_root }}/{{ zuul_temp_winrm_name }}.pfx"
zuul_temp_winrm_remote_tempfile: "~/appdata/local/temp/{{ zuul_temp_winrm_name }}.pfx"

View File

@ -0,0 +1,4 @@
Remove the per-build WinRM certificate from all hosts
The complement to :zuul:role:`add-build-winrm-cert`. It removes the
build's WinRM certificate from WSMan registry of all Windows hosts.

View File

@ -0,0 +1,11 @@
- name: Remove the build WinRM cert
when: ansible_os_family == "Windows"
# The script itself may succeed, but we're unable to obtain the
# result due to the lost credentials.
ignore_errors: true # noqa ignore-errors
win_shell: |
$cert = get-childitem cert:/localmachine/root | where-object {$_.Subject -match "{{ zuul.build }}"}
get-childitem wsman:/localhost/clientcertificate | where-object {$_.Keys -match "Issuer=$($cert.Thumbprint)"} | remove-item -recurse
get-childitem cert:/localmachine/root | where-object {$_.Subject -match "{{ zuul.build }}"} | remove-item