From 0df071b5ab05dfb234ae120f2d304dbfd09f8e4c Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 23 Feb 2026 12:32:35 +0100 Subject: [PATCH 1/3] fix(pull): attempt to fix unkown authority error for db pull --- internal/gen/types/types.go | 13 +- internal/gen/types/types_integration_test.go | 178 +++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 internal/gen/types/types_integration_test.go diff --git a/internal/gen/types/types.go b/internal/gen/types/types.go index fef6a97726..19f97775e1 100644 --- a/internal/gen/types/types.go +++ b/internal/gen/types/types.go @@ -129,7 +129,18 @@ func GetRootCA(ctx context.Context, dbURL string, options ...func(*pgx.ConnConfi } func isRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnConfig)) (bool, error) { - conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", options...) + + // pgx v4's sslmode=require verifies the server certificate against system CAs, + // unlike libpq where require skips verification. Since this probe only detects + // whether the server speaks TLS (not whether its cert is trusted), we must skip + // verification here. The actual cert validation happens downstream in the migra/ + // pgdelta Deno scripts using the CA bundle returned by GetRootCA. + opts := append(options, func(cc *pgx.ConnConfig) { + if cc.TLSConfig != nil { + cc.TLSConfig.InsecureSkipVerify = true + } + }) + conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", opts...) if err != nil { if strings.HasSuffix(err.Error(), "(server refused TLS connection)") { return false, nil diff --git a/internal/gen/types/types_integration_test.go b/internal/gen/types/types_integration_test.go new file mode 100644 index 0000000000..75976b0a2c --- /dev/null +++ b/internal/gen/types/types_integration_test.go @@ -0,0 +1,178 @@ +package types + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/containerd/errdefs" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/go-connections/nat" + "github.com/jackc/pgx/v4" + "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/utils" +) + +// TestGetRootCA_SSLProbeAgainstRealPostgres verifies that isRequireSSL and GetRootCA +// work correctly when connecting to a real Postgres instance with SSL enabled (certificate +// signed by a CA not in the system trust store). This reproduces the "x509: certificate +// signed by unknown authority" failure that affected db pull against Supabase poolers. +func TestGetRootCA_SSLProbeAgainstRealPostgres(t *testing.T) { + ctx := context.Background() + if !utils.IsDockerRunning(ctx) { + t.Skip("Docker is not running, skipping SSL probe integration test") + } + + // Generate self-signed CA and server certificate so Postgres uses TLS with + // a cert not in the system CA store (same situation as Supabase pooler). + tmpDir := t.TempDir() + _, serverCertPEM, serverKeyPEM, err := generateTestCertificates() + require.NoError(t, err) + + serverCertPath := filepath.Join(tmpDir, "server.crt") + serverKeyPath := filepath.Join(tmpDir, "server.key") + require.NoError(t, os.WriteFile(serverCertPath, serverCertPEM, 0600)) + require.NoError(t, os.WriteFile(serverKeyPath, serverKeyPEM, 0600)) + + // Use postgres:15 from Docker Hub (no registry transform). + const postgresImage = "postgres:15" + if _, err := utils.Docker.ImageInspect(ctx, postgresImage); err != nil { + if errdefs.IsNotFound(err) { + out, pullErr := utils.Docker.ImagePull(ctx, postgresImage, image.PullOptions{}) + require.NoError(t, pullErr) + _, _ = io.Copy(io.Discard, out) + _ = out.Close() + } else { + require.NoError(t, err) + } + } + + // Use a fixed port for the test; avoid conflicts by using a high port. + hostPort := "15433" + config := container.Config{ + Image: postgresImage, + Env: []string{"POSTGRES_PASSWORD=test"}, + Cmd: []string{ + "postgres", + "-c", "ssl=on", + "-c", "ssl_cert_file=/ssl/server.crt", + "-c", "ssl_key_file=/ssl/server.key", + }, + } + hostConfig := container.HostConfig{ + Binds: []string{tmpDir + ":/ssl:ro"}, + PortBindings: nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}}, + AutoRemove: true, + } + // Create and start container manually so we can use docker.io/postgres:15 + // without going through the registry URL transform. + created, err := utils.Docker.ContainerCreate(ctx, &config, &hostConfig, nil, nil, "") + require.NoError(t, err) + containerID := created.ID + defer func() { + _ = utils.Docker.ContainerStop(ctx, containerID, container.StopOptions{}) + }() + require.NoError(t, utils.Docker.ContainerStart(ctx, containerID, container.StartOptions{})) + + // Wait for Postgres to accept connections. + dbURL := fmt.Sprintf("postgres://postgres:test@127.0.0.1:%s/postgres?connect_timeout=5", hostPort) + require.Eventually(t, func() bool { + conn, err := utils.ConnectByUrl(ctx, dbURL+"&sslmode=disable") + if err != nil { + return false + } + _ = conn.Close(ctx) + return true + }, 15*time.Second, 500*time.Millisecond, "postgres did not become ready") + + // Force certificate verification in this test to avoid relying on pgx/libpq + // sslmode=require defaults. The pool intentionally excludes the test CA. + forceVerifyOpt := func(cc *pgx.ConnConfig) { + if cc.TLSConfig == nil { + return + } + pool, poolErr := x509.SystemCertPool() + if poolErr != nil || pool == nil { + pool = x509.NewCertPool() + } + cc.TLSConfig.RootCAs = pool + cc.TLSConfig.InsecureSkipVerify = false + } + // Sanity check: requiring ssl with forced verification should fail against + // our test server cert when bypass is not applied. + _, err = utils.ConnectByUrl(ctx, dbURL+"&sslmode=require", forceVerifyOpt) + require.Error(t, err) + + // Probe with sslmode=require. With the fix, the probe appends + // InsecureSkipVerify=true and succeeds; without the fix this will fail. + requireSSL, err := isRequireSSL(ctx, dbURL, forceVerifyOpt) + require.NoError(t, err) + require.True(t, requireSSL, "isRequireSSL should detect TLS and return true") + + // GetRootCA should return the bundled Supabase CA bundle (so that downstream + // migra/pgdelta can verify the connection). The probe must succeed for this to run. + ca, err := GetRootCA(ctx, dbURL, forceVerifyOpt) + require.NoError(t, err) + require.NotEmpty(t, ca, "GetRootCA should return the CA bundle when SSL is required") + + // Ensure the returned bundle looks like PEM (for downstream use). + require.Contains(t, ca, "-----BEGIN CERTIFICATE-----") +} + +func generateTestCertificates() (caPEM, serverCertPEM, serverKeyPEM []byte, err error) { + // Generate CA key and cert. + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, nil, err + } + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{Organization: []string{"Test CA"}}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return nil, nil, nil, err + } + caPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCertDER}) + + // Generate server key and cert signed by CA. + serverKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, nil, err + } + serverTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{Organization: []string{"Test Server"}}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, + DNSNames: []string{"localhost", "127.0.0.1"}, + } + caCert, _ := x509.ParseCertificate(caCertDER) + serverCertDER, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) + if err != nil { + return nil, nil, nil, err + } + serverCertPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCertDER}) + serverKeyPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(serverKey)}) + return caPEM, serverCertPEM, serverKeyPEM, nil +} From c0f65043908c7dede638d07ea1c45036e0b33d45 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 23 Feb 2026 13:05:24 +0100 Subject: [PATCH 2/3] fix(pull): allow to disable CA verify opt-in via env variable --- internal/gen/types/types.go | 20 ++- internal/gen/types/types_integration_test.go | 178 ------------------- 2 files changed, 11 insertions(+), 187 deletions(-) delete mode 100644 internal/gen/types/types_integration_test.go diff --git a/internal/gen/types/types.go b/internal/gen/types/types.go index 19f97775e1..3655ebc9f3 100644 --- a/internal/gen/types/types.go +++ b/internal/gen/types/types.go @@ -131,15 +131,17 @@ func GetRootCA(ctx context.Context, dbURL string, options ...func(*pgx.ConnConfi func isRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnConfig)) (bool, error) { // pgx v4's sslmode=require verifies the server certificate against system CAs, - // unlike libpq where require skips verification. Since this probe only detects - // whether the server speaks TLS (not whether its cert is trusted), we must skip - // verification here. The actual cert validation happens downstream in the migra/ - // pgdelta Deno scripts using the CA bundle returned by GetRootCA. - opts := append(options, func(cc *pgx.ConnConfig) { - if cc.TLSConfig != nil { - cc.TLSConfig.InsecureSkipVerify = true - } - }) + // unlike libpq where require skips verification. When SUPABASE_CA_SKIP_VERIFY=true, + // skip verification for this probe only (detects whether the server speaks TLS). + // Cert validation happens downstream in the migra/pgdelta Deno scripts using GetRootCA. + opts := options + if os.Getenv("SUPABASE_CA_SKIP_VERIFY") == "true" { + opts = append(opts, func(cc *pgx.ConnConfig) { + if cc.TLSConfig != nil { + cc.TLSConfig.InsecureSkipVerify = true + } + }) + } conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", opts...) if err != nil { if strings.HasSuffix(err.Error(), "(server refused TLS connection)") { diff --git a/internal/gen/types/types_integration_test.go b/internal/gen/types/types_integration_test.go deleted file mode 100644 index 75976b0a2c..0000000000 --- a/internal/gen/types/types_integration_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package types - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "io" - "math/big" - "net" - "os" - "path/filepath" - "testing" - "time" - - "github.com/containerd/errdefs" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/go-connections/nat" - "github.com/jackc/pgx/v4" - "github.com/stretchr/testify/require" - "github.com/supabase/cli/internal/utils" -) - -// TestGetRootCA_SSLProbeAgainstRealPostgres verifies that isRequireSSL and GetRootCA -// work correctly when connecting to a real Postgres instance with SSL enabled (certificate -// signed by a CA not in the system trust store). This reproduces the "x509: certificate -// signed by unknown authority" failure that affected db pull against Supabase poolers. -func TestGetRootCA_SSLProbeAgainstRealPostgres(t *testing.T) { - ctx := context.Background() - if !utils.IsDockerRunning(ctx) { - t.Skip("Docker is not running, skipping SSL probe integration test") - } - - // Generate self-signed CA and server certificate so Postgres uses TLS with - // a cert not in the system CA store (same situation as Supabase pooler). - tmpDir := t.TempDir() - _, serverCertPEM, serverKeyPEM, err := generateTestCertificates() - require.NoError(t, err) - - serverCertPath := filepath.Join(tmpDir, "server.crt") - serverKeyPath := filepath.Join(tmpDir, "server.key") - require.NoError(t, os.WriteFile(serverCertPath, serverCertPEM, 0600)) - require.NoError(t, os.WriteFile(serverKeyPath, serverKeyPEM, 0600)) - - // Use postgres:15 from Docker Hub (no registry transform). - const postgresImage = "postgres:15" - if _, err := utils.Docker.ImageInspect(ctx, postgresImage); err != nil { - if errdefs.IsNotFound(err) { - out, pullErr := utils.Docker.ImagePull(ctx, postgresImage, image.PullOptions{}) - require.NoError(t, pullErr) - _, _ = io.Copy(io.Discard, out) - _ = out.Close() - } else { - require.NoError(t, err) - } - } - - // Use a fixed port for the test; avoid conflicts by using a high port. - hostPort := "15433" - config := container.Config{ - Image: postgresImage, - Env: []string{"POSTGRES_PASSWORD=test"}, - Cmd: []string{ - "postgres", - "-c", "ssl=on", - "-c", "ssl_cert_file=/ssl/server.crt", - "-c", "ssl_key_file=/ssl/server.key", - }, - } - hostConfig := container.HostConfig{ - Binds: []string{tmpDir + ":/ssl:ro"}, - PortBindings: nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}}, - AutoRemove: true, - } - // Create and start container manually so we can use docker.io/postgres:15 - // without going through the registry URL transform. - created, err := utils.Docker.ContainerCreate(ctx, &config, &hostConfig, nil, nil, "") - require.NoError(t, err) - containerID := created.ID - defer func() { - _ = utils.Docker.ContainerStop(ctx, containerID, container.StopOptions{}) - }() - require.NoError(t, utils.Docker.ContainerStart(ctx, containerID, container.StartOptions{})) - - // Wait for Postgres to accept connections. - dbURL := fmt.Sprintf("postgres://postgres:test@127.0.0.1:%s/postgres?connect_timeout=5", hostPort) - require.Eventually(t, func() bool { - conn, err := utils.ConnectByUrl(ctx, dbURL+"&sslmode=disable") - if err != nil { - return false - } - _ = conn.Close(ctx) - return true - }, 15*time.Second, 500*time.Millisecond, "postgres did not become ready") - - // Force certificate verification in this test to avoid relying on pgx/libpq - // sslmode=require defaults. The pool intentionally excludes the test CA. - forceVerifyOpt := func(cc *pgx.ConnConfig) { - if cc.TLSConfig == nil { - return - } - pool, poolErr := x509.SystemCertPool() - if poolErr != nil || pool == nil { - pool = x509.NewCertPool() - } - cc.TLSConfig.RootCAs = pool - cc.TLSConfig.InsecureSkipVerify = false - } - // Sanity check: requiring ssl with forced verification should fail against - // our test server cert when bypass is not applied. - _, err = utils.ConnectByUrl(ctx, dbURL+"&sslmode=require", forceVerifyOpt) - require.Error(t, err) - - // Probe with sslmode=require. With the fix, the probe appends - // InsecureSkipVerify=true and succeeds; without the fix this will fail. - requireSSL, err := isRequireSSL(ctx, dbURL, forceVerifyOpt) - require.NoError(t, err) - require.True(t, requireSSL, "isRequireSSL should detect TLS and return true") - - // GetRootCA should return the bundled Supabase CA bundle (so that downstream - // migra/pgdelta can verify the connection). The probe must succeed for this to run. - ca, err := GetRootCA(ctx, dbURL, forceVerifyOpt) - require.NoError(t, err) - require.NotEmpty(t, ca, "GetRootCA should return the CA bundle when SSL is required") - - // Ensure the returned bundle looks like PEM (for downstream use). - require.Contains(t, ca, "-----BEGIN CERTIFICATE-----") -} - -func generateTestCertificates() (caPEM, serverCertPEM, serverKeyPEM []byte, err error) { - // Generate CA key and cert. - caKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, err - } - caTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{Organization: []string{"Test CA"}}, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - IsCA: true, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - BasicConstraintsValid: true, - } - caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) - if err != nil { - return nil, nil, nil, err - } - caPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCertDER}) - - // Generate server key and cert signed by CA. - serverKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, nil, err - } - serverTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(2), - Subject: pkix.Name{Organization: []string{"Test Server"}}, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, - DNSNames: []string{"localhost", "127.0.0.1"}, - } - caCert, _ := x509.ParseCertificate(caCertDER) - serverCertDER, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) - if err != nil { - return nil, nil, nil, err - } - serverCertPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCertDER}) - serverKeyPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(serverKey)}) - return caPEM, serverCertPEM, serverKeyPEM, nil -} From f8d73967a363b9ae441a9bfe01d6ebf79c56ca4f Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 23 Feb 2026 14:39:36 +0100 Subject: [PATCH 3/3] fix: mute gosec on explicit skip --- internal/gen/types/types.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/gen/types/types.go b/internal/gen/types/types.go index 3655ebc9f3..e5e7d73633 100644 --- a/internal/gen/types/types.go +++ b/internal/gen/types/types.go @@ -138,6 +138,8 @@ func isRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnCo if os.Getenv("SUPABASE_CA_SKIP_VERIFY") == "true" { opts = append(opts, func(cc *pgx.ConnConfig) { if cc.TLSConfig != nil { + // #nosec G402 -- Intentionally skipped for this TLS capability probe only. + // Downstream migra/pgdelta flows still validate certificates using GetRootCA. cc.TLSConfig.InsecureSkipVerify = true } })