fix(db): trust verified Supabase root CA for deployed TLS

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
camilafagundesandCursor committed 2026-10-23 19:42:46 -03:00
1 parent 60f15a3576
commit af559ca455
3 files changed
+104 -7

No files matched your search

+10 -4
View File
@@ -1,5 +1,6 @@
import type { EnvMap } from "@/config/env"; import type { EnvMap } from "@/config/env";
import { isDeployedEnv } from "@/config/env"; import { isDeployedEnv } from "@/config/env";
import { SUPABASE_ROOT_CA_2021 } from "./supabase-root-ca";
/** Projeto autorizado a receber migrations. Não é segredo. */ /** Projeto autorizado a receber migrations. Não é segredo. */
export const AUTHORIZED_HML_PROJECT = "adaptador-planilha-hml"; export const AUTHORIZED_HML_PROJECT = "adaptador-planilha-hml";
@@ -169,7 +170,7 @@ export type PostgresClientOptions = {
connect_timeout: number; connect_timeout: number;
idle_timeout: number; idle_timeout: number;
max_lifetime: number; max_lifetime: number;
ssl?: { rejectUnauthorized: true }; ssl?: { rejectUnauthorized: true; ca: string };
}; };
/** /**
@@ -177,8 +178,13 @@ export type PostgresClientOptions = {
* *
* `connection` no postgres.js é um parâmetro de startup, não uma opção do * `connection` no postgres.js é um parâmetro de startup, não uma opção do
* socket. Enviar `family=4` faz o PostgreSQL recusar a sessão com 42704 * socket. Enviar `family=4` faz o PostgreSQL recusar a sessão com 42704
* (`unrecognized configuration parameter "family"`) antes da primeira * antes da primeira consulta. O IPv4 continua no host do Session Pooler.
* consulta. O IPv4 continua garantido pelo host do Session Pooler. *
* O pooler apresenta uma cadeia emitida pela raiz pública Supabase Root 2021
* CA. Essa raiz não está na confiança padrão do Node, e por isso a verificação
* falha com SELF_SIGNED_CERT_IN_CHAIN. `ca` fixa somente essa raiz neste
* socket. `rejectUnauthorized` permanece verdadeiro, e o driver continua
* conferindo o hostname. A confiança padrão dos outros processos não muda.
*/ */
export function buildPostgresOptions(connectionString: string, purpose: DbPurpose, env: EnvMap = process.env): PostgresClientOptions { export function buildPostgresOptions(connectionString: string, purpose: DbPurpose, env: EnvMap = process.env): PostgresClientOptions {
const deployed = isDeployedEnv(env); const deployed = isDeployedEnv(env);
@@ -191,7 +197,7 @@ export function buildPostgresOptions(connectionString: string, purpose: DbPurpos
idle_timeout: 20, idle_timeout: 20,
max_lifetime: 60 * 30, max_lifetime: 60 * 30,
}; };
if (deployed) options.ssl = { rejectUnauthorized: true }; if (deployed) options.ssl = { rejectUnauthorized: true, ca: SUPABASE_ROOT_CA_2021 };
return options; return options;
} }
+28
View File
@@ -0,0 +1,28 @@
/**
* CA raiz pública do Supabase, publicada como prod-ca-2021.
* Não contém chave privada. A impressão SHA-256 é conferida nos testes.
*/
export const SUPABASE_ROOT_CA_2021 = `-----BEGIN CERTIFICATE-----
MIIDxDCCAqygAwIBAgIUbLxMod62P2ktCiAkxnKJwtE9VPYwDQYJKoZIhvcNAQEL
BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l
dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh
c2UgUm9vdCAyMDIxIENBMB4XDTIxMDQyODEwNTY1M1oXDTMxMDQyNjEwNTY1M1ow
azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD
YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug
Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQXW
QyHOB+qR2GJobCq/CBmQ40G0oDmCC3mzVnn8sv4XNeWtE5XcEL0uVih7Jo4Dkx1Q
DmGHBH1zDfgs2qXiLb6xpw/CKQPypZW1JssOTMIfQppNQ87K75Ya0p25Y3ePS2t2
GtvHxNjUV6kjOZjEn2yWEcBdpOVCUYBVFBNMB4YBHkNRDa/+S4uywAoaTWnCJLUi
cvTlHmMw6xSQQn1UfRQHk50DMCEJ7Cy1RxrZJrkXXRP3LqQL2ijJ6F4yMfh+Gyb4
O4XajoVj/+R4GwywKYrrS8PrSNtwxr5StlQO8zIQUSMiq26wM8mgELFlS/32Uclt
NaQ1xBRizkzpZct9DwIDAQABo2AwXjALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFKjX
uXY32CztkhImng4yJNUtaUYsMB8GA1UdIwQYMBaAFKjXuXY32CztkhImng4yJNUt
aUYsMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB8spzNn+4VU
tVxbdMaX+39Z50sc7uATmus16jmmHjhIHz+l/9GlJ5KqAMOx26mPZgfzG7oneL2b
VW+WgYUkTT3XEPFWnTp2RJwQao8/tYPXWEJDc0WVQHrpmnWOFKU/d3MqBgBm5y+6
jB81TU/RG2rVerPDWP+1MMcNNy0491CTL5XQZ7JfDJJ9CCmXSdtTl4uUQnSuv/Qx
Cea13BX2ZgJc7Au30vihLhub52De4P/4gonKsNHYdbWjg7OWKwNv/zitGDVDB9Y2
CMTyZKG3XEu5Ghl1LEnI3QmEKsqaCLv12BnVjbkSeZsMnevJPs1Ye6TjjJwdik5P
o/bKiIz+Fq8=
-----END CERTIFICATE-----
`;
+66 -3
View File
@@ -1,3 +1,8 @@
import { X509Certificate } from "node:crypto";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import tls from "node:tls";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
AUTHORIZED_HML_PROJECT, AUTHORIZED_HML_PROJECT,
@@ -13,6 +18,7 @@ import {
import { readFileSync, readdirSync } from "node:fs"; import { readFileSync, readdirSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { assertInitAuthorized, projectIdentityDefinitionSql, projectIdentityRevertSql } from "@/infra/db/project-identity"; import { assertInitAuthorized, projectIdentityDefinitionSql, projectIdentityRevertSql } from "@/infra/db/project-identity";
import { SUPABASE_ROOT_CA_2021 } from "@/infra/db/supabase-root-ca";
import { assertAdminTarget, evaluateAdminIdentity, evaluateProjectIdentity, evaluateRuntimeRole, type AdminAttestation } from "@/infra/db/target-guard"; import { assertAdminTarget, evaluateAdminIdentity, evaluateProjectIdentity, evaluateRuntimeRole, type AdminAttestation } from "@/infra/db/target-guard";
import type { Db } from "@/infra/db/types"; import type { Db } from "@/infra/db/types";
@@ -167,16 +173,73 @@ describe("barreira do banco de homologação", () => {
expect(resolvePoolMax("web", env)).toBe(2); expect(resolvePoolMax("web", env)).toBe(2);
expect(resolvePoolMax("worker", env)).toBe(2); expect(resolvePoolMax("worker", env)).toBe(2);
expect(resolvePoolMax("admin", env)).toBe(1); expect(resolvePoolMax("admin", env)).toBe(1);
const root = new X509Certificate(SUPABASE_ROOT_CA_2021);
expect(root.ca).toBe(true);
expect(root.fingerprint256).toBe("80:70:25:AD:50:D4:ED:21:9D:2C:9C:7D:29:9C:00:4F:82:4E:B0:0C:F7:F6:5A:FE:F6:07:D0:7B:72:E6:CA:FA");
expect(root.subject).toContain("CN=Supabase Root 2021 CA");
expect(SUPABASE_ROOT_CA_2021).not.toContain("PRIVATE KEY");
const deployed = buildPostgresOptions(adminUrl(), "admin", env); const deployed = buildPostgresOptions(adminUrl(), "admin", env);
const production = buildPostgresOptions(adminUrl(), "admin", { ...env, ADAPTER_ENV: "production" }); const production = buildPostgresOptions(adminUrl(), "admin", { ...env, ADAPTER_ENV: "production" });
for (const options of [deployed, production]) { for (const options of [deployed, production]) {
expect(options.ssl).toEqual({ rejectUnauthorized: true }); expect(options.ssl).toEqual({ rejectUnauthorized: true, ca: SUPABASE_ROOT_CA_2021 });
expect(options).not.toHaveProperty("connection"); expect(options).not.toHaveProperty("connection");
expect(JSON.stringify(options)).not.toContain("family"); expect(JSON.stringify(options)).not.toContain("family");
expect(JSON.stringify(options)).not.toContain("rejectUnauthorized\":false");
} }
expect(buildPostgresOptions("postgres://adaptador:local@127.0.0.1:5432/adaptador", "local", { ADAPTER_ENV: "development" })).not.toHaveProperty( const local = buildPostgresOptions("postgres://adaptador:local@127.0.0.1:5432/adaptador", "local", {
"connection", ADAPTER_ENV: "development",
});
expect(local).not.toHaveProperty("connection");
expect(local.ssl).toBeUndefined();
});
it("a CA pública não libera outro destino nem aceita certificado divergente", async () => {
expect(() =>
buildPostgresOptions(adminUrl(`postgres.${REF}`, "verify-full", "aws-0-us-west-2.pooler.supabase.com"), "admin", env),
).toThrow(/destino_incompativel/);
expect(() => assertInitAuthorized({ ADAPTER_ENV: "staging", ADAPTER_DB_PROJECT_REF: REF })).toThrow(/init_nao_autorizada/);
const options = buildPostgresOptions(adminUrl(), "admin", env);
expect(options.ssl).not.toHaveProperty("checkServerIdentity");
expect(options.ssl).not.toHaveProperty("servername");
expect(options.ssl?.rejectUnauthorized).toBe(true);
const dir = mkdtempSync(path.join(tmpdir(), "ca-divergente-"));
const keyPath = path.join(dir, "key.pem");
const certPath = path.join(dir, "cert.pem");
try {
execFileSync(
"openssl",
["req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1", "-keyout", keyPath, "-out", certPath, "-days", "1", "-nodes", "-subj", "/CN=divergente.example"],
{ stdio: "ignore" },
); );
const server = tls.createServer({ key: readFileSync(keyPath), cert: readFileSync(certPath) }, (socket) => socket.end());
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("porta_local_ausente");
try {
await new Promise<void>((resolve, reject) => {
const socket = tls.connect({
host: "127.0.0.1",
port: address.port,
servername: "divergente.example",
rejectUnauthorized: true,
ca: SUPABASE_ROOT_CA_2021,
});
socket.once("secureConnect", () => {
socket.end();
reject(new Error("certificado_divergente_aceito"));
});
socket.once("error", (error: NodeJS.ErrnoException) => {
expect(error.code).toMatch(/SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE/);
resolve();
});
});
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
}); });
it("recusa a credencial administrativa na web e BYPASSRLS no worker", () => { it("recusa a credencial administrativa na web e BYPASSRLS no worker", () => {