diff --git a/.gitignore b/.gitignore
index 2b44906871e27fc44baa6d0a61bc17d4860154c1..fc2bd5b1f451f5bb45beac9cb22abe51f46a7504 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ temp
 *.tfstate.backup
 **/*.sw[pon]
 /ssh-bastion.conf
+**/*.sw[pon]
diff --git a/cluster.yml b/cluster.yml
index 1a08283b596615490888f26b5240a9e78450bc69..01b2df105ee10540a942626a4702cdacbdc63461 100644
--- a/cluster.yml
+++ b/cluster.yml
@@ -30,15 +30,24 @@
     - { role: docker, tags: docker }
     - { role: rkt, tags: rkt, when: "'rkt' in [ etcd_deployment_type, kubelet_deployment_type ]" }
 
-- hosts: etcd:!k8s-cluster
+- hosts: all
+  any_errors_fatal: true
+  roles:
+    - { role: vault, tags: vault, vault_bootstrap: true, when: "cert_management == 'vault'" }
+
+- hosts: etcd:k8s-cluster
   any_errors_fatal: true
   roles:
     - { role: etcd, tags: etcd }
 
+- hosts: etcd:k8s-cluster:vault
+  any_errors_fatal: true
+  roles:
+    - { role: vault, tags: vault, when: "cert_management == 'vault'"}
+
 - hosts: k8s-cluster
   any_errors_fatal: true
   roles:
-    - { role: etcd, tags: etcd }
     - { role: kubernetes/node, tags: node }
     - { role: network_plugin, tags: network }
 
diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml
index 8242b5fd9c8f796aaeaca91f68a807f75bf127ef..f69efd1db94ece7117c3ef51595f010270d880fa 100644
--- a/inventory/group_vars/all.yml
+++ b/inventory/group_vars/all.yml
@@ -206,3 +206,9 @@ etcd_deployment_type: docker
 kubelet_deployment_type: docker
 
 efk_enabled: false
+
+## Certificate Management
+## This setting determines whether certs are generated via scripts or whether a
+## cluster of Hashicorp's Vault is started to issue certificates (using etcd
+## as a backend). Options are "script" or "vault"
+cert_management: script
diff --git a/roles/adduser/defaults/main.yml b/roles/adduser/defaults/main.yml
index b3a69229c15aca990cd255d6c0f0ddf68a4f9daa..bd8611d3bc1507bb9aa731bc17fe4d686eb436ea 100644
--- a/roles/adduser/defaults/main.yml
+++ b/roles/adduser/defaults/main.yml
@@ -14,6 +14,14 @@ addusers:
     system: yes
     group: "{{ kube_cert_group }}"
     createhome: no
+  vault:
+    comment: "Hashicorp Vault user"
+    createhome: no
+    name: vault
+    shell: /sbin/nologin
+    system: yes
+
+
 
 adduser:
   name: "{{ user.name }}"
diff --git a/roles/download/defaults/main.yml b/roles/download/defaults/main.yml
index 6fc594a49d4142e59549c70f7d8851d1eaf8c69d..1e1f2f6e8361e25e3ffd7222f2bc4388371578f9 100644
--- a/roles/download/defaults/main.yml
+++ b/roles/download/defaults/main.yml
@@ -26,6 +26,7 @@ calico_cni_version: "v1.5.5"
 weave_version: 1.8.2
 flannel_version: v0.6.2
 pod_infra_version: 3.0
+vault_version: 0.6.3
 
 # Download URL's
 etcd_download_url: "https://storage.googleapis.com/kargo/{{etcd_version}}_etcd"
diff --git a/roles/etcd/defaults/main.yml b/roles/etcd/defaults/main.yml
index 9f117da762fd19ddc87bd925562923f3f34a00bc..a5ba5a1b376e3a3da005c01008ffa0a8f2075bc3 100644
--- a/roles/etcd/defaults/main.yml
+++ b/roles/etcd/defaults/main.yml
@@ -2,6 +2,7 @@
 etcd_bin_dir: "{{ local_release_dir }}/etcd/etcd-{{ etcd_version }}-linux-amd64/"
 
 etcd_config_dir: /etc/ssl/etcd
+# Role vault.boostrap has an implicit requirement on this var. It should be set at a higher level (inventory+)
 etcd_cert_dir: "{{ etcd_config_dir }}/ssl"
 etcd_cert_group: root
 
diff --git a/roles/vault/defaults/main.yml b/roles/vault/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b42345a18473bcf63b9ad1546d8119a3cc88cfab
--- /dev/null
+++ b/roles/vault/defaults/main.yml
@@ -0,0 +1,65 @@
+---
+
+vault_bootstrap: false
+vault_ca_options:
+  common_name: kube-cluster-ca
+  format: pem
+  ttl: 87600h
+vault_cert_dir: "{{ vault_config_dir }}/ssl"
+vault_client_headers:
+  Accept: "application/json"
+  Content-Type: "application/json"
+vault_config:
+  backend:
+    etcd:
+      address: "https://{{ hostvars[groups.etcd[0]]['ansible_default_ipv4']['address'] }}:2379"
+      ha_enabled: "true"
+      redirect_addr: "https://{{ ansible_default_ipv4.address }}:{{ vault_port }}"
+      tls_ca_file: "{{ vault_cert_dir }}/ca.pem"
+  cluster_name: "kubernetes-vault"
+  default_lease_ttl: "{{ vault_default_lease_ttl }}"
+  listener:
+    tcp:
+      address: "0.0.0.0:{{ vault_port }}"
+      tls_cert_file: "{{ vault_cert_dir }}/api.pem"
+      tls_key_file: "{{ vault_cert_dir }}/api-key.pem"
+  max_lease_ttl: 720h
+vault_config_dir: /etc/vault
+vault_container_name: kube-hashicorp-vault
+vault_default_lease_ttl: 720h
+vault_default_role_permissions:
+  allow_any_name: true
+vault_deployment_type: docker
+vault_etcd_needs_gen: false
+vault_etcd_sync_hosts: []
+vault_max_lease_ttl: 87600h 
+vault_needs_gen: false
+vault_port: 8200
+vault_secret_shares: 1
+vault_secret_threshold: 1
+vault_secrets_dir: "{{ vault_config_dir }}/secrets"
+vault_temp_config:
+  default_lease_ttl: "{{ vault_default_lease_ttl }}"
+  backend:
+    file:
+      path: /vault/file
+  listener:
+    tcp:
+      address: "0.0.0.0:{{ vault_temp_port }}"
+      tls_disable: "true"
+vault_temp_port: 8201
+
+# This should be set higher up, but setting defaults here to avoid issues
+etcd_cert_dir: /etc/ssl/etcd/ssl
+kube_cert_dir: /etc/kubernetes/ssl
+
+# Sync cert defaults (should be role, once include_role is fixed)
+sync_file: ''
+sync_file_dir: ''
+sync_file_host_count: 0
+sync_file_is_cert: false
+sync_file_key_path: ''
+sync_file_key_srcs: []
+sync_file_path: ''
+sync_file_results: []
+sync_file_srcs: []
diff --git a/roles/vault/meta/main.yml b/roles/vault/meta/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..747c3ad0db3b06bb7c7919fee69c54a50af98af9
--- /dev/null
+++ b/roles/vault/meta/main.yml
@@ -0,0 +1,6 @@
+---
+# Implicit requirement on sync_cert role (include_role used in tasks)
+
+dependencies:
+  - role: download
+    file: "{{ downloads.vault }}"
diff --git a/roles/vault/tasks/bootstrap/ca_trust.yml b/roles/vault/tasks/bootstrap/ca_trust.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4ba877aacc41fe32430f88ff06e0956e5c2b299f
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/ca_trust.yml
@@ -0,0 +1,32 @@
+---
+
+- name: trust_ca | pull CA from cert from groups.vault|first
+  command: "cat {{ vault_cert_dir }}/ca.pem"
+  register: vault_cert_file_cat
+  when: inventory_hostname == groups.vault|first
+
+# This part is mostly stolen from the etcd role
+- name: trust_ca | target ca-certificate store file
+  set_fact:
+    ca_cert_path: >-
+      {% if ansible_os_family == "Debian" -%}
+      /usr/local/share/ca-certificates/kube-cluster-ca.crt
+      {%- elif ansible_os_family == "RedHat" -%}
+      /etc/pki/ca-trust/source/anchors/kube-cluster-ca.crt
+      {%- elif ansible_os_family == "CoreOS" -%}
+      /etc/ssl/certs/kube-cluster-ca.pem
+      {%- endif %}
+
+- name: trust_ca | add CA to trusted CA dir
+  copy:
+    content: "{{ hostvars[groups.vault|first]['vault_cert_file_cat']['stdout'] }}"
+    dest: "{{ ca_cert_path }}"
+  register: vault_ca_cert
+
+- name: trust_ca | update ca-certificates (Debian/Ubuntu/CoreOS)
+  command: update-ca-certificates
+  when: vault_ca_cert.changed and ansible_os_family in ["Debian", "CoreOS"]
+
+- name: trust_ca | update ca-certificates (RedHat)
+  command: update-ca-trust extract
+  when: vault_ca_cert.changed and ansible_os_family == "RedHat"
diff --git a/roles/vault/tasks/bootstrap/gen_etcd_certs.yml b/roles/vault/tasks/bootstrap/gen_etcd_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5619aaa9300a9954ea19cd50a34b5d68908584ad
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/gen_etcd_certs.yml
@@ -0,0 +1,29 @@
+---
+
+- name: bootstrap/gen_etcd_certs | Add the etcd role
+  uri:
+    url: "http://{{ groups.vault|first }}:{{ vault_temp_port }}/v1/pki/roles/etcd"
+    headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    method: POST
+    body_format: json
+    body:
+      allow_any_name: true
+    status_code: 204
+  when: inventory_hostname == groups.etcd|first
+
+- include: ../gen_cert.yml
+  vars:
+    gen_cert_alt_names: "{{ groups.etcd | join(',') }},localhost"
+    gen_cert_copy_ca: "{{ true if item == vault_etcd_certs_needed|first else false }}"
+    gen_cert_hosts: "{{ groups.etcd }}"
+    gen_cert_ip_sans: >-
+        {%- for host in groups.etcd  -%}
+        {{ hostvars[host]["ansible_default_ipv4"]["address"] }}
+        {%- if not loop.last -%},{%- endif -%}
+        {%- endfor -%}
+        ,127.0.0.1,::1
+    gen_cert_path: "{{ item }}"
+    gen_cert_vault_headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    gen_cert_vault_role: etcd
+    gen_cert_vault_url: "http://{{ groups.vault|first }}:{{ vault_temp_port }}"
+  with_items: "{{ vault_etcd_certs_needed|default([]) }}"
diff --git a/roles/vault/tasks/bootstrap/gen_etcd_node_certs.yml b/roles/vault/tasks/bootstrap/gen_etcd_node_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3ef48fd579251aa68d9d5e4a629d4766dab00541
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/gen_etcd_node_certs.yml
@@ -0,0 +1,29 @@
+---
+
+- name: bootstrap/gen_etcd_node_certs | Add the etcd role
+  uri:
+    url: "http://{{ groups.vault|first }}:{{ vault_temp_port }}/v1/pki/roles/etcd"
+    headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    method: POST
+    body_format: json
+    body:
+      allow_any_name: true
+    status_code: 204
+  when: inventory_hostname == groups["k8s-cluster"]|first
+
+- include: ../gen_cert.yml
+  vars:
+    gen_cert_alt_names: "{{ groups['k8s-cluster'] | union(groups.etcd) | join(',') }},localhost"
+    gen_cert_copy_ca: "{{ true if item == vault_etcd_node_certs_needed|first else false }}"
+    gen_cert_hosts: "{{ groups['k8s-cluster'] | union(groups.etcd) }}"
+    gen_cert_ip_sans: >-
+        {%- for host in groups["k8s-cluster"] | union(groups.etcd) -%}
+        {{ hostvars[host]["ansible_default_ipv4"]["address"] }}
+        {%- if not loop.last -%},{%- endif -%}
+        {%- endfor -%}
+        ,127.0.0.1,::1
+    gen_cert_path: "{{ item }}"
+    gen_cert_vault_headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    gen_cert_vault_role: etcd
+    gen_cert_vault_url: "http://{{ groups.vault|first }}:{{ vault_temp_port }}"
+  with_items: "{{ vault_etcd_node_certs_needed|default([]) }}"
diff --git a/roles/vault/tasks/bootstrap/gen_vault_certs.yml b/roles/vault/tasks/bootstrap/gen_vault_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..79b60e541ce70db248b866b0214c64abd70f5bdd
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/gen_vault_certs.yml
@@ -0,0 +1,66 @@
+---
+
+- name: bootstrap/gen_vault_certs | Ensure vault_cert_dir exists
+  file:
+    path: "{{ vault_cert_dir }}"
+    state: directory
+
+- name: bootstrap/gen_vault_certs | Generate Root CA in vault-temp
+  uri:
+    url: "http://localhost:{{ vault_temp_port }}/v1/pki/root/generate/exported"
+    headers: "{{ vault_headers }}"
+    method: POST
+    body_format: json
+    body: "{{ vault_ca_options }}"
+  register: vault_ca_gen
+  when: inventory_hostname == groups.vault|first and vault_ca_cert_needed
+
+- name: bootstrap/gen_vault_certs | Set facts for ca cert and key
+  set_fact:
+    vault_ca_cert: "{{ vault_ca_gen.json.data.certificate }}"
+    vault_ca_key: "{{ vault_ca_gen.json.data.private_key }}"
+  when: inventory_hostname == groups.vault|first and vault_ca_cert_needed
+
+- name: bootstrap/gen_vault_certs | Set cert and key facts for all hosts other than groups.vault|first
+  set_fact:
+    vault_ca_cert: "{{ hostvars[groups.vault|first]['vault_ca_cert'] }}"
+    vault_ca_key: "{{ hostvars[groups.vault|first]['vault_ca_key'] }}"
+  when: inventory_hostname != groups.vault|first and vault_ca_cert_needed
+
+- name: bootstrap/gen_vault_certs | Copy root CA cert locally
+  copy:
+    content: "{{ vault_ca_cert }}"
+    dest: "{{ vault_cert_dir }}/ca.pem"
+  when: vault_ca_cert_needed
+
+- name: bootstrap/gen_vault_certs | Copy root CA key locally
+  copy:
+    content: "{{vault_ca_key}}"
+    dest: "{{vault_cert_dir}}/ca-key.pem"
+  when: vault_ca_cert_needed
+
+- name: boostrap/gen_vault_certs | Add the vault role
+  uri:
+    url: "http://localhost:{{ vault_temp_port }}/v1/pki/roles/vault"
+    headers: "{{ vault_headers }}"
+    method: POST
+    body_format: json
+    body: "{{ vault_default_role_permissions }}"
+    status_code: 204
+  when: inventory_hostname == groups.vault|first and vault_api_cert_needed
+
+- include: ../gen_cert.yml
+  vars:
+    gen_cert_alt_names: "{{ groups.vault | join(',') }},localhost"
+    gen_cert_hosts: "{{ groups.vault }}"
+    gen_cert_ip_sans: >-
+        {%- for host in groups.vault -%}
+        {{ hostvars[host]["ansible_default_ipv4"]["address"] }}
+        {%- if not loop.last -%},{%- endif -%}
+        {%- endfor -%}
+        ,127.0.0.1,::1
+    gen_cert_path: "{{ vault_cert_dir }}/api.pem"
+    gen_cert_vault_headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    gen_cert_vault_role: vault
+    gen_cert_vault_url: "http://{{ groups.vault|first }}:{{ vault_temp_port }}"
+  when: vault_api_cert_needed
diff --git a/roles/vault/tasks/bootstrap/main.yml b/roles/vault/tasks/bootstrap/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4f73bd78cf4609ef21184e834854eb7d702bca29
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/main.yml
@@ -0,0 +1,60 @@
+---
+
+## Sync Certs
+
+- include: bootstrap/sync_vault_certs.yml
+  when: inventory_hostname in groups.vault
+
+- include: bootstrap/sync_etcd_certs.yml
+  when: inventory_hostname in groups.etcd
+
+- include: bootstrap/sync_etcd_node_certs.yml
+  when: inventory_hostname in groups["k8s-cluster"] | union(groups.etcd)
+
+## Generate Certs
+
+# Start a temporary instance of Vault
+- include: bootstrap/start_vault_temp.yml
+  when: >-
+        ( hostvars[groups.etcd|first].get("vault_etcd_certs_needed", [])|length > 0 or
+        hostvars[groups.etcd|first].get("vault_etcd_node_certs_needed", [])|length > 0 or
+        hostvars[groups.vault|first]["vault_ca_cert_needed"] ) and
+        inventory_hostname == groups.vault|first
+
+# Generate root CA certs for Vault if none exist
+- include: bootstrap/gen_vault_certs.yml
+  when: >-
+        ( hostvars[groups.vault|first]["vault_ca_cert_needed"] or
+        hostvars[groups.vault|first]["vault_api_cert_needed"] ) and
+        inventory_hostname in groups.vault
+
+# Change vault-temp's issuing CA to use existing ca.pem/ca-key.pem
+- include: config_ca.yml
+  vars:
+    vault_url: "http://{{ groups.vault|first }}:{{ vault_temp_port }}"
+  when: >-
+        ( hostvars[groups.etcd|first].get("vault_etcd_certs_needed", [])|length > 0 or
+        hostvars[groups["k8s-cluster"]|first].get("vault_etcd_node_certs_needed", [])|length > 0 or
+        hostvars[groups.vault|first]["vault_api_cert_needed"] ) and
+        not hostvars[groups.vault|first]["vault_ca_cert_needed"] and
+        inventory_hostname == groups.vault|first
+
+# Generate etcd certs for etcd cluster members
+- include: bootstrap/gen_etcd_certs.yml
+  when: >- 
+        hostvars[groups.etcd|first].get("vault_etcd_certs_needed", [])|length > 0 and
+        inventory_hostname in groups.etcd
+
+# Generate etcd node certs for all k8s-cluster
+- include: bootstrap/gen_etcd_node_certs.yml
+  when: >-
+        hostvars[groups["k8s-cluster"]|first].get("vault_etcd_node_certs_needed", [])|length > 0 and
+        inventory_hostname in groups["k8s-cluster"] | union(groups.etcd)
+
+# Stop temporary vault
+- include: bootstrap/stop_vault_temp.yml
+  when: >-
+        inventory_hostname == groups.vault|first and
+        hostvars[groups.vault|first]["vault_temp_start"]|succeeded
+
+- include: ca_trust.yml
diff --git a/roles/vault/tasks/bootstrap/start_vault_temp.yml b/roles/vault/tasks/bootstrap/start_vault_temp.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9fbc9719ed5326fce6d4a66254cedbdcd68126f5
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/start_vault_temp.yml
@@ -0,0 +1,55 @@
+---
+
+- name: boostrap/start_vault_temp | Ensure vault-temp isn't already running
+  shell: if docker rm -f vault-temp 2>&1 1>/dev/null;then echo true;else echo false;fi
+  register: vault_temp_stop_check
+  changed_when: "{{ 'true' in vault_temp_stop_check.stdout }}"
+
+- name: bootstrap/start_vault_temp | Start single node Vault with file backend
+  command: >
+           docker run -d --cap-add=IPC_LOCK --name vault-temp -p {{ vault_temp_port }}:{{ vault_temp_port }}
+           -e 'VAULT_LOCAL_CONFIG={{ vault_temp_config|to_json }}'
+           -v /etc/vault:/etc/vault
+           {{ vault_image_repo }}:{{ vault_version }} server
+  register: vault_temp_start
+
+- name: bootstrap/start_vault_temp | Initialize vault-temp
+  uri:
+    url: "http://localhost:{{ vault_temp_port }}/v1/sys/init"
+    headers: "{{ vault_client_headers }}"
+    method: PUT
+    body_format: json
+    body:
+      secret_shares: 1
+      secret_threshold: 1
+  register: vault_temp_init
+
+# NOTE: vault_headers and vault_url are used by subsequent gen_cert calls
+- name: bootstrap/start_vault_temp | Set needed vault facts
+  set_fact:
+    vault_temp_unseal_keys: "{{ vault_temp_init.json['keys'] }}"
+    vault_temp_root_token: "{{ vault_temp_init.json.root_token }}"
+    vault_headers: "{{ vault_client_headers|combine({'X-Vault-Token': vault_temp_init.json.root_token}) }}"
+
+- name: bootstrap/start_vault_temp | Unseal vault-temp
+  uri:
+    url: "http://localhost:{{ vault_temp_port }}/v1/sys/unseal"
+    headers: "{{ vault_headers }}"
+    method: POST
+    body_format: json
+    body:
+      key: "{{ item }}"
+  with_items: "{{ vault_temp_unseal_keys|default([]) }}"
+
+- name: bootstrap/start_vault_temp | Create new PKI mount
+  uri:
+    url: "http://localhost:{{ vault_temp_port }}/v1/sys/mounts/pki"
+    headers: "{{ vault_headers }}"
+    method: POST
+    body_format: json
+    body:
+      config:
+        default_lease_ttl: "{{ vault_default_lease_ttl }}"
+        max_lease_ttl: "{{ vault_max_lease_ttl }}"
+      type: pki
+    status_code: 204
diff --git a/roles/vault/tasks/bootstrap/stop_vault_temp.yml b/roles/vault/tasks/bootstrap/stop_vault_temp.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1699eebc7c381c9db5b02a0e818a3de0e4cfceca
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/stop_vault_temp.yml
@@ -0,0 +1,4 @@
+---
+
+- name: stop vault-temp container
+  command: docker stop vault-temp
diff --git a/roles/vault/tasks/bootstrap/sync_etcd_certs.yml b/roles/vault/tasks/bootstrap/sync_etcd_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d6ae8e4cc660d214448cf67f2ab63ae869eee0b1
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/sync_etcd_certs.yml
@@ -0,0 +1,38 @@
+---
+
+- name: bootstrap/sync_etcd_certs | Create list of certs needing creation
+  set_fact: 
+    vault_etcd_cert_list: >-
+        {{ vault_etcd_cert_list|default([]) +  [
+        "admin-" + item + ".pem",
+        "member-" + item + ".pem"
+        ] }}
+  with_items: "{{ groups.etcd }}"
+
+- include: ../sync_file.yml
+  vars: 
+    sync_file: "{{ item }}"
+    sync_file_dir: "{{ etcd_cert_dir }}"
+    sync_file_hosts: "{{ groups.etcd }}"
+    sync_file_is_cert: true
+  with_items: "{{ vault_etcd_cert_list|default([]) }}"
+
+- name: bootstrap/sync_etcd_certs | Set facts for etcd sync_file results
+  set_fact:
+    vault_etcd_certs_needed: "{{ vault_etcd_certs_needed|default([]) + [item.path] }}"
+  with_items: "{{ sync_file_results }}"
+  when: item.no_srcs|bool
+
+- name: bootstrap/sync_etcd_certs | Unset sync_file_results after etcd certs sync
+  set_fact:
+    sync_file_results: []
+
+- include: ../sync_file.yml
+  vars:
+    sync_file: ca.pem
+    sync_file_dir: "{{ etcd_cert_dir }}"
+    sync_file_hosts: "{{ groups.etcd }}"
+
+- name: bootstrap/sync_etcd_certs | Unset sync_file_results after ca.pem sync
+  set_fact:
+    sync_file_results: []
diff --git a/roles/vault/tasks/bootstrap/sync_etcd_node_certs.yml b/roles/vault/tasks/bootstrap/sync_etcd_node_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8a50a5208606c59b125aad213907421d7d7c52e9
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/sync_etcd_node_certs.yml
@@ -0,0 +1,34 @@
+---
+
+- name: bootstrap/sync_etcd_node_certs | Create list of certs needing creation
+  set_fact: 
+    vault_etcd_node_cert_list: "{{ vault_etcd_node_cert_list|default([]) +  ['node-' + item + '.pem'] }}"
+  with_items: "{{ groups['k8s-cluster'] | union(groups.etcd) }}"
+
+- include: ../sync_file.yml
+  vars: 
+    sync_file: "{{ item }}"
+    sync_file_dir: "{{ etcd_cert_dir }}"
+    sync_file_hosts: "{{ groups['k8s-cluster'] | union(groups.etcd) }}"
+    sync_file_is_cert: true
+  with_items: "{{ vault_etcd_node_cert_list|default([]) }}"
+
+- name: bootstrap/sync_etcd_node_certs | Set facts for etcd sync_file results
+  set_fact:
+    vault_etcd_node_certs_needed: "{{ vault_etcd_node_certs_needed|default([]) + [item.path] }}"
+  with_items: "{{ sync_file_results }}"
+  when: item.no_srcs|bool
+
+- name: bootstrap/sync_etcd_node_certs | Unset sync_file_results after etcd node certs
+  set_fact:
+    sync_file_results: []
+
+- include: ../sync_file.yml
+  vars: 
+    sync_file: ca.pem
+    sync_file_dir: "{{ etcd_cert_dir }}"
+    sync_file_hosts: "{{ groups['k8s-cluster']| union(groups.etcd) }}"
+
+- name: bootstrap/sync_etcd_node_certs | Unset sync_file_results after ca.pem
+  set_fact:
+    sync_file_results: []
diff --git a/roles/vault/tasks/bootstrap/sync_vault_certs.yml b/roles/vault/tasks/bootstrap/sync_vault_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ad2c498533a8c092a20d909662b8e445e51c57f7
--- /dev/null
+++ b/roles/vault/tasks/bootstrap/sync_vault_certs.yml
@@ -0,0 +1,32 @@
+---
+
+- include: ../sync_file.yml
+  vars:
+    sync_file: "ca.pem"
+    sync_file_dir: "{{ vault_cert_dir }}"
+    sync_file_hosts: "{{ groups.vault }}"
+    sync_file_is_cert: true
+
+- name: "bootstrap/sync_vault_certs | Set facts for vault sync_file results"
+  set_fact:
+    vault_ca_cert_needed: "{{ true if sync_file_results|length > 0 else false }}"
+
+- name: bootstrap/sync_vault_certs | Unset sync_file_results after ca.pem sync
+  set_fact:
+    sync_file_results: []
+
+- include: ../sync_file.yml
+  vars:
+    sync_file: "api.pem"
+    sync_file_dir: "{{ vault_cert_dir }}"
+    sync_file_hosts: "{{ groups.vault }}"
+    sync_file_is_cert: true
+
+- name: bootstrap/sync_vault_certs | Set fact if Vault's API cert is needed
+  set_fact:
+    vault_api_cert_needed: "{{ true if sync_file_results|length > 0 else false }}"
+
+- name: bootstrap/sync_vault_certs | Unset sync_file_results after api.pem sync
+  set_fact:
+    sync_file_results: []
+
diff --git a/roles/vault/tasks/check_vault.yml b/roles/vault/tasks/check_vault.yml
new file mode 100644
index 0000000000000000000000000000000000000000..575d1207fd1c16e19813c86104c443c3842c668f
--- /dev/null
+++ b/roles/vault/tasks/check_vault.yml
@@ -0,0 +1,77 @@
+---
+
+# Check if vault is reachable on the localhost
+- name: check_vault | Attempt to pull local vault health
+  uri:
+    url: "https://localhost:{{ vault_port }}/v1/sys/health"
+    headers: "{{ vault_client_headers }}"
+    validate_certs: no
+  ignore_errors: true
+  register: vault_local_service_health
+
+- name: check_vault | Set facts about local Vault health
+  set_fact:
+    vault_is_running: "{{ vault_local_service_health|succeeded }}"
+    vault_is_initialized: "{{ vault_local_service_health.get('json', {}).get('initialized', false) }}"
+    vault_is_sealed: "{{ vault_local_service_health.get('json', {}).get('sealed', true) }}"
+    vault_in_standby: "{{ vault_local_service_health.get('json', {}).get('standby', true) }}"
+    vault_run_version: "{{ vault_local_service_health.get('json', {}).get('version', '') }}"
+
+- name: check_vault | Set fact about the Vault cluster's initialization state
+  set_fact:
+    vault_cluster_is_initialized: "{{ vault_is_initialized or hostvars[item]['vault_is_initialized'] }}"
+  with_items: "{{ groups.vault }}"
+
+- name: check_vault | Set fact about the Vault Cluster's available hosts
+  set_fact:
+    vault_available_hosts: "{{ vault_available_hosts|default([]) + [item] }}"
+  with_items: "{{ groups.vault }}"
+  when: "hostvars[item]['vault_is_running'] and not hostvars[item]['vault_is_sealed']"
+
+- include: sync_file.yml
+  vars:
+    sync_file: "{{ item }}"
+    sync_file_dir: "{{ vault_secrets_dir }}"
+    sync_file_hosts: "{{ groups.vault }}"
+  with_items:
+    - root_token
+    - unseal_keys
+
+# Logic is hard to follow on this one, probably need to simplify somehow
+- name: "check_vault | Set fact based on sync_file_results"
+  set_fact:
+    vault_secrets_available: "{{ vault_secrets_available|default(true) and not item.no_srcs }}"
+  with_items: "{{ sync_file_results }}"
+
+- name: "check_vault | Reset sync_file_results to avoid variable bleed"
+  set_fact:
+    sync_file_results: []
+
+- name: "check_vault | Print out warning message if secrets are not available"
+  pause:
+    prompt: >
+         Vault orchestration may not be able to proceed. The Vault cluster is initialzed, but
+         'root_token' or 'unseal_keys' were not found in {{ vault_secrets_dir }}. These are
+         needed for many orchestration steps.
+  when: vault_cluster_is_initialized and not vault_secrets_available
+
+- name: "check_vault | Cat root_token from a vault host"
+  command: "cat {{ vault_secrets_dir }}/root_token"
+  register: vault_root_token_cat
+  when: vault_secrets_available and inventory_hostname == groups.vault|first
+
+- name: "check_vault | Cat unseal_keys from a vault host"
+  command: "cat {{ vault_secrets_dir }}/unseal_keys"
+  register: vault_unseal_keys_cat
+  when: vault_secrets_available and inventory_hostname == groups.vault|first
+
+- name: "check_vault | Set needed facts for Vault API interaction when Vault is already running"
+  set_fact:
+    vault_root_token: "{{ hostvars[groups.vault|first]['vault_root_token_cat']['stdout'] }}"
+    vault_unseal_keys: "{{ hostvars[groups.vault|first]['vault_unseal_keys_cat']['stdout_lines'] }}"
+  when: vault_secrets_available
+
+- name: "check-vault | Update vault_headers if we have the root_token"
+  set_fact:
+    vault_headers: "{{ vault_client_headers | combine({'X-Vault-Token': vault_root_token}) }}"
+  when: vault_secrets_available
diff --git a/roles/vault/tasks/cluster/docker.yml b/roles/vault/tasks/cluster/docker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ba7d3a3aab608595a67b53d62c8b89fd9abb52da
--- /dev/null
+++ b/roles/vault/tasks/cluster/docker.yml
@@ -0,0 +1,25 @@
+---
+
+- name: docker | Check on state of docker instance
+  command: "docker inspect {{ vault_container_name }}"
+  ignore_errors: true
+  register: vault_container_inspect
+
+- name: docker | Set fact on container status
+  set_fact:
+      vault_container_inspect_json: "{{ vault_container_inspect.stdout|from_json }}"
+  when: vault_container_inspect|succeeded
+
+# Not sure if State.Running is the best check here...
+- name: docker | Remove old container if it's not currently running
+  command: "docker rm {{ vault_container_name }}"
+  when: vault_container_inspect|succeeded and not vault_container_inspect_json[0]["State"]["Running"]|bool
+
+- name: docker | Start a new Vault instance
+  command: >
+           docker run -d --cap-add=IPC_LOCK --name {{vault_container_name}} -p {{vault_port}}:{{vault_port}}
+           -e 'VAULT_LOCAL_CONFIG={{ vault_config|to_json }}'
+           -v /etc/vault:/etc/vault
+           {{vault_image_repo}}:{{vault_version}} server
+  register: vault_docker_start
+  when: vault_container_inspect|failed or not vault_container_inspect_json[0]["State"]["Running"]|bool
diff --git a/roles/vault/tasks/cluster/gen_kube_master_certs.yml b/roles/vault/tasks/cluster/gen_kube_master_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bf886af66d353c1d66ac3ab30284256c071468d3
--- /dev/null
+++ b/roles/vault/tasks/cluster/gen_kube_master_certs.yml
@@ -0,0 +1,33 @@
+---
+
+- name: "cluster/gen_kube_node_certs | Ensure kube_cert_dir exists"
+  file:
+    path: "{{ kube_cert_dir }}" 
+    state: directory
+
+- name: gen_kube_master_certs | Add the kube role
+  uri:
+    url: "https://{{ hostvars[groups.vault|first]['vault_leader'] }}:{{ vault_port }}/v1/pki/roles/kubernetes"
+    headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    method: POST
+    body_format: json
+    body: "{{ vault_default_role_permissions }}"
+    status_code: 204
+  when: inventory_hostname == groups["kube-master"]|first
+
+- include: ../gen_cert.yml
+  vars:
+    gen_cert_alt_names: "{{ groups['kube-master'] | join(',') }},localhost"
+    gen_cert_copy_ca: "{{ true if item == vault_kube_master_certs_needed|first else false }}"
+    gen_cert_hosts: "{{ groups['kube-master'] }}"
+    gen_cert_ip_sans: >-
+        {%- for host in groups["kube-master"] -%}
+        {{ hostvars[host]["ansible_default_ipv4"]["address"] }}
+        {%- if not loop.last -%},{%- endif -%}
+        {%- endfor -%}
+        ,127.0.0.1,::1
+    gen_cert_path: "{{ item }}"
+    gen_cert_vault_headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    gen_cert_vault_role: kubernetes
+    gen_cert_vault_url: "https://{{ hostvars[groups.vault|first]['vault_leader'] }}:{{ vault_port }}"
+  with_items: "{{ vault_kube_master_certs_needed|default([]) }}"
diff --git a/roles/vault/tasks/cluster/gen_kube_node_certs.yml b/roles/vault/tasks/cluster/gen_kube_node_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ea48d4c43ac7e9d5990ea61baf264eaa4fcff578
--- /dev/null
+++ b/roles/vault/tasks/cluster/gen_kube_node_certs.yml
@@ -0,0 +1,33 @@
+---
+
+- name: "cluster/gen_kube_node_certs | Ensure kube_cert_dir exists"
+  file:
+    path: "{{ kube_cert_dir }}" 
+    state: directory
+
+- name: "cluster/gen_kube_node_certs | Add the kubernetes role"
+  uri:
+    url: "https://{{ hostvars[groups.vault|first]['vault_leader'] }}:{{ vault_port }}/v1/pki/roles/kubernetes"
+    headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    method: POST
+    body_format: json
+    body: "{{ vault_default_role_permissions }}"
+    status_code: 204
+  when: inventory_hostname == groups["k8s-cluster"]|first
+
+- include: ../gen_cert.yml
+  vars:
+    gen_cert_alt_names: "{{ groups['k8s-cluster'] | join(',') }},localhost"
+    gen_cert_copy_ca: "{{ true if item == vault_kube_node_certs_needed|first else false }}"
+    gen_cert_hosts: "{{ groups['k8s-cluster'] }}"
+    gen_cert_ip_sans: >-
+        {%- for host in groups["k8s-cluster"] -%}
+        {{ hostvars[host]["ansible_default_ipv4"]["address"] }}
+        {%- if not loop.last -%},{%- endif -%}
+        {%- endfor -%}
+        ,127.0.0.1,::1
+    gen_cert_path: "{{ item }}"
+    gen_cert_vault_headers: "{{ hostvars[groups.vault|first]['vault_headers'] }}"
+    gen_cert_vault_role: kubernetes
+    gen_cert_vault_url: "https://{{ hostvars[groups.vault|first]['vault_leader'] }}:{{ vault_port }}"
+  with_items: "{{ vault_kube_node_certs_needed|default([]) }}"
diff --git a/roles/vault/tasks/cluster/init.yml b/roles/vault/tasks/cluster/init.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9f124869a30ea6b89be57d8eb58cde1341773b91
--- /dev/null
+++ b/roles/vault/tasks/cluster/init.yml
@@ -0,0 +1,49 @@
+---
+
+- name: cluster/init | Initialize Vault
+  uri:
+    url: "https://{{ groups.vault|first }}:{{ vault_port }}/v1/sys/init"
+    headers: "{{ vault_client_headers }}"
+    method: POST
+    body_format: json
+    body:
+      secret_shares: "{{ vault_secret_shares }}"
+      secret_threshold: "{{ vault_secret_threshold }}"
+    validate_certs: false
+  register: vault_init_result
+  when: not vault_cluster_is_initialized and inventory_hostname == groups.vault|first
+
+- name: cluster/init | Set facts on the results of the initialization
+  set_fact:
+    vault_unseal_keys: "{{ vault_init_result.json['keys'] }}"
+    vault_root_token: "{{ vault_init_result.json.root_token }}"
+    vault_headers: "{{ vault_client_headers|combine({'X-Vault-Token': vault_init_result.json.root_token}) }}"
+  when: not vault_cluster_is_initialized and inventory_hostname == groups.vault|first
+
+- name: cluster/init | Ensure all hosts have these facts
+  set_fact:
+    vault_unseal_keys: "{{ hostvars[groups.vault|first]['vault_unseal_keys'] }}"
+    vault_root_token: "{{ hostvars[groups.vault|first]['vault_root_token'] }}"
+  when: not vault_cluster_is_initialized and inventory_hostname != groups.vault|first
+
+- name: cluster/init | Ensure the vault_secrets_dir exists
+  file:
+    path: "{{ vault_secrets_dir }}"
+    state: directory
+
+- name: cluster/init | Ensure all in groups.vault have the unseal_keys locally
+  copy:
+    content: "{{ vault_unseal_keys|join('\n') }}"
+    dest: "{{ vault_secrets_dir }}/unseal_keys"
+  when: not vault_cluster_is_initialized
+
+- name: cluster/init | Ensure all in groups.vault have the root_token locally
+  copy:
+    content: "{{ vault_root_token }}"
+    dest: "{{ vault_secrets_dir }}/root_token"
+  when: not vault_cluster_is_initialized
+
+- name: cluster/init | Ensure vault_headers and vault statuses are updated
+  set_fact:
+    vault_headers: "{{ vault_client_headers | combine({'X-Vault-Token': vault_root_token})}}"
+    vault_cluster_is_initialized: true
diff --git a/roles/vault/tasks/cluster/main.yml b/roles/vault/tasks/cluster/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..14c3adaafb1da25ab0fbd4aa5b872386414b7ab0
--- /dev/null
+++ b/roles/vault/tasks/cluster/main.yml
@@ -0,0 +1,30 @@
+---
+
+## Vault Cluster Setup
+
+- include: docker.yml
+  when: inventory_hostname in groups.vault and vault_deployment_type == "docker"
+- include: init.yml
+  when: inventory_hostname in groups.vault
+- include: unseal.yml
+  when: inventory_hostname in groups.vault
+- include: pki_mount.yml
+  when: 'inventory_hostname == hostvars[groups.vault|first]["vault_leader"]'
+- include: config_ca.yml
+  vars:
+    vault_url: "https://{{ vault_leader }}:{{ vault_port }}"
+  when: 'inventory_hostname == hostvars[groups.vault|first]["vault_leader"]'
+
+## Sync Kubernetes Certs
+
+- include: sync_kube_master_certs.yml
+  when: inventory_hostname in groups["kube-master"]
+- include: sync_kube_node_certs.yml
+  when: inventory_hostname in groups["k8s-cluster"]
+
+## Generate Kubernetes Certs
+
+- include: gen_kube_master_certs.yml
+  when: inventory_hostname in groups["kube-master"]
+- include: gen_kube_node_certs.yml
+  when: inventory_hostname in groups["k8s-cluster"]
diff --git a/roles/vault/tasks/cluster/pki_mount.yml b/roles/vault/tasks/cluster/pki_mount.yml
new file mode 100644
index 0000000000000000000000000000000000000000..266c5f666d8a9f3952fd051dee7bd708011a68d3
--- /dev/null
+++ b/roles/vault/tasks/cluster/pki_mount.yml
@@ -0,0 +1,23 @@
+---
+
+- name: cluster/pki_mount | Test if default PKI mount exists
+  uri:
+    url: "https://localhost:{{ vault_port }}/v1/sys/mounts/pki/tune"
+    headers: "{{ vault_headers }}"
+    validate_certs: false
+  ignore_errors: true
+  register: vault_pki_mount_check
+
+- name: cluster/pki_mount | Mount default PKI mount if needed
+  uri:
+    url: "https://localhost:{{ vault_port }}/v1/sys/mounts/pki"
+    headers: "{{ vault_headers }}"
+    method: POST
+    body_format: json
+    body:
+      config:
+        default_lease_ttl: "{{ vault_default_lease_ttl }}"
+        max_lease_ttl: "{{ vault_max_lease_ttl }}"
+      type: pki
+    status_code: 204
+  when: vault_pki_mount_check | failed
diff --git a/roles/vault/tasks/cluster/sync_kube_master_certs.yml b/roles/vault/tasks/cluster/sync_kube_master_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..db201a911485c90b478d8764dfb2b528309a18e9
--- /dev/null
+++ b/roles/vault/tasks/cluster/sync_kube_master_certs.yml
@@ -0,0 +1,38 @@
+---
+
+- name: cluster/sync_kube_master_certs | Create list of needed certs
+  set_fact:
+    vault_kube_master_cert_list: >-
+        {{  vault_kube_master_cert_list|default([]) + [
+        "admin-" + item + ".pem",
+        "apiserver-" + item + ".pem"
+        ] }}
+  with_items: "{{ groups['kube-master'] }}"
+
+- include: ../sync_file.yml
+  vars: 
+    sync_file: "{{ item }}"
+    sync_file_dir: "{{ kube_cert_dir }}"
+    sync_file_hosts: "{{ groups['kube-master'] }}"
+    sync_file_is_cert: true
+  with_items: "{{ vault_kube_master_cert_list|default([]) }}"
+
+- name: cluster/sync_kube_master_certs | Set facts for kube-master sync_file results
+  set_fact:
+    vault_kube_master_certs_needed: "{{ vault_kube_master_certs_needed|default([]) + [item.path] }}"
+  with_items: "{{ sync_file_results }}"
+  when: item.no_srcs|bool
+
+- name: cluster/sync_kube_master_certs | Unset sync_file_results after kube master certs
+  set_fact:
+    sync_file_results: []
+
+- include: ../sync_file.yml
+  vars: 
+    sync_file: ca.pem
+    sync_file_dir: "{{ kube_cert_dir }}"
+    sync_file_hosts: "{{ groups['kube-master'] }}"
+
+- name: cluster/sync_kube_master_certs | Unset sync_file_results after ca.pem
+  set_fact:
+    sync_file_results: []
diff --git a/roles/vault/tasks/cluster/sync_kube_node_certs.yml b/roles/vault/tasks/cluster/sync_kube_node_certs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9a73b787056b028a34ccf9307cb2491e32028372
--- /dev/null
+++ b/roles/vault/tasks/cluster/sync_kube_node_certs.yml
@@ -0,0 +1,34 @@
+---
+
+- name: cluster/sync_kube_node_certs | Create list of needed certs
+  set_fact:
+    vault_kube_node_cert_list: "{{ vault_kube_node_cert_list|default([]) + ['node-' + item + '.pem'] }}"
+  with_items: "{{ groups['k8s-cluster'] }}"
+
+- include: ../sync_file.yml
+  vars: 
+    sync_file: "{{ item }}"
+    sync_file_dir: "{{ kube_cert_dir }}"
+    sync_file_hosts: "{{ groups['k8s-cluster'] }}"
+    sync_file_is_cert: true
+  with_items: "{{ vault_kube_node_cert_list|default([]) }}"
+
+- name: cluster/sync_kube_node_certs | Set facts for kube-master sync_file results
+  set_fact:
+    vault_kube_node_certs_needed: "{{ vault_kube_node_certs_needed|default([]) + [item.path] }}"
+  with_items: "{{ sync_file_results }}"
+  when: item.no_srcs|bool
+
+- name: cluster/sync_kube_node_certs | Unset sync_file_results after kube node certs
+  set_fact:
+    sync_file_results: []
+
+- include: ../sync_file.yml
+  vars: 
+    sync_file: ca.pem
+    sync_file_dir: "{{ kube_cert_dir }}"
+    sync_file_hosts: "{{ groups['k8s-cluster'] }}"
+
+- name: cluster/sync_kube_node_certs | Unset sync_file_results after ca.pem
+  set_fact:
+    sync_file_results: []
diff --git a/roles/vault/tasks/cluster/unseal.yml b/roles/vault/tasks/cluster/unseal.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ea74694eaf8ba0c93a1b731bed4b9ec8e456b07f
--- /dev/null
+++ b/roles/vault/tasks/cluster/unseal.yml
@@ -0,0 +1,26 @@
+---
+
+- name: cluster/unseal | Unseal Vault
+  uri:
+    url: "https://localhost:{{ vault_port }}/v1/sys/unseal"
+    headers: "{{ vault_headers }}"
+    method: POST
+    body_format: json
+    body:
+      key: "{{ item }}"
+  with_items: "{{ vault_unseal_keys|default([]) }}"
+  when: vault_is_sealed
+
+- name: cluster/unseal | Find the current leader
+  uri:
+    url: "https://localhost:{{ vault_port }}/v1/sys/health"
+    headers: "{{ vault_headers }}"
+    method: HEAD
+    status_code: 200,429
+  register: vault_leader_check
+
+- name: cluster/unseal | Set fact for current leader
+  set_fact:
+    vault_leader: "{{ item }}"
+  with_items: "{{ groups.vault }}"
+  when: 'hostvars[item]["vault_leader_check"]["status"] == 200'
diff --git a/roles/vault/tasks/config_ca.yml b/roles/vault/tasks/config_ca.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ef3c08cf10d27c0a46fec2434baf9008288d3b21
--- /dev/null
+++ b/roles/vault/tasks/config_ca.yml
@@ -0,0 +1,19 @@
+---
+
+- name: config_ca | Read root CA cert for Vault
+  command: cat /etc/vault/ssl/ca.pem
+  register: vault_ca_cert_cat
+
+- name: config_ca | Read root CA key for Vault
+  command: cat /etc/vault/ssl/ca-key.pem
+  register: vault_ca_key_cat
+
+- name: config_ca | Configure pki mount to use the found root CA cert and key
+  uri:
+    url: "{{ vault_url }}/v1/pki/config/ca"
+    headers: "{{ vault_headers }}"
+    method: POST
+    body_format: json
+    body:
+      pem_bundle: "{{ vault_ca_cert_cat.stdout + '\n' + vault_ca_key_cat.stdout }}"
+    status_code: 204
diff --git a/roles/vault/tasks/gen_cert.yml b/roles/vault/tasks/gen_cert.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9dc5d30f40cdc7250d4acfa7c5898d4b76ae1bc3
--- /dev/null
+++ b/roles/vault/tasks/gen_cert.yml
@@ -0,0 +1,50 @@
+---
+
+# This could be a role or custom module
+
+- name: gen_cert | Ensure target directory exists
+  file:
+    path: "{{ gen_cert_path | dirname }}" 
+    state: directory
+
+- name: gen_cert | Generate the cert
+  uri:
+    url: "{{ gen_cert_vault_url}}/v1/pki/issue/{{ gen_cert_vault_role }}"
+    headers: "{{ gen_cert_vault_headers }}"
+    method: POST
+    body_format: json
+    body:
+      alt_names: "{{ gen_cert_alt_names|default([]) }}"
+      common_name: "{{ gen_cert_path.rsplit('/', 1)[1].rsplit('.', 1)[0] }}"
+      format: "{{ gen_cert_format|default('pem') }}"
+      ip_sans: "{{ gen_cert_ip_sans|default([]) }}"
+  register: gen_cert_result
+  when: inventory_hostname == gen_cert_hosts|first
+
+- name: gen_cert | Copy the cert to all hosts
+  copy:
+    content: "{{ hostvars[gen_cert_hosts|first]['gen_cert_result']['json']['data']['certificate'] }}"
+    dest: "{{ gen_cert_path }}"
+
+- name: gen_cert | Copy the key to all hosts
+  copy:
+    content: "{{ hostvars[gen_cert_hosts|first]['gen_cert_result']['json']['data']['private_key'] }}"
+    dest: "{{ gen_cert_path.rsplit('.', 1)|first + '-key.' + gen_cert_path.rsplit('.', 1)|last }}"
+
+- name: gen_cert | Copy issuing CA cert
+  copy:
+    content: "{{ hostvars[gen_cert_hosts|first]['gen_cert_result']['json']['data']['issuing_ca'] }}"
+    dest: "{{ gen_cert_path | dirname }}/ca.pem"
+  when: gen_cert_copy_ca|default(false)|bool
+
+- name: gen_cert | Unset common variables to avoid bleed over
+  set_fact:
+    gen_cert_copy_ca: false
+    gen_cert_alt_names: []
+    gen_cert_format: pem
+    gen_cert_hosts: []
+    gen_cert_ip_sans: []
+    gen_cert_path: ''
+    gen_cert_vault_headers: ''
+    gen_cert_vault_role: ''
+    gen_cert_vault_url: ''
diff --git a/roles/vault/tasks/main.yml b/roles/vault/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59c8bb396a9b4c80d3ca4cd6b565fba69e3d3101
--- /dev/null
+++ b/roles/vault/tasks/main.yml
@@ -0,0 +1,13 @@
+---
+
+- include: check_vault.yml
+  when: inventory_hostname in groups.vault
+
+# bootstrap.yml's sole purpose is to ensure certs exist for Vault and Etcd
+# prior to startup, so TLS can be enabled.
+- include: bootstrap/main.yml
+  when: vault_bootstrap|bool
+
+# cluster.yml should only run after the backend service is ready (default etcd)
+- include: cluster/main.yml
+  when: not vault_bootstrap|bool
diff --git a/roles/vault/tasks/sync.yml b/roles/vault/tasks/sync.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0c92724570310881ec95d755599959d94c4caaf2
--- /dev/null
+++ b/roles/vault/tasks/sync.yml
@@ -0,0 +1,38 @@
+---
+
+- name: "sync_file | Cat the file"
+  command: "cat {{ sync_file_path }}"
+  register: sync_file_cat
+  when: inventory_hostname == sync_file_srcs|first
+
+- name: "sync_file | Cat the key file"
+  command: "cat {{ sync_file_key_path }}"
+  register: sync_file_key_cat
+  when: sync_file_is_cert|bool and inventory_hostname == sync_file_srcs|first
+
+- name: "sync_file | Set facts for file contents"
+  set_fact:
+    sync_file_contents: "{{ hostvars[sync_file_srcs|first]['sync_file_cat']['stdout'] }}"
+
+- name: "sync_file | Set fact for key contents"
+  set_fact:
+    sync_file_key_contents: "{{ hostvars[sync_file_srcs|first]['sync_file_key_cat']['stdout'] }}"
+  when: sync_file_is_cert|bool
+
+- name: "sync_file | Ensure the directory exists"
+  file:
+    path: "{{ sync_file_dir }}"
+    state: directory
+  when: inventory_hostname not in sync_file_srcs
+
+- name: "sync_file | Copy the file to hosts that don't have it"
+  copy:
+    content: "{{ sync_file_contents }}"
+    dest: "{{ sync_file_path }}"
+  when: inventory_hostname not in sync_file_srcs
+
+- name: "sync_file | Copy the key file to hosts that don't have it"
+  copy:
+    content: "{{ sync_file_key_contents }}"
+    dest: "{{ sync_file_key_path }}"
+  when: sync_file_is_cert|bool and inventory_hostname not in sync_file_srcs
diff --git a/roles/vault/tasks/sync_file.yml b/roles/vault/tasks/sync_file.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4db1d4637e43cce7148f60b4b413d379aea9bb76
--- /dev/null
+++ b/roles/vault/tasks/sync_file.yml
@@ -0,0 +1,97 @@
+---
+
+# NOTE: This should be a role (or custom module), but currently include_role is too buggy to use
+
+- name: "sync_file | Set facts for directory and file when sync_file_path is defined"
+  set_fact:
+    sync_file_dir: "{{ sync_file_path | dirname }}"
+    sync_file: "{{ sync_file_path | basename }}"
+  when: sync_file_path|bool
+
+- name: "sync_file | Set fact for sync_file_path when undefined"
+  set_fact:
+    sync_file_path: "{{ (sync_file_dir, sync_file)|join('/') }}"
+  when: not sync_file_path|bool
+
+- name: "sync_file | Set fact for key path name"
+  set_fact:
+    sync_file_key_path: "{{ sync_file_path.rsplit('.', 1)|first + '-key.' + sync_file_path.rsplit('.', 1)|last }}"
+  when: sync_file_is_cert|bool and not sync_file_key_path|bool
+
+- name: "sync_file | Check if file exists"
+  stat:
+    path: "{{ sync_file_path }}"
+  register: sync_file_stat
+
+- name: "sync_file | Check if key file exists"
+  stat:
+    path: "{{ sync_file_key_path }}"
+  register: sync_file_key_stat
+  when: sync_file_is_cert|bool
+
+- name: "sync_file | Combine all possible file sync sources"
+  set_fact:
+    sync_file_srcs: "{{ sync_file_srcs|default([]) + [host_item] }}"
+  with_items: "{{ sync_file_hosts | unique }}"
+  loop_control:
+    loop_var: host_item
+  when: hostvars[host_item]["sync_file_stat"]["stat"]["exists"]|bool
+
+- name: "sync_file | Combine all possible key file sync sources"
+  set_fact:
+    sync_file_key_srcs: "{{ sync_file_key_srcs|default([]) + [host_item] }}"
+  with_items: "{{ sync_file_hosts | unique }}"
+  loop_control:
+    loop_var: host_item
+  when: sync_file_is_cert|bool and hostvars[host_item]["sync_file_key_stat"]["stat"]["exists"]|bool
+
+- name: "sync_file | Remove sync sources with files that do not match sync_file_srcs|first"
+  set_fact:
+    _: "{% if inventory_hostname in sync_file_srcs %}{{ sync_file_srcs.remove(inventory_hostname) }}{% endif %}"
+  when: >-
+        sync_file_srcs|length > 0 and
+        inventory_hostname != sync_file_srcs|first and
+        sync_file_stat.stat.get("checksum") != hostvars[sync_file_srcs|first]["sync_file_stat"]["stat"]["checksum"]
+
+- name: "sync_file | Remove sync sources with keys that do not match sync_file_srcs|first"
+  set_fact:
+    _: "{% if inventory_hostname in sync_file_srcs %}{{ sync_file_srcs.remove(inventory_hostname) }}{% endif %}"
+  when: >-
+        sync_file_is_cert|bool and
+        sync_file_key_srcs|length > 0 and
+        inventory_hostname != sync_file_srcs|first and
+        sync_file_key_stat.stat.checksum != hostvars[sync_file_srcs|first]["sync_file_key_stat"]["stat"]["checksum"]
+
+- name: "sync_file | Consolidate file and key sources"
+  set_fact:
+    sync_file_srcs: "{{ sync_file_srcs | intersect(sync_file_key_srcs) }}"
+  when: sync_file_is_cert|bool
+
+- name: "sync_file | Set facts for situations where sync is not needed"
+  set_fact:
+    sync_file_no_srcs: "{{ true if sync_file_srcs|length == 0 else false }}"
+    sync_file_unneeded: "{{ true if sync_file_srcs|length == sync_file_hosts|length else false }}"
+
+- name: "sync_file | Set sync_file_result fact"
+  set_fact:
+    sync_file_result:
+      no_srcs: "{{ sync_file_no_srcs }}"
+      path: "{{ sync_file_path }}"
+      sync_unneeded: "{{ sync_file_unneeded }}"
+
+- name: "sync_file | Update sync_file_results fact"
+  set_fact:
+    sync_file_results: "{{ sync_file_results|default([]) + [sync_file_result] }}"
+
+- include: sync.yml
+  when: not (sync_file_no_srcs or sync_file_unneeded)
+
+- name: "Unset local vars to avoid variable bleed into next iteration"
+  set_fact:
+    sync_file_dir: ''
+    sync_file: ''
+    sync_file_key_path: ''
+    sync_file_hosts: []
+    sync_file_path: ''
+    sync_file_srcs: []
+    sync_file_key_srcs: []