Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
45 changes: 38 additions & 7 deletions internal/database/postgres/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package migrations

import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -513,8 +514,9 @@ func GetMigrationVersion(ctx context.Context, pool *pgxpool.Pool, migrationsPath
}

// buildMigrateDSN builds a connection string for golang-migrate from pgx config.
// sslmode is inferred from the pgx TLSConfig ("require" when TLS is configured,
// "disable" otherwise).
// The SSL mode is recovered from the parsed TLSConfig so strict modes
// (verify-ca / verify-full) are preserved rather than silently downgraded to
// require. See sslModeFromTLSConfig for the exact mapping.
func buildMigrateDSN(config *pgxpool.Config) string {
// Extract connection details from pgx config
host := config.ConnConfig.Host
Expand All @@ -527,11 +529,7 @@ func buildMigrateDSN(config *pgxpool.Config) string {
encodedUser := url.QueryEscape(user)
encodedPassword := url.QueryEscape(password)

// Infer sslmode from the TLS config.
sslMode := "require"
if config.ConnConfig.TLSConfig == nil {
sslMode = "disable"
}
sslMode := sslModeFromTLSConfig(config.ConnConfig.TLSConfig)

// Build DSN (golang-migrate uses postgres:// format)
// Don't add connection options - RDS Proxy doesn't support them
Expand All @@ -546,6 +544,39 @@ func buildMigrateDSN(config *pgxpool.Config) string {
)
}

// sslModeFromTLSConfig recovers the libpq sslmode string that pgx derived from
// the original DSN, so the migration DSN matches the security policy the
// application enforces in normal operation. pgx (v5) maps sslmode -> *tls.Config
// as follows, which we invert here:
//
// - disable -> nil
// - require -> InsecureSkipVerify=true, no custom chain/host verification
// - verify-ca -> InsecureSkipVerify=true + a VerifyPeerCertificate callback
// (verifies the chain but not the hostname)
// - verify-full -> InsecureSkipVerify=false + ServerName set
// (verifies both chain and hostname)
//
// Collapsing every TLS-enabled mode to "require" would silently drop the
// certificate-validation guarantees of verify-ca / verify-full during
// migrations, so we distinguish them explicitly.
func sslModeFromTLSConfig(tlsConfig *tls.Config) string {
if tlsConfig == nil {
return "disable"
}
// verify-full is the only mode where pgx leaves InsecureSkipVerify=false:
// the standard library then verifies both the chain and the hostname.
if !tlsConfig.InsecureSkipVerify {
return "verify-full"
}
// verify-ca skips the standard hostname check but installs a custom
// VerifyPeerCertificate callback to validate the chain; plain require
// installs neither.
if tlsConfig.VerifyPeerCertificate != nil {
return "verify-ca"
}
return "require"
}

// ValidateMigrationsPath checks if migrations directory exists.
func ValidateMigrationsPath(path string) error {
info, err := os.Stat(path)
Expand Down
20 changes: 20 additions & 0 deletions internal/database/postgres/migrations/migrate_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ func TestBuildMigrateDSN_PasswordNotInLogs(t *testing.T) {
"buildMigrateDSN must not emit the database password to the log output")
}

// TestBuildMigrateDSN_PreservesSSLMode guards against silently downgrading the
// migration DSN's sslmode: a strict mode (verify-ca / verify-full) must survive
// the DSN -> pgx *tls.Config -> migration DSN round-trip rather than collapsing
// to require. This also pins the pgx-version-specific TLSConfig mapping in
// sslModeFromTLSConfig.
func TestBuildMigrateDSN_PreservesSSLMode(t *testing.T) {
for _, mode := range []string{"disable", "require", "verify-ca", "verify-full"} {
t.Run(mode, func(t *testing.T) {
rawDSN := fmt.Sprintf("postgres://user:pass@localhost:5432/db?sslmode=%s", mode)
poolCfg, err := pgxpool.ParseConfig(rawDSN)
require.NoError(t, err, "pgxpool.ParseConfig must accept sslmode=%s", mode)

result := buildMigrateDSN(poolCfg)

assert.Contains(t, result, "sslmode="+mode,
"buildMigrateDSN must preserve the configured sslmode, not downgrade it")
})
}
}

// TestMaybeForceVersion_NonNumericError ensures a non-numeric
// CUDLY_FORCE_MIGRATION_VERSION produces an error without logging the
// bad value to stdout.
Expand Down
Loading