Cyber Security

Secrets for Small PHP Applications - Keep Credentials Out of Code and Ready to Rotate

Secrets for Small PHP Applications - Keep Credentials Out of Code and Ready to Rotate

A small PHP application eventually needs a database password, an API token, or a key for signing messages. Where should that value live? Putting it directly in a PHP file is simple, but it ties a credential to the code that consumes it. Moving it into an environment variable feels cleaner, yet that alone does not answer who can read it, how it reaches the server, or what happens when it leaks.

The useful goal is not to find an invisible hiding place. The application must receive a secret in plaintext at some point if it needs to use that secret. A more realistic goal is to separate code from credentials, restrict each credential's reach, reduce unnecessary copies, and make replacement routine rather than frightening.

This article develops that model for a small PHP application on a VPS or home server. It does not assume an enterprise vault, and it does not claim that one storage method is universally safest. The right mechanism depends on the service manager, operating system, deployment path, and threat model.

First decide what is actually secret

A secret is a value whose possession grants some capability or trust. Database passwords, bearer API tokens, webhook signing secrets, and private keys are common examples. A public hostname, locale, log level, or feature flag may vary between deployments without needing confidentiality.

This distinction matters because configuration and secrets overlap, but they are not identical. The Twelve-Factor App treats credentials as deploy-varying configuration and argues that such configuration should be separate from code. That is a useful boundary. It does not mean every configuration value needs encrypted storage, nor that every environment variable is protected like a secret.

User passwords are another category. An application normally verifies them using purpose-built password hashing rather than retrieving the original password later. An operational database password, by contrast, must usually be recoverable by the application so it can authenticate to the database. Mixing those two problems leads to bad advice: hashing is appropriate when the original value is not needed, but it cannot replace storage for a credential the application must present.

Why hard-coding fails before an attacker appears

MITRE's CWE-798 defines hard-coded credentials as a software weakness. The immediate danger is disclosure to anyone who can read the source or artifact, but the operational problem is just as important. A credential embedded in code tends to be shared across deployments, copied into examples, and changed through a code release. Its lifecycle becomes entangled with the application lifecycle.

Version control makes an accidental disclosure durable. Adding .env to .gitignore is a useful preventive step for an untracked local file, but the Git documentation is explicit that ignore rules do not affect files already tracked. Renaming the file or deleting it in the latest commit also does not invalidate the credential contained in older history.

Templates need discipline too. A committed .env.example can document required variable names and harmless sample formats, but it should contain placeholders rather than working development or production credentials:

APP_ENV=production
DB_HOST=127.0.0.1
DB_NAME=app
DB_USER=replace-me
DB_PASS=replace-me
WEBHOOK_SIGNING_SECRET=replace-me

The example describes an interface between deployment and application. It is not itself a secret store.

Separate five concerns, not just two files

"Keep secrets out of Git" is necessary, but incomplete. A maintainable design separates five concerns:

  • Code: knows the names and expected forms of required values, but not production values.
  • Storage: holds the authoritative value with access controls suitable for the deployment.
  • Delivery: makes the value available to the intended process at startup or runtime.
  • Use: keeps the value out of URLs, routine logs, error pages, and unrelated child processes.
  • Lifecycle: records ownership, scope, creation, rotation, expiration where supported, and revocation.

The OWASP Secrets Management Cheat Sheet treats access control, auditing, rotation, revocation, expiration, backup, and recovery as parts of the same system. A small site may implement them with a short inventory and a restricted local mechanism rather than a large platform. The questions remain the same even when the tooling is modest.

Make missing configuration a startup failure

PHP can read process environment variables with getenv(). According to the PHP manual, a named lookup returns false when the variable does not exist. Required secrets should therefore be validated explicitly instead of silently falling back to an empty string, a test password, or a value embedded in code.

<?php
declare(strict_types=1);

function requiredEnvironmentValue(string $name): string
{
    $value = getenv($name);

    if ($value === false || $value === '') {
        throw new RuntimeException("Missing required configuration: {$name}");
    }

    return $value;
}

$pdo = new PDO(
    'mysql:host=' . requiredEnvironmentValue('DB_HOST')
        . ';dbname=' . requiredEnvironmentValue('DB_NAME')
        . ';charset=utf8mb4',
    requiredEnvironmentValue('DB_USER'),
    requiredEnvironmentValue('DB_PASS'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

This example establishes only one property: the application refuses to start without required values. It does not validate hostnames, choose database privileges, conceal the process environment, or configure PHP-FPM. Those remain deployment responsibilities. A production error handler should also prevent the exception and stack trace from becoming a public response.

It is safer to log the missing variable's name than its value. The same rule applies to diagnostic pages, support bundles, CI output, shell tracing, and serialized configuration. Redaction after logging is fragile; avoiding the value in ordinary output is the better default.

Environment variables solve separation, not the whole threat model

Environment variables are convenient because service managers, containers, shells, and CI systems can inject them without changing PHP code. They also give one build different values in development and production. For many small deployments, that is a meaningful improvement over committed credentials.

However, an environment variable is a delivery channel and process state, not a complete secret-management service. Its exposure depends on the operating system, service manager, debugging tools, process relationships, crash handling, and who can inspect deployment configuration. Values may also be inherited by child processes unless the runtime prevents it.

A local .env file adds another layer: a library reads a plaintext file and places values into the application's environment or configuration. The file must therefore be outside the public document root, absent from version control, readable only by the necessary deployment or service identity, and covered deliberately by backup policy. Calling the file “environment” does not change its filesystem risk.

Environment delivery can be a reasonable baseline when the host has few trusted administrators, file and service permissions are controlled, and the credentials have narrow privileges. It is less persuasive when many workloads share an account, deployments copy values through several systems, or frequent rotation is required.

A restricted file may be simpler and more honest

A root-provisioned configuration file outside the repository and web root can be an understandable option on one server. The application service account receives read access; the web server and unrelated users do not receive access merely for convenience. The file path can remain stable while deployment tooling replaces the contents.

This is not automatically safer than an environment variable. Plaintext still exists on disk, backup copies may multiply it, and a compromised application process can read any credential it legitimately uses. The advantage is that filesystem ownership, permissions, mount boundaries, and file access can be reasoned about directly. The disadvantage is that lifecycle features such as dynamic issuance, audit trails, and automatic rotation must be built elsewhere.

Do not place the decryption key beside an encrypted secret and call the problem solved. Encryption at rest can protect a copied disk or backup under the right key model, but if the same account can read both ciphertext and key, it adds little against that account's compromise.

On systemd, consider service credentials

For a compatible Debian deployment, systemd provides another option. The project's System and Service Credentials documentation describes credentials that are acquired when a service starts and exposed as files beneath the directory named by $CREDENTIALS_DIRECTORY. Access is restricted to the service user, and the credential data itself is not propagated down the process tree as an environment value.

systemd supports loading credentials from files and can also handle encrypted credentials. Exact directives and encryption capabilities depend on the installed systemd version, so the local manual must be checked before deployment. Application code also needs to read the credential file, or a bootstrap layer must translate it into the interface the application expects.

One warning is easy to miss: systemd says literal SetCredential= data in a unit file should not be used for sensitive secrets because unit files are readable. File-loading and encrypted-credential directives have different properties. Copying a password into a new kind of public configuration is still copying a password.

Reduce what each credential can do

Storage cannot compensate for an unnecessarily powerful credential. A public-facing application usually does not need a database administrator account. A thumbnail worker may need write access to one media area but no permission to manage users. A webhook verifier needs the signing secret for one endpoint, not every external API token.

OWASP and CWE-798 both emphasize restricted access and limited privileges. In practice, this means separate credentials by service and environment, grant only required database or API operations, and avoid one shared “master” value. Shorter-lived credentials can reduce the useful lifetime of a copy when the issuer and deployment support them, but expiration without reliable renewal can also become an availability failure.

Keep an inventory that answers four basic questions: who owns this secret, which service consumes it, what authority it grants, and how it is replaced. Do not put the secret value in the inventory. A list of names, owners, consumers, and rotation procedures is enough to make forgotten credentials visible.

Design rotation before the first emergency

Rotation is not just generating a new random string. The issuer must accept the new credential, the application must receive it, the deployment must be verified, and the old credential must then be revoked. Some systems permit an overlap with two valid values; others require a coordinated cutover. The provider's contract determines the safe sequence.

  1. Create a replacement with the minimum required scope.
  2. Make it available through the chosen storage and delivery mechanism.
  3. Restart or reload only the consumers that require it.
  4. Verify the intended operation without printing the value.
  5. Revoke the old credential and confirm it no longer works where safe to test.
  6. Update the inventory and remove obsolete copies from deployment systems and backups according to policy.

A failed rotation should have a defined rollback or recovery path, but that does not always mean re-enabling the old value. If compromise triggered the change, restoring the exposed credential recreates the incident.

If a secret reaches Git, treat it as exposed

The first response is to contain capability, not to make the repository look clean. GitHub's secret-scanning guidance says to rotate an affected credential immediately. Revocation matters because forks, clones, caches, logs, pull-request text, and other copies may survive a history edit.

A measured response is to identify the credential and its scope, revoke or rotate it through the issuer, deploy the replacement, review available usage logs, and search for additional copies without printing them. Repository cleanup may still be appropriate, particularly for sensitive data that must be removed, but it is not a substitute for invalidating the old credential. Rewriting shared history also has coordination costs and should follow the hosting provider's current procedure.

A compact review for a small PHP service

  • No production credential is embedded in PHP, a committed example, a container image, or a public document-root file.
  • Required values fail closed at startup; development defaults cannot silently reach production.
  • The PHP service identity can read only the credentials it needs.
  • Database users and API tokens have the narrowest practical permissions.
  • Logs, error pages, diagnostics, CI output, and support bundles do not print secret values.
  • Backups and copied deployment files are included in the exposure model.
  • Each secret has an owner, consumer, purpose, and tested replacement procedure.
  • An exposed value can be revoked without relying on Git history cleanup.

Conclusion

Secret management for a small PHP application does not begin with buying a vault. It begins by refusing to bind credentials to code, then asking where each value is stored, how it reaches one process, what that process can do with it, and how the value will be replaced.

Environment variables may be an adequate first delivery mechanism; a restricted file may be easier to audit on one host; systemd credentials may provide a tighter service boundary where supported; a dedicated secret manager may become worthwhile as identities, deployments, and rotation demands grow. None removes the need for least privilege or leak response.

The honest test is not whether a secret is impossible to find. It is whether unnecessary readers and copies have been removed, and whether one exposed credential can be replaced before it becomes a permanent part of the application.

References