simplify production database configuration
This commit is contained in:
1 parent
8cb8b5e463
commit
b26f9679ab
17 files changed
+199
-234
No files matched your search
@@ -49,7 +49,10 @@ func ParseConfig(getenv Getenv, readFile ReadFile) (Config, error) {
|
||||
ConnectionTimeout: 10 * time.Second,
|
||||
StatementTimeout: 30 * time.Second,
|
||||
ApplicationName: "zhinian-go",
|
||||
SSLMode: SSLDisable,
|
||||
// DATABASE_URL is the only database setting. Production defaults to
|
||||
// full TLS verification; sslrootcert may be supplied in the URL when
|
||||
// the RDS CA is not part of the container's system trust store.
|
||||
SSLMode: SSLVerifyFull,
|
||||
}
|
||||
|
||||
backend := strings.ToLower(strings.TrimSpace(getenv("ZHINIAN_DATA_BACKEND")))
|
||||
@@ -93,37 +96,36 @@ func ParseConfig(getenv Getenv, readFile ReadFile) (Config, error) {
|
||||
if parsed.Scheme != "postgres" && parsed.Scheme != "postgresql" {
|
||||
return Config{}, fmt.Errorf("DATABASE_URL must use the postgres:// or postgresql:// scheme")
|
||||
}
|
||||
for key := range parsed.Query() {
|
||||
if strings.HasPrefix(strings.ToLower(key), "ssl") {
|
||||
return Config{}, fmt.Errorf("DATABASE_URL must not contain SSL query parameters (%s); use DATABASE_SSL_MODE and DATABASE_CA_CERT_PATH", key)
|
||||
query := parsed.Query()
|
||||
for key := range query {
|
||||
lower := strings.ToLower(key)
|
||||
if strings.HasPrefix(lower, "ssl") && lower != "sslmode" && lower != "sslrootcert" {
|
||||
return Config{}, fmt.Errorf("DATABASE_URL contains unsupported SSL query parameter %s; use sslmode and optional sslrootcert", key)
|
||||
}
|
||||
}
|
||||
|
||||
mode := strings.ToLower(strings.TrimSpace(getenv("DATABASE_SSL_MODE")))
|
||||
if mode != "" {
|
||||
if mode := strings.ToLower(strings.TrimSpace(query.Get("sslmode"))); mode != "" {
|
||||
cfg.SSLMode = SSLMode(mode)
|
||||
}
|
||||
switch cfg.SSLMode {
|
||||
case SSLDisable:
|
||||
case SSLVerifyFull:
|
||||
path := strings.TrimSpace(getenv("DATABASE_CA_CERT_PATH"))
|
||||
if path == "" {
|
||||
return Config{}, fmt.Errorf("DATABASE_CA_CERT_PATH is required when DATABASE_SSL_MODE=verify-full")
|
||||
cfg.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
if path := strings.TrimSpace(query.Get("sslrootcert")); path != "" {
|
||||
if readFile == nil {
|
||||
return Config{}, fmt.Errorf("a certificate reader is required when DATABASE_URL contains sslrootcert")
|
||||
}
|
||||
pem, readErr := readFile(path)
|
||||
if readErr != nil {
|
||||
return Config{}, fmt.Errorf("read DATABASE_URL sslrootcert: %w", readErr)
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM(pem) {
|
||||
return Config{}, fmt.Errorf("DATABASE_URL sslrootcert does not contain a valid CA certificate")
|
||||
}
|
||||
cfg.TLSConfig.RootCAs = roots
|
||||
}
|
||||
if readFile == nil {
|
||||
return Config{}, fmt.Errorf("CA certificate reader is required")
|
||||
}
|
||||
pem, readErr := readFile(path)
|
||||
if readErr != nil {
|
||||
return Config{}, fmt.Errorf("read DATABASE_CA_CERT_PATH: %w", readErr)
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM(pem) {
|
||||
return Config{}, fmt.Errorf("DATABASE_CA_CERT_PATH does not contain a valid CA certificate")
|
||||
}
|
||||
cfg.TLSConfig = &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}
|
||||
default:
|
||||
return Config{}, fmt.Errorf("DATABASE_SSL_MODE must be 'disable' or 'verify-full'")
|
||||
return Config{}, fmt.Errorf("DATABASE_URL sslmode must be 'disable' or 'verify-full'")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -38,14 +39,13 @@ func TestParseConfigRequiresPostgresURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfigAcceptsOnlyPostgresSchemesAndRejectsSSLQueryParameters(t *testing.T) {
|
||||
func TestParseConfigAcceptsOnlyPostgresSchemesAndRejectsUnsupportedSSLQueryParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
}{
|
||||
{name: "wrong scheme", url: "https://db.example/app"},
|
||||
{name: "sslmode", url: "postgres://db.example/app?sslmode=require"},
|
||||
{name: "mixed case ssl parameter", url: "postgresql://db.example/app?SSLcert=x"},
|
||||
{name: "mixed case unsupported ssl parameter", url: "postgresql://db.example/app?SSLcert=x"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -60,7 +60,7 @@ func TestParseConfigAcceptsOnlyPostgresSchemesAndRejectsSSLQueryParameters(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfigBuildsVerifyFullTLSFromCA(t *testing.T) {
|
||||
func TestParseConfigBuildsVerifyFullTLSFromURLCA(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
caPath := filepath.Join(dir, "ca.pem")
|
||||
const ca = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n"
|
||||
@@ -71,10 +71,8 @@ func TestParseConfigBuildsVerifyFullTLSFromCA(t *testing.T) {
|
||||
return []byte(ca), nil
|
||||
}
|
||||
_, err := ParseConfig(env(map[string]string{
|
||||
"ZHINIAN_DATA_BACKEND": "postgres",
|
||||
"DATABASE_URL": "postgresql://db.example/app",
|
||||
"DATABASE_SSL_MODE": "verify-full",
|
||||
"DATABASE_CA_CERT_PATH": caPath,
|
||||
"ZHINIAN_DATA_BACKEND": "postgres",
|
||||
"DATABASE_URL": "postgresql://db.example/app?sslmode=verify-full&sslrootcert=" + url.QueryEscape(caPath),
|
||||
}), read)
|
||||
if err == nil || !strings.Contains(err.Error(), "CA certificate") {
|
||||
t.Fatalf("ParseConfig() error = %v, want invalid CA certificate error", err)
|
||||
@@ -97,10 +95,8 @@ func TestParseConfigVerifyFullBuildsRootsWithoutDisablingVerification(t *testing
|
||||
}
|
||||
ca := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
cfg, err := ParseConfig(env(map[string]string{
|
||||
"ZHINIAN_DATA_BACKEND": "postgres",
|
||||
"DATABASE_URL": "postgresql://db.example/app",
|
||||
"DATABASE_SSL_MODE": "verify-full",
|
||||
"DATABASE_CA_CERT_PATH": "/ca.pem",
|
||||
"ZHINIAN_DATA_BACKEND": "postgres",
|
||||
"DATABASE_URL": "postgresql://db.example/app?sslmode=verify-full&sslrootcert=%2Fca.pem",
|
||||
}), func(string) ([]byte, error) { return ca, nil })
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConfig() error = %v", err)
|
||||
@@ -124,9 +120,12 @@ func TestParseConfigDefaultsAndNumericValidation(t *testing.T) {
|
||||
if cfg.PoolMax != 10 || cfg.IdleTimeout != 30*time.Second || cfg.ConnectionTimeout != 10*time.Second || cfg.StatementTimeout != 30*time.Second {
|
||||
t.Fatalf("unexpected defaults: %+v", cfg)
|
||||
}
|
||||
if cfg.ApplicationName != "zhinian-go" || cfg.SSLMode != SSLDisable {
|
||||
if cfg.ApplicationName != "zhinian-go" || cfg.SSLMode != SSLVerifyFull {
|
||||
t.Fatalf("unexpected identity/TLS defaults: %+v", cfg)
|
||||
}
|
||||
if cfg.TLSConfig == nil || cfg.TLSConfig.InsecureSkipVerify {
|
||||
t.Fatal("default PostgreSQL configuration must verify the server certificate")
|
||||
}
|
||||
|
||||
for name, value := range map[string]string{
|
||||
"DATABASE_POOL_MAX": "0",
|
||||
|
||||
@@ -42,7 +42,7 @@ func Open(ctx context.Context, config Config) (*Module, error) {
|
||||
poolConfig.ConnConfig.Fallbacks = nil
|
||||
case SSLVerifyFull:
|
||||
if config.TLSConfig == nil {
|
||||
return nil, fmt.Errorf("TLS configuration is required when DATABASE_SSL_MODE=verify-full")
|
||||
return nil, fmt.Errorf("TLS configuration is required when DATABASE_URL sslmode=verify-full")
|
||||
}
|
||||
tlsConfig := config.TLSConfig.Clone()
|
||||
if tlsConfig.ServerName == "" {
|
||||
@@ -51,7 +51,7 @@ func Open(ctx context.Context, config Config) (*Module, error) {
|
||||
poolConfig.ConnConfig.TLSConfig = tlsConfig
|
||||
poolConfig.ConnConfig.Fallbacks = nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DATABASE_SSL_MODE %q", config.SSLMode)
|
||||
return nil, fmt.Errorf("unsupported DATABASE_URL sslmode %q", config.SSLMode)
|
||||
}
|
||||
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
|
||||
if err != nil {
|
||||
|
||||
@@ -37,8 +37,6 @@ func TestOpenPostgresDoesNotProbeBeforeReadiness(t *testing.T) {
|
||||
func TestParseConfigIgnoresPostgresTLSSettingsForLocalBackend(t *testing.T) {
|
||||
cfg, err := ParseConfig(env(map[string]string{
|
||||
"ZHINIAN_DATA_BACKEND": "local",
|
||||
"DATABASE_SSL_MODE": "verify-full",
|
||||
"DATABASE_CA_CERT_PATH": "/missing/ca.pem",
|
||||
"DATABASE_POOL_MAX": "0",
|
||||
"DATABASE_CONNECTION_TIMEOUT_MS": "invalid",
|
||||
}), nil)
|
||||
@@ -56,7 +54,7 @@ func TestOpenPostgresRejectsInvalidConfiguredTLSMode(t *testing.T) {
|
||||
DatabaseURL: "postgresql://app:secret@db.example/app",
|
||||
SSLMode: SSLMode("prefer"),
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "DATABASE_SSL_MODE") {
|
||||
t.Fatalf("Open() error = %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported DATABASE_URL sslmode") {
|
||||
t.Fatalf("Open() error = %v, want DATABASE_URL sslmode error", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user