From b915857951249cec3c6380cc0d9745b1fff2c25a Mon Sep 17 00:00:00 2001 From: "t.indorf" Date: Fri, 9 Oct 2026 23:55:16 +0200 Subject: [PATCH] fix(deploy): preserve credential blocking through hosting parser --- deploy/README.md | 2 +- docker-compose.coolify.yml | 12 ++++++------ scripts/verify-runtime-isolation.mjs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 05e3310..771a55a 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -4,7 +4,7 @@ Die Anwendung startet ohne Schemaänderungen und ohne Demo-Seeding. Datenbankmig ## Konfiguration -Werte aus `production.env.example` als Hosting-Secrets hinterlegen. Im Compose-Stack erzeugt der Dienst `migrate` automatisch ein separates Runtime-Passwort im privaten Secret-Volume, wendet die Migrationen an, vergibt die eingeschränkten DB-Rechte und bereitet die Upload-/Cache-Volumes vor. Danach startet `app` mit UID/GID 1001. Administrative Zugangsdaten werden im App-Service ausdrücklich leer überschrieben, damit eine zusätzliche globale `env_file`-Weitergabe durch das Hosting sie nicht in den App-Container einschleust. Der Start entfernt diese Variablen zusätzlich vor dem App- und Worker-Prozess. Das Eigentümerpasswort steht nur PostgreSQL und dem Initialisierungsdienst zur Verfügung. Für manuelle CLI-Befehle außerhalb von Compose bleibt `DATABASE_URL` die jeweilige administrative oder eingeschränkte URL. Keine echten Secrets im Repository speichern. +Werte aus `production.env.example` als Hosting-Secrets hinterlegen. Im Compose-Stack erzeugt der Dienst `migrate` automatisch ein separates Runtime-Passwort im privaten Secret-Volume, wendet die Migrationen an, vergibt die eingeschränkten DB-Rechte und bereitet die Upload-/Cache-Volumes vor. Danach startet `app` mit UID/GID 1001. Administrative Zugangsdaten werden im App-Service ausdrücklich mit dem harmlosen Platzhalter `unavailable-in-app` überschrieben (leere Werte entfernt Coolify bei der Verarbeitung), damit eine zusätzliche globale `env_file`-Weitergabe durch das Hosting sie nicht in den App-Container einschleust. Der Start entfernt diese Variablen zusätzlich vor dem App- und Worker-Prozess. Das Eigentümerpasswort steht nur PostgreSQL und dem Initialisierungsdienst zur Verfügung. Für manuelle CLI-Befehle außerhalb von Compose bleibt `DATABASE_URL` die jeweilige administrative oder eingeschränkte URL. Keine echten Secrets im Repository speichern. `AUTH_URL`, `NEXTAUTH_URL` und `NEXT_PUBLIC_SITE_URL` müssen dieselbe kanonische HTTPS-Origin verwenden. Der Reverse Proxy terminiert TLS, routet intern auf App-Port 3000 und überschreibt weitergeleitete Host-/IP-Header. `TRUSTED_CLIENT_IP_HEADER` nur setzen, wenn der Proxy den gewählten Header (`x-real-ip` oder `cf-connecting-ip`) zuverlässig überschreibt. Ohne diese Einstellung gelten weiterhin gemeinsame globale und identitätsbezogene Datenbanklimits. Proxyseitig Body-Limit von höchstens 26 MiB, Timeouts und zusätzliche Request-Limits setzen. diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index e84a951..a4ec4a2 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -27,12 +27,12 @@ services: NODE_ENV: production PORT: 3000 HOSTNAME: 0.0.0.0 - # Override host-wide env_file injection: administrative credentials belong to migrate only. - DATABASE_URL: "" - MIGRATION_DATABASE_URL: "" - APP_DATABASE_PASSWORD: "" - POSTGRES_PASSWORD: "" - PGADMIN_DEFAULT_PASSWORD: "" + # A nonempty sentinel survives hosting Compose parsers and blocks global env_file credentials. + DATABASE_URL: "unavailable-in-app" + MIGRATION_DATABASE_URL: "unavailable-in-app" + APP_DATABASE_PASSWORD: "unavailable-in-app" + POSTGRES_PASSWORD: "unavailable-in-app" + PGADMIN_DEFAULT_PASSWORD: "unavailable-in-app" APP_DATABASE_PASSWORD_FILE: /app/runtime-secrets/database-password APP_DATABASE_NAME: ${POSTGRES_DB:-kita_planer} AUTH_SECRET: ${AUTH_SECRET:?} diff --git a/scripts/verify-runtime-isolation.mjs b/scripts/verify-runtime-isolation.mjs index faeff87..80d78c5 100644 --- a/scripts/verify-runtime-isolation.mjs +++ b/scripts/verify-runtime-isolation.mjs @@ -6,10 +6,10 @@ try { const index = line.indexOf('='); return [line.slice(0, index), line.slice(index + 1)]; })); for (const key of ['POSTGRES_PASSWORD', 'PGADMIN_DEFAULT_PASSWORD', 'APP_DATABASE_PASSWORD', 'MIGRATION_DATABASE_URL']) { - if (process.env[key] || main[key]) throw new Error('Administrative credential reached the app.'); + if ((process.env[key] && process.env[key] !== 'unavailable-in-app') || main[key]) throw new Error('Administrative credential reached the app.'); } // Configured global/legacy URLs must not expose an administrative login either. - if (process.env.DATABASE_URL) throw new Error('Hosting injected a legacy database URL.'); + if (process.env.DATABASE_URL && process.env.DATABASE_URL !== 'unavailable-in-app') throw new Error('Hosting injected a legacy database URL.'); if (new URL(main.DATABASE_URL).username !== 'kita_runtime') throw new Error('Wrong application database role.'); prisma = new PrismaClient({ datasourceUrl: main.DATABASE_URL }); const [row] = await prisma.$queryRawUnsafe('SELECT current_user');