The six media providers share their one credential with every consumer (ADR 0158)

nzbget, qbittorrent, jackett, sonarr, radarr and lidarr each hold one
credential; the offer now names the provider's own secret as the
provision's credential, that secret is taken at start, and a
custom-cont-init script applies the file to the software on every start
(nzbget: NZBGET_USER beside the password it already took as a start
option; qbittorrent: the WebUI login's PBKDF2 hash; jackett: APIKey in
ServerConfig.json; the arrs: <ApiKey> in config.xml). Nothing is accepted
per consumer any more; rotating the provider's secret reaches everyone.
This commit is contained in:
2026-10-01 13:05:03 +02:00
parent 1f0e9b2c89
commit 11da93e966
6 changed files with 151 additions and 24 deletions
+27 -4
View File
@@ -4,7 +4,10 @@
"provides": [
{
"name": "jackett-api",
"scope": "mesh"
"scope": "mesh",
"credential": {
"own": "api-key"
}
}
],
"serves": {
@@ -52,6 +55,18 @@
"mode": "0644",
"content": "# The identity this module's process runs as on THIS machine: the owner of its\n# data here, which the definition may not know (novox/hq ADR 0112, issue 153). Set puid and\n# pgid on the assignment \u2014 the mesh-wide layer carries the image's default, a node's its own.\nPUID=${setting:puid}\nPGID=${setting:pgid}\n"
},
{
"id": "mesh-init",
"type": "directory",
"mode": "0755"
},
{
"id": "api-key-init",
"type": "file",
"path": "${dir:mesh-init}/10-mesh-api-key.sh",
"mode": "0755",
"content": "#!/bin/bash\n# Written by the mesh (ADR 0158): this module's one credential is the vault's, shared with every\n# consumer; the file is applied here, before the service starts, on every start.\nset -euo pipefail\nCONF=/config/Jackett/ServerConfig.json\nKEY=$(cat /run/secrets/api-key)\nmkdir -p /config/Jackett\nif [ -s \"$CONF\" ]; then\n jq --arg k \"$KEY\" '.APIKey = $k' \"$CONF\" > \"$CONF.mesh\" && mv \"$CONF.mesh\" \"$CONF\"\nelse\n jq -n --arg k \"$KEY\" '{APIKey: $k}' > \"$CONF\"\nfi\necho \"[mesh] Jackett API key set from the vault's credential\"\n"
},
{
"id": "server",
"type": "container",
@@ -64,13 +79,17 @@
"9117"
],
"volumes": [
"${dir:config}:/config"
"${dir:config}:/config",
"${dir:mesh-init}:/custom-cont-init.d:ro",
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
],
"env-file": [
"${dir:state}/identity.env"
],
"restart-on": [
"identity"
"identity",
"needs-api-key",
"api-key-init"
]
},
{
@@ -104,7 +123,11 @@
}
],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
"broker": "${dir:mesh-state}/broker",
"api-key": {
"path": "${dir:mesh-state}/api-key",
"taken": "at-start"
}
},
"requires": [
"route"
+21 -4
View File
@@ -4,7 +4,10 @@
"provides": [
{
"name": "lidarr-api",
"scope": "mesh"
"scope": "mesh",
"credential": {
"own": "api-key"
}
}
],
"serves": {
@@ -23,7 +26,11 @@
],
"consumes": [],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
"broker": "${dir:mesh-state}/broker",
"api-key": {
"path": "${dir:mesh-state}/api-key",
"taken": "at-start"
}
},
"listens": [
{
@@ -82,6 +89,13 @@
"mode": "0644",
"content": "# The identity this module's process runs as on THIS machine: the owner of its\n# data here, which the definition may not know (novox/hq ADR 0112, issue 153). Set puid and\n# pgid on the assignment \u2014 the mesh-wide layer carries the image's default, a node's its own.\nPUID=${setting:puid}\nPGID=${setting:pgid}\n"
},
{
"id": "api-key-init",
"type": "file",
"path": "${dir:custom-cont-init}/00-mesh-api-key.sh",
"mode": "0755",
"content": "#!/bin/bash\n# Written by the mesh (ADR 0158): this module's one credential is the vault's, shared with every\n# consumer; the file is applied here, before the service starts, on every start.\nset -euo pipefail\nCONF=/config/config.xml\nKEY=$(cat /run/secrets/api-key)\nif [ -s \"$CONF\" ] && grep -q '<ApiKey>' \"$CONF\"; then\n sed -i -E \"s#<ApiKey>[^<]*</ApiKey>#<ApiKey>$KEY</ApiKey>#\" \"$CONF\"\nelif [ -s \"$CONF\" ]; then\n sed -i -E \"s#</Config># <ApiKey>$KEY</ApiKey>\\n</Config>#\" \"$CONF\"\nelse\n printf '<Config>\\n <ApiKey>%s</ApiKey>\\n</Config>\\n' \"$KEY\" > \"$CONF\"\nfi\necho \"[mesh] API key set from the vault's credential\"\n"
},
{
"id": "server",
"type": "container",
@@ -98,13 +112,16 @@
"${dir:custom-cont-init}:/custom-cont-init.d",
"${dir:custom-services}:/custom-services.d",
"${access:music}:/music",
"${access:downloads}:/downloads"
"${access:downloads}:/downloads",
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
],
"env-file": [
"${dir:state}/identity.env"
],
"restart-on": [
"identity"
"identity",
"needs-api-key",
"api-key-init"
]
},
{
+21 -4
View File
@@ -11,7 +11,10 @@
"consumes": [],
"own-secrets": {
"broker": "${dir:mesh-state}/broker",
"password": "${dir:mesh-state}/password"
"password": {
"path": "${dir:mesh-state}/password",
"taken": "at-start"
}
},
"listens": [
{
@@ -55,6 +58,13 @@
"mode": "0644",
"content": "# The identity this module's process runs as on THIS machine: the owner of its\n# data here, which the definition may not know (novox/hq ADR 0112, issue 153). Set puid and\n# pgid on the assignment \u2014 the mesh-wide layer carries the image's default, a node's its own.\nPUID=${setting:puid}\nPGID=${setting:pgid}\n"
},
{
"id": "server-env",
"type": "file",
"path": "${dir:state}/server.env",
"mode": "0644",
"content": "NZBGET_USER=${setting:username}\n"
},
{
"id": "server",
"type": "container",
@@ -74,10 +84,12 @@
],
"restart-on": [
"needs-password",
"identity"
"identity",
"server-env"
],
"env-file": [
"${dir:state}/identity.env"
"${dir:state}/identity.env",
"${dir:state}/server.env"
]
},
{
@@ -114,7 +126,12 @@
}
],
"provides": [
"nzbget-api"
{
"name": "nzbget-api",
"credential": {
"own": "password"
}
}
],
"serves": {
"nzbget-api": {
+28 -4
View File
@@ -12,7 +12,10 @@
"consumes": [],
"own-secrets": {
"broker": "${dir:mesh-state}/broker",
"password": "${dir:mesh-state}/password"
"password": {
"path": "${dir:mesh-state}/password",
"taken": "at-start"
}
},
"listens": [
{
@@ -70,6 +73,18 @@
"mode": "0644",
"content": "# The identity this module's process runs as on THIS machine: the owner of its\n# data here, which the definition may not know (novox/hq ADR 0112, issue 153). Set puid and\n# pgid on the assignment \u2014 the mesh-wide layer carries the image's default, a node's its own.\nPUID=${setting:puid}\nPGID=${setting:pgid}\n"
},
{
"id": "mesh-init",
"type": "directory",
"mode": "0755"
},
{
"id": "webui-login-init",
"type": "file",
"path": "${dir:mesh-init}/10-mesh-webui-login.sh",
"mode": "0755",
"content": "#!/bin/bash\n# Written by the mesh (ADR 0158): this module's one credential is the vault's, shared with every\n# consumer; the file is applied here, before the service starts, on every start.\nset -euo pipefail\nCONF=/config/qBittorrent/qBittorrent.conf\nUSER='${setting:username}'\nPASS=$(cat /run/secrets/password)\nmkdir -p /config/qBittorrent\nHASH=$(python3 - \"$PASS\" <<'PY'\nimport sys, os, base64, hashlib\npw = sys.argv[1].encode(); salt = os.urandom(16)\ndk = hashlib.pbkdf2_hmac(\"sha512\", pw, salt, 100000, dklen=64)\nprint(\"@ByteArray(\" + base64.b64encode(salt).decode() + \":\" + base64.b64encode(dk).decode() + \")\")\nPY\n)\ntouch \"$CONF\"\ngrep -q '^\\[Preferences\\]' \"$CONF\" || printf '\\n[Preferences]\\n' >> \"$CONF\"\nfor kv in \"WebUI\\\\Username=$USER\" \"WebUI\\\\Password_PBKDF2=\\\"$HASH\\\"\"; do\n key=${kv%%=*}\n if grep -q \"^${key//\\\\/\\\\\\\\}=\" \"$CONF\"; then\n awk -v kv=\"$kv\" -v key=\"$key\" 'BEGIN{FS=OFS=\"=\"} $1==key {print kv; next} {print}' \"$CONF\" > \"$CONF.mesh\" && mv \"$CONF.mesh\" \"$CONF\"\n else\n awk -v kv=\"$kv\" '{print} /^\\[Preferences\\]/ && !done {print kv; done=1}' \"$CONF\" > \"$CONF.mesh\" && mv \"$CONF.mesh\" \"$CONF\"\n fi\ndone\necho \"[mesh] qBittorrent WebUI login set for $USER from the vault's credential\"\n"
},
{
"id": "server",
"type": "container",
@@ -82,14 +97,18 @@
},
"volumes": [
"${dir:config}:/config",
"${access:downloads}:/downloads"
"${access:downloads}:/downloads",
"${dir:mesh-init}:/custom-cont-init.d:ro",
"${dir:mesh-state}/password:/run/secrets/password:ro"
],
"network": "host",
"env-file": [
"${dir:state}/identity.env"
],
"restart-on": [
"identity"
"identity",
"needs-password",
"webui-login-init"
]
},
{
@@ -126,7 +145,12 @@
}
],
"provides": [
"qbittorrent-api"
{
"name": "qbittorrent-api",
"credential": {
"own": "password"
}
}
],
"serves": {
"qbittorrent-api": {
+27 -4
View File
@@ -4,7 +4,10 @@
"provides": [
{
"name": "radarr-api",
"scope": "mesh"
"scope": "mesh",
"credential": {
"own": "api-key"
}
}
],
"serves": {
@@ -23,7 +26,11 @@
],
"consumes": [],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
"broker": "${dir:mesh-state}/broker",
"api-key": {
"path": "${dir:mesh-state}/api-key",
"taken": "at-start"
}
},
"listens": [
{
@@ -72,6 +79,18 @@
"mode": "0644",
"content": "# The identity this module's process runs as on THIS machine: the owner of its\n# data here, which the definition may not know (novox/hq ADR 0112, issue 153). Set puid and\n# pgid on the assignment \u2014 the mesh-wide layer carries the image's default, a node's its own.\nPUID=${setting:puid}\nPGID=${setting:pgid}\n"
},
{
"id": "mesh-init",
"type": "directory",
"mode": "0755"
},
{
"id": "api-key-init",
"type": "file",
"path": "${dir:mesh-init}/10-mesh-api-key.sh",
"mode": "0755",
"content": "#!/bin/bash\n# Written by the mesh (ADR 0158): this module's one credential is the vault's, shared with every\n# consumer; the file is applied here, before the service starts, on every start.\nset -euo pipefail\nCONF=/config/config.xml\nKEY=$(cat /run/secrets/api-key)\nif [ -s \"$CONF\" ] && grep -q '<ApiKey>' \"$CONF\"; then\n sed -i -E \"s#<ApiKey>[^<]*</ApiKey>#<ApiKey>$KEY</ApiKey>#\" \"$CONF\"\nelif [ -s \"$CONF\" ]; then\n sed -i -E \"s#</Config># <ApiKey>$KEY</ApiKey>\\n</Config>#\" \"$CONF\"\nelse\n printf '<Config>\\n <ApiKey>%s</ApiKey>\\n</Config>\\n' \"$KEY\" > \"$CONF\"\nfi\necho \"[mesh] API key set from the vault's credential\"\n"
},
{
"id": "server",
"type": "container",
@@ -86,13 +105,17 @@
"volumes": [
"${dir:config}:/config",
"${access:movies}:/movies",
"${access:downloads}:/downloads"
"${access:downloads}:/downloads",
"${dir:mesh-init}:/custom-cont-init.d:ro",
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
],
"env-file": [
"${dir:state}/identity.env"
],
"restart-on": [
"identity"
"identity",
"needs-api-key",
"api-key-init"
]
},
{
+27 -4
View File
@@ -4,7 +4,10 @@
"provides": [
{
"name": "sonarr-api",
"scope": "mesh"
"scope": "mesh",
"credential": {
"own": "api-key"
}
}
],
"serves": {
@@ -23,7 +26,11 @@
],
"consumes": [],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
"broker": "${dir:mesh-state}/broker",
"api-key": {
"path": "${dir:mesh-state}/api-key",
"taken": "at-start"
}
},
"listens": [
{
@@ -77,6 +84,18 @@
"mode": "0644",
"content": "# The identity this module's process runs as on THIS machine: the owner of its\n# data here, which the definition may not know (novox/hq ADR 0112, issue 153). Set puid and\n# pgid on the assignment \u2014 the mesh-wide layer carries the image's default, a node's its own.\nPUID=${setting:puid}\nPGID=${setting:pgid}\n"
},
{
"id": "mesh-init",
"type": "directory",
"mode": "0755"
},
{
"id": "api-key-init",
"type": "file",
"path": "${dir:mesh-init}/10-mesh-api-key.sh",
"mode": "0755",
"content": "#!/bin/bash\n# Written by the mesh (ADR 0158): this module's one credential is the vault's, shared with every\n# consumer; the file is applied here, before the service starts, on every start.\nset -euo pipefail\nCONF=/config/config.xml\nKEY=$(cat /run/secrets/api-key)\nif [ -s \"$CONF\" ] && grep -q '<ApiKey>' \"$CONF\"; then\n sed -i -E \"s#<ApiKey>[^<]*</ApiKey>#<ApiKey>$KEY</ApiKey>#\" \"$CONF\"\nelif [ -s \"$CONF\" ]; then\n sed -i -E \"s#</Config># <ApiKey>$KEY</ApiKey>\\n</Config>#\" \"$CONF\"\nelse\n printf '<Config>\\n <ApiKey>%s</ApiKey>\\n</Config>\\n' \"$KEY\" > \"$CONF\"\nfi\necho \"[mesh] API key set from the vault's credential\"\n"
},
{
"id": "server",
"type": "container",
@@ -92,13 +111,17 @@
"${dir:config}:/config",
"${access:series}:/series",
"${access:anime}:/anime",
"${access:downloads}:/downloads"
"${access:downloads}:/downloads",
"${dir:mesh-init}:/custom-cont-init.d:ro",
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
],
"env-file": [
"${dir:state}/identity.env"
],
"restart-on": [
"identity"
"identity",
"needs-api-key",
"api-key-init"
]
},
{