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