diff --git a/control-shell.sh b/control-shell.sh
index 501357195822c8a3067bc5d55b945decf888164d..fb03b3602ff782ac5ec83c32fd79e8f3031b234c 100755
--- a/control-shell.sh
+++ b/control-shell.sh
@@ -11,5 +11,5 @@ docker run --rm -it \
     --hostname=control \
     -v /oas:/oas \
     -v /oas/local/control:/control/local \
-    docker.greenhost.net/openappstack/control \
+    docker.greenhost.net/openappstack/bootstrap/control \
     $command
diff --git a/control/Dockerfile b/control/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..6aa9ab612d1c8bbc5e52a81236a6fb751e0e6497
--- /dev/null
+++ b/control/Dockerfile
@@ -0,0 +1,19 @@
+FROM linuxbrew/linuxbrew
+MAINTAINER arie@greenhost.nl
+
+RUN brew update
+ADD brew/kubernetes-cli.rb /brew/kubernetes-cli.rb
+RUN brew install /brew/kubernetes-cli.rb
+RUN brew install kubernetes-helm helmfile
+RUN mkdir -p $HOME/.helm/plugins && \
+    helm plugin install https://github.com/databus23/helm-diff
+ADD bin/rke /control/bin/
+ADD control /control/bin/
+ADD cluster.yml.template /control/data/
+ADD k8s-config/ /control/k8s-config
+ADD bashrc /control/.bashrc
+USER root
+ENV PATH="/control/bin:${PATH}" \
+    HOME="/oas" \
+    HELM_HOME="/control/local/helm" \
+    KUBECONFIG="/control/local/kube/config"
diff --git a/control/README.md b/control/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8a8b589626b31eed8b76dd2ebf504ac464c80e8c
--- /dev/null
+++ b/control/README.md
@@ -0,0 +1,4 @@
+This folder contains the files necessary to make the `control` docker container.
+
+This container can be used to manage your Kubernetes cluster from a container
+*within kubernetes*.
diff --git a/control/bashrc b/control/bashrc
new file mode 100644
index 0000000000000000000000000000000000000000..2a1000c9db76b2f108f517ad5d2f6546de76b473
--- /dev/null
+++ b/control/bashrc
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+export PS1="\[\e[1;31m\]control shell \$\[\e[0m\] "
+
+pluginPath="$HELM_HOME/plugins/helm-diff"
+if ! [ -e "$pluginPath" ]
+then
+    # Create a symlink to the helm-diff plugin which was installed in the
+    # docker image.
+    ln -s "/home/linuxbrew/.helm/plugins/helm-diff" "$pluginPath"
+fi
+
+cd
+
+echo 'This is the "control" docker image for managing your oas cluster.'
+echo 'The following programs are available to view and administer the cluster:'
+echo '  kubectl'
+echo '  helm'
+echo '  helmfile'
+
+echo 'These are the currently installed helm releases:'
+echo '======== `helm ls`'
+helm ls
+echo '========'
diff --git a/control/bin/rke b/control/bin/rke
new file mode 100755
index 0000000000000000000000000000000000000000..8abf7099f9f76bde69ab67eabc339d8df3fdface
Binary files /dev/null and b/control/bin/rke differ
diff --git a/control/brew/kubernetes-cli.rb b/control/brew/kubernetes-cli.rb
new file mode 100644
index 0000000000000000000000000000000000000000..41972efb54314dccb7b8fa945d51d15618d0cddb
--- /dev/null
+++ b/control/brew/kubernetes-cli.rb
@@ -0,0 +1,59 @@
+class KubernetesCli < Formula
+  desc "Kubernetes command-line interface"
+  homepage "https://kubernetes.io/"
+  url "https://github.com/kubernetes/kubernetes.git",
+      :tag      => "v1.12.2",
+      :revision => "17c77c7898218073f14c8d573582e8d2313dc740"
+  head "https://github.com/kubernetes/kubernetes.git"
+
+  bottle do
+    cellar :any_skip_relocation
+    sha256 "3aa43db83e2181069bc3faca3ff83d9e724272d689047a8b4c648168e67b2bc4" => :mojave
+    sha256 "23a10b46914920291f84bff6cd7ca725f9f13b4e72d055a6200989602c973c1c" => :high_sierra
+    sha256 "908d289d262b55d3331e1d6f3b335297622299869d1d1705c9fa56ed58702d4d" => :sierra
+  end
+
+  depends_on "go" => :build
+  depends_on "rsync" => :build unless OS.mac?
+
+  def install
+    ENV["GOPATH"] = buildpath
+    os = OS.linux? ? "linux" : "darwin"
+    arch = MacOS.prefer_64_bit? ? "amd64" : "x86"
+    dir = buildpath/"src/k8s.io/kubernetes"
+    dir.install buildpath.children - [buildpath/".brew_home"]
+
+    cd dir do
+      # Race condition still exists in OS X Yosemite
+      # Filed issue: https://github.com/kubernetes/kubernetes/issues/34635
+      ENV.deparallelize { system "make", "generated_files" }
+
+      # Make binary
+      system "make", "kubectl"
+      bin.install "_output/local/bin/#{os}/#{arch}/kubectl"
+
+      # Install bash completion
+      output = Utils.popen_read("#{bin}/kubectl completion bash")
+      (bash_completion/"kubectl").write output
+
+      # Install zsh completion
+      output = Utils.popen_read("#{bin}/kubectl completion zsh")
+      (zsh_completion/"_kubectl").write output
+
+      prefix.install_metafiles
+    end
+  end
+
+  test do
+    run_output = shell_output("#{bin}/kubectl 2>&1")
+    assert_match "kubectl controls the Kubernetes cluster manager.", run_output
+
+    version_output = shell_output("#{bin}/kubectl version --client 2>&1")
+    assert_match "GitTreeState:\"clean\"", version_output
+    if build.stable?
+      assert_match stable.instance_variable_get(:@resource)
+                         .instance_variable_get(:@specs)[:revision],
+                   version_output
+    end
+  end
+end
diff --git a/control/cluster.yml.template b/control/cluster.yml.template
new file mode 100644
index 0000000000000000000000000000000000000000..35fbc5735aea497b6d14fc173091a683e4dac438
--- /dev/null
+++ b/control/cluster.yml.template
@@ -0,0 +1,124 @@
+nodes:
+- address: $NODE_ADDRESS
+  # port: "22"
+  # internal_address: ""
+  role:
+  - controlplane
+  - worker
+  - etcd
+  hostname_override: oas
+  user: root
+  # docker_socket: /var/run/docker.sock
+  # ssh_key: ""
+  ssh_key_path: /oas/config/ssh_key
+  # labels: {}
+services:
+  etcd:
+    image: ""
+    extra_args: {}
+    extra_binds: []
+    extra_env: []
+    external_urls: []
+    ca_cert: ""
+    cert: ""
+    key: ""
+    path: ""
+    snapshot: false
+    retention: ""
+    creation: ""
+  kube-api:
+    image: ""
+    extra_args: {}
+    extra_binds: []
+    extra_env: []
+    service_cluster_ip_range: 10.43.0.0/16
+    service_node_port_range: ""
+    pod_security_policy: false
+  kube-controller:
+    image: ""
+    extra_args: {}
+    extra_binds: []
+    extra_env: []
+    cluster_cidr: 10.42.0.0/16
+    service_cluster_ip_range: 10.43.0.0/16
+  scheduler:
+    image: ""
+    extra_args: {}
+    extra_binds: []
+    extra_env: []
+  kubelet:
+    image: ""
+    extra_args: {}
+    extra_binds: []
+    extra_env: []
+    cluster_domain: cluster.local
+    infra_container_image: ""
+    cluster_dns_server: 10.43.0.10
+    fail_swap_on: false
+  kubeproxy:
+    image: ""
+    extra_args: {}
+    extra_binds: []
+    extra_env: []
+network:
+  plugin: canal
+  options: {}
+authentication:
+  strategy: x509
+  options: {}
+  sans: []
+addons: ""
+addons_include: []
+system_images:
+  etcd: rancher/coreos-etcd:v3.2.18
+  alpine: rancher/rke-tools:v0.1.15
+  nginx_proxy: rancher/rke-tools:v0.1.15
+  cert_downloader: rancher/rke-tools:v0.1.15
+  kubernetes_services_sidecar: rancher/rke-tools:v0.1.15
+  kubedns: rancher/k8s-dns-kube-dns-amd64:1.14.10
+  dnsmasq: rancher/k8s-dns-dnsmasq-nanny-amd64:1.14.10
+  kubedns_sidecar: rancher/k8s-dns-sidecar-amd64:1.14.10
+  kubedns_autoscaler: rancher/cluster-proportional-autoscaler-amd64:1.0.0
+  kubernetes: rancher/hyperkube:v1.11.3-rancher1
+  flannel: rancher/coreos-flannel:v0.10.0
+  flannel_cni: rancher/coreos-flannel-cni:v0.3.0
+  calico_node: rancher/calico-node:v3.1.3
+  calico_cni: rancher/calico-cni:v3.1.3
+  calico_controllers: ""
+  calico_ctl: rancher/calico-ctl:v2.0.0
+  canal_node: rancher/calico-node:v3.1.3
+  canal_cni: rancher/calico-cni:v3.1.3
+  canal_flannel: rancher/coreos-flannel:v0.10.0
+  wave_node: weaveworks/weave-kube:2.1.2
+  weave_cni: weaveworks/weave-npc:2.1.2
+  pod_infra_container: rancher/pause-amd64:3.1
+  ingress: rancher/nginx-ingress-controller:0.16.2-rancher1
+  ingress_backend: rancher/nginx-ingress-controller-defaultbackend:1.4
+  metrics_server: rancher/metrics-server-amd64:v0.2.1
+ssh_agent_auth: false
+authorization:
+  mode: rbac
+  options: {}
+ignore_docker_version: false
+kubernetes_version: ""
+private_registries: []
+ingress:
+  # Set this to none, so we can install traefik ourselves.
+  provider: none
+  options: {}
+  node_selector: {}
+  extra_args: {}
+cluster_name: ""
+cloud_provider:
+  name: ""
+prefix_path: ""
+addon_job_timeout: 0
+bastion_host:
+  address: ""
+  port: ""
+  user: ""
+  ssh_key: ""
+  ssh_key_path: ""
+monitoring:
+  provider: ""
+  options: {}
diff --git a/control/control b/control/control
new file mode 100755
index 0000000000000000000000000000000000000000..6ac099ed022f687da6c02307ec2cab7520a2dc86
--- /dev/null
+++ b/control/control
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+command=$1
+read -r ip < "/oas/config/ip"
+
+buildCluster()
+{
+    echo "Setting up OpenAppStack cluster."
+    mkdir -p "/control/config"
+    clusterConfigFile="/control/config/cluster.yml"
+    cp "/control/data/cluster.yml.template" "$clusterConfigFile"
+    mkdir -p "${HOME}/.ssh"
+    sed -i -e "s/\$NODE_ADDRESS/${ip}/" "$clusterConfigFile"
+    {
+        echo "# Inserted by OAS control"
+        ssh-keyscan -H "$ip" 2> /dev/null
+    } >> "${HOME}/.ssh/known_hosts"
+    rke up --config="$clusterConfigFile" || exit
+    mkdir -p "/control/local/kube"
+    cp "/control/config/kube_config_cluster.yml" "/control/local/kube/config"
+}
+
+installTiller()
+{
+    kubectl apply -f "/control/k8s-config/tiller-permissions.yml"
+    helm init --service-account=tiller
+}
+
+createStorage()
+{
+    pushd "/control/k8s-config/storage" || return
+    shopt -s nullglob
+    for yaml in ./*.yaml ./*.yml
+    do
+        kubectl apply -f "$yaml"
+        storageDir=$(grep '/var/local/k8s/[^"]*' -o "$yaml")
+        ssh -i "/oas/config/ssh_key" "root@${ip}" "mkdir -p \"$storageDir\" && chmod a+w \"$storageDir\""
+    done
+}
+
+getRepos()
+{
+    reposDir="/oas/source/repos"
+    mkdir -p "$reposDir"
+    for repo in "helmfiles" "charts"
+    do
+        target="${reposDir}/${repo}"
+        if ! [ -d "$target" ]
+        then
+            echo "Getting ${repo}."
+            git clone "https://code.greenhost.net/openappstack/${repo}" "$target" || exit
+        else
+            echo "Already have ${repo}, updating."
+            pushd "$target"
+            git pull
+            popd
+        fi
+    done
+}
+
+configureKeycloak()
+{
+    kubectl create secret generic realm-secret "--from-file=/control/k8s-config/realm.json"
+}
+
+configFiles()
+{
+    mkdir -p "/oas/config/values/apps"
+    for app in "traefik keycloak"
+    do
+        # Create corresponding file to set extra overriding values.
+        touch "/oas/config/values/apps/${app}.yaml"
+    done
+}
+
+helmfile()
+{
+    helmfile -e oas -f "/oas/source/repos/helmfiles/helmfile.d" apply
+}
+
+case "$command" in
+  setup)
+    buildCluster
+    installTiller
+    createStorage
+    getRepos
+    configureKeycloak
+    configFiles
+    helmfile
+    ;;
+  *)
+    $command
+esac
diff --git a/control/k8s-config/realm.json b/control/k8s-config/realm.json
new file mode 100644
index 0000000000000000000000000000000000000000..67f171ab6f335cc002cf0f1bce932e89a53ff1bb
--- /dev/null
+++ b/control/k8s-config/realm.json
@@ -0,0 +1,19 @@
+{
+    "realm": "OpenAppStack",
+    "enabled": true,
+    "sslRequired": "external",
+    "registrationAllowed": true,
+    "requiredCredentials": [ "password" ],
+    "roles" : {
+        "realm" : [
+            {
+                "name": "user",
+                "description": "User privileges"
+            },
+            {
+                "name": "admin",
+                "description": "Administrator privileges"
+            }
+        ]
+    }
+}
diff --git a/control/k8s-config/storage/keycloak-postgres-storage.yml b/control/k8s-config/storage/keycloak-postgres-storage.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aaa2932b747dd5f03a225cc6cd4707630c3c8a3a
--- /dev/null
+++ b/control/k8s-config/storage/keycloak-postgres-storage.yml
@@ -0,0 +1,15 @@
+kind: PersistentVolume
+apiVersion: v1
+metadata:
+  name: keycloak-postgres
+  labels:
+    type: local
+spec:
+  capacity:
+    storage: 1Gi
+  accessModes:
+    - ReadWriteOnce
+  persistentVolumeReclaimPolicy: Recycle
+  hostPath:
+    path: "/var/local/k8s/keycloak-postgres"
+    type: DirectoryOrCreate
diff --git a/control/k8s-config/storage/traefik-storage.yml b/control/k8s-config/storage/traefik-storage.yml
new file mode 100644
index 0000000000000000000000000000000000000000..06f88329e6f2338c3fb609ecb0e64ba9c5232b71
--- /dev/null
+++ b/control/k8s-config/storage/traefik-storage.yml
@@ -0,0 +1,14 @@
+kind: PersistentVolume
+apiVersion: v1
+metadata:
+  name: traefik-pv-acme
+  labels:
+    type: local
+spec:
+  capacity:
+    storage: 10Mi
+  accessModes:
+    - ReadWriteOnce
+  hostPath:
+    path: "/var/local/k8s/traefik-pv-acme"
+    type: DirectoryOrCreate
diff --git a/control/k8s-config/tiller-permissions.yml b/control/k8s-config/tiller-permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d3ed1ad46e59dc90dfa95857904d4b079777e09a
--- /dev/null
+++ b/control/k8s-config/tiller-permissions.yml
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: tiller
+  namespace: kube-system
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1beta1
+metadata:
+  name: tiller-clusterrolebinding
+subjects:
+- kind: ServiceAccount
+  name: tiller
+  namespace: kube-system
+roleRef:
+  kind: ClusterRole
+  name: cluster-admin
+  apiGroup: ""
diff --git a/get-control.sh b/get-control.sh
index e68516283ed2a06e10c059c2cdb56790f23267eb..4f641e3a4f6482ae396067052e1b0e8853d98822 100644
--- a/get-control.sh
+++ b/get-control.sh
@@ -20,4 +20,4 @@ then
 fi
 
 # Get control docker image.
-docker pull "docker.greenhost.net/openappstack/control"
+docker pull "docker.greenhost.net/openappstack/bootstrap/control"