Skip to content

Commit

Permalink
feat: Save postgres superuser password in secretstore
Browse files Browse the repository at this point in the history
Resolves edgexfoundry#5050. Save postgres superuser password in secretstore.

Signed-off-by: Lindsey Cheng <[email protected]>
  • Loading branch information
lindseysimple committed Jan 13, 2025
1 parent 2801c4a commit e5b3c77
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ..."
Expand Down
13 changes: 12 additions & 1 deletion internal/security/bootstrapper/helper/postgres_script.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,6 +17,7 @@ package helper

import (
"bufio"
"errors"
"fmt"
"os"
"text/template"
Expand Down Expand Up @@ -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
}
25 changes: 24 additions & 1 deletion internal/security/bootstrapper/helper/postgres_script_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2024 IOTech Ltd
// Copyright (C) 2024-2025 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand Down Expand Up @@ -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)
}
3 changes: 2 additions & 1 deletion internal/security/bootstrapper/postgres/configure.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2024 IOTech Ltd
// Copyright (C) 2024-2025 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand Down Expand Up @@ -51,6 +51,7 @@ func Configure(ctx context.Context,
true,
bootstrapConfig.ServiceTypeOther,
[]interfaces.BootstrapHandler{
handlers.SetupPasswordFile,
handlers.SetupDBScriptFiles,
},
)
Expand Down
60 changes: 58 additions & 2 deletions internal/security/bootstrapper/postgres/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2024 IOTech Ltd
// Copyright (C) 2024-2025 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
30 changes: 29 additions & 1 deletion internal/security/secretstore/init.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}

0 comments on commit e5b3c77

Please sign in to comment.