|
4 | 4 | "context" |
5 | 5 | _ "embed" |
6 | 6 | "fmt" |
| 7 | + "net/url" |
7 | 8 | "os" |
| 9 | + "runtime" |
8 | 10 | "strings" |
9 | 11 | "time" |
10 | 12 |
|
@@ -120,37 +122,135 @@ var ( |
120 | 122 | ) |
121 | 123 |
|
122 | 124 | func GetRootCA(ctx context.Context, dbURL string, options ...func(*pgx.ConnConfig)) (string, error) { |
| 125 | + debugf := func(string, ...any) {} |
| 126 | + if IsSSLDebugEnabled() { |
| 127 | + debugf = LogSSLDebugf |
| 128 | + } |
| 129 | + debugf("GetRootCA start db_url=%s", redactPostgresURL(dbURL)) |
| 130 | + debugf("env SUPABASE_CA_SKIP_VERIFY=%q SUPABASE_SSL_DEBUG=%q PGSSLROOTCERT=%q SSL_CERT_FILE=%q SSL_CERT_DIR=%q", |
| 131 | + os.Getenv("SUPABASE_CA_SKIP_VERIFY"), |
| 132 | + os.Getenv("SUPABASE_SSL_DEBUG"), |
| 133 | + os.Getenv("PGSSLROOTCERT"), |
| 134 | + os.Getenv("SSL_CERT_FILE"), |
| 135 | + os.Getenv("SSL_CERT_DIR"), |
| 136 | + ) |
| 137 | + debugf("runtime goos=%s goarch=%s go=%s", runtime.GOOS, runtime.GOARCH, runtime.Version()) |
123 | 138 | // node-postgres does not support sslmode=prefer |
124 | | - if require, err := isRequireSSL(ctx, dbURL, options...); !require { |
| 139 | + require, err := isRequireSSL(ctx, dbURL, options...) |
| 140 | + debugf("GetRootCA probe_result require_ssl=%t err=%v", require, err) |
| 141 | + if !require { |
125 | 142 | return "", err |
126 | 143 | } |
127 | 144 | // Merge all certs to support --db-url flag |
128 | | - return caStaging + caProd + caSnap, nil |
| 145 | + ca := caStaging + caProd + caSnap |
| 146 | + debugf("GetRootCA return ca_bundle_len=%d", len(ca)) |
| 147 | + return ca, nil |
129 | 148 | } |
130 | 149 |
|
131 | 150 | func isRequireSSL(ctx context.Context, dbUrl string, options ...func(*pgx.ConnConfig)) (bool, error) { |
132 | | - |
| 151 | + debugf := func(string, ...any) {} |
| 152 | + if IsSSLDebugEnabled() { |
| 153 | + debugf = LogSSLDebugf |
| 154 | + } |
133 | 155 | // pgx v4's sslmode=require verifies the server certificate against system CAs, |
134 | 156 | // unlike libpq where require skips verification. When SUPABASE_CA_SKIP_VERIFY=true, |
135 | 157 | // skip verification for this probe only (detects whether the server speaks TLS). |
| 158 | + // pgconn may still install VerifyPeerCertificate callback when sslrootcert is set, |
| 159 | + // so we also clear custom verification callbacks on all TLS configs. |
136 | 160 | // Cert validation happens downstream in the migra/pgdelta Deno scripts using GetRootCA. |
137 | | - opts := options |
| 161 | + opts := append([]func(*pgx.ConnConfig){}, options...) |
138 | 162 | if os.Getenv("SUPABASE_CA_SKIP_VERIFY") == "true" { |
| 163 | + fmt.Fprintln(os.Stderr, "WARNING: TLS certificate verification disabled for SSL probe (SUPABASE_CA_SKIP_VERIFY=true)") |
139 | 164 | opts = append(opts, func(cc *pgx.ConnConfig) { |
| 165 | + // #nosec G402 -- Intentionally skipped for this TLS capability probe only. |
| 166 | + // Downstream migra/pgdelta flows still validate certificates using GetRootCA. |
140 | 167 | if cc.TLSConfig != nil { |
141 | | - // #nosec G402 -- Intentionally skipped for this TLS capability probe only. |
142 | | - // Downstream migra/pgdelta flows still validate certificates using GetRootCA. |
143 | 168 | cc.TLSConfig.InsecureSkipVerify = true |
| 169 | + cc.TLSConfig.VerifyPeerCertificate = nil |
| 170 | + cc.TLSConfig.VerifyConnection = nil |
| 171 | + } |
| 172 | + for _, fc := range cc.Fallbacks { |
| 173 | + if fc.TLSConfig == nil { |
| 174 | + continue |
| 175 | + } |
| 176 | + fc.TLSConfig.InsecureSkipVerify = true |
| 177 | + fc.TLSConfig.VerifyPeerCertificate = nil |
| 178 | + fc.TLSConfig.VerifyConnection = nil |
144 | 179 | } |
145 | 180 | }) |
146 | 181 | } |
| 182 | + debugf("isRequireSSL probe db_url=%s skip_verify=%t", redactPostgresURL(dbUrl), os.Getenv("SUPABASE_CA_SKIP_VERIFY") == "true") |
| 183 | + if IsSSLDebugEnabled() { |
| 184 | + opts = append(opts, logTLSConfigState("isRequireSSL", dbUrl)) |
| 185 | + } |
147 | 186 | conn, err := utils.ConnectByUrl(ctx, dbUrl+"&sslmode=require", opts...) |
148 | 187 | if err != nil { |
| 188 | + debugf("isRequireSSL probe_error err=%v", err) |
149 | 189 | if strings.HasSuffix(err.Error(), "(server refused TLS connection)") { |
| 190 | + debugf("isRequireSSL result require_ssl=false reason=server_refused_tls") |
150 | 191 | return false, nil |
151 | 192 | } |
152 | 193 | return false, err |
153 | 194 | } |
154 | 195 | // SSL is not supported in debug mode |
155 | | - return !viper.GetBool("DEBUG"), conn.Close(ctx) |
| 196 | + require := !viper.GetBool("DEBUG") |
| 197 | + debugf("isRequireSSL result require_ssl=%t debug_mode=%t", require, viper.GetBool("DEBUG")) |
| 198 | + return require, conn.Close(ctx) |
| 199 | +} |
| 200 | + |
| 201 | +func IsSSLDebugEnabled() bool { |
| 202 | + return strings.EqualFold(os.Getenv("SUPABASE_SSL_DEBUG"), "true") |
| 203 | +} |
| 204 | + |
| 205 | +func LogSSLDebugf(format string, args ...any) { |
| 206 | + fmt.Fprintf(os.Stderr, "[ssl-debug] "+format+"\n", args...) |
| 207 | +} |
| 208 | + |
| 209 | +func redactPostgresURL(raw string) string { |
| 210 | + parsed, err := url.Parse(raw) |
| 211 | + if err != nil { |
| 212 | + return "<invalid-url>" |
| 213 | + } |
| 214 | + if parsed.User != nil { |
| 215 | + username := parsed.User.Username() |
| 216 | + if username == "" { |
| 217 | + parsed.User = url.UserPassword("redacted", "xxxxx") |
| 218 | + } else { |
| 219 | + parsed.User = url.UserPassword(username, "xxxxx") |
| 220 | + } |
| 221 | + } |
| 222 | + return parsed.String() |
| 223 | +} |
| 224 | + |
| 225 | +func logTLSConfigState(scope, dbUrl string) func(*pgx.ConnConfig) { |
| 226 | + return func(cc *pgx.ConnConfig) { |
| 227 | + if cc.TLSConfig == nil { |
| 228 | + LogSSLDebugf("%s tls_config=nil db_url=%s fallbacks=%d", scope, redactPostgresURL(dbUrl), len(cc.Fallbacks)) |
| 229 | + return |
| 230 | + } |
| 231 | + LogSSLDebugf("%s tls_config skip_verify=%t verify_peer_cb=%t verify_conn_cb=%t root_cas=%t server_name=%q fallbacks=%d", |
| 232 | + scope, |
| 233 | + cc.TLSConfig.InsecureSkipVerify, |
| 234 | + cc.TLSConfig.VerifyPeerCertificate != nil, |
| 235 | + cc.TLSConfig.VerifyConnection != nil, |
| 236 | + cc.TLSConfig.RootCAs != nil, |
| 237 | + cc.TLSConfig.ServerName, |
| 238 | + len(cc.Fallbacks), |
| 239 | + ) |
| 240 | + for i, fc := range cc.Fallbacks { |
| 241 | + if fc == nil || fc.TLSConfig == nil { |
| 242 | + LogSSLDebugf("%s fallback[%d] tls_config=nil", scope, i) |
| 243 | + continue |
| 244 | + } |
| 245 | + LogSSLDebugf("%s fallback[%d] skip_verify=%t verify_peer_cb=%t verify_conn_cb=%t root_cas=%t server_name=%q", |
| 246 | + scope, |
| 247 | + i, |
| 248 | + fc.TLSConfig.InsecureSkipVerify, |
| 249 | + fc.TLSConfig.VerifyPeerCertificate != nil, |
| 250 | + fc.TLSConfig.VerifyConnection != nil, |
| 251 | + fc.TLSConfig.RootCAs != nil, |
| 252 | + fc.TLSConfig.ServerName, |
| 253 | + ) |
| 254 | + } |
| 255 | + } |
156 | 256 | } |
0 commit comments