diff --git a/doc/source/general-roles.rst b/doc/source/general-roles.rst index 5bfb30046..738446539 100644 --- a/doc/source/general-roles.rst +++ b/doc/source/general-roles.rst @@ -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 diff --git a/roles/add-build-winrm-cert/README.rst b/roles/add-build-winrm-cert/README.rst new file mode 100644 index 000000000..692dce61f --- /dev/null +++ b/roles/add-build-winrm-cert/README.rst @@ -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. diff --git a/roles/add-build-winrm-cert/defaults/main.yaml b/roles/add-build-winrm-cert/defaults/main.yaml new file mode 100644 index 000000000..adb5269ba --- /dev/null +++ b/roles/add-build-winrm-cert/defaults/main.yaml @@ -0,0 +1 @@ +build_winrm_cert_change_password: false diff --git a/roles/add-build-winrm-cert/tasks/create-key-and-replace.yaml b/roles/add-build-winrm-cert/tasks/create-key-and-replace.yaml new file mode 100644 index 000000000..5ca20c4eb --- /dev/null +++ b/roles/add-build-winrm-cert/tasks/create-key-and-replace.yaml @@ -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: diff --git a/roles/add-build-winrm-cert/tasks/main.yaml b/roles/add-build-winrm-cert/tasks/main.yaml new file mode 100644 index 000000000..d4c076be4 --- /dev/null +++ b/roles/add-build-winrm-cert/tasks/main.yaml @@ -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 diff --git a/roles/add-build-winrm-cert/vars/main.yaml b/roles/add-build-winrm-cert/vars/main.yaml new file mode 100644 index 000000000..a1e2cfaa7 --- /dev/null +++ b/roles/add-build-winrm-cert/vars/main.yaml @@ -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" diff --git a/roles/remove-build-winrm-cert/README.rst b/roles/remove-build-winrm-cert/README.rst new file mode 100644 index 000000000..9373444f6 --- /dev/null +++ b/roles/remove-build-winrm-cert/README.rst @@ -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. diff --git a/roles/remove-build-winrm-cert/tasks/main.yaml b/roles/remove-build-winrm-cert/tasks/main.yaml new file mode 100644 index 000000000..0c1e70b90 --- /dev/null +++ b/roles/remove-build-winrm-cert/tasks/main.yaml @@ -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