skip to content

Where should a PHP application keep its staging and production database passwords, and who can read the value in each place?

level: seniorimportance: should knowfreq 38%

answer

  1. the repository is the widest audience
  2. config file returning an array
  3. environment: phpinfo() and child processes
  4. secret file outside the web root
  5. validate once at boot, never log values

basics

~20 s

Never in the repository: commit a PHP config file that returns an array without the secret, and inject each environment's password through an environment variable or a secret file outside the document root; phpinfo() and child processes can expose environment values.

solid answer

~40 s

The code and a committed `config/database.php` that `return`s an array should hold only the **shape** of the settings; the password arrives per environment. An environment variable is simple and works everywhere, but it is readable by anything in the process: `phpinfo()` prints it, child processes started with `exec()` or `proc_open()` inherit it, and debug pages that dump `$_SERVER` show it. A secret mounted as a file outside the document root and read with `file_get_contents()` narrows exposure to code that opens that path and to OS users with read permission. INI or PHP config files with real values must sit outside the web root with tight permissions. Whatever the source, read and validate it once at boot and never log it.

code

php · 24 lines
php
<?php
declare(strict_types=1);

$secret = static function (string $name): string {
    $file = getenv($name . '_FILE');
    if ($file !== false) {
        $value = file_get_contents($file);
        if ($value === false) {
            throw new RuntimeException("Cannot read secret file for {$name}");
        }
        return rtrim($value, "\r\n");
    }
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Missing secret {$name}"); // name only, never the value
    }
    return $value;
};

return [
    'dsn' => 'pgsql:host=' . getenv('DB_HOST') . ';dbname=app',
    'user' => getenv('DB_USER') ?: 'app',
    'password' => $secret('DB_PASSWORD'),
];

go deeper

for a junior

Recall that passwords never go into the repository and that PHP config files returning arrays should read secrets from the environment or a file instead of holding them.

for a middle

Explain how phpinfo(), $_SERVER dumps and child processes expose environment variables, and why secret files and files outside the document root narrow the audience.

for a senior

Design the per-environment credential flow end to end: same names in staging and production, platform-injected values or secret files, boot-time validation, no logging of values, rotation after exposure.

for a principal

Own the trade-off between simple environment variables and a managed secret delivery with rotation, weighing operational cost against how many people and processes can read production credentials.

## The goal A PHP application that runs in **staging** and **production** needs a different database password in each. The question is not only where the password lives but **who can read it** there: developers, the CI system, other processes on the host, anyone who can make the app print a debug page. A good setup keeps the audience of the production password as small as possible. ## The options compared | Where | Who can read it | Main risk | |---|---|---| | hard-coded in PHP source | everyone with the repository, forever | leaked with every clone, fork and backup | | committed config file per environment | same as above | the same, plus copy-paste between environments | | config file returning an array, **not committed**, outside the web root | OS users with read permission; PHP code | file permissions and backups | | environment variable | the process, its child processes, anything that dumps the environment | `phpinfo()`, `$_SERVER` dumps, inherited by spawned commands | | secret file mounted by the platform (read at boot) | code that reads that path; OS users with permission | permissions on the mount | | INI file parsed with `parse_ini_file()` | like any config file | web server serving `.ini` as text from the web root | ## Config files that return arrays A common PHP pattern is a file that does nothing but return configuration: ```php <?php return [ 'dsn' => 'pgsql:host=' . getenv('DB_HOST') . ';dbname=app', 'user' => getenv('DB_USER'), 'password' => getenv('DB_PASSWORD'), ]; ``` and is loaded with `$config = require __DIR__ . '/../config/database.php';`. Its strengths: - it is plain PHP, so values have real types and can be computed; - OPcache compiles it like any script, so loading it is cheap; - the committed version carries **structure and non-secret defaults**, while secrets come from `getenv()` or a secret file. The mistake is filling that file with the real production password and committing it. A per-environment variant (`config/production.php`) is fine as long as it holds names and references, not values. ## What exposes environment variables Environment variables are the most common transport and are a reasonable default, but know their leaks: - **`phpinfo()`** prints the environment and the `$_SERVER`/`$_ENV` contents. A forgotten `info.php` in the document root publishes the database password. - **Child processes** started with `exec()`, `shell_exec()` or `proc_open()` inherit the whole environment unless you pass an explicit one. - **Error pages and debug toolbars** that dump `$_SERVER` show every variable the SAPI placed there. - **Stack traces** can include a password passed as a function argument, for example to a connection constructor. - **Other OS users** generally cannot read another user's process environment, but root and the same user can. ## Secret files Many platforms can mount a secret as a file, for example under `/run/secrets/`. PHP reads it once at boot: 1. `$password = file_get_contents('/run/secrets/db_password');` 2. check the result is not `false` and trim the trailing newline; 3. pass it into the connection factory and keep it out of anything that is logged. A frequent convention lets each variable have a `_FILE` twin: if `DB_PASSWORD_FILE` is set, read the file it names; otherwise use `DB_PASSWORD`. The file stays out of `phpinfo()` and out of child process environments. ## Rules that apply to every option - **Outside the document root.** No config, INI or `.env` file with a secret should be reachable by URL. - **Read once, validate once.** Load secrets during bootstrap, fail fast with the variable name (not the value) if one is missing. - **Same names, different values.** Staging and production read identical keys; only the injected values differ, so no code path chooses credentials by environment name. - **Separate credentials per environment.** A staging password must never open the production database, so a staging leak stays a staging problem. - **Rotate on exposure.** A password that ever sat in the repository or a published `phpinfo()` page is compromised, whatever was deleted since.

  • Why is a forgotten phpinfo() page a credentials leak when the password is in an environment variable?
    `phpinfo()` prints the environment and the contents of `$_SERVER` and `$_ENV`, so every variable the process received — database passwords included — appears on the page for anyone who can load it. Delete such files from the document root and rotate any password that was shown.
  • Your PHP app runs a shell command with exec() to resize images. What does that command receive, and how do you limit it?
    It inherits the PHP process's whole environment, including `DB_PASSWORD` if the secret came in that way. Reading secrets from a file keeps them out of the environment, and `proc_open()` accepts an explicit environment array so the child gets only what it needs.

saying these in an interview costs you the question

  • A private repository is a safe place for the production password
  • Environment variables cannot leak because they are not files
  • A config .php file in the document root is safe because PHP executes it instead of showing it
  • Staging and production can share one database password if the hosts differ
  • Deleting a leaked password from git history makes rotation unnecessary