-
-
Notifications
You must be signed in to change notification settings - Fork 38
[lib-audit] S2-21b {secret_key} placeholder is never substituted in manifest install.env — Linkwarden NEXTAUTH_SECRET stays static #2816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| ### Security | ||
| - DockerInstaller now substitutes the per-app `{secret_key}` placeholder in | ||
| `install.env` values (not just `config_files` content), so Linkwarden's | ||
| `NEXTAUTH_SECRET` is a stable 64-hex-char secret persisted in | ||
| `<app_dir>/.secret_key` instead of the shipped default. Previously every host | ||
| ran Linkwarden with the publicly-known session-signing secret `changeme`, | ||
| allowing session forgery. | ||
| - Linkwarden manifest drops the unused `DATABASE_URL` (no Postgres companion is | ||
| started) and sets `NEXTAUTH_SECRET: "{secret_key}"`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,30 @@ def __init__(self, apps_dir: Path | None = None): | |
| def _compose_path(self, app_id: str) -> Path: | ||
| return self.apps_dir / app_id / "docker-compose.yaml" | ||
|
|
||
| def _get_or_create_secret_key(self, app_id: str) -> str: | ||
| """Load or create the per-app secret key, persisted in ``<app_dir>/.secret_key``. | ||
|
|
||
| Reused by both ``_write_config_files`` and ``_generate_compose`` so that | ||
| apps shipping ``{secret_key}`` in either ``config_files`` content or | ||
| ``install.env`` values get the same stable per-app secret. The key is | ||
| regenerated when a prior write left it missing, empty, or malformed. | ||
| """ | ||
| app_dir = self.apps_dir / app_id | ||
| secret_key_path = app_dir / ".secret_key" | ||
| secret_key = "" | ||
| if secret_key_path.exists(): | ||
| secret_key = secret_key_path.read_text().strip() | ||
| if len(secret_key) != 64 or not all(c in "0123456789abcdef" for c in secret_key): | ||
| secret_key = secrets.token_hex(32) | ||
| app_dir.mkdir(parents=True, exist_ok=True) | ||
| secret_key_path.write_text(secret_key) | ||
| secret_key_path.chmod(0o600) | ||
|
Comment on lines
+38
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
sed -n '1,230p' tinyagentos/installers/docker_installer.py
printf '\n--- targeted callers and file writes ---\n'
rg -n -C 3 '_get_or_create_secret_key|docker-compose\.yaml|write_text|yaml\.safe_dump|dump\(' tinyagentos testsRepository: jaylfc/taOS Length of output: 50368 🤖 get_repo_knowledge executed:
Length of output: 5023 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- docker_installer.py 1-230 ---'
sed -n '1,230p' tinyagentos/installers/docker_installer.py
printf '%s\n' '--- relevant tests 60-260 ---'
sed -n '60,260p' tests/test_installers.pyRepository: jaylfc/taOS Length of output: 20086 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exact write and secret references ---'
rg -n -C 5 'secret_key|NEXTAUTH_SECRET|docker-compose\.yaml|compose\.yaml|write_text|open\(' tinyagentos/installers/docker_installer.py
printf '%s\n' '--- install and compose generation context ---'
sed -n '85,175p' tinyagentos/installers/docker_installer.py
sed -n '175,215p' tinyagentos/installers/docker_installer.pyRepository: jaylfc/taOS Length of output: 13137 Sensitive Data Exposure (CWE-522): Insufficiently Protected Credentials Reachability: Internal · Exploitability: Moderate Create secret-bearing files with owner-only permissions.
📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| return secret_key | ||
|
|
||
| def _substitute_secret_key(self, value: str, secret_key: str) -> str: | ||
| """Replace the ``{secret_key}`` placeholder in a single string value.""" | ||
| return value.replace("{secret_key}", secret_key) | ||
|
|
||
| def _write_config_files(self, app_id: str, install_config: dict) -> None: | ||
| """Write declarative config files from the manifest to the app directory. | ||
|
|
||
|
|
@@ -68,21 +92,13 @@ def _write_config_files(self, app_id: str, install_config: dict) -> None: | |
| # Persist secret_key per app so re-installs don't rotate it. It signs | ||
| # sessions, so keep it owner-only and regenerate if a prior write left | ||
| # it empty or malformed. | ||
| secret_key_path = app_dir / ".secret_key" | ||
| secret_key = "" | ||
| if secret_key_path.exists(): | ||
| secret_key = secret_key_path.read_text().strip() | ||
| if len(secret_key) != 64 or not all(c in "0123456789abcdef" for c in secret_key): | ||
| secret_key = secrets.token_hex(32) | ||
| app_dir.mkdir(parents=True, exist_ok=True) | ||
| secret_key_path.write_text(secret_key) | ||
| secret_key_path.chmod(0o600) | ||
| secret_key = self._get_or_create_secret_key(app_id) | ||
|
|
||
| for entry in config_files: | ||
| path = entry["path"] | ||
| content = entry["content"] | ||
| if "{secret_key}" in content: | ||
| content = content.replace("{secret_key}", secret_key) | ||
| content = self._substitute_secret_key(content, secret_key) | ||
| full_path = app_dir / path | ||
| full_path.parent.mkdir(parents=True, exist_ok=True) | ||
| full_path.write_text(content) | ||
|
|
@@ -125,7 +141,18 @@ def _generate_compose( | |
| if self._is_named_volume(source): | ||
| named_volumes[source] = None | ||
| if "env" in install_config: | ||
| service["environment"] = install_config["env"] | ||
| # Substitute the per-app {secret_key} placeholder in every env | ||
| # string value (e.g. NEXTAUTH_SECRET), reusing the persisted key in | ||
| # <app_dir>/.secret_key so re-installs don't rotate it. The key is | ||
| # only created when an env value actually carries the placeholder. | ||
| env = install_config["env"] | ||
| if any("{secret_key}" in v for v in env.values() if isinstance(v, str)): | ||
| secret_key = self._get_or_create_secret_key(app_id) | ||
| env = { | ||
| k: self._substitute_secret_key(v, secret_key) if isinstance(v, str) else v | ||
| for k, v in env.items() | ||
| } | ||
| service["environment"] = env | ||
|
|
||
| # Collect the container-internal ports from the manifest. | ||
| container_ports: list[int] = [] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Restore Linkwarden PostgreSQL configuration.
Linkwarden requires PostgreSQL. Removing
DATABASE_URLand documenting SQLite support breaks this manifest contract.app-catalog/services/linkwarden/manifest.yaml#L24-L25: RestoreDATABASE_URLand remove the SQLite claim. Handle the PostgreSQL companion as separate work.changelog.d/tsk-teaogm-env-secret-key-substitution.md#L8-L9: Remove the statement thatDATABASE_URLis unused.📍 Affects 2 files
app-catalog/services/linkwarden/manifest.yaml#L24-L25(this comment)changelog.d/tsk-teaogm-env-secret-key-substitution.md#L8-L9🤖 Prompt for AI Agents