From 59d7af0e67a00a69987895cff07305ac6ae553b0 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Sat, 9 Apr 2022 13:29:41 -0700 Subject: [PATCH] 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 --- doc/source/general-roles.rst | 2 + roles/add-build-winrm-cert/README.rst | 60 +++++++++++++++++++ roles/add-build-winrm-cert/defaults/main.yaml | 1 + .../tasks/create-key-and-replace.yaml | 49 +++++++++++++++ roles/add-build-winrm-cert/tasks/main.yaml | 17 ++++++ roles/add-build-winrm-cert/vars/main.yaml | 5 ++ roles/remove-build-winrm-cert/README.rst | 4 ++ roles/remove-build-winrm-cert/tasks/main.yaml | 11 ++++ 8 files changed, 149 insertions(+) create mode 100644 roles/add-build-winrm-cert/README.rst create mode 100644 roles/add-build-winrm-cert/defaults/main.yaml create mode 100644 roles/add-build-winrm-cert/tasks/create-key-and-replace.yaml create mode 100644 roles/add-build-winrm-cert/tasks/main.yaml create mode 100644 roles/add-build-winrm-cert/vars/main.yaml create mode 100644 roles/remove-build-winrm-cert/README.rst create mode 100644 roles/remove-build-winrm-cert/tasks/main.yaml 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