Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Save postgres superuser password in secretstore #5051

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash
# ----------------------------------------------------------------------------------
# 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.
Expand Down 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,43 @@ 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

cloudxxx8 marked this conversation as resolved.
Show resolved Hide resolved
# Remove the POSTGRES_PASSWORD_FILE
rm -f "$POSTGRES_PASSWORD_FILE"

# Check if the file has been removed successfully
if [ -e "$POSTGRES_PASSWORD_FILE" ]; then
echo "$(date) Failed to remove the POSTGRES_PASSWORD_FILE in: $POSTGRES_PASSWORD_FILE"
fi

# 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 @@
"github.com/edgexfoundry/go-mod-bootstrap/v4/di"
)

const postgresSecretName = "postgres"
const (
postgresSecretName = "postgres" // nolint:gosec
Fixed Show fixed Hide fixed
cloudxxx8 marked this conversation as resolved.
Show resolved Hide resolved
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 @@
}
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
}
Loading