Skip to content

Commit 6161942

Browse files
Merge pull request #16 from lpiwowar/lpiwowar/postgresql-password
Fix PostgreSQL credential security and extract random string generation
2 parents 6973f5e + 505896b commit 6161942

16 files changed

Lines changed: 350 additions & 123 deletions
Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
#!/bin/bash
2-
2+
# This script prepares databases for lightspeed-stack and llama-stack (OGX) with
3+
# postgres_bootstrap.sql.
4+
#
5+
# Note:
6+
# - lightspeed-stack database: Auto-created by container image via POSTGRESQL_DATABASE.
7+
# - llama-stack database: Explicitly created by this script via POSTGRESQL_LLAMA_STACK_DATABASE.
8+
# - POSTGRESQL_ADMIN_PASSWORD is intentionally not set. The postgres superuser has no password
9+
# by default, which restricts it to local connections only — a deliberate security improvement.
10+
# Setting POSTGRESQL_ADMIN_PASSWORD would enable remote login for the postgres account.
311
set -e
412

513
cat /var/lib/pgsql/data/userdata/postgresql.conf
614

7-
echo "attempting to create llama-stack database and pg_trgm extension if they do not exist"
8-
9-
_psql () { psql --set ON_ERROR_STOP=1 "$@" ; }
10-
11-
# Create database for llama-stack conversation storage
12-
DB_NAME="llamastack"
13-
14-
echo "SELECT 'CREATE DATABASE $DB_NAME' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$DB_NAME')\gexec" | _psql -d "$POSTGRESQL_DATABASE"
15-
16-
# Create pg_trgm extension in default database (for OpenStack Lightspeed conversation cache)
17-
echo "CREATE EXTENSION IF NOT EXISTS pg_trgm;" | _psql -d "$POSTGRESQL_DATABASE"
18-
19-
# Create pg_trgm extension in llama-stack database (for text search if needed)
20-
echo "CREATE EXTENSION IF NOT EXISTS pg_trgm;" | _psql -d $DB_NAME
15+
echo "Bootstrapping PostgreSQL databases and permissions"
2116

22-
# Create schemas for isolating different components' data
23-
echo "CREATE SCHEMA IF NOT EXISTS lcore;" | _psql -d "$POSTGRESQL_DATABASE"
24-
echo "CREATE SCHEMA IF NOT EXISTS quota;" | _psql -d "$POSTGRESQL_DATABASE"
25-
echo "CREATE SCHEMA IF NOT EXISTS conversation_cache;" | _psql -d "$POSTGRESQL_DATABASE"
17+
psql \
18+
-v ON_ERROR_STOP=1 \
19+
-v postgresql_user="$POSTGRESQL_USER" \
20+
-v postgresql_lightspeed_stack_database="$POSTGRESQL_DATABASE" \
21+
-v postgresql_llama_stack_database="$POSTGRESQL_LLAMA_STACK_DATABASE" \
22+
-f "$POSTGRESQL_BOOTSTRAP_SQL_FILE"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- 1) LIGHTSPEED STACK DATABASE CONFIGURATION ---------------------------------
2+
\c :postgresql_lightspeed_stack_database
3+
4+
-- PostgreSQL 15+ removed the default CREATE/USAGE grants on the public schema.
5+
-- Therefore we have to explicitly grant the permissions to the user.
6+
GRANT USAGE, CREATE ON SCHEMA public TO :"postgresql_user";
7+
8+
-- lightspeed-stack requires rights to create additional schemas under its database
9+
GRANT CREATE ON DATABASE :"postgresql_lightspeed_stack_database" TO :"postgresql_user";
10+
11+
-- pg_trgm is the trigram similarity extension for PostgreSQL (enables e.g. fuzzy text search)
12+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
13+
-------------------------------------------------------------------------------
14+
15+
-- 2) LLAMA STACK DATABASE CONFIGURATION --------------------------------------
16+
-- Create postgresql_llama_stack_database.
17+
SELECT format('CREATE DATABASE %I', :'postgresql_llama_stack_database')
18+
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = :'postgresql_llama_stack_database')\gexec
19+
20+
-- Connect and configure postgresql_llama_stack_database
21+
\c :postgresql_llama_stack_database
22+
23+
-- PostgreSQL 15+ removed the default CREATE/USAGE grants on the public schema.
24+
-- Therefore we have to explicitly grant the permissions to the user.
25+
GRANT USAGE, CREATE ON SCHEMA public TO :"postgresql_user";
26+
27+
-- pg_trgm is the trigram similarity extension for PostgreSQL (enables e.g. fuzzy text search)
28+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
29+
-------------------------------------------------------------------------------

internal/controller/common.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package controller
1818

1919
import (
2020
"context"
21+
"crypto/rand"
2122
_ "embed"
23+
"encoding/hex"
2224
"encoding/json"
2325
"errors"
2426
"fmt"
@@ -82,6 +84,16 @@ func getConfigMapResourceVersion(ctx context.Context, h *common_helper.Helper, n
8284
return cm.ResourceVersion, nil
8385
}
8486

87+
// getSecretResourceVersion retrieves the resource version of a Secret.
88+
func getSecretResourceVersion(ctx context.Context, h *common_helper.Helper, name string, namespace string) (string, error) {
89+
secret := &corev1.Secret{}
90+
err := h.GetClient().Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, secret)
91+
if err != nil {
92+
return "", fmt.Errorf("failed to get secret %s: %w", name, err)
93+
}
94+
return secret.ResourceVersion, nil
95+
}
96+
8597
// providerNameToEnvVarName converts a provider name to a valid environment variable name.
8698
// It uppercases the string and replaces hyphens and dots with underscores.
8799
func providerNameToEnvVarName(providerName string) string {
@@ -174,3 +186,17 @@ func getDeployment(ctx context.Context, h *common_helper.Helper, name string, na
174186

175187
return deployment, nil
176188
}
189+
190+
// generateRandomString generates a random hex string of the given length.
191+
func generateRandomString(secretLength int) (string, error) {
192+
// Ceiling division: hex.EncodeToString doubles the byte count, so we need ceil(secretLength/2) bytes.
193+
randDataLen := (secretLength + 1) / 2
194+
195+
b := make([]byte, randDataLen)
196+
if _, err := rand.Read(b); err != nil {
197+
return "", fmt.Errorf("failed to generate secret: %w", err)
198+
}
199+
200+
randString := hex.EncodeToString(b)
201+
return randString[:secretLength], nil
202+
}

internal/controller/common_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func TestGenerateRandomStringLength(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
length int
27+
}{
28+
{
29+
name: "Zero length",
30+
length: 0,
31+
},
32+
{
33+
name: "Odd length 1",
34+
length: 1,
35+
},
36+
{
37+
name: "Even length 2",
38+
length: 2,
39+
},
40+
{
41+
name: "Odd length 7",
42+
length: 7,
43+
},
44+
{
45+
name: "Even length 8",
46+
length: 8,
47+
},
48+
{
49+
name: "Odd length 15",
50+
length: 15,
51+
},
52+
{
53+
name: "Even length 16",
54+
length: 16,
55+
},
56+
{
57+
name: "Even length 32",
58+
length: 32,
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
result, err := generateRandomString(tt.length)
65+
if err != nil {
66+
t.Errorf("generateRandomString(%d) unexpected error: %v", tt.length, err)
67+
}
68+
if len(result) != tt.length {
69+
t.Errorf("generateRandomString(%d) returned length %d, want %d", tt.length, len(result), tt.length)
70+
}
71+
})
72+
}
73+
}
74+
75+
func TestGenerateRandomStringHexCharacters(t *testing.T) {
76+
result, err := generateRandomString(32)
77+
if err != nil {
78+
t.Fatalf("generateRandomString(32) unexpected error: %v", err)
79+
}
80+
for i, c := range result {
81+
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
82+
t.Errorf("generateRandomString(32) character at index %d is %q, not a lowercase hex character", i, c)
83+
}
84+
}
85+
}
86+
87+
func TestGenerateRandomStringUniqueness(t *testing.T) {
88+
const length = 16
89+
a, err := generateRandomString(length)
90+
if err != nil {
91+
t.Fatalf("first call unexpected error: %v", err)
92+
}
93+
b, err := generateRandomString(length)
94+
if err != nil {
95+
t.Fatalf("second call unexpected error: %v", err)
96+
}
97+
if a == b {
98+
t.Errorf("generateRandomString(%d) returned identical values across two calls: %q", length, a)
99+
}
100+
}

internal/controller/constants.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,32 @@ const (
5050
PostgresConfigMapName = "lightspeed-postgres-conf"
5151
PostgresNetworkPolicyName = "lightspeed-postgres-server"
5252
PostgresServicePort = int32(5432)
53-
PostgresDefaultUser = "postgres"
54-
PostgresDefaultDbName = "postgres"
53+
PostgresLightspeedStackDbName = "lightspeed-stack"
54+
PostgresLlamaStackDbName = "llamastack"
5555
PostgresSharedBuffers = "256MB"
5656
PostgresMaxConnections = 100
5757
OpenStackLightspeedComponentPasswordFileName = "password"
58-
PostgresExtensionScript = "create-extensions.sh"
58+
PostgresUsernameSecretKey = "username"
59+
PostgresBootstrapScript = "postgres_bootstrap.sh"
60+
PostgresBootstrapSQLScript = "postgres_bootstrap.sql"
5961
PostgresConfigKey = "postgresql.conf.sample"
60-
PostgresBootstrapVolumeMountPath = "/usr/share/container-scripts/postgresql/start/create-extensions.sh"
62+
PostgresBootstrapVolumeMountPath = "/usr/share/container-scripts/postgresql/start/postgres_bootstrap.sh"
63+
PostgresBootstrapSQLVolumeMountPath = "/usr/share/container-scripts/postgresql/start/postgres_bootstrap.sql"
6164
PostgresConfigVolumeMountPath = "/usr/share/pgsql/postgresql.conf.sample"
6265
PostgresDataVolume = "postgres-data"
6366
PostgresDataVolumeMountPath = "/var/lib/pgsql"
6467
PostgresDataPVCName = "openstack-lightspeed-database"
6568
PostgresDataPVCDefaultSize = "1Gi"
6669
PostgresVarRunVolumeName = "lightspeed-postgres-var-run"
6770
PostgresVarRunVolumeMountPath = "/var/run/postgresql"
68-
TmpVolumeName = "tmp-writable-volume"
69-
TmpVolumeMountPath = "/tmp"
71+
72+
// Non-admin user that should be used by lightspeed-stack and llama-stack (OGX) to access
73+
// the PostgreSQL database. This user gets created by the PostgreSQL container by setting
74+
// the POSTGRESQL_USER and POSTGRESQL_PASSWORD environment variable.
75+
PostgresSQLUsername = "lightspeed-app-user"
76+
77+
TmpVolumeName = "tmp-writable-volume"
78+
TmpVolumeMountPath = "/tmp"
7079
// Health probe settings for the PostgreSQL container.
7180
// Startup probe allows up to 300s for initialization (e.g. WAL recovery).
7281
// Liveness uses a longer period/timeout to avoid killing a busy-but-healthy instance.
@@ -231,6 +240,7 @@ const (
231240
// By recording the resource version of a ConfigMap in a Deployment, StatefulSet, or similar resource,
232241
// changes to the referenced ConfigMaps can be detected and trigger rollouts or reconciliation in the operator.
233242
PostgresConfigMapResourceVersionAnnotation = "ols.openshift.io/postgres-configmap-version"
243+
PostgresSecretResourceVersionAnnotation = "ols.openshift.io/postgres-secret-version" // #nosec G101 -- annotation key, not a credential
234244
VectorDBScriptsConfigMapVersionAnnotation = "ols.openshift.io/vector-db-scripts-configmap-version"
235245
LlamaStackConfigMapResourceVersionAnnotation = "ols.openshift.io/llamastack-configmap-version"
236246
LCoreConfigMapResourceVersionAnnotation = "ols.openshift.io/lcore-configmap-version"
@@ -314,6 +324,11 @@ const (
314324
//go:embed assets/postgres_bootstrap.sh
315325
var PostgresBootStrapScriptContent string
316326

327+
// PostgreSQL Bootstrap SQL - creates database, extensions, and schemas
328+
//
329+
//go:embed assets/postgres_bootstrap.sql
330+
var PostgresBootStrapSQLContent string
331+
317332
// PostgreSQL Configuration - SSL and TLS settings
318333
//
319334
//go:embed assets/postgres.conf

internal/controller/lcore_config.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,14 @@ func buildLCoreDatabaseConfig(h *common_helper.Helper, _ *apiv1beta1.OpenStackLi
131131
"postgres": map[string]interface{}{
132132
"host": PostgresServiceName + "." + h.GetBeforeObject().GetNamespace() + ".svc",
133133
"port": PostgresServicePort,
134-
"db": PostgresDefaultDbName,
135-
"user": PostgresDefaultUser,
134+
"db": PostgresLightspeedStackDbName,
135+
"user": "${env.POSTGRESQL_USER}",
136136
"ssl_mode": PostgresDefaultSSLMode,
137137
"gss_encmode": "disable",
138138
"ca_cert_path": CABundleMountPath,
139139

140140
// Environment variable substitution via llama_stack.core.stack.replace_env_vars
141-
"password": "${env.POSTGRES_PASSWORD}",
141+
"password": "${env.POSTGRESQL_PASSWORD}",
142142

143143
// Separate schema for LCore to avoid conflicts with App Server
144144
"namespace": "lcore",
@@ -163,9 +163,9 @@ func buildLCoreConversationCacheConfig(h *common_helper.Helper, _ *apiv1beta1.Op
163163
"postgres": map[string]interface{}{
164164
"host": PostgresServiceName + "." + h.GetBeforeObject().GetNamespace() + ".svc",
165165
"port": PostgresServicePort,
166-
"db": PostgresDefaultDbName,
167-
"user": PostgresDefaultUser,
168-
"password": "${env.POSTGRES_PASSWORD}",
166+
"db": PostgresLightspeedStackDbName,
167+
"user": "${env.POSTGRESQL_USER}",
168+
"password": "${env.POSTGRESQL_PASSWORD}",
169169
"ssl_mode": PostgresDefaultSSLMode,
170170
"gss_encmode": "disable",
171171
"ca_cert_path": CABundleMountPath,

internal/controller/lcore_deployment.go

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,9 @@ func buildLlamaStackEnvVars(h *common_helper.Helper, ctx context.Context, instan
557557
}
558558
}
559559

560-
// Postgres password for ${env.POSTGRES_PASSWORD} substitution in llama-stack config
561-
envVars = append(envVars, buildPostgresPasswordEnvVar())
560+
// Postgres credentials for ${env.POSTGRESQL_PASSWORD} and ${env.POSTGRESQL_USER}
561+
// substitution in llama-stack config
562+
envVars = append(envVars, buildPostgresCredsEnvVars()...)
562563

563564
// PostgreSQL SSL configuration for OGX (llama-stack).
564565
// OGX's PostgresSqlStoreConfig does not support ssl_mode/ca_cert_path fields yet
@@ -597,16 +598,30 @@ func buildLlamaStackEnvVars(h *common_helper.Helper, ctx context.Context, instan
597598
return envVars, nil
598599
}
599600

600-
// buildPostgresPasswordEnvVar returns the POSTGRES_PASSWORD env var sourced from the postgres secret.
601-
func buildPostgresPasswordEnvVar() corev1.EnvVar {
602-
return corev1.EnvVar{
603-
Name: "POSTGRES_PASSWORD",
604-
ValueFrom: &corev1.EnvVarSource{
605-
SecretKeyRef: &corev1.SecretKeySelector{
606-
LocalObjectReference: corev1.LocalObjectReference{
607-
Name: PostgresSecretName,
601+
// buildPostgresCredsEnvVars returns the POSTGRESQL_PASSWORD and POSTGRESQL_USER env var sourced from
602+
// the postgres secret generated by the operator.
603+
func buildPostgresCredsEnvVars() []corev1.EnvVar {
604+
return []corev1.EnvVar{
605+
{
606+
Name: "POSTGRESQL_PASSWORD",
607+
ValueFrom: &corev1.EnvVarSource{
608+
SecretKeyRef: &corev1.SecretKeySelector{
609+
LocalObjectReference: corev1.LocalObjectReference{
610+
Name: PostgresSecretName,
611+
},
612+
Key: OpenStackLightspeedComponentPasswordFileName,
613+
},
614+
},
615+
},
616+
{
617+
Name: "POSTGRESQL_USER",
618+
ValueFrom: &corev1.EnvVarSource{
619+
SecretKeyRef: &corev1.SecretKeySelector{
620+
LocalObjectReference: corev1.LocalObjectReference{
621+
Name: PostgresSecretName,
622+
},
623+
Key: PostgresUsernameSecretKey,
608624
},
609-
Key: OpenStackLightspeedComponentPasswordFileName,
610625
},
611626
},
612627
}
@@ -619,12 +634,12 @@ func buildLightspeedStackEnvVars(instance *apiv1beta1.OpenStackLightspeed) []cor
619634
Name: "LIGHTSPEED_STACK_LOG_LEVEL",
620635
Value: instance.Spec.Logging.LightspeedStackLogLevel,
621636
},
622-
buildPostgresPasswordEnvVar(),
623637
}
624638
envVars = append(envVars, corev1.EnvVar{
625639
Name: "RH_SERVER_OKP",
626640
Value: fmt.Sprintf("http://%s.%s.svc:%d", OKPServiceName, instance.GetNamespace(), OKPServicePort),
627641
})
642+
envVars = append(envVars, buildPostgresCredsEnvVars()...)
628643
return envVars
629644
}
630645

@@ -736,5 +751,14 @@ func buildConfigMapAnnotations(h *common_helper.Helper, ctx context.Context) (ma
736751
annotations[CABundleConfigMapVersionAnnotation] = caBundleVersion
737752
}
738753

754+
postgresSecretVersion, err := getSecretResourceVersion(ctx, h, PostgresSecretName, h.GetBeforeObject().GetNamespace())
755+
if err != nil {
756+
if !errors.IsNotFound(err) {
757+
return nil, fmt.Errorf("failed to get postgres secret resource version: %w", err)
758+
}
759+
} else {
760+
annotations[PostgresSecretResourceVersionAnnotation] = postgresSecretVersion
761+
}
762+
739763
return annotations, nil
740764
}

0 commit comments

Comments
 (0)