diff --git a/.gitignore b/.gitignore
index 4df491aa10218c8602b6dae1ad435179501eebdc..4791280e95cd57c24d279101a3416512dc9b6ed8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ __pycache__/
 
 # Distribution / packaging
 .Python
+artifacts/
 env/
 build/
 credentials/
diff --git a/cluster.yml b/cluster.yml
index 7b842d917041efa69b3c5d7fdf411e55846404ff..d9240ac9701638f09ce08f81db323f7dd3b67436 100644
--- a/cluster.yml
+++ b/cluster.yml
@@ -82,6 +82,7 @@
     - { role: kubespray-defaults}
     - { role: kubernetes-apps/network_plugin, tags: network }
     - { role: kubernetes-apps/policy_controller, tags: policy-controller }
+    - { role: kubernetes/client, tags: client }
 
 - hosts: calico-rr
   any_errors_fatal: "{{ any_errors_fatal | default(true) }}"
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 65b590a2ff1f15d5b169b83832125bfbb31bc04c..f0c7c0014b9a2a353858de0d5ea6cbdda3143988 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -101,3 +101,17 @@ access the Kubernetes Dashboard at the following URL:
 To see the password, refer to the section above, titled *Connecting to
 Kubernetes*. The host can be any kube-master or kube-node or loadbalancer
 (when enabled).
+
+Accessing Kubernetes API
+------------------------
+
+The main client of Kubernetes is `kubectl`. It is installed on each kube-master
+host and can optionally be configured on your ansible host by setting
+`kubeconfig_localhost: true` in the configuration. If enabled, kubectl and
+admin.conf will appear in the artifacts/ directory after deployment. You can
+see a list of nodes by running the following commands:
+
+    cd artifacts/
+    ./kubectl --kubeconfig admin.conf get nodes
+
+If desired, copy kubectl to your bin dir and admin.conf to ~/.kube/config.
diff --git a/inventory/group_vars/k8s-cluster.yml b/inventory/group_vars/k8s-cluster.yml
index bdcca73d4f3acfb78a2fed9e70656610b76cdcb4..c185fe46c29fdf447d234bdcef2c309be9709275 100644
--- a/inventory/group_vars/k8s-cluster.yml
+++ b/inventory/group_vars/k8s-cluster.yml
@@ -152,6 +152,11 @@ efk_enabled: false
 # Helm deployment
 helm_enabled: false
 
+# Make a copy of kubeconfig on the host that runs Ansible in GITDIR/artifacts
+# kubeconfig_localhost: false
+# Download kubectl onto the host that runs Ansible in GITDIR/artifacts
+# kubectl_localhost: false
+
 # dnsmasq
 # dnsmasq_upstream_dns_servers:
 #  - /resolvethiszone.with/10.0.4.250
diff --git a/roles/kubernetes/client/defaults/main.yml b/roles/kubernetes/client/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5864e991f524009b9d19fc2dd454119c20655ef6
--- /dev/null
+++ b/roles/kubernetes/client/defaults/main.yml
@@ -0,0 +1,7 @@
+---
+kubeconfig_localhost: false
+kubectl_localhost: false
+artifacts_dir: "./artifacts"
+
+kube_config_dir: "/etc/kubernetes"
+kube_apiserver_port: "6443"
diff --git a/roles/kubernetes/client/tasks/main.yml b/roles/kubernetes/client/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..06df5af681bfc7cc8a2ce4a1bd08486a7c75c27f
--- /dev/null
+++ b/roles/kubernetes/client/tasks/main.yml
@@ -0,0 +1,66 @@
+---
+- name: Set first kube master
+  set_fact:
+    first_kube_master: "{{ hostvars[groups['kube-master'][0]]['access_ip'] | default(hostvars[groups['kube-master'][0]]['ip'] | default(hostvars[groups['kube-master'][0]]['ansible_default_ipv4']['address'])) }}"
+
+- name: Set external kube-apiserver endpoint
+  set_fact:
+    external_apiserver_endpoint: >-
+      {%- if loadbalancer_apiserver is defined and loadbalancer_apiserver.port is defined -%}
+      https://{{ apiserver_loadbalancer_domain_name|default('lb-apiserver.kubernetes.local') }}:{{ loadbalancer_apiserver.port|default(kube_apiserver_port) }}
+      {%- else -%}
+      https://{{ first_kube_master }}:{{ kube_apiserver_port }}
+      {%- endif -%}
+  tags: facts
+
+- name: Gather certs for admin kubeconfig
+  slurp:
+    src: "{{ item }}"
+  delegate_to: "{{ groups['kube-master'][0] }}"
+  delegate_facts: no
+  register: admin_certs
+  with_items:
+    - "{{ kube_cert_dir }}/ca.pem"
+    - "{{ kube_cert_dir }}/admin-{{ inventory_hostname }}.pem"
+    - "{{ kube_cert_dir }}/admin-{{ inventory_hostname }}-key.pem"
+  when: not kubeadm_enabled|d(false)|bool
+
+- name: Write admin kubeconfig
+  template:
+    src: admin.conf.j2
+    dest: "{{ kube_config_dir }}/admin.conf"
+  when: not kubeadm_enabled|d(false)|bool
+
+- name: Create kube config dir
+  file:
+    path: "/root/.kube"
+    mode: "0700"
+    state: directory
+
+- name: Copy admin kubeconfig to root user home
+  copy:
+    src: "{{ kube_config_dir }}/admin.conf"
+    dest: "/root/.kube/config"
+    remote_src: yes
+    mode: "0700"
+    backup: yes
+
+- name: Copy admin kubeconfig to ansible host
+  fetch:
+    src: "{{ kube_config_dir }}/admin.conf"
+    dest: "{{ artifacts_dir }}/admin.conf"
+    flat: yes
+    validate_checksum: no
+  become: no
+  run_once: yes
+  when: kubeconfig_localhost|default(false)
+
+- name: Copy kubectl binary to ansible host
+  fetch:
+    src: "{{ bin_dir }}/kubectl"
+    dest: "{{ artifacts_dir }}/kubectl"
+    flat: yes
+    validate_checksum: no
+  become: no
+  run_once: yes
+  when: kubectl_localhost|default(false)
diff --git a/roles/kubernetes/client/templates/admin.conf.j2 b/roles/kubernetes/client/templates/admin.conf.j2
new file mode 100644
index 0000000000000000000000000000000000000000..b1640c1c563006408bca2212014783b11d554cb0
--- /dev/null
+++ b/roles/kubernetes/client/templates/admin.conf.j2
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Config
+current-context: admin-{{ cluster_name }}
+preferences: {}
+clusters:
+- cluster:
+    certificate-authority-data: {{ admin_certs.results[0]['content'] }}
+    server: {{ external_apiserver_endpoint }}
+  name: {{ cluster_name }}
+contexts:
+- context:
+    cluster: {{ cluster_name }}
+    user: admin-{{ cluster_name }}
+  name: admin-{{ cluster_name }}
+users:
+- name: admin-{{ cluster_name }}
+  user:
+    client-certificate-data: {{ admin_certs.results[1]['content'] }}
+    client-key-data: {{ admin_certs.results[2]['content'] }}
diff --git a/roles/kubespray-defaults/defaults/main.yaml b/roles/kubespray-defaults/defaults/main.yaml
index 5bd2fdc14e8bc566b086ee49c2af0981268b9266..3c07147da6947a3603d2bed388988f4587d81bb6 100644
--- a/roles/kubespray-defaults/defaults/main.yaml
+++ b/roles/kubespray-defaults/defaults/main.yaml
@@ -118,6 +118,11 @@ vault_deployment_type: docker
 kubeadm_enabled: false
 kubeadm_token: "abcdef.0123456789abcdef"
 
+# Make a copy of kubeconfig on the host that runs Ansible in GITDIR/artifacts
+kubeconfig_localhost: false
+# Download kubectl onto the host that runs Ansible in GITDIR/artifacts
+kubectl_localhost: false
+
 # K8s image pull policy (imagePullPolicy)
 k8s_image_pull_policy: IfNotPresent
 efk_enabled: false
diff --git a/tests/cloud_playbooks/create-gce.yml b/tests/cloud_playbooks/create-gce.yml
index 2b48227231d8ada87644d49060e6a13c5a6aa08d..11cd9a646df59898a2c662926f8efb43da38bb38 100644
--- a/tests/cloud_playbooks/create-gce.yml
+++ b/tests/cloud_playbooks/create-gce.yml
@@ -63,4 +63,4 @@
   gather_facts: false
   tasks:
     - name: Wait for SSH to come up.
-      local_action: wait_for host={{inventory_hostname}} port=22 delay=60 timeout=240 state=started
+      local_action: wait_for host={{ansible_host}} port=22 delay=60 timeout=240 state=started
diff --git a/upgrade-cluster.yml b/upgrade-cluster.yml
index 3dcb69f293a359dea734934e1d81747324d14df7..b836815252a1949bf31f1cddbd8545c65b92523d 100644
--- a/upgrade-cluster.yml
+++ b/upgrade-cluster.yml
@@ -89,6 +89,7 @@
     - { role: kubespray-defaults}
     - { role: kubernetes-apps/network_plugin, tags: network }
     - { role: kubernetes-apps/policy_controller, tags: policy-controller }
+    - { role: kubernetes/client, tags: client }
 
 - hosts: calico-rr
   any_errors_fatal: "{{ any_errors_fatal | default(true) }}"