From 9305a54cddf20401f4a908817202a45cad002944 Mon Sep 17 00:00:00 2001 From: Michael Russell Date: Tue, 4 Jun 2019 10:19:21 +0200 Subject: [PATCH] [elasticsearch] Keystore integration Closes: #90 Adds a kubernetes native way to add strings and files to the Elasticsearch keystore. Previously you needed to manually create the keystore and upload it as a secret. There were a couple of issues with this approach. 1. The Elasticsearch keystore has an internal version for the format. If this is changed it meant needing to recreate each keystore again. 2. If you wanted to add a single new value it meant recreating the entire keystore again --- elasticsearch/README.md | 16 ++--- elasticsearch/examples/config/Makefile | 18 +++++ elasticsearch/examples/config/README.md | 3 + elasticsearch/examples/config/test/goss.yaml | 25 +++++++ elasticsearch/examples/config/values.yaml | 38 +++++++++++ .../examples/config/watcher_encryption_key | 1 + elasticsearch/templates/statefulset.yaml | 47 +++++++++++++ elasticsearch/tests/elasticsearch_test.py | 67 +++++++++++++++++++ elasticsearch/values.yaml | 8 +++ helpers/matrix.yml | 1 + 10 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 elasticsearch/examples/config/Makefile create mode 100644 elasticsearch/examples/config/README.md create mode 100644 elasticsearch/examples/config/test/goss.yaml create mode 100644 elasticsearch/examples/config/values.yaml create mode 100644 elasticsearch/examples/config/watcher_encryption_key diff --git a/elasticsearch/README.md b/elasticsearch/README.md index 924800a48..a101f0e7c 100644 --- a/elasticsearch/README.md +++ b/elasticsearch/README.md @@ -111,6 +111,7 @@ helm install --name elasticsearch elastic/elasticsearch --set imageTag=7.3.0 | `schedulerName` | Name of the [alternate scheduler](https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/#specify-schedulers-for-pods) | `nil` | | `masterTerminationFix` | A workaround needed for Elasticsearch < 7.2 to prevent master status being lost during restarts [#63](https://github.com/elastic/helm-charts/issues/63) | `false` | | `lifecycle` | Allows you to add lifecycle configuration. See [values.yaml](./values.yaml) for an example of the formatting. | `{}` | +| `keystore` | Allows you map Kubernetes secrets into the keystore. See the [config example](/elasticsearch/examples/config/values.yaml) and [how to use the keystore](#how-to-use-the-keystore) | `enabled: false` | ## Try it out @@ -171,18 +172,9 @@ There are a couple reasons we recommend this. #### How to use the keystore? -1. Create a Kubernetes secret containing the [keystore](https://www.elastic.co/guide/en/elasticsearch/reference/current/secure-settings.html) - ``` - $ kubectl create secret generic elasticsearch-keystore --from-file=./elasticsearch.keystore - ``` -2. Mount it into the container via `secretMounts` - ``` - secretMounts: - - name: elasticsearch-keystore - secretName: elasticsearch-keystore - path: /usr/share/elasticsearch/config/elasticsearch.keystore - subPath: elasticsearch.keystore - ``` +Take a look a the [config example](/elasticsearch/examples/config/values.yaml) which has a tested version of adding strings and files to the keystore. + +If you have basic authentication enabled you will also need to set `keystore.strings.bootstrap.password: '${ELASTIC_PASSWORD}'`. #### How to enable snapshotting? diff --git a/elasticsearch/examples/config/Makefile b/elasticsearch/examples/config/Makefile new file mode 100644 index 000000000..613ec3c77 --- /dev/null +++ b/elasticsearch/examples/config/Makefile @@ -0,0 +1,18 @@ +default: test +include ../../../helpers/examples.mk + +RELEASE := helm-es-config + +install: + helm upgrade --wait --timeout=600 --install $(RELEASE) --values ./values.yaml ../../ ; \ + +secrets: + kubectl delete secret elastic-config-credentials elastic-config-secret elastic-config-slack || true + kubectl create secret generic elastic-config-credentials --from-literal=password=changeme --from-literal=username=elastic + kubectl create secret generic elastic-config-slack --from-literal=slack_url='https://hooks.slack.com/services/asdasdasd/asdasdas/asdasd' + kubectl create secret generic elastic-config-secret --from-file=./watcher_encryption_key + +test: secrets install goss + +purge: + helm del --purge $(RELEASE) diff --git a/elasticsearch/examples/config/README.md b/elasticsearch/examples/config/README.md new file mode 100644 index 000000000..d98d836bf --- /dev/null +++ b/elasticsearch/examples/config/README.md @@ -0,0 +1,3 @@ +# Config + +An example testing suite for testing some of the optional features of this chart. diff --git a/elasticsearch/examples/config/test/goss.yaml b/elasticsearch/examples/config/test/goss.yaml new file mode 100644 index 000000000..ed7982eb4 --- /dev/null +++ b/elasticsearch/examples/config/test/goss.yaml @@ -0,0 +1,25 @@ +http: + http://localhost:9200/_cluster/health: + status: 200 + timeout: 2000 + body: + - 'green' + - '"number_of_nodes":1' + - '"number_of_data_nodes":1' + + http://localhost:9200: + status: 200 + timeout: 2000 + body: + - '"cluster_name" : "config"' + - '"name" : "config-master-0"' + - 'You Know, for Search' + +command: + "elasticsearch-keystore list": + exit-status: 0 + stdout: + - keystore.seed + - bootstrap.password + - xpack.notification.slack.account.monitoring.secure_url + - xpack.watcher.encryption_key diff --git a/elasticsearch/examples/config/values.yaml b/elasticsearch/examples/config/values.yaml new file mode 100644 index 000000000..fee419d1d --- /dev/null +++ b/elasticsearch/examples/config/values.yaml @@ -0,0 +1,38 @@ +--- + +clusterName: "config" +replicas: 1 + +extraEnvs: + - name: ELASTIC_PASSWORD + valueFrom: + secretKeyRef: + name: elastic-credentials + key: password + - name: ELASTIC_USERNAME + valueFrom: + secretKeyRef: + name: elastic-credentials + key: username + - name: SLACK_URL + valueFrom: + secretKeyRef: + name: elastic-config-slack + key: slack_url + +esConfig: + elasticsearch.yml: | + path.data: /usr/share/elasticsearch/data + +secretMounts: + - name: elastic-config-secret + secretName: elastic-config-secret + path: /usr/share/elasticsearch/config/secret + +keystore: + enabled: true + strings: + bootstrap.password: '${ELASTIC_PASSWORD}' + xpack.notification.slack.account.monitoring.secure_url: '${SLACK_URL}' + files: + xpack.watcher.encryption_key: /usr/share/elasticsearch/config/secret/watcher_encryption_key diff --git a/elasticsearch/examples/config/watcher_encryption_key b/elasticsearch/examples/config/watcher_encryption_key new file mode 100644 index 000000000..b5f907866 --- /dev/null +++ b/elasticsearch/examples/config/watcher_encryption_key @@ -0,0 +1 @@ +supersecret diff --git a/elasticsearch/templates/statefulset.yaml b/elasticsearch/templates/statefulset.yaml index 30dcec51e..cf5c95920 100644 --- a/elasticsearch/templates/statefulset.yaml +++ b/elasticsearch/templates/statefulset.yaml @@ -111,6 +111,10 @@ spec: configMap: name: {{ template "uname" . }}-config {{- end }} +{{ if .Values.keystore.enabled }} + - name: keystore + emptyDir: {} +{{ end }} {{- if .Values.extraVolumes }} {{ tpl .Values.extraVolumes . | indent 6 }} {{- end }} @@ -129,6 +133,44 @@ spec: resources: {{ toYaml .Values.initResources | indent 10 }} {{- end }} +{{ if .Values.keystore.enabled }} + - name: keystore + image: "{{ .Values.image }}:{{ .Values.imageTag }}" + command: + - sh + - -c + - | + #!/usr/bin/env bash + set -euo pipefail + elasticsearch-keystore create + {{ range $path, $file := .Values.keystore.files -}} + echo 'Adding file "{{ $file }}" to keystore at path "{{ $path }}"' + elasticsearch-keystore add-file "{{ $path }}" "{{ $file }}" + {{- end -}} + {{ range $path, $string := .Values.keystore.strings }} + echo 'Adding string to path "{{ $path }}"' + echo "{{ $string }}" | elasticsearch-keystore add -x "{{ $path }}" + {{- end }} + cp -a /usr/share/elasticsearch/config/elasticsearch.keystore /tmp/keystore/ +{{- if .Values.extraEnvs }} + env: +{{ toYaml .Values.extraEnvs | indent 10 }} +{{- end }} + resources: +{{ toYaml .Values.initResources | indent 10 }} + volumeMounts: +{{ if .Values.keystore.enabled }} + - name: keystore + mountPath: /tmp/keystore +{{ end }} + {{- range .Values.secretMounts }} + - name: {{ .name }} + mountPath: {{ .path }} + {{- if .subPath }} + subPath: {{ .subPath }} + {{- end }} + {{- end }} +{{ end }} {{- if .Values.extraInitContainers }} {{ tpl .Values.extraInitContainers . | indent 6 }} {{- end }} @@ -219,6 +261,11 @@ spec: - name: "{{ template "uname" . }}" mountPath: /usr/share/elasticsearch/data {{- end }} +{{ if .Values.keystore.enabled }} + - name: keystore + mountPath: /usr/share/elasticsearch/config/elasticsearch.keystore + subPath: elasticsearch.keystore +{{ end }} {{- range .Values.secretMounts }} - name: {{ .name }} mountPath: {{ .path }} diff --git a/elasticsearch/tests/elasticsearch_test.py b/elasticsearch/tests/elasticsearch_test.py index cdc08a79a..8cc4ec3b7 100755 --- a/elasticsearch/tests/elasticsearch_test.py +++ b/elasticsearch/tests/elasticsearch_test.py @@ -793,3 +793,70 @@ def test_adding_pod_labels(): ''' r = helm_template(config) assert r['statefulset'][uname]['metadata']['labels']['app.kubernetes.io/name'] == 'elasticsearch' + +def test_keystore_enable(): + config = '' + + r = helm_template(config) + s = r['statefulset'][uname]['spec']['template']['spec'] + + assert s['volumes'] == None + + config = ''' +keystore: + enabled: true + ''' + + r = helm_template(config) + s = r['statefulset'][uname]['spec']['template']['spec'] + + assert {'name': 'keystore', 'emptyDir': {}} in s['volumes'] + +def test_keystore_init_container(): + config = '' + + r = helm_template(config) + i = r['statefulset'][uname]['spec']['template']['spec']['initContainers'][-1] + + assert i['name'] != 'keystore' + + config = ''' +keystore: + enabled: true + ''' + + r = helm_template(config) + i = r['statefulset'][uname]['spec']['template']['spec']['initContainers'][-1] + + assert i['name'] == 'keystore' + +def test_keystore_mount(): + config = ''' +keystore: + enabled: true +''' + + r = helm_template(config) + s = r['statefulset'][uname]['spec']['template']['spec'] + assert s['containers'][0]['volumeMounts'][-1] == { + 'mountPath': '/usr/share/elasticsearch/config/elasticsearch.keystore', + 'subPath': 'elasticsearch.keystore', + 'name': 'keystore' + } + +def test_keystore_bootstrap(): + config = ''' +keystore: + enabled: true + strings: + bootstrap.password: '${ELASTIC_PASSWORD}' + xpack.notification.slack.account.monitoring.secure_url: "https://hooks.slack.com/services/asdasdasd/asdasdas/asdasd" + files: + gcs.client.default.credentials_file: /usr/share/elasticsearch/config/gcs-credentials.json +''' + r = helm_template(config) + i = r['statefulset'][uname]['spec']['template']['spec']['initContainers'][1] + command = " ".join(i['command']) + assert 'echo "${ELASTIC_PASSWORD}" | elasticsearch-keystore add -x "bootstrap.password"' in command + assert 'echo "https://hooks.slack.com/services/asdasdasd/asdasdas/asdasd" | elasticsearch-keystore add -x "xpack.notification.slack.account.monitoring.secure_url"' in command + assert 'elasticsearch-keystore add-file "gcs.client.default.credentials_file" "/usr/share/elasticsearch/config/gcs-credentials.json"' in command diff --git a/elasticsearch/values.yaml b/elasticsearch/values.yaml index 13ca0626d..a2d975ac6 100755 --- a/elasticsearch/values.yaml +++ b/elasticsearch/values.yaml @@ -210,3 +210,11 @@ lifecycle: {} sysctlInitContainer: enabled: true + +keystore: + enabled: false +# strings: +# bootstrap.password: '${ELASTIC_PASSWORD}' +# xpack.notification.slack.account.monitoring.secure_url: "hello world" +# files: +# gcs.client.default.credentials_file: /usr/share/elasticsearch/config/gcs-credentials.json diff --git a/helpers/matrix.yml b/helpers/matrix.yml index d8dbf5396..221b35e47 100644 --- a/helpers/matrix.yml +++ b/helpers/matrix.yml @@ -5,6 +5,7 @@ CHART: - metricbeat ES_SUITE: - default + - config - multi - oss - security