Skip to content

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

Open

Description

@deatherick

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:

  1. Secret values sourced from env_file: get flattened into literal environment: entries (plaintext secret written to disk), instead of staying only in the referenced .env file.
  2. A ${VAR} substitution reference can come back written as $$VAR (double-dollar), which per the Compose Specification means "a literal, escaped $ followed by literal text VAR" — i.e. it is not a variable reference anymore. Any container recreated from that file ends up with the literal string $VAR as the value, not the resolved secret. In our case this silently broke a login-protected app (CredentialsSignin errors) 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:/config

with a sibling .env containing real values for PEANUT_WEB_USERNAME, PEANUT_WEB_PASSWORD, AUTH_SECRET:

  1. casaos-cli app-management install --file compose.yaml (or the CasaOS UI's Custom Install).
  2. Later, casaos-cli app-management apply <id> --file compose.yaml with an unrelated change (in our case, only adding an x-casaos.port_map field — no environment change requested at all).
  3. 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_USERNAME

Two distinct problems here:

  • PEANUT_WEB_PASSWORD/PEANUT_WEB_USERNAME are new literal environment: keys that were never declared that way in the input file — they were only ever meant to be sourced from env_file:. CasaOS appears to introspect the live container's fully-resolved environment (which merges env_file: contents into the runtime env) and serializes all of it back as literal environment: 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 $VAR as 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 with CredentialsSignin until the container was recreated with a hand-corrected file via plain docker compose up -d --force-recreate (bypassing casaos-cli entirely).

Expected

  • apply/install should not introduce new literal environment: keys for values that only exist via env_file: — those should stay out of the serialized environment: 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 $VAR single-dollar form), or resolved by looking up the reference — never re-emitted as an escaped, no-longer-substituting $$VAR.
  • More generally: install/apply should not write resolved secret values from the live container's environment back into the on-disk compose file at all, for any app using env_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-management compose 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions