Skip to content
This repository has been archived by the owner on May 16, 2023. It is now read-only.

[kibana] create a secret with es token #1720

Merged
merged 15 commits into from
Nov 15, 2022
1 change: 1 addition & 0 deletions kibana/examples/upgrade/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ upgrade-es:
kubectl delete pod --selector=app=$(ES_CLUSTER)-master

upgrade-kb:
sleep 120 # wait that ES pods are started
helm upgrade $(RELEASE) ../../ --wait --values values.yaml
kubectl rollout status deployment $(RELEASE)-kibana

Expand Down
165 changes: 154 additions & 11 deletions kibana/templates/configmap-helm-scripts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,161 @@ kind: ConfigMap
metadata:
name: {{ template "kibana.fullname" . }}-helm-scripts
labels: {{ include "kibana.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade,post-delete
"helm.sh/hook-delete-policy": hook-succeeded
{{- if .Values.annotations }}
{{- range $key, $value := .Values.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
data:
kibana-entrypoint.sh: |
#!/bin/bash
set -euo pipefail
manage-es-token.js: |
const https = require('https');
const fs = require('fs');

echo "export ELASTICSEARCH_SERVICEACCOUNTTOKEN=$({{ template "kibana.home_dir" . }}/node/bin/node {{ template "kibana.home_dir" . }}/helm-scripts/parse-token.js {{ template "kibana.home_dir" . }}/config/tokens/{{ template "kibana.fullname" . }}.json)" > $HOME/.elasticsearch-serviceaccounttoken
source $HOME/.elasticsearch-serviceaccounttoken
// Elasticsearch API
const esPath = '_security/service/elastic/kibana/credential/token/{{ template "kibana.fullname" . }}';
const esUrl = '{{ .Values.elasticsearchHosts }}' + '/' + esPath
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will .Values.elasticsearchHosts always only have a single value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, it seems it was supposed to support multiple values but a bug that was never fixed prevented it (#603).

As this Kibana chart is supposed to connect to an Elasticsearch cluster deployed via the Elastic Helm charts, all Elasticsearch pods are supposed to be available via a single K8S service that should be used in .Values.elasticsearchHosts.

IMHO, It's not worth fixing #603 and making the script compatible with multiple URLs. I'll update the doc to mention that a single URL (Elasticsearch service URL) is expected for this value, to make it clear.

const esUsername = process.env.ELASTICSEARCH_USERNAME;
const esPassword = process.env.ELASTICSEARCH_PASSWORD;
const esAuth = esUsername + ':' + esPassword;
pugnascotia marked this conversation as resolved.
Show resolved Hide resolved
const esCaFile = process.env.ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES;
const esCa = fs.readFileSync(esCaFile);

# https://github.com/elastic/dockerfiles/blob/a405a4d692031b72cefcb8523bd464aa3221ec47/kibana/Dockerfile#L131
exec /bin/tini -- /usr/local/bin/kibana-docker "$@"
// Kubernetes API
const k8sHostname = process.env.KUBERNETES_SERVICE_HOST;
const k8sPort = process.env.KUBERNETES_SERVICE_PORT_HTTPS;
const k8sPostSecretPath = 'api/v1/namespaces/{{ .Release.Namespace }}/secrets';
const k8sDeleteSecretPath = 'api/v1/namespaces/{{ .Release.Namespace }}/secrets/{{ template "kibana.fullname" . }}-es-token';
const k8sPostSecretUrl = 'https://' + k8sHostname + ':' + k8sPort + '/' + k8sPostSecretPath;
const k8sDeleteSecretUrl = 'https://' + k8sHostname + ':' + k8sPort + '/' + k8sDeleteSecretPath;
jmlrt marked this conversation as resolved.
Show resolved Hide resolved
const k8sBearer = fs.readFileSync('/run/secrets/kubernetes.io/serviceaccount/token');
const k8sCa = fs.readFileSync('/run/secrets/kubernetes.io/serviceaccount/ca.crt');

parse-token.js: |
let dataFile = process.argv[2];
let dataContent = require(dataFile.toString());
console.log(dataContent.token.value);
// Post Data
const esTokenDeleteOptions = {
method: 'DELETE',
auth: esAuth,
ca: esCa,
};
const esTokenCreateOptions = {
method: 'POST',
auth: esAuth,
ca: esCa,
};
const secretCreateOptions = {
method: 'POST',
ca: k8sCa,
headers: {
'Authorization': 'Bearer ' + k8sBearer,
'Accept': 'application/json',
'Content-Type': 'application/json',
}
};
const secretDeleteOptions = {
method: 'DELETE',
ca: k8sCa,
headers: {
'Authorization': 'Bearer ' + k8sBearer,
'Accept': 'application/json',
'Content-Type': 'application/json',
}
};

// With thanks to https://stackoverflow.com/questions/57332374/how-to-chain-http-request
function requestPromise(url, options, payload) {
return new Promise((resolve, reject) => {
const request = https.request(url, options, response => {

console.log('statusCode:', response.statusCode);
pugnascotia marked this conversation as resolved.
Show resolved Hide resolved

// TODO: remove 404 and handle it during esToken deletion
const isSuccess = response.statusCode >= 200 && response.statusCode < 300 || response.statusCode == 404;

let data = '';
response.on('data', chunk => data += chunk); // accumulate data
response.once('end', () => isSuccess ? resolve(data) : reject(data)); // resolve promise here
});

request.once('error', err => {
// This won't log anything for e.g. an HTTP 404 or 500 response,
// since from HTTP's point-of-view we successfully received a
// response.
console.log(`${options.method} ${options.path} failed: `, err.message || err);
reject(err); // if promise is not already resolved, then we can reject it here
});

if (payload) {
request.write(payload);
}
request.end();
});
}

function createEsToken() {
// Chaining requests
console.log('Cleaning previous token');
requestPromise(esUrl, esTokenDeleteOptions).then(() => {
console.log('Creating new token');
requestPromise(esUrl, esTokenCreateOptions).then(response => {
const body = JSON.parse(response);
const token = body.token.value

// Encode the token in base64
const base64Token = Buffer.from(token, 'utf8').toString('base64');

// Prepare the k8s secret
secretData = JSON.stringify({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
secretData = JSON.stringify({
const secretData = JSON.stringify({

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops I missed the merge, didn't refresh in time. Sorry about the confusion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'll follow up with a PR)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"namespace": "{{ .Release.Namespace }}",
"name": "{{ template "kibana.fullname" . }}-es-token",
},
"type": "Opaque",
"data": {
"token": base64Token,
}
})

// Create the k8s secret
console.log('Creating K8S secret');
requestPromise(k8sPostSecretUrl, secretCreateOptions, secretData).then().catch(err => {
console.error(err)
});
return;
})
}).catch(err => {
console.error(err);
});
}

function cleanEsToken() {
// Chaining requests
console.log('Cleaning token');
requestPromise(esUrl, esTokenDeleteOptions).then(() => {
// Create the k8s secret
console.log('Delete K8S secret');
requestPromise(k8sDeleteSecretUrl, secretDeleteOptions).then().catch(err => {
console.error(err)
});
return;
}).catch(err => {
console.error(err);
});
}

const command = process.argv[2];
switch (command) {
case 'create':
console.log('Creating a new Elasticsearch token for Kibana')
createEsToken();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be cleaner to always return the promises created by requestPromise, so that they are chained together, and put a single top-level .catch(...) here that logs any errors. If that isn't clear, I can put the suggested changes in a rustpad.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean something like that?

function createEsToken() {
  // Chaining requests
  console.log('Cleaning previous token');
  return requestPromise(esUrl, esTokenDeleteOptions).then(() => {
    console.log('Creating new token');
    requestPromise(esUrl, esTokenCreateOptions).then(response => {
      // ...
      requestPromise(k8sPostSecretUrl, secretCreateOptions, secretData)
    });
  });
}

function cleanEsToken() {
  // Chaining requests
  console.log('Cleaning token');
  return requestPromise(esUrl, esTokenDeleteOptions).then(() => {
    // Create the k8s secret
    console.log('Delete K8S secret');
    requestPromise(k8sDeleteSecretUrl, secretDeleteOptions)
  });
}

const command = process.argv[2];
switch (command) {
  case 'create':
    console.log('Creating a new Elasticsearch token for Kibana')
    createEsToken().catch(err => {
      console.error(err);
    });
    break;
  case 'clean':
    console.log('Cleaning the Kibana Elasticsearch token')
    cleanEsToken().catch(err => {
      console.error(err);
    });
    break;
  default:
    console.log('Unknown command');
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, just make sure you're always returning the value of requestPromise, and the chain of promises will finally resolve in the switch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in e3ef0c1

break;
case 'clean':
console.log('Cleaning the Kibana Elasticsearch token')
cleanEsToken();
break;
default:
console.log('Unknown command');
jmlrt marked this conversation as resolved.
Show resolved Hide resolved
}
39 changes: 6 additions & 33 deletions kibana/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ spec:
volumes:
- name: kibana-tokens
emptyDir: {}
- name: kibana-helm-scripts
configMap:
name: {{ template "kibana.fullname" . }}-helm-scripts
defaultMode: 0755
- name: elasticsearch-certs
secret:
secretName: {{ .Values.elasticsearchCertificateSecret }}
Expand Down Expand Up @@ -86,31 +82,6 @@ spec:
{{ toYaml .Values.imagePullSecrets | indent 8 }}
{{- end }}
initContainers:
- name: configure-kibana-token
image: "{{ .Values.image }}:{{ .Values.imageTag }}"
imagePullPolicy: "{{ .Values.imagePullPolicy }}"
# TODO add retries
command:
- sh
- -c
- curl --output {{ template "kibana.home_dir" . }}/config/tokens/{{ template "kibana.fullname" . }}.json --fail -XPOST --cacert {{ template "kibana.home_dir" . }}/config/certs/{{ .Values.elasticsearchCertificateAuthoritiesFile }} -u "$(ELASTICSEARCH_USERNAME):$(ELASTICSEARCH_PASSWORD)" "{{ .Values.elasticsearchHosts }}/_security/service/elastic/kibana/credential/token/{{ template "kibana.fullname" . }}?pretty"
env:
- name: "ELASTICSEARCH_USERNAME"
valueFrom:
secretKeyRef:
name: {{ .Values.elasticsearchCredentialSecret }}
key: username
- name: "ELASTICSEARCH_PASSWORD"
valueFrom:
secretKeyRef:
name: {{ .Values.elasticsearchCredentialSecret }}
key: password
volumeMounts:
- name: elasticsearch-certs
mountPath: {{ template "kibana.home_dir" . }}/config/certs
readOnly: true
- name: kibana-tokens
mountPath: {{ template "kibana.home_dir" . }}/config/tokens
{{- if .Values.extraInitContainers }}
# Currently some extra blocks accept strings
# to continue with backwards compatibility this is being kept
Expand All @@ -127,8 +98,6 @@ spec:
{{ toYaml .Values.securityContext | indent 10 }}
image: "{{ .Values.image }}:{{ .Values.imageTag }}"
imagePullPolicy: "{{ .Values.imagePullPolicy }}"
command: ["/bin/bash"]
args: ["-c","{{ template "kibana.home_dir" . }}/helm-scripts/kibana-entrypoint.sh"]
env:
{{- if .Values.elasticsearchURL }}
- name: ELASTICSEARCH_URL
Expand All @@ -141,6 +110,12 @@ spec:
value: "{{ template "kibana.home_dir" . }}/config/certs/{{ .Values.elasticsearchCertificateAuthoritiesFile }}"
- name: SERVER_HOST
value: "{{ .Values.serverHost }}"
- name: ELASTICSEARCH_SERVICEACCOUNTTOKEN
valueFrom:
secretKeyRef:
name: {{ template "kibana.fullname" . }}-es-token
key: token
optional: false
{{- if .Values.extraEnvs }}
{{ toYaml .Values.extraEnvs | indent 10 }}
{{- end }}
Expand Down Expand Up @@ -194,8 +169,6 @@ spec:
- name: kibana-tokens
mountPath: {{ template "kibana.home_dir" . }}/config/tokens
readOnly: true
- name: kibana-helm-scripts
mountPath: {{ template "kibana.home_dir" . }}/helm-scripts
{{- range .Values.secretMounts }}
- name: {{ .name }}
mountPath: {{ .path }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ template "kibana.fullname" . }}-post-delete
name: post-delete-{{ template "kibana.fullname" . }}
labels: {{ include "kibana.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-delete
Expand All @@ -20,14 +20,10 @@ spec:
- name: clean-kibana-token
image: "{{ .Values.image }}:{{ .Values.imageTag }}"
imagePullPolicy: "{{ .Values.imagePullPolicy }}"
command: ["curl"]
command: ["{{ template "kibana.home_dir" . }}/node/bin/node"]
args:
- -XDELETE
- --cacert
- {{ template "kibana.home_dir" . }}/config/certs/{{ .Values.elasticsearchCertificateAuthoritiesFile }}
- -u
- "$(ELASTICSEARCH_USERNAME):$(ELASTICSEARCH_PASSWORD)"
- "{{ .Values.elasticsearchHosts }}/_security/service/elastic/kibana/credential/token/{{ template "kibana.fullname" . }}"
- {{ template "kibana.home_dir" . }}/helm-scripts/manage-es-token.js
- clean
env:
- name: "ELASTICSEARCH_USERNAME"
valueFrom:
Expand All @@ -39,11 +35,20 @@ spec:
secretKeyRef:
name: {{ .Values.elasticsearchCredentialSecret }}
key: password
- name: ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES
value: "{{ template "kibana.home_dir" . }}/config/certs/{{ .Values.elasticsearchCertificateAuthoritiesFile }}"
volumeMounts:
- name: elasticsearch-certs
mountPath: {{ template "kibana.home_dir" . }}/config/certs
readOnly: true
- name: kibana-helm-scripts
mountPath: {{ template "kibana.home_dir" . }}/helm-scripts
serviceAccount: post-delete-{{ template "kibana.fullname" . }}
volumes:
- name: elasticsearch-certs
secret:
secretName: {{ .Values.elasticsearchCertificateSecret }}
- name: kibana-helm-scripts
configMap:
name: {{ template "kibana.fullname" . }}-helm-scripts
defaultMode: 0755
20 changes: 20 additions & 0 deletions kibana/templates/post-delete-role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: post-delete-{{ template "kibana.fullname" . }}
labels: {{ include "kibana.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-delete-policy": hook-succeeded
{{- if .Values.annotations }}
{{- range $key, $value := .Values.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- delete
21 changes: 21 additions & 0 deletions kibana/templates/post-delete-rolebinding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: post-delete-{{ template "kibana.fullname" . }}
labels: {{ include "kibana.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-delete-policy": hook-succeeded
{{- if .Values.annotations }}
{{- range $key, $value := .Values.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
subjects:
- kind: ServiceAccount
name: post-delete-{{ template "kibana.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
roleRef:
kind: Role
name: post-delete-{{ template "kibana.fullname" . }}
apiGroup: rbac.authorization.k8s.io
13 changes: 13 additions & 0 deletions kibana/templates/post-delete-serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: post-delete-{{ template "kibana.fullname" . }}
labels: {{ include "kibana.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-delete-policy": hook-succeeded
{{- if .Values.annotations }}
{{- range $key, $value := .Values.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
Loading