Repository navigation
install/apply can write resolved env_file secrets into the compose file as plaintext, and can corrupt ${VAR} references into unresolved literal $VAR strings #2584
Description
Summary
When installing or applying a compose app that uses env_file: alongside environment: variable substitution (${VAR}), the compose file CasaOS writes back can end up with two related problems:
- Secret values sourced from
env_file:get flattened into literalenvironment:entries (plaintext secret written to disk), instead of staying only in the referenced.envfile. - A
${VAR}substitution reference can come back written as$$VAR(double-dollar), which per the Compose Specification means "a literal, escaped$followed by literal textVAR" — i.e. it is not a variable reference anymore. Any container recreated from that file ends up with the literal string$VARas the value, not the resolved secret. In our case this silently broke a login-protected app (CredentialsSigninerrors) with no error at apply time.
Steps to reproduce
Given a compose file like:
name: peanut
services:
peanut:
image: brandawg93/peanut:5.16.0
container_name: peanut
restart: unless-stopped
ports:
- 8080:8080
environment:
- WEB_PORT=8080
- WEB_USERNAME=${PEANUT_WEB_USERNAME}
- WEB_PASSWORD=${PEANUT_WEB_PASSWORD}
- AUTH_SECRET=${AUTH_SECRET}
env_file:
- .env
volumes:
- ./config:/configwith a sibling .env containing real values for PEANUT_WEB_USERNAME, PEANUT_WEB_PASSWORD, AUTH_SECRET:
casaos-cli app-management install --file compose.yaml(or the CasaOS UI's Custom Install).- Later,
casaos-cli app-management apply <id> --file compose.yamlwith an unrelated change (in our case, only adding anx-casaos.port_mapfield — no environment change requested at all). - Inspect the resulting file CasaOS writes to
/var/lib/casaos/apps/<id>/docker-compose.yml(or wherever the app's compose is now stored).
Observed
The environment: block comes back rewritten as:
environment:
AUTH_SECRET: $$AUTH_SECRET
PEANUT_WEB_PASSWORD: <the real plaintext secret value>
PEANUT_WEB_USERNAME: <the real plaintext value>
WEB_PASSWORD: $$PEANUT_WEB_PASSWORD
WEB_PORT: "8080"
WEB_USERNAME: $$PEANUT_WEB_USERNAMETwo distinct problems here:
PEANUT_WEB_PASSWORD/PEANUT_WEB_USERNAMEare new literalenvironment:keys that were never declared that way in the input file — they were only ever meant to be sourced fromenv_file:. CasaOS appears to introspect the live container's fully-resolved environment (which mergesenv_file:contents into the runtime env) and serializes all of it back as literalenvironment:entries.WEB_PASSWORD/WEB_USERNAME/AUTH_SECRET, which were declared with${VAR}substitution in the input, come back as$$VAR— which is the Compose Specification's escape syntax for a literal dollar sign, not a substitution. Any container recreated from this file gets the literal string$VARas the value instead of the resolved secret. We confirmed this by comparing the resolved value inside the container (docker exec <c> sh -c 'echo -n "$WEB_PASSWORD" | sha256sum') against the actual secret's hash — they didn't match; the app's login started failing withCredentialsSigninuntil the container was recreated with a hand-corrected file via plaindocker compose up -d --force-recreate(bypassing casaos-cli entirely).
Expected
apply/installshould not introduce new literalenvironment:keys for values that only exist viaenv_file:— those should stay out of the serializedenvironment:block entirely, or at minimum not duplicate them as plaintext.- Any
${VAR}reference present in the input compose should either be preserved as${VAR}(or the equivalent-safe$VARsingle-dollar form), or resolved by looking up the reference — never re-emitted as an escaped, no-longer-substituting$$VAR. - More generally:
install/applyshould not write resolved secret values from the live container's environment back into the on-disk compose file at all, for any app usingenv_file:. The file is a natural candidate for git version control, and this makes CasaOS an active vector for committing plaintext secrets to source control.
Environment
- CasaOS version: v0.4.15 (
casaos-cli version/casaos version) - Installed via the official installer (
curl -fsSL https://get.casaos.io | bash) - Reproduced on a Proxmox LXC (Ubuntu 24.04), but nothing LXC-specific about the mechanism itself — this is a
casaos-app-managementcompose serialization behavior.
Additional related observation (same root cause area)
Separately, we found install/apply also fails outright (visible error, not silent) when a compose references env_file: .env with a relative path, or relies on Compose's implicit same-directory .env auto-load with no env_file: declared at all — because the install process stages only the single --file compose into a temp directory (/tmp/casaos-compose-app-*/), and nothing else. Using an absolute env_file: path avoids that specific error, but does not prevent the plaintext-flattening/$$VAR issue described above.
Happy to help narrow this down further or test a candidate fix if useful — this came up while building tooling to keep compose files under external git version control alongside CasaOS management, so a secrets-safe round-trip matters a lot for that workflow.
Activity