Pracandy
← Newsroom

Features

Handling Secrets When There Is No Secrets Manager

Environment files, file permissions, what not to put in a git repository, and how to rotate something that leaked.

17 September 2026

Wikimedia Foundation Servers 8055 02
Photo: Victorgrigas, CC BY-SA 3.0, via Wikimedia Commons

Where the secrets live

Store secrets on a VPS safely by keeping them in environment files with strict permissions, ensuring they never enter version control, and treating the file itself as the source of truth. For a single-server setup like ours, this approach is simpler and more auditable than introducing a dedicated secrets manager that adds its own attack surface and operational overhead.

Pracandy runs several distinct products from one machine, but the server-side logic is concentrated in two main processes. A single Next.js instance serves four domains, including the film publication PracandyFlix, using host-based routing. Separately, Instalaz runs as its own Node process on port 3050, proxied by Caddy. Both processes share a single PostgreSQL 16 database. GamingCap and Invoiceful are static builds served directly from disk by Caddy; they have no server process and therefore no server-side secrets to protect. This distinction matters because the security effort is focused entirely on the two long-running Node processes.

Environment files and permissions

Each Node process loads its configuration from a local environment file. These files reside in the application directory on the VPS, not in the user’s home directory, to keep deployment paths consistent. The critical step is file ownership and permissions. The files are owned by the specific system user that runs the service, with permissions set to 600. This ensures that only the service account can read the file. Even though the root user can read anything, limiting access to the specific service user reduces the blast radius if another local process is compromised.

On a machine with 12GB of RAM, processes share resources, but file permissions provide a hard boundary. If a vulnerability in a library allows an attacker to read files, they are limited to files readable by the service user. This is a basic but effective isolation technique. We do not use symbolic links for these files, as they can introduce unexpected permission inheritance issues. The environment variables are injected into the process memory at startup. Once in memory, they are as safe as any other string in the heap, but the file on disk remains the primary artifact that needs protection.

What stays out of Git

The repository contains a sample environment file, often named .env.example, which lists the required variable names but contains no values. This file is committed to Git so that developers know what is needed. The actual .env file is listed in .gitignore. This is non-negotiable. Once a secret is committed to a repository, it is considered compromised, even if you delete the commit from the branch. Git history is persistent and can be reconstructed. For a small team, the risk of accidental commit is higher because there is less code review. Discipline is required to ensure that no partial values or test keys are committed.

We do not use Git hooks to block commits containing secrets, as this adds complexity and can be bypassed. Instead, the rule is strict: if it works locally with a dummy value, it should not be in the file. For database connections, the password is the only secret; the host and port are configuration, not secrets, and can be in the code or a separate config file if desired. Keeping the distinction clear helps avoid confusion during deployment.

Rotating a leaked secret

If a secret is exposed, the first step is to revoke it. For a database password, this means generating a new password in PostgreSQL and updating the environment file. For an API key, this means generating a new key in the provider’s dashboard and updating the environment file. The second step is to restart the service. Since the secrets are loaded at startup, the running process will continue to use the old secret until it is restarted. On our VPS, this is done via systemd. We use systemd oneshot services with timers for scheduled work, but the main web processes are standard services. A simple systemctl restart command reloads the environment file and applies the new secrets.

Rotation should be tested in a staging