Fehlende Umgebungsvariablen durchreichen - und dauerhaft absichern
Der Dienst "app" zaehlt seine Umgebungsvariablen einzeln auf; ein env_file gibt es nicht. Drei Werte, die die Anwendung ausliest, standen dort nicht: ERSTEINRICHTUNG_GEHEIMNIS, VERTRAUE_PROXY (beide seit v1.11.0) und S3_SSE (seit v1.11.1). Wer sie laut .env.example und INSTALLATION.md eintrug, brachte sie damit nicht in den Behaelter - die Anleitung war an dieser Stelle schlicht falsch. Besonders unangenehm bei VERTRAUE_PROXY: Der Wert fehlt still, die Anmeldesperre zaehlt dann nur nach E-Mail-Adresse statt zusaetzlich nach Anschluss, und im Betrieb faellt nichts auf. VERTRAUE_PROXY bekommt bewusst KEINE Vorgabe "true". Die strengere Einstellung bleibt die Voreinstellung; wer einen Proxy davorstellt, sagt es ausdruecklich. Ohne Proxy ist der weitergereichte Kopf frei erfunden. Der eigentliche Ertrag ist der Test: Er liest, welche Variablen env.ts ausliest - ueber process.env ebenso wie ueber pflicht(), zahl(), schalter() und schluessel32() -, vergleicht das mit dem, was die Compose-Datei dem Dienst mitgibt, und meldet jede Luecke. Ausnahmen sind aufgezaehlt und begruendet. Damit wiederholt sich der Fehler beim naechsten neuen Wert nicht: Wer ihn einfuehrt und das Durchreichen vergisst, sieht es sofort. Gegen drei Mutationen geprueft: Wert wieder entfernt, Vorgabe "true" gesetzt, S3_SSE entfernt - jede wird rot. Refs #43
This commit is contained in:
@@ -93,6 +93,15 @@ services:
|
|||||||
AUTH_SECRET: ${AUTH_SECRET:?AUTH_SECRET fehlt in der .env}
|
AUTH_SECRET: ${AUTH_SECRET:?AUTH_SECRET fehlt in der .env}
|
||||||
AUTH_URL: ${AUTH_URL:?AUTH_URL fehlt in der .env}
|
AUTH_URL: ${AUTH_URL:?AUTH_URL fehlt in der .env}
|
||||||
AUTH_TRUST_HOST: 'true'
|
AUTH_TRUST_HOST: 'true'
|
||||||
|
# Ohne diesen Wert laesst sich der erste Zugang nicht anlegen - siehe
|
||||||
|
# src/server/actions/ersteinrichtung.ts. Leer heisst: gesperrt.
|
||||||
|
ERSTEINRICHTUNG_GEHEIMNIS: ${ERSTEINRICHTUNG_GEHEIMNIS:-}
|
||||||
|
# Bewusst OHNE Vorgabe "true": Die strengere Einstellung bleibt die
|
||||||
|
# Voreinstellung. Wer einen Reverse Proxy davorstellt - der
|
||||||
|
# dokumentierte Betrieb tut das -, sagt es in der .env ausdruecklich.
|
||||||
|
# Ohne den Wert zaehlt die Anmeldesperre nur nach E-Mail-Adresse.
|
||||||
|
VERTRAUE_PROXY: ${VERTRAUE_PROXY:-}
|
||||||
|
VERTRAUE_PROXY_STUFEN: ${VERTRAUE_PROXY_STUFEN:-}
|
||||||
ENC_KEY_V1: ${ENC_KEY_V1:?ENC_KEY_V1 fehlt in der .env - ohne ihn sind die verschluesselten Felder nicht lesbar}
|
ENC_KEY_V1: ${ENC_KEY_V1:?ENC_KEY_V1 fehlt in der .env - ohne ihn sind die verschluesselten Felder nicht lesbar}
|
||||||
# Bleibt leer, solange nicht rotiert wird. Die Anwendung ueberspringt
|
# Bleibt leer, solange nicht rotiert wird. Die Anwendung ueberspringt
|
||||||
# leere Schluesselvariablen - siehe src/lib/env.ts.
|
# leere Schluesselvariablen - siehe src/lib/env.ts.
|
||||||
@@ -110,6 +119,9 @@ services:
|
|||||||
S3_BUCKET: ${S3_BUCKET:-betreuungs-cockpit}
|
S3_BUCKET: ${S3_BUCKET:-betreuungs-cockpit}
|
||||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||||
|
# Leer lassen. Das mitgelieferte MinIO laeuft ohne KMS und lehnt jede
|
||||||
|
# Ablage ab, sobald hier etwas steht - siehe .env.example.
|
||||||
|
S3_SSE: ${S3_SSE:-}
|
||||||
UPLOAD_MAX_BYTES: ${UPLOAD_MAX_BYTES:-26214400}
|
UPLOAD_MAX_BYTES: ${UPLOAD_MAX_BYTES:-26214400}
|
||||||
OCR_AKTIV: ${OCR_AKTIV:-true}
|
OCR_AKTIV: ${OCR_AKTIV:-true}
|
||||||
SMTP_HOST: ${SMTP_HOST:-}
|
SMTP_HOST: ${SMTP_HOST:-}
|
||||||
|
|||||||
@@ -184,6 +184,13 @@ cp .env.example .env
|
|||||||
nano .env
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Die `.env` allein genügt nicht.** Die `docker-compose.yml` reicht die
|
||||||
|
> Werte einzeln an den Behälter durch — sie hat kein `env_file:`. Ein
|
||||||
|
> selbst ergänzter Wert, der dort nicht aufgeführt ist, erreicht die
|
||||||
|
> Anwendung nicht, auch wenn er in der `.env` steht. Alle hier
|
||||||
|
> beschriebenen Werte sind eingetragen; ein Test wacht darüber
|
||||||
|
> (`tests/umgebung-durchreichen.test.ts`).
|
||||||
|
|
||||||
Diese Werte **musst** du setzen:
|
Diese Werte **musst** du setzen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was die Anwendung liest, muss der Behälter auch bekommen.
|
||||||
|
*
|
||||||
|
* Die `docker-compose.yml` zählt die Umgebungsvariablen des Dienstes `app`
|
||||||
|
* einzeln auf — sie hat kein `env_file:`. Wer einen neuen Wert einführt,
|
||||||
|
* ihn in `.env.example` dokumentiert und in `env.ts` ausliest, hat damit
|
||||||
|
* noch nichts erreicht: Im Behälter kommt er nicht an.
|
||||||
|
*
|
||||||
|
* Genau das ist mit v1.11.0 passiert. `ERSTEINRICHTUNG_GEHEIMNIS` und
|
||||||
|
* `VERTRAUE_PROXY` standen in der Anleitung, in der Beispieldatei und im
|
||||||
|
* Code — nur nicht in der Compose-Datei. Die Anleitung war damit schlicht
|
||||||
|
* falsch, und der Fehler war von außen nicht zu sehen: `VERTRAUE_PROXY`
|
||||||
|
* fehlt still, die Anmeldesperre zählt dann eine Dimension weniger.
|
||||||
|
*
|
||||||
|
* Dieser Test liest beide Seiten und vergleicht sie. Er prüft kein
|
||||||
|
* Verhalten, sondern die Verpackung — wie `skripte.test.ts`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const compose = readFileSync('docker-compose.yml', 'utf8')
|
||||||
|
const envDatei = readFileSync('src/lib/env.ts', 'utf8')
|
||||||
|
const beispiel = readFileSync('.env.example', 'utf8')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alles, was `env.ts` über seine Helfer ausliest.
|
||||||
|
*
|
||||||
|
* `process.env.X` direkt wird mitgenommen, ebenso `pflicht('X')`,
|
||||||
|
* `zahl('X', …)`, `schalter('X', …)` und `schluessel32('X')`.
|
||||||
|
*/
|
||||||
|
function gelesen(): Set<string> {
|
||||||
|
const namen = new Set<string>()
|
||||||
|
|
||||||
|
for (const treffer of envDatei.matchAll(/process\.env\.([A-Z_0-9]+)/g)) {
|
||||||
|
namen.add(treffer[1]!)
|
||||||
|
}
|
||||||
|
for (const treffer of envDatei.matchAll(
|
||||||
|
/(?:pflicht|zahl|schalter|schluessel32)\('([A-Z_0-9]+)'/g,
|
||||||
|
)) {
|
||||||
|
namen.add(treffer[1]!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return namen
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die Variablen, die der Dienst `app` durchreicht. */
|
||||||
|
function durchgereicht(): Set<string> {
|
||||||
|
const abschnitt = compose.slice(compose.indexOf('\n app:'), compose.indexOf('\n sicherung:'))
|
||||||
|
const namen = new Set<string>()
|
||||||
|
for (const treffer of abschnitt.matchAll(/^ {6}([A-Z_0-9]+):/gm)) {
|
||||||
|
namen.add(treffer[1]!)
|
||||||
|
}
|
||||||
|
return namen
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was absichtlich nicht durchgereicht wird - jeweils mit Grund.
|
||||||
|
*
|
||||||
|
* Wer hier etwas ergänzt, nimmt es bewusst aus der Prüfung. Das ist
|
||||||
|
* erlaubt, aber es soll dabeistehen, warum.
|
||||||
|
*/
|
||||||
|
const AUSNAHMEN = new Map<string, string>([
|
||||||
|
['NODE_ENV', 'wird im Dienst gesetzt, kommt nicht aus der .env'],
|
||||||
|
['NEXT_PUBLIC_GIT_COMMIT', 'Bauargument, steckt schon im Abbild'],
|
||||||
|
['NEXT_PUBLIC_BUILD_ZEIT', 'Bauargument, steckt schon im Abbild'],
|
||||||
|
['DATEI_SPEICHER_PFAD', 'nur fuer den Dateitreiber; im Behaelter laeuft s3'],
|
||||||
|
['TESSERACT_BEFEHL', 'Standard "tesseract" stimmt im Abbild immer'],
|
||||||
|
])
|
||||||
|
|
||||||
|
describe('Umgebungsvariablen erreichen den Behälter', () => {
|
||||||
|
it('reicht jede Variable durch, die die Anwendung ausliest', () => {
|
||||||
|
const fehlend = [...gelesen()]
|
||||||
|
.filter((name) => !durchgereicht().has(name))
|
||||||
|
.filter((name) => !AUSNAHMEN.has(name))
|
||||||
|
// ENC_KEY_V2, V3 ... werden dynamisch gesucht; nur V1 ist Pflicht.
|
||||||
|
.filter((name) => !/^ENC_KEY_V\d+$/.test(name))
|
||||||
|
.sort()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
fehlend,
|
||||||
|
'Diese Werte liest die Anwendung, aber die docker-compose.yml gibt sie ' +
|
||||||
|
'dem Dienst "app" nicht mit. In der .env stehen sie dann wirkungslos.',
|
||||||
|
).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('erklärt jede Ausnahme', () => {
|
||||||
|
for (const [name, grund] of AUSNAHMEN) {
|
||||||
|
expect(grund.length, `Ausnahme ${name} ohne Begründung`).toBeGreaterThan(10)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dokumentiert die beiden Werte aus v1.11.0 in der Beispieldatei', () => {
|
||||||
|
// Sie waren der Anlass. Verschwinden sie aus .env.example, merkt es
|
||||||
|
// sonst niemand, bis jemand eine Neuinstallation versucht.
|
||||||
|
expect(beispiel).toMatch(/ERSTEINRICHTUNG_GEHEIMNIS=/)
|
||||||
|
expect(beispiel).toMatch(/VERTRAUE_PROXY=/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lässt VERTRAUE_PROXY ohne Vorgabewert', () => {
|
||||||
|
// Die strengere Einstellung bleibt die Voreinstellung. Ein Vorgabewert
|
||||||
|
// "true" waere bequem und falsch: Ohne Proxy ist der weitergereichte
|
||||||
|
// Kopf frei erfunden.
|
||||||
|
expect(compose).toMatch(/VERTRAUE_PROXY: \$\{VERTRAUE_PROXY:-\}/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findet überhaupt etwas - sonst prüft der Test nichts', () => {
|
||||||
|
// Ein stiller Ausfall der Erkennung waere schlimmer als eine Luecke:
|
||||||
|
// Der Test staende gruen da und pruefte nichts.
|
||||||
|
expect(gelesen().size).toBeGreaterThan(15)
|
||||||
|
expect(durchgereicht().size).toBeGreaterThan(15)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user