Six modules take their secrets from files; the rest say precisely why not

From the survey of every env-file secret (ADR 0086, issue 041): amqp-ping,
minio, mongodb and grafana use the _FILE twin their software honours;
mesh-catalog and model-usage read DATABASE_URL_FILE (a file the mesh
templates, mounted where only the runtime reads it); grafana's secret files
belong to its own account. Two dead deliveries removed: a line nothing read
in amqp-email-forwarder, and mailu's secret.env on four containers that
never read it. The 25 exceptions that remain carry the surveyed reason —
convertible and awaiting a bed, convertible through a generated config file,
the application's own code, or not convertible.
This commit is contained in:
2026-09-21 12:29:25 +02:00
parent db597bcb71
commit 1a28e5aec6
26 changed files with 106 additions and 96 deletions
+2 -2
View File
@@ -31,7 +31,7 @@
"type": "file", "type": "file",
"path": "/var/lib/amqp-email-forwarder/app.env", "path": "/var/lib/amqp-email-forwarder/app.env",
"mode": "0600", "mode": "0600",
"content": "AMQP_HOST=${bound:amqp:at}\nAMQP_PORT=${bound:amqp:port}\nAMQP_USER=${bound:amqp:as}\nAMQP_PASSWORD=${secret:amqp}\nAMQP_VHOST=EMAILDELIVERY_T\nAMQP_EXCHANGE=News.TransactionalEmailing.Command\nAMQP_QUEUE=email-forwarder\nAMQP_URL=amqp://${bound:amqp:as}:${secret:amqp}@${bound:amqp:at}:${bound:amqp:port}/EMAILDELIVERY_T\nSMTP_HOST=mail.novox.be\nSMTP_PORT=587\nSMTP_USER=${secret:smtp-user}\nSMTP_PASSWORD=${secret:smtp-password}\n" "content": "AMQP_HOST=${bound:amqp:at}\nAMQP_PORT=${bound:amqp:port}\nAMQP_USER=${bound:amqp:as}\nAMQP_VHOST=EMAILDELIVERY_T\nAMQP_EXCHANGE=News.TransactionalEmailing.Command\nAMQP_QUEUE=email-forwarder\nAMQP_URL=amqp://${bound:amqp:as}:${secret:amqp}@${bound:amqp:at}:${bound:amqp:port}/EMAILDELIVERY_T\nSMTP_HOST=mail.novox.be\nSMTP_PORT=587\nSMTP_USER=${secret:smtp-user}\nSMTP_PASSWORD=${secret:smtp-password}\n"
}, },
{ {
"id": "net", "id": "net",
@@ -50,7 +50,7 @@
"restart-on": [ "restart-on": [
"app-env" "app-env"
], ],
"secrets-in-environment": "the runtime reads its SMTP and AMQP settings from the environment; a file twin in the SDK is the per-module work of issue 041" "secrets-in-environment": "the application's own code reads AMQP_URL, SMTP_USER and SMTP_PASSWORD from the environment (amqp-email-forwarder app.js); converting is that repository's change"
} }
] ]
} }
+6 -5
View File
@@ -36,7 +36,7 @@
"type": "file", "type": "file",
"path": "/var/lib/amqp-ping/amqp.env", "path": "/var/lib/amqp-ping/amqp.env",
"mode": "0600", "mode": "0600",
"content": "MESH_AMQP_HOST=${bound:amqp:at}\nMESH_AMQP_PORT=${bound:amqp:port}\nMESH_AMQP_USER=${bound:amqp:as}\nMESH_AMQP_VHOST=${bound:amqp:as}\nMESH_AMQP_PASSWORD=${secret:amqp}\n" "content": "MESH_AMQP_HOST=${bound:amqp:at}\nMESH_AMQP_PORT=${bound:amqp:port}\nMESH_AMQP_USER=${bound:amqp:as}\nMESH_AMQP_VHOST=${bound:amqp:as}\n"
}, },
{ {
"id": "net", "id": "net",
@@ -49,10 +49,12 @@
"name": "amqp-ping", "name": "amqp-ping",
"network": "amqp-ping", "network": "amqp-ping",
"volumes": [ "volumes": [
"/var/lib/mesh/amqp-ping/broker:/run/secrets/broker:ro" "/var/lib/mesh/amqp-ping/broker:/run/secrets/broker:ro",
"/var/lib/amqp-ping/amqp.secret:/run/secrets/amqp:ro"
], ],
"env": { "env": {
"MESH_BROKER_FILE": "/run/secrets/broker" "MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_AMQP_PASSWORD_FILE": "/run/secrets/amqp"
}, },
"env-file": [ "env-file": [
"/var/lib/amqp-ping/amqp.env" "/var/lib/amqp-ping/amqp.env"
@@ -60,8 +62,7 @@
"restart-on": [ "restart-on": [
"amqp-env" "amqp-env"
], ],
"artifact": "runtime", "artifact": "runtime"
"secrets-in-environment": "the runtime reads MESH_AMQP_* from the environment; a file twin in the SDK is the per-module work of issue 041"
} }
], ],
"build": { "build": {
+1 -1
View File
@@ -86,7 +86,7 @@
"volumes": [ "volumes": [
"/services/baserow/data:/baserow/data" "/services/baserow/data:/baserow/data"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "baserow reads DATABASE_PASSWORD, REDIS_PASSWORD and SECRET_KEY with os.getenv and has no _FILE twin (settings/base.py); not convertible"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+1 -1
View File
@@ -60,7 +60,7 @@
"ports": [ "ports": [
"35621:35621" "35621:35621"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "the application's own code reads SMTP_AUTH_USER/PASS from the environment (de-spiegel server/index.js); converting is that repository's change"
} }
] ]
} }
+2 -2
View File
@@ -114,7 +114,7 @@
"volumes": [ "volumes": [
"/services/gitea/gitea:/data" "/services/gitea/gitea:/data"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "gitea honours GITEA__database__PASSWD__FILE and GITEA__security__INTERNAL_TOKEN__FILE; convertible, awaiting a bed that proves it"
}, },
{ {
"id": "admin-bootstrap", "id": "admin-bootstrap",
@@ -139,7 +139,7 @@
"-c", "-c",
"su-exec git gitea admin user create --admin --username \"$MESH_GITEA_ADMIN_USER\" --email mesh-admin@localhost --password \"$(cat /run/secrets/admin)\" --must-change-password=false || true" "su-exec git gitea admin user create --admin --username \"$MESH_GITEA_ADMIN_USER\" --email mesh-admin@localhost --password \"$(cat /run/secrets/admin)\" --must-change-password=false || true"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "gitea honours GITEA__database__PASSWD__FILE and GITEA__security__INTERNAL_TOKEN__FILE; convertible, awaiting a bed that proves it"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+7 -13
View File
@@ -32,13 +32,6 @@
"path": "/var/lib/grafana-module", "path": "/var/lib/grafana-module",
"mode": "0700" "mode": "0700"
}, },
{
"id": "server-env",
"type": "file",
"path": "/var/lib/grafana-module/server.env",
"mode": "0600",
"content": "GF_SECURITY_ADMIN_PASSWORD=${secret:admin}\n"
},
{ {
"id": "data", "id": "data",
"type": "directory", "type": "directory",
@@ -51,16 +44,16 @@
"type": "container", "type": "container",
"name": "grafana", "name": "grafana",
"image": "grafana/grafana@sha256:f772d434e8fab0049deb2b1b30abd43342bcfca1537614aa8d36080232cf4283", "image": "grafana/grafana@sha256:f772d434e8fab0049deb2b1b30abd43342bcfca1537614aa8d36080232cf4283",
"env-file": [
"/var/lib/grafana-module/server.env"
],
"ports": [ "ports": [
"3000" "3000"
], ],
"volumes": [ "volumes": [
"/services/grafana/data:/var/lib/grafana" "/services/grafana/data:/var/lib/grafana",
"/var/lib/grafana-module/admin.secret:/run/secrets/admin:ro"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "env": {
"GF_SECURITY_ADMIN_PASSWORD__FILE": "/run/secrets/admin"
}
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
@@ -122,5 +115,6 @@
"from": "Dockerfile" "from": "Dockerfile"
} }
] ]
} },
"secrets-owner": "472:472"
} }
+1 -1
View File
@@ -53,7 +53,7 @@
"ports": [ "ports": [
"8000" "8000"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "the image seds ICECAST_*_PASSWORD into icecast.xml and has no _FILE; convertible by mounting a generated icecast.xml, not yet done"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+1 -1
View File
@@ -66,7 +66,7 @@
"/services/influxdb/data:/var/lib/influxdb2", "/services/influxdb/data:/var/lib/influxdb2",
"/services/influxdb/config:/etc/influxdb2" "/services/influxdb/config:/etc/influxdb2"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "the image honours DOCKER_INFLUXDB_INIT_PASSWORD_FILE and _ADMIN_TOKEN_FILE; convertible, awaiting a bed that proves it"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+1 -1
View File
@@ -100,7 +100,7 @@
"ports": [ "ports": [
"9000" "9000"
], ],
"secrets-in-environment": "the API reads its settings from the environment; converting is the per-module work of issue 041" "secrets-in-environment": "the application's own code reads MONGO_URL and MINIO_SECRET from the environment (invoicing-app server/src/config.js); converting is that repository's change"
} }
] ]
} }
+1 -1
View File
@@ -97,7 +97,7 @@
"ports": [ "ports": [
"8080" "8080"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "KC_DB_PASSWORD is convertible through a generated keycloak.conf (db-password=); KEYCLOAK_ADMIN_PASSWORD is env-only before Keycloak 26; not yet converted"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+2 -2
View File
@@ -67,7 +67,7 @@
"ports": [ "ports": [
"8283" "8283"
], ],
"secrets-in-environment": "the runtime reads its settings from the environment; converting is the per-module work of issue 041" "secrets-in-environment": "the letta image is env-driven and its file-source support could not be verified; the mesh runtime can take its password from config.json (client.ts) \u2014 not yet converted"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
@@ -105,7 +105,7 @@
"runtime-config" "runtime-config"
], ],
"artifact": "runtime", "artifact": "runtime",
"secrets-in-environment": "the runtime reads its settings from the environment; converting is the per-module work of issue 041" "secrets-in-environment": "the letta image is env-driven and its file-source support could not be verified; the mesh runtime can take its password from config.json (client.ts) \u2014 not yet converted"
} }
], ],
"build": { "build": {
+14 -22
View File
@@ -218,7 +218,7 @@
"/var/lib/mailu/mailu.env", "/var/lib/mailu/mailu.env",
"/var/lib/mailu/secret.env" "/var/lib/mailu/secret.env"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
}, },
{ {
"id": "redis", "id": "redis",
@@ -246,7 +246,7 @@
"/services/mailu/data/data:/data", "/services/mailu/data/data:/data",
"/services/mailu/data/dkim:/dkim" "/services/mailu/data/dkim:/dkim"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
}, },
{ {
"id": "imap", "id": "imap",
@@ -255,14 +255,12 @@
"image": "ghcr.io/mailu/dovecot@sha256:46d18ba51032be8ebd6841aa49c1ef8762c729038c5fd86a081b5b884d478af9", "image": "ghcr.io/mailu/dovecot@sha256:46d18ba51032be8ebd6841aa49c1ef8762c729038c5fd86a081b5b884d478af9",
"network": "mailu", "network": "mailu",
"env-file": [ "env-file": [
"/var/lib/mailu/mailu.env", "/var/lib/mailu/mailu.env"
"/var/lib/mailu/secret.env"
], ],
"volumes": [ "volumes": [
"/services/mailu/data/mail:/mail", "/services/mailu/data/mail:/mail",
"/services/mailu/data/overrides/dovecot:/overrides:ro" "/services/mailu/data/overrides/dovecot:/overrides:ro"
], ]
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041"
}, },
{ {
"id": "smtp", "id": "smtp",
@@ -271,14 +269,12 @@
"image": "ghcr.io/mailu/postfix@sha256:bbf882880f68849511710b35237a933f3fe80c4b28bf48ff20205dbd1f1433d7", "image": "ghcr.io/mailu/postfix@sha256:bbf882880f68849511710b35237a933f3fe80c4b28bf48ff20205dbd1f1433d7",
"network": "mailu", "network": "mailu",
"env-file": [ "env-file": [
"/var/lib/mailu/mailu.env", "/var/lib/mailu/mailu.env"
"/var/lib/mailu/secret.env"
], ],
"volumes": [ "volumes": [
"/services/mailu/data/mailqueue:/queue", "/services/mailu/data/mailqueue:/queue",
"/services/mailu/data/overrides/postfix:/overrides:ro" "/services/mailu/data/overrides/postfix:/overrides:ro"
], ]
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041"
}, },
{ {
"id": "antispam", "id": "antispam",
@@ -287,14 +283,12 @@
"image": "ghcr.io/mailu/rspamd@sha256:e87ab93dd252cc69499caa5317dd10d445fd4291a7ecf6bca09793c7d475a0c8", "image": "ghcr.io/mailu/rspamd@sha256:e87ab93dd252cc69499caa5317dd10d445fd4291a7ecf6bca09793c7d475a0c8",
"network": "mailu", "network": "mailu",
"env-file": [ "env-file": [
"/var/lib/mailu/mailu.env", "/var/lib/mailu/mailu.env"
"/var/lib/mailu/secret.env"
], ],
"volumes": [ "volumes": [
"/services/mailu/data/filter:/var/lib/rspamd", "/services/mailu/data/filter:/var/lib/rspamd",
"/services/mailu/data/overrides/rspamd:/etc/rspamd/override.d:ro" "/services/mailu/data/overrides/rspamd:/etc/rspamd/override.d:ro"
], ]
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041"
}, },
{ {
"id": "antivirus", "id": "antivirus",
@@ -309,7 +303,7 @@
"volumes": [ "volumes": [
"/services/mailu/data/filter:/data" "/services/mailu/data/filter:/data"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
}, },
{ {
"id": "webmail", "id": "webmail",
@@ -325,7 +319,7 @@
"/services/mailu/data/webmail:/data", "/services/mailu/data/webmail:/data",
"/services/mailu/data/overrides/roundcube:/overrides:ro" "/services/mailu/data/overrides/roundcube:/overrides:ro"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
}, },
{ {
"id": "webdav", "id": "webdav",
@@ -340,7 +334,7 @@
"volumes": [ "volumes": [
"/services/mailu/data/dav:/data" "/services/mailu/data/dav:/data"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
}, },
{ {
"id": "fetchmail", "id": "fetchmail",
@@ -355,7 +349,7 @@
"volumes": [ "volumes": [
"/services/mailu/data/data/fetchmail:/data" "/services/mailu/data/data/fetchmail:/data"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
}, },
{ {
"id": "front", "id": "front",
@@ -364,8 +358,7 @@
"image": "ghcr.io/mailu/nginx@sha256:09f28ab6d36367fcacc7994f7021f132ac845bdc05f04bf80906102d11aaa057", "image": "ghcr.io/mailu/nginx@sha256:09f28ab6d36367fcacc7994f7021f132ac845bdc05f04bf80906102d11aaa057",
"network": "mailu", "network": "mailu",
"env-file": [ "env-file": [
"/var/lib/mailu/mailu.env", "/var/lib/mailu/mailu.env"
"/var/lib/mailu/secret.env"
], ],
"ports": [ "ports": [
"25", "25",
@@ -377,8 +370,7 @@
"volumes": [ "volumes": [
"/services/mailu/data/certs:/certs", "/services/mailu/data/certs:/certs",
"/services/mailu/data/overrides/nginx:/overrides:ro" "/services/mailu/data/overrides/nginx:/overrides:ro"
], ]
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+11 -10
View File
@@ -50,11 +50,11 @@
"mode": "0700" "mode": "0700"
}, },
{ {
"id": "db-env", "id": "database-url",
"type": "file", "type": "file",
"path": "/var/lib/mesh-catalog/db.env", "path": "/var/lib/mesh-catalog/database.url",
"mode": "0600", "mode": "0600",
"content": "DATABASE_URL=postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n" "content": "postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n"
}, },
{ {
"id": "runtime", "id": "runtime",
@@ -63,19 +63,20 @@
"network": "host", "network": "host",
"volumes": [ "volumes": [
"/var/lib/mesh/mesh-catalog/broker:/run/secrets/broker:ro", "/var/lib/mesh/mesh-catalog/broker:/run/secrets/broker:ro",
"/var/lib/mesh-catalog:/run/state" "/var/lib/mesh-catalog:/run/state",
"/var/lib/mesh-catalog/database.url:/run/secrets/database-url:ro"
], ],
"env": { "env": {
"MESH_BROKER_FILE": "/run/secrets/broker" "MESH_BROKER_FILE": "/run/secrets/broker",
"DATABASE_URL_FILE": "/run/secrets/database-url"
}, },
"env-file": [
"/var/lib/mesh-catalog/db.env"
],
"artifact": "runtime", "artifact": "runtime",
"restart-on": [ "restart-on": [
"db-env" "database-url"
], ],
"secrets-in-environment": "the mesh's own runtime reads MESH_STORE_* from the environment; a file twin in the SDK is the per-module work of issue 041" "env-file": [
"/var/lib/mesh-catalog/db.env"
]
} }
], ],
"build": { "build": {
+14 -1
View File
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
// The module graph (novox/hq ADR 0070, ADR 0072). // The module graph (novox/hq ADR 0070, ADR 0072).
// //
// **This graph links module-versions to each other and knows nothing about nodes.** Which machine // **This graph links module-versions to each other and knows nothing about nodes.** Which machine
@@ -138,7 +139,9 @@ export class Graph {
private constructor(private readonly pool: PgPool) {} private constructor(private readonly pool: PgPool) {}
static fromEnv(env: NodeJS.ProcessEnv = process.env): Graph { static fromEnv(env: NodeJS.ProcessEnv = process.env): Graph {
const url = env["DATABASE_URL"]; // As a file first (novox/hq ADR 0086): the connection string carries the password, and the
// mesh writes it where only this process reads it; the plain variable remains for a hand-run.
const url = env["DATABASE_URL"] ?? readMaybe(env["DATABASE_URL_FILE"]);
if (!url) { if (!url) {
throw new Error( throw new Error(
"no DATABASE_URL: the catalogue holds the module graph and cannot hold it in memory, " + "no DATABASE_URL: the catalogue holds the module graph and cannot hold it in memory, " +
@@ -390,3 +393,13 @@ export class Graph {
await this.pool.end(); await this.pool.end();
} }
} }
/** The content of a file the environment names, its line ending gone — or undefined when it names none. */
function readMaybe(path: string | undefined): string | undefined {
if (!path) return undefined;
try {
return readFileSync(path, "utf8").replace(/\r?\n$/, "");
} catch {
return undefined;
}
}
+6 -3
View File
@@ -63,7 +63,7 @@
"type": "file", "type": "file",
"path": "/var/lib/minio/root.env", "path": "/var/lib/minio/root.env",
"mode": "0600", "mode": "0600",
"content": "MINIO_ROOT_USER=meshroot\nMINIO_ROOT_PASSWORD=${secret:root}\n" "content": "MINIO_ROOT_USER=meshroot\n"
}, },
{ {
"id": "data", "id": "data",
@@ -95,9 +95,12 @@
"9000" "9000"
], ],
"volumes": [ "volumes": [
"/services/minio/data/data1-1:/data" "/services/minio/data/data1-1:/data",
"/var/lib/minio/root.secret:/run/secrets/root:ro"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "env": {
"MINIO_ROOT_PASSWORD_FILE": "/run/secrets/root"
}
}, },
{ {
"id": "runtime", "id": "runtime",
+8 -7
View File
@@ -39,11 +39,11 @@
"mode": "0700" "mode": "0700"
}, },
{ {
"id": "db-env", "id": "database-url",
"type": "file", "type": "file",
"path": "/var/lib/model-usage/db.env", "path": "/var/lib/model-usage/database.url",
"mode": "0600", "mode": "0600",
"content": "DATABASE_URL=postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n" "content": "postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n"
}, },
{ {
"id": "runtime", "id": "runtime",
@@ -53,15 +53,16 @@
"network": "host", "network": "host",
"volumes": [ "volumes": [
"/var/lib/mesh/model-usage/broker:/run/secrets/broker:ro", "/var/lib/mesh/model-usage/broker:/run/secrets/broker:ro",
"/var/lib/model-usage:/run/state" "/var/lib/model-usage:/run/state",
"/var/lib/model-usage/database.url:/run/secrets/database-url:ro"
], ],
"env": { "env": {
"MESH_BROKER_FILE": "/run/secrets/broker" "MESH_BROKER_FILE": "/run/secrets/broker",
"DATABASE_URL_FILE": "/run/secrets/database-url"
}, },
"env-file": [ "env-file": [
"/var/lib/model-usage/db.env" "/var/lib/model-usage/db.env"
], ]
"secrets-in-environment": "the mesh's own runtime reads MESH_STORE_* from the environment; a file twin in the SDK is the per-module work of issue 041"
} }
] ]
} }
+15 -1
View File
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
// The vendor-neutral usage store (novox/hq ADR 0054). ONE table holds BOTH grains — licence and // The vendor-neutral usage store (novox/hq ADR 0054). ONE table holds BOTH grains — licence and
// session — which differ only in `consumer`; a reading is one row `(licence, consumer, period, // session — which differ only in `consumer`; a reading is one row `(licence, consumer, period,
// metric, value)` plus its `raw` vendor payload. The store keeps the LATEST reading per // metric, value)` plus its `raw` vendor payload. The store keeps the LATEST reading per
@@ -47,7 +48,10 @@ export class UsageStore {
/** Build a store from the resolved environment — DATABASE_URL is the granted postgres connection, /** Build a store from the resolved environment — DATABASE_URL is the granted postgres connection,
* templated into the module's env-file from the mesh's binding (umami's DATABASE_URL precedent). */ * templated into the module's env-file from the mesh's binding (umami's DATABASE_URL precedent). */
static fromEnv(env: NodeJS.ProcessEnv = process.env): UsageStore { static fromEnv(env: NodeJS.ProcessEnv = process.env): UsageStore {
return new UsageStore(new Pool({ connectionString: requireEnv("DATABASE_URL", env) })); // As a file first (novox/hq ADR 0086): the connection string carries the password.
const url = env["DATABASE_URL"] ?? readMaybe(env["DATABASE_URL_FILE"]);
if (!url) throw new Error("DATABASE_URL_FILE (or DATABASE_URL) is not set — model-usage cannot reach its database");
return new UsageStore(new Pool({ connectionString: url }));
} }
/** Create the one table if it is not there. Run once by the migrate entry before the consumer /** Create the one table if it is not there. Run once by the migrate entry before the consumer
@@ -84,3 +88,13 @@ export class UsageStore {
await this.pool.end(); await this.pool.end();
} }
} }
/** The content of a file the environment names, its line ending gone — or undefined when it names none. */
function readMaybe(path: string | undefined): string | undefined {
if (!path) return undefined;
try {
return readFileSync(path, "utf8").replace(/\r?\n$/, "");
} catch {
return undefined;
}
}
+5 -14
View File
@@ -60,13 +60,6 @@
"path": "/var/lib/mongodb/grants", "path": "/var/lib/mongodb/grants",
"mode": "0700" "mode": "0700"
}, },
{
"id": "root-env",
"type": "file",
"path": "/var/lib/mongodb/root.env",
"mode": "0600",
"content": "MONGO_INITDB_ROOT_PASSWORD=${secret:root}\n"
},
{ {
"id": "data", "id": "data",
"type": "directory", "type": "directory",
@@ -85,18 +78,16 @@
"image": "mongo@sha256:e3fa459b4f4b72f3257c67a23c145e250b8b5700f033860392c68539b998bbe3", "image": "mongo@sha256:e3fa459b4f4b72f3257c67a23c145e250b8b5700f033860392c68539b998bbe3",
"network": "mongodb", "network": "mongodb",
"env": { "env": {
"MONGO_INITDB_ROOT_USERNAME": "root" "MONGO_INITDB_ROOT_USERNAME": "root",
"MONGO_INITDB_ROOT_PASSWORD_FILE": "/run/secrets/root"
}, },
"env-file": [
"/var/lib/mongodb/root.env"
],
"ports": [ "ports": [
"27017" "27017"
], ],
"volumes": [ "volumes": [
"/services/mongodb/db-data:/data/db" "/services/mongodb/db-data:/data/db",
], "/var/lib/mongodb/root.secret:/run/secrets/root:ro"
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" ]
}, },
{ {
"id": "runtime", "id": "runtime",
+1 -1
View File
@@ -92,7 +92,7 @@
"volumes": [ "volumes": [
"/services/mssql/db-data:/var/opt/mssql" "/services/mssql/db-data:/var/opt/mssql"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "the image documents only MSSQL_SA_PASSWORD, no _FILE and no configuration field; not convertible without a wrapper entrypoint"
}, },
{ {
"id": "runtime", "id": "runtime",
+1 -1
View File
@@ -79,7 +79,7 @@
"volumes": [ "volumes": [
"/services/n8n/n8n-data:/home/node/.n8n" "/services/n8n/n8n-data:/home/node/.n8n"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "n8n's loader honours <VAR>_FILE for every setting; convertible, awaiting a bed that proves it (N8N_BASIC_AUTH_* was removed in n8n 1.0 and is likely dead)"
} }
] ]
} }
+1 -1
View File
@@ -88,7 +88,7 @@
"volumes": [ "volumes": [
"/services/nextcloud/html:/var/www/html" "/services/nextcloud/html:/var/www/html"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "the image honours POSTGRES_PASSWORD_FILE and NEXTCLOUD_ADMIN_PASSWORD_FILE (entrypoint file_env); OBJECTSTORE_S3_SECRET has none and needs a generated config fragment; convertible, awaiting a bed"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+1 -1
View File
@@ -110,7 +110,7 @@
"/services/only-office/redis:/var/lib/redis", "/services/only-office/redis:/var/lib/redis",
"/services/only-office/fonts:/usr/share/fonts/truetype/custom" "/services/only-office/fonts:/usr/share/fonts/truetype/custom"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "run-document-server.sh regenerates JWT_SECRET from the environment on every start and overwrites local.json; not convertible"
} }
] ]
} }
+1 -1
View File
@@ -75,7 +75,7 @@
"ports": [ "ports": [
"9000" "9000"
], ],
"secrets-in-environment": "the server reads its settings from the environment; converting is the per-module work of issue 041" "secrets-in-environment": "the application's own code reads MONGO_URL and MINIO_SECRET from the environment (photos server/src/config.js); converting is that repository's change"
}, },
{ {
"id": "admin-client", "id": "admin-client",
+1 -1
View File
@@ -71,7 +71,7 @@
"ports": [ "ports": [
"8080" "8080"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "SEARXNG_SECRET is env-only, but settings.yml carries server.secret_key; convertible by mounting a generated settings.yml, not yet done"
}, },
{ {
"id": "runtime-config", "id": "runtime-config",
+1 -1
View File
@@ -104,7 +104,7 @@
"/var/lib/step-ca:/home/step", "/var/lib/step-ca:/home/step",
"/var/lib/mesh/step-ca:/run/mesh:ro" "/var/lib/mesh/step-ca:/run/mesh:ro"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "the entrypoint honours DOCKER_STEPCA_INIT_PASSWORD_FILE; convertible, awaiting a bed that proves it"
} }
] ]
} }
+1 -1
View File
@@ -102,7 +102,7 @@
"ports": [ "ports": [
"3000" "3000"
], ],
"secrets-in-environment": "the image reads its configuration from the environment only; converting to a file is the per-module work of issue 041" "secrets-in-environment": "a Next.js/Prisma application: DATABASE_URL and APP_SECRET are read from the environment only; not convertible"
}, },
{ {
"id": "runtime", "id": "runtime",