diff --git a/.gitleaksignore b/.gitleaksignore index 885c8386..6c55ac62 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,2 +1,2 @@ -/github/workspace/api/v1/loggingservice_types.go:generic-api-key:694 -/github/workspace/api/v1/loggingservice_types.go:generic-api-key:697 +/github/workspace/api/v1/loggingservice_types.go:generic-api-key:695 +/github/workspace/api/v1/loggingservice_types.go:generic-api-key:698 diff --git a/api/v1/loggingservice_types.go b/api/v1/loggingservice_types.go index 60f35a01..0d3d0874 100644 --- a/api/v1/loggingservice_types.go +++ b/api/v1/loggingservice_types.go @@ -107,6 +107,7 @@ type Graylog struct { InitSetupImage string `json:"initSetupImage"` User string `json:"-"` Password string `json:"-"` + ElasticsearchHost string `json:"-"` NodeSelectorKey string `json:"nodeSelectorKey,omitempty"` InitContainerDockerImage string `json:"initContainerDockerImage,omitempty"` GraylogSecretName string `json:"graylogSecretName"` diff --git a/charts/qubership-logging-operator/templates/graylog/secret.yaml b/charts/qubership-logging-operator/templates/graylog/secret.yaml index 1643713a..4af12b00 100644 --- a/charts/qubership-logging-operator/templates/graylog/secret.yaml +++ b/charts/qubership-logging-operator/templates/graylog/secret.yaml @@ -18,6 +18,7 @@ data: elasticsearchHost: {{ .elasticsearchHost | default "" | b64enc }} password: {{ .password | default "" | b64enc }} user: {{ .user | default "admin" | b64enc }} + rootPasswordSha2: {{ .password | default "" | sha256sum | b64enc }} {{- if .s3Archive }} awsAccessKey: {{ .awsAccessKey | default ""| b64enc }} awsSecretKey: {{ .awsSecretKey | default "" | b64enc }} diff --git a/charts/qubership-logging-operator/templates/operator/deployment.yaml b/charts/qubership-logging-operator/templates/operator/deployment.yaml index 062fe548..6e855d2b 100644 --- a/charts/qubership-logging-operator/templates/operator/deployment.yaml +++ b/charts/qubership-logging-operator/templates/operator/deployment.yaml @@ -89,23 +89,6 @@ spec: value: {{ .Values.podMonitor.scrapeTimeout | default "10s" }} - name: LOG_LEVEL value: {{ .Values.logLevel | default "info" }} - {{- if .Values.graylog.install }} - - name: GRAYLOG_USERNAME - valueFrom: - secretKeyRef: - key: user - name: {{ default "graylog-secret" .Values.graylog.graylogSecretName }} - - name: GRAYLOG_PASSWORD - valueFrom: - secretKeyRef: - key: password - name: {{ default "graylog-secret" .Values.graylog.graylogSecretName }} - - name: ELASTICSEARCH_HOST - valueFrom: - secretKeyRef: - key: elasticsearchHost - name: {{ default "graylog-secret" .Values.graylog.graylogSecretName }} - {{- end }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/controllers/graylog/assets/statefulset.yaml b/controllers/graylog/assets/statefulset.yaml index 6029bd6e..47e69b80 100644 --- a/controllers/graylog/assets/statefulset.yaml +++ b/controllers/graylog/assets/statefulset.yaml @@ -54,6 +54,17 @@ spec: - key: node-id path: node-id defaultMode: 0666 + - name: graylog-credentials-files + secret: + secretName: {{ .Values.Graylog.GraylogSecretName }} + defaultMode: 0440 + items: + - key: elasticsearchHost + path: elasticsearchHost + - key: user + path: user + - key: rootPasswordSha2 + path: rootPasswordSha2 {{ if .Values.Graylog.AuthProxy }} {{ if .Values.Graylog.AuthProxy.Install }} - name: graylog-auth-proxy-config @@ -294,21 +305,12 @@ spec: {{ if and .Values.Graylog.TLS .Values.Graylog.TLS.HTTP }} -Djavax.net.ssl.trustStore=/usr/share/graylog/data/ssl/cacerts.jks {{ end }} - - name: GRAYLOG_ELASTICSEARCH_HOSTS - valueFrom: - secretKeyRef: - key: elasticsearchHost - name: {{ .Values.Graylog.GraylogSecretName }} - - name: GRAYLOG_USERNAME - valueFrom: - secretKeyRef: - key: user - name: {{ .Values.Graylog.GraylogSecretName }} - - name: GRAYLOG_PASSWORD - valueFrom: - secretKeyRef: - key: password - name: {{ .Values.Graylog.GraylogSecretName }} + - name: GRAYLOG_ELASTICSEARCH_HOSTS__FILE + value: /usr/share/graylog/data/k8s-graylog-secret/elasticsearchHost + - name: GRAYLOG_ROOT_USERNAME__FILE + value: /usr/share/graylog/data/k8s-graylog-secret/user + - name: GRAYLOG_ROOT_PASSWORD_SHA2__FILE + value: /usr/share/graylog/data/k8s-graylog-secret/rootPasswordSha2 - name: GRAYLOG_SNAPSHOT_DIRECTORY value: {{ .Values.Graylog.PathRepo }} {{- if .Values.Graylog.S3Archive }} @@ -345,6 +347,9 @@ spec: successThreshold: 1 timeoutSeconds: 5 volumeMounts: + - name: graylog-credentials-files + mountPath: /usr/share/graylog/data/k8s-graylog-secret + readOnly: true - name: data mountPath: /usr/share/graylog/data readOnly: false diff --git a/controllers/graylog/reconciler.go b/controllers/graylog/reconciler.go index 230a6014..2644709f 100644 --- a/controllers/graylog/reconciler.go +++ b/controllers/graylog/reconciler.go @@ -41,6 +41,9 @@ func (r *GraylogReconciler) Run(ctx context.Context, cr *loggingService.LoggingS r.Log.Info("Start Graylog reconciliation") if cr.Spec.Graylog != nil && cr.Spec.Graylog.IsInstall() { + if err := r.setCredentials(cr); err != nil { + return err + } connector, err := utils.CreateConnector(ctx, cr, configs, clientSet) if err != nil { return err @@ -213,6 +216,18 @@ func (r *GraylogReconciler) setCredentials(cr *loggingService.LoggingService) er return err } cr.Spec.Graylog.Password = pwd + + if secret.Data != nil && len(secret.Data["elasticsearchHost"]) > 0 { + cr.Spec.Graylog.ElasticsearchHost = string(secret.Data["elasticsearchHost"]) + } else { + return errors.New("can not find elasticsearchHost for Graylog in the secret " + cr.Spec.Graylog.GraylogSecretName + " in the namespace " + cr.GetNamespace()) + } + + if utils.EnsureSecretRootPasswordSHA2(secret, pwd) { + if err := r.Client.Update(context.TODO(), secret); err != nil { + return err + } + } return nil } diff --git a/controllers/graylog/utils/connector.go b/controllers/graylog/utils/connector.go index 28567730..bf163b97 100644 --- a/controllers/graylog/utils/connector.go +++ b/controllers/graylog/utils/connector.go @@ -5,9 +5,9 @@ import ( "context" "crypto/tls" "embed" + "fmt" "net/http" "net/url" - "os" "strings" "time" @@ -78,20 +78,12 @@ func CreateConnector(ctx context.Context, cr *loggingService.LoggingService, ass Timeout: time.Duration(util.ConnectionTimeout) * time.Second, } - if len(cr.Spec.Graylog.User) != 0 && len(cr.Spec.Graylog.Password) != 0 { - user = &util.Creds{ - Name: cr.Spec.Graylog.User, - Password: cr.Spec.Graylog.Password, - } - } else { - name := os.Getenv("GRAYLOG_USERNAME") - pwd := os.Getenv("GRAYLOG_PASSWORD") - if len(name) != 0 && len(pwd) != 0 { - user = &util.Creds{ - Name: name, - Password: pwd, - } - } + if len(cr.Spec.Graylog.User) == 0 || len(cr.Spec.Graylog.Password) == 0 { + return nil, fmt.Errorf("graylog API credentials are empty; expected user and password from Secret %q (reconcile loads them into spec)", cr.Spec.Graylog.GraylogSecretName) + } + user = &util.Creds{ + Name: cr.Spec.Graylog.User, + Password: cr.Spec.Graylog.Password, } restClient := &util.RestClient{ @@ -124,8 +116,12 @@ func CreateConnector(ctx context.Context, cr *loggingService.LoggingService, ass } if len(host) == 0 { + esHost := cr.Spec.Graylog.ElasticsearchHost + if esHost == "" { + return nil, fmt.Errorf("OpenSearch/Elasticsearch host is empty; expected elasticsearchHost in Secret %q (reconcile loads it into spec) or spec.graylog.openSearch", cr.Spec.Graylog.GraylogSecretName) + } var u *url.URL - u, err = url.Parse(os.Getenv("ELASTICSEARCH_HOST")) + u, err = url.Parse(esHost) if err != nil { return nil, err } diff --git a/controllers/graylog/utils/root_password_sha2.go b/controllers/graylog/utils/root_password_sha2.go new file mode 100644 index 00000000..71463f38 --- /dev/null +++ b/controllers/graylog/utils/root_password_sha2.go @@ -0,0 +1,30 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" + "strings" + + util "github.com/Netcracker/qubership-logging-operator/controllers/utils" + corev1 "k8s.io/api/core/v1" +) + +// EnsureSecretRootPasswordSHA2 sets secret.Data[util.GraylogSecretKeyRootPasswordSHA2] to SHA256hex(plainPassword) +// when missing or different. Returns true if the secret was modified (caller must persist with Update). +func EnsureSecretRootPasswordSHA2(secret *corev1.Secret, plainPassword string) bool { + sum := sha256.Sum256([]byte(plainPassword)) + want := hex.EncodeToString(sum[:]) + key := util.GraylogSecretKeyRootPasswordSHA2 + got := "" + if secret.Data != nil && secret.Data[key] != nil { + got = strings.TrimSpace(string(secret.Data[key])) + } + if got == want { + return false + } + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + secret.Data[key] = []byte(want) + return true +} diff --git a/controllers/graylog/utils/watch-secret.go b/controllers/graylog/utils/watch-secret.go index d0ef4d49..b33b9de1 100644 --- a/controllers/graylog/utils/watch-secret.go +++ b/controllers/graylog/utils/watch-secret.go @@ -148,50 +148,63 @@ func (w *SecretEventWatcher) updatePassword(secret *corev1.Secret, cr *loggingSe } if cr.Spec.Graylog.Password == pwd { w.Log.Info("Password did not change") - return nil - } else { + return w.syncRootPasswordSHA2SecretKey(cr, pwd) + } - cr.Spec.Graylog.Password = pwd + cr.Spec.Graylog.Password = pwd - cm, err := w.Clientset.CoreV1().ConfigMaps(cr.GetNamespace()).Get(context.TODO(), util.GraylogComponentName, metav1.GetOptions{}) + cm, err := w.Clientset.CoreV1().ConfigMaps(cr.GetNamespace()).Get(context.TODO(), util.GraylogComponentName, metav1.GetOptions{}) + if err != nil { + return err + } + config := cm.Data[util.GraylogConfigFileName] + if config == "" { + w.Log.Info("Config of Graylog is empty in configmap", "configmap", util.GraylogComponentName, "namespace", cr.GetNamespace()) + return w.syncRootPasswordSHA2SecretKey(cr, pwd) + } + var lines []string + scanner := bufio.NewScanner(strings.NewReader(config)) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err = scanner.Err(); err != nil { + return err + } + var result strings.Builder + var changed bool + for i := range lines { + if strings.HasPrefix(lines[i], util.GraylogPasswordField) { + h := sha256.New() + h.Write([]byte(pwd)) + bs := h.Sum(nil) + lines[i] = fmt.Sprintf("%s = %s", util.GraylogPasswordField, hex.EncodeToString(bs)) + changed = true + } + result.WriteString(lines[i]) + result.WriteString("\r\n") + } + if changed { + cm.Data[util.GraylogConfigFileName] = result.String() + updatedCm, err := w.Clientset.CoreV1().ConfigMaps(cr.GetNamespace()).Update(context.TODO(), cm, metav1.UpdateOptions{}) if err != nil { + w.Log.Error(err, "Update of config map failed", "configmap", updatedCm.GetName(), "namespace", updatedCm.GetNamespace()) return err } - config := cm.Data[util.GraylogConfigFileName] - if config == "" { - w.Log.Info("Config of Graylog is empty in configmap", "configmap", util.GraylogComponentName, "namespace", cr.GetNamespace()) - return nil - } - var lines []string - scanner := bufio.NewScanner(strings.NewReader(config)) - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - if err = scanner.Err(); err != nil { - return err - } - var result strings.Builder - var changed bool - for i := range lines { - if strings.HasPrefix(lines[i], util.GraylogPasswordField) { - h := sha256.New() - h.Write([]byte(pwd)) - bs := h.Sum(nil) - lines[i] = fmt.Sprintf("%s = %s", util.GraylogPasswordField, hex.EncodeToString(bs)) - changed = true - } - result.WriteString(lines[i]) - result.WriteString("\r\n") - } - if changed { - cm.Data[util.GraylogConfigFileName] = result.String() - updatedCm, err := w.Clientset.CoreV1().ConfigMaps(cr.GetNamespace()).Update(context.TODO(), cm, metav1.UpdateOptions{}) - if err != nil { - w.Log.Error(err, "Update of config map failed", "configmap", updatedCm.GetName(), "namespace", updatedCm.GetNamespace()) - return err - } - } } + return w.syncRootPasswordSHA2SecretKey(cr, pwd) +} +func (w *SecretEventWatcher) syncRootPasswordSHA2SecretKey(cr *loggingService.LoggingService, plainPassword string) error { + sec, err := w.Clientset.CoreV1().Secrets(cr.GetNamespace()).Get(context.TODO(), cr.Spec.Graylog.GraylogSecretName, metav1.GetOptions{}) + if err != nil { + return err + } + if !EnsureSecretRootPasswordSHA2(sec, plainPassword) { + return nil + } + if _, err := w.Clientset.CoreV1().Secrets(cr.GetNamespace()).Update(context.TODO(), sec, metav1.UpdateOptions{}); err != nil { + w.Log.Error(err, "Update of graylog secret failed", "secret", sec.GetName(), "namespace", sec.GetNamespace()) + return err + } return nil } diff --git a/controllers/utils/vars.go b/controllers/utils/vars.go index 58a3429b..fee758d3 100644 --- a/controllers/utils/vars.go +++ b/controllers/utils/vars.go @@ -202,6 +202,7 @@ var ( GraylogMongoUpgradeJobTimeout = time.Minute * 2 GraylogLabels = map[string]string{"name": "graylog"} GraylogSecretSelector = "graylog=secret" + GraylogSecretKeyRootPasswordSHA2 = "rootPasswordSha2" GraylogConfigFileName = "graylog.conf" GraylogPasswordField = "root_password_sha2" GraylogUserField = "root_username"