Skip to content

Migrating off the hardcoded BETTER_AUTH_SECRET (as the startup banner instructs) orphans all encrypted env vars, and services then deploy with an empty environment #4833

Description

@tanaymishra

To Reproduce

  1. Run a Dokploy instance that has never set BETTER_AUTH_SECRET (the default for many existing installs). On startup it prints the deprecation banner:

    ⚠️  [DEPRECATED AUTH CONFIG]
    BETTER_AUTH_SECRET is not set via environment variable or Docker secret.
    Falling back to the insecure hardcoded default — this is a CRITICAL SECURITY RISK.
    This mode WILL BE REMOVED in a future release.
    
    Please migrate to Docker Secrets:
      curl -sSL https://dokploy.com/security/0.29.3.sh | bash
    
  2. Create an application and add environment variables in the Environment tab, including a harmless marker such as DOKPLOY_ENV_TEST=true. Deploy it and confirm the variables are present in the container.

  3. Follow the banner's instruction and migrate to a real secret, either by running the linked script or by setting BETTER_AUTH_SECRET / BETTER_AUTH_SECRET_FILE manually. Restart Dokploy.

  4. Open the application's Environment tab. The values are now shown as enc:v1:... ciphertext instead of the original variables.

  5. Redeploy the application. The deployment completes successfully with status done and no error.

  6. Inspect the deployed service:

    docker service inspect <service> --format '{{ len .Spec.TaskTemplate.ContainerSpec.Env }}'

    It returns 0, and docker exec <container> sh -lc 'echo $DOKPLOY_ENV_TEST' prints nothing. The application is running in production with no environment variables at all.

Current vs. Expected behavior

Current behavior: environment variables are encrypted at rest with a key derived from BETTER_AUTH_SECRET. The decryption keyring is built once at startup and only ever contains the current key, plus (when a dedicated ENCRYPTION_KEY is configured) the key derived from the current auth secret. There is no slot for a previous BETTER_AUTH_SECRET, and there is no re-encryption or migration step anywhere.

As a result, changing BETTER_AUTH_SECRET makes every previously encrypted env, buildArgs and buildSecrets value permanently undecryptable. This is not a hypothetical path: the product itself instructs users to make this exact change on every startup, and the code comments already acknowledge the failure mode.

The damage is then hidden by fail-open decryption. When decryption fails, the encrypted column returns the raw ciphertext instead of raising. That ciphertext is passed to prepareEnvironmentVariables, which runs it through dotenv's parse. A string like enc:v1:AbCd... matches no KEY=VALUE pair, so it parses to {}. The service is then deployed with an empty environment and the deployment is reported as successful. The only trace is a console.error on the server, which never reaches the deployment log or the UI.

Expected behavior: migrating from the hardcoded legacy secret to a real BETTER_AUTH_SECRET (which the product actively asks users to do) should keep existing encrypted values readable. More generally, a key change should either be supported through a documented rotation path or should fail loudly, and a service should never be deployed with a silently emptied environment.

Provide environment information

This is an environment independent logic issue in the encryption keyring and
the encrypted column mapper, so it reproduces on any OS, arch, and VPS provider.

Dokploy version: canary, and any release that includes environment variable
                 encryption at rest (AES-256-GCM)
Affects: self-hosted installs still running the hardcoded legacy auth secret
         that follow the startup banner and migrate to a real secret

Which area(s) are affected? (Select all that apply)

  • Application
  • Docker Compose
  • Docker

Are you deploying the applications where Dokploy is installed or on a remote server?

Both

Additional context

The key is derived from the auth secret, and the keyring has no slot for a previous secret, packages/server/src/lib/encryption.ts lines 19 to 29:

const deriveKey = (secret: string) =>
	createHmac("sha256", secret).update("dokploy:db-encryption:v1").digest();

const primaryKey = deriveKey(encryptionSecret ?? betterAuthSecret);

// Installs that adopt a dedicated ENCRYPTION_KEY still hold values encrypted
// with the auth-secret-derived key; keep it as a decrypt fallback so they
// lazily re-encrypt on the next write instead of becoming unreadable.
const decryptionKeys = encryptionSecret
	? [primaryKey, deriveKey(betterAuthSecret)]
	: [primaryKey];

The existing fallback covers only the "adopt an ENCRYPTION_KEY" path. It does not cover the "replace the hardcoded legacy secret with a real one" path, which is the migration the banner asks for. The error text at lines 96 to 98 already names this exact scenario:

throw new Error(
	"Failed to decrypt a stored secret. This usually means ENCRYPTION_KEY or BETTER_AUTH_SECRET changed after the value was encrypted.",
);

The banner that triggers the migration, packages/server/src/lib/auth-secret.ts lines 3 and 15 to 25, where HARDCODED_LEGACY_SECRET is "better-auth-secret-123456789".

Fail-open decryption, packages/server/src/db/schema/utils.ts lines 19 to 31:

fromDriver(value) {
	try {
		return decryptValue(value);
	} catch {
		// Fail open so a key mismatch ... degrades to showing ciphertext
		console.error("Failed to decrypt an encrypted column; returning the raw value. ...");
		return value;
	}
},

The ciphertext then reaches dotenv via prepareEnvironmentVariables (packages/server/src/utils/docker/utils.ts lines 399 to 406). dotenv.parse("enc:v1:...") returns {}, which is how a failed decrypt becomes a successful deploy with zero environment variables.

Relationship to #4817: that issue reported the same end symptom (a deploy silently removing all runtime environment variables) for Dokploy Cloud, where the cause was a key mismatch between the web process and the deployment worker, and it was closed. This report is about a different trigger that is still present in canary: a single instance migrating its own auth secret, exactly as the startup banner instructs. The fail-open behaviour that turns any key mismatch into a silent empty-environment deploy is also still present.

Suggested fix: since HARDCODED_LEGACY_SECRET is a published constant in this repository, keeping deriveKey(HARDCODED_LEGACY_SECRET) in the decryption keyring as a decrypt-only fallback discloses nothing that was not already derivable by anyone with the source and a database dump, while allowing values written under the legacy secret to stay readable and to lazily re-encrypt under the new key on the next write. That mirrors the fallback already implemented for the ENCRYPTION_KEY adoption path. A documented way to supply a previous secret would additionally cover general rotation, and reconsidering the fail-open polarity (or at least surfacing the failure in the deployment log) would stop a key mismatch from silently emptying a running service's environment.

Will you send a PR to fix it?

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions