From e5b3c77b3219e8d84423fae272ddcf0ac29a4f3b Mon Sep 17 00:00:00 2001 From: Lindsey Cheng Date: Mon, 13 Jan 2025 13:16:41 +0800 Subject: [PATCH] feat: Save postgres superuser password in secretstore Resolves #5050. Save postgres superuser password in secretstore. Signed-off-by: Lindsey Cheng --- .../postgres_wait_install.sh | 30 +++++++--- .../bootstrapper/helper/postgres_script.go | 13 +++- .../helper/postgres_script_test.go | 25 +++++++- .../bootstrapper/postgres/configure.go | 3 +- .../postgres/handlers/handlers.go | 60 ++++++++++++++++++- internal/security/secretstore/init.go | 30 +++++++++- 6 files changed, 148 insertions(+), 13 deletions(-) diff --git a/cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh index 81787168a6..5d393af926 100755 --- a/cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh +++ b/cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh @@ -49,6 +49,13 @@ if [ "$(id -u)" = '0' ]; then fi find "${DATABASECONFIG_PATH}" \! -user postgres -exec chown postgres '{}' + chmod 700 "${DATABASECONFIG_PATH}" + + if [ ! -f "/run/secrets/postgres_password" ]; then + ehco "$(date) Error: password file /run/secrets/postgres_password not exists" + exit 1 + fi + find "/run/secrets" \! -user postgres -exec chown postgres '{}' + + chmod 700 "/run/secrets" fi # customizing of Postgres startup process by including the docker-entrypoint script @@ -62,26 +69,35 @@ if [ "$(id -u)" = '0' ]; then exec gosu postgres "$BASH_SOURCE" "$@" fi +export POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password +PASSWORD=$(<"$POSTGRES_PASSWORD_FILE") +if [ -z "$PASSWORD" ]; then + echo "$(date) Error: no superuser password define in the /run/secrets/postgres_password file" + exit 1 +fi + +# Export POSTGRES_PASSWORD to satisfy the entrypoint script +export POSTGRES_PASSWORD="$PASSWORD" + + # run additional initialize db scripts not located in /docker-entrypoint-initdb.d dir if database is initialized for the first time if [ -z "$DATABASE_ALREADY_EXISTS" ]; then docker_verify_minimum_env docker_init_database_dir pg_setup_hba_conf - # only required for '--auth[-local]=md5' on POSTGRES_INITDB_ARGS - export PGPASSWORD="${PGPASSWORD:-$POSTGRES_PASSWORD}" - docker_temp_server_start "$@" -c max_locks_per_transaction=256 docker_setup_db docker_process_init_files /docker-entrypoint-initdb.d/* - docker_process_init_files ${DATABASECONFIG_PATH}/* - docker_temp_server_stop else docker_temp_server_start "$@" - docker_process_init_files ${DATABASECONFIG_PATH}/* - docker_temp_server_stop + + # Update the superuser password with the value of POSTGRES_PASSWORD + docker_process_sql <<<"ALTER USER postgres WITH PASSWORD '${POSTGRES_PASSWORD}';" fi +docker_process_init_files ${DATABASECONFIG_PATH}/* +docker_temp_server_stop # starting postgres echo "$(date) Starting edgex-postgres ..." diff --git a/internal/security/bootstrapper/helper/postgres_script.go b/internal/security/bootstrapper/helper/postgres_script.go index fb393f46b1..ca580e283e 100644 --- a/internal/security/bootstrapper/helper/postgres_script.go +++ b/internal/security/bootstrapper/helper/postgres_script.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2024 IOTech Ltd + * Copyright (C) 2024-2025 IOTech Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -17,6 +17,7 @@ package helper import ( "bufio" + "errors" "fmt" "os" "text/template" @@ -73,3 +74,13 @@ func GeneratePostgresScript(confFile *os.File, credMap []map[string]any) error { return nil } + +// GeneratePasswordFile creates a random password and writes it to the Postgres password file +func GeneratePasswordFile(confFile *os.File, password string) error { + if password == "" { + return errors.New("failed to GeneratePasswordFile: password is empty") + } + + _, err := confFile.WriteString(password) + return err +} diff --git a/internal/security/bootstrapper/helper/postgres_script_test.go b/internal/security/bootstrapper/helper/postgres_script_test.go index 4bc1e4f98b..59ceba2b69 100644 --- a/internal/security/bootstrapper/helper/postgres_script_test.go +++ b/internal/security/bootstrapper/helper/postgres_script_test.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2024 IOTech Ltd +// Copyright (C) 2024-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -53,3 +53,26 @@ func TestGeneratePostgresScript(t *testing.T) { require.Equal(t, 17, len(outputlines)) require.Equal(t, expectedCreateScript, strings.TrimSpace(outputlines[11])) } + +func TestGeneratePasswordFile(t *testing.T) { + fileName := "testPasswordFile" + testfile, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + require.NoError(t, err) + defer func() { + _ = testfile.Close() + _ = os.RemoveAll(fileName) + }() + + mockPassword := "password123" + + err = GeneratePasswordFile(testfile, mockPassword) + require.NoError(t, err) + + content, readErr := os.ReadFile(testfile.Name()) + require.NoError(t, readErr) + require.Equal(t, mockPassword, string(content)) + + // test with empty password + err = GeneratePasswordFile(testfile, "") + require.Error(t, err) +} diff --git a/internal/security/bootstrapper/postgres/configure.go b/internal/security/bootstrapper/postgres/configure.go index 2c922abfd7..2b9de56cac 100644 --- a/internal/security/bootstrapper/postgres/configure.go +++ b/internal/security/bootstrapper/postgres/configure.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2024 IOTech Ltd +// Copyright (C) 2024-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -51,6 +51,7 @@ func Configure(ctx context.Context, true, bootstrapConfig.ServiceTypeOther, []interfaces.BootstrapHandler{ + handlers.SetupPasswordFile, handlers.SetupDBScriptFiles, }, ) diff --git a/internal/security/bootstrapper/postgres/handlers/handlers.go b/internal/security/bootstrapper/postgres/handlers/handlers.go index e53b7a57c6..a1643cfeb0 100644 --- a/internal/security/bootstrapper/postgres/handlers/handlers.go +++ b/internal/security/bootstrapper/postgres/handlers/handlers.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2024 IOTech Ltd +// Copyright (C) 2024-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -21,7 +21,11 @@ import ( "github.com/edgexfoundry/go-mod-bootstrap/v4/di" ) -const postgresSecretName = "postgres" +const ( + postgresSecretName = "postgres" + passwordFileDir = "/run/secrets" + passwordFileName = "postgres_password" +) // SetupDBScriptFiles dynamically creates Postgres init-db script file with the retrieved credentials for multiple EdgeX services func SetupDBScriptFiles(_ context.Context, _ *sync.WaitGroup, _ startup.Timer, dic *di.Container) bool { @@ -107,3 +111,55 @@ func getServiceCredentials(dic *di.Container, scriptFile *os.File) error { } return nil } + +// SetupPasswordFile creates the Postgres superuser password file with the credential retrieved from secret provider +func SetupPasswordFile(_ context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { + lc := bootstrapContainer.LoggingClientFrom(dic.Get) + config := container.ConfigurationFrom(dic.Get) + + if err := helper.CreateDirectoryIfNotExists(passwordFileDir); err != nil { + lc.Errorf("failed to create database superuser password file directory %s: %v", passwordFileDir, err) + return false + } + + // Create the Postgres superuser password file + confFile, err := helper.CreateConfigFile(passwordFileDir, passwordFileName, lc) + if err != nil { + lc.Error(err.Error()) + return false + } + defer func() { + _ = confFile.Close() + }() + + // GetCredentials retrieves the Postgres database credentials from secretstore + secretProvider := bootstrapContainer.SecretProviderFrom(dic.Get) + + var superuserPass string + + for startupTimer.HasNotElapsed() { + // retrieve database credentials from secretstore + secrets, err := secretProvider.GetSecret(config.Database.Type) + if err == nil { + superuserPass = secrets[secret.PasswordKey] + break + } + + lc.Warnf("Could not retrieve database credentials (startup timer has not expired): %s", err.Error()) + startupTimer.SleepForInterval() + } + + if superuserPass == "" { + lc.Error("Failed to retrieve database credentials before startup timer expired") + return false + } + + // Writing the Postgres password file with the Postgres credentials got from secret store + if genErr := helper.GeneratePasswordFile(confFile, superuserPass); genErr != nil { + lc.Errorf("cannot write password to file %s: %v", passwordFileName, genErr) + return false + } + + lc.Info("Postgres password file has been set") + return true +} diff --git a/internal/security/secretstore/init.go b/internal/security/secretstore/init.go index c8a3f328ef..7f94bca435 100644 --- a/internal/security/secretstore/init.go +++ b/internal/security/secretstore/init.go @@ -1,7 +1,7 @@ /******************************************************************************* * Copyright 2022-2023 Intel Corporation * Copyright 2019 Dell Inc. - * Copyright 2024 IOTech Ltd + * Copyright 2024-2025 IOTech Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -856,5 +856,33 @@ func genPostgresCredentials(dic *di.Container, secretStore Cred, knownSecretsToA } } } + + postgresCred, err := getCredential(common.SecurityBootstrapperPostgresKey, secretStore, postgresSecretName) + if err != nil { + if !errors.Is(err, errNotFound) { + lc.Errorf("failed to determine if Postgres superuser credentials already exist or not: %s", err.Error()) + return err + } + + lc.Info("Generating superuser password for Postgres DB") + superuserPassword, genErr := secretStore.GeneratePassword(ctx) + if genErr != nil { + lc.Error("failed to generate superuser password for postgres") + return genErr + } + + postgresCred = UserPasswordPair{ + User: postgresSecretName, + Password: superuserPassword, + } + } else { + lc.Info("Postgres DB credentials exist, skipping generating new password") + } + + err = storeCredential(lc, common.SecurityBootstrapperPostgresKey, secretStore, postgresSecretName, postgresCred) + if err != nil { + lc.Error(err.Error()) + return err + } return nil }