Symfony Secrets and Vault Management with HashiCorp Vault
AI generated
SF
{ }
Symfony · Vault · Secrets Management · Security
Symfony Secrets and Vault Management
with HashiCorp Vault instead of static .env files

A .env file with plaintext passwords inside the deployment artifact is a security risk many Symfony teams silently accept. Vault management with HashiCorp Vault replaces static secrets with dynamic, short lived credentials that rotate automatically and are logged without gaps. This article shows how Symfony is connected to Vault in practice.

18 min read Dynamic credentials · Rotation · Vault Agent · Policies Symfony 7 · HashiCorp Vault 1.17 · Kubernetes

1. Why .env files are not enough for secrets in production

Most Symfony applications start out with secrets in an .env.local file: the database password, API keys, the JWT signing key, all in plaintext on the server's disk or inside the Docker image. This pattern works fine for local development but quickly becomes a problem in production. Anyone with access to the server, the image or a backup sees every secret at a glance, without any access ever being logged. This is exactly where vault management comes in.

A second problem with static secrets is rotation. If a database password is compromised, it has to be manually swapped in every .env file on every server, which takes hours in larger infrastructures and is easily forgotten. Vault management with HashiCorp Vault solves both problems at once: secrets are managed centrally, never stored in plaintext on disk, and can be automatically reissued per application and per time window without a human ever intervening.

The shift from static to dynamic secrets is the core idea behind modern vault management. Instead of a single, permanently valid database password, every Symfony instance receives its own, time limited credential that becomes invalid automatically once it expires. Even if an attacker intercepts such a credential, the damage is limited to the narrow window during which it was valid.

2. HashiCorp Vault basics: secrets engines and policies

HashiCorp Vault organizes secrets through so called secrets engines, each managing a different kind of secret. The KV (key value) engine stores static secrets such as API keys, the database engine generates dynamic database credentials on demand, and the PKI engine issues short lived TLS certificates. For vault management in a Symfony application, the KV engine for static configuration values and the database engine for database access are usually the most relevant.

Access to secrets is governed through policies, written in HashiCorp Configuration Language. A policy defines exactly which paths an identity may read, write or manage. For Symfony this means: every application environment, such as staging and production, gets its own policy that grants read only access exclusively to the secret paths relevant to that environment. This least privilege principle is a central building block of serious vault management and prevents a compromised staging access from also exposing production secrets.


#!/usr/bin/env bash
# vault-setup.sh — enable engines and write a least-privilege policy
set -euo pipefail

# Enable the KV v2 engine for static application secrets
vault secrets enable -path=symfony-app kv-v2

# Enable the database engine for dynamic credentials
vault secrets enable -path=symfony-db database

# Write a policy restricted to this application's own path
cat <<'EOF' | vault policy write symfony-prod-read -
path "symfony-app/data/prod/*" {
  capabilities = ["read"]
}
path "symfony-db/creds/prod-readonly" {
  capabilities = ["read"]
}
EOF

echo "[OK] Vault engines and policy configured for Symfony production"

3. Connecting Symfony to Vault: loading secrets at runtime

There are two fundamentally different strategies for connecting Symfony to Vault. The first loads secrets directly at application startup through a Vault client inside PHP, usually as a custom environment variable processor extending Symfony's built in secrets system. The second has secrets supplied from the outside, for example through a Vault Agent or sidecar, as a file or environment variable, so the Symfony application itself needs to know nothing about Vault.

The second approach is the more pragmatic entry point into vault management for most teams, because it decouples the application from the Vault client library and requires no PHP extension or additional Composer dependency for the Vault API. The Vault Agent authenticates itself against Vault independently, renders the retrieved secrets into a file following a configured template, and automatically updates that file as soon as the underlying secret rotates. Symfony then reads the values in exactly the normal way through environment variables.


<?php
// config/services.yaml equivalent — reading Vault-provided values
// via Symfony's standard environment variable resolution.
// The actual file at APP_SECRETS_FILE is written by the Vault Agent
// and updated automatically whenever the underlying secret rotates.

declare(strict_types=1);

namespace App\Kernel;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Dotenv\Dotenv;

final class VaultSecretsLoader
{
    /**
     * Loads secrets rendered by the Vault Agent into a local file
     * and exposes them as environment variables for Symfony.
     */
    public static function load(string $secretsFile): void
    {
        if (!is_readable($secretsFile)) {
            throw new \RuntimeException(
                sprintf('Vault-rendered secrets file not found: %s', $secretsFile)
            );
        }

        (new Dotenv())->usePutenv(true)->load($secretsFile);
    }
}

4. Dynamic database credentials instead of static passwords

The biggest jump in maturity for vault management comes with dynamic database credentials. Instead of a single database user shared by all Symfony instances, Vault generates a fresh, unique user on every request with a defined time to live, for example one hour. Once that time expires, Vault automatically removes the database user again, with no manual cleanup required.

For the database this means: instead of a password that stays unchanged for months or years, only a few short lived accesses exist at any time, each uniquely attributable to a specific application instance and a specific time window. The audit log then shows exactly which instance used which database access at which point in time, which considerably simplifies forensic analysis after a security incident. This traceability is one of the strongest advantages vault management offers over static secrets.


#!/usr/bin/env bash
# fetch-db-credentials.sh — request short-lived database credentials
# valid for one hour, automatically revoked afterward by Vault
set -euo pipefail

RESPONSE=$(vault read -format=json symfony-db/creds/prod-readonly)

DB_USER=$(echo "$RESPONSE" | jq -r '.data.username')
DB_PASS=$(echo "$RESPONSE" | jq -r '.data.password')
LEASE_ID=$(echo "$RESPONSE" | jq -r '.lease_id')
LEASE_TTL=$(echo "$RESPONSE" | jq -r '.lease_duration')

echo "[OK] Issued credential ${DB_USER}, valid for ${LEASE_TTL}s (lease: ${LEASE_ID})"

# Written to the location the Symfony application reads DATABASE_URL from
printf 'DATABASE_URL="mysql://%s:%s@db.internal:3306/shop"\n' "$DB_USER" "$DB_PASS" \
  > /run/secrets/database.env

5. Secret rotation without an application restart

Static secrets in an .env file require an application restart after every rotation for the new value to be loaded. With vault management through the Vault Agent, this restart is unnecessary, because the agent updates the rendered secrets file while the application keeps running, and the application re reads the file when needed, for example on the next database connection, instead of permanently keeping the old value in memory.

For long running PHP FPM workers, it matters that database connections are not kept open for the entire lifetime of the worker process, but rebuilt regularly, for example after a few hundred requests via pm.max_requests. That way, every new connection automatically picks up the currently valid, rotated credentials, without any explicit rotation logic needing to be implemented in the application itself. This combination of the Vault Agent and regularly recycled worker processes makes rotation practically invisible to the application under vault management.

6. Secrets in Kubernetes: Vault Agent Injector and sidecar

In Kubernetes environments, Vault offers an injector that automatically extends pods with a Vault Agent sidecar container as soon as certain annotations are present in the pod manifest. The sidecar authenticates against Vault using the pod's Kubernetes service account identity, with no additional token that needs to be manually distributed. For vault management in Kubernetes, this approach is the most practical way to supply secrets without going through Kubernetes' own, by default unencrypted secret objects.

The retrieved secrets land in a shared, in memory volume visible only to the actual Symfony container, not to other pods in the same namespace. This significantly reduces the attack surface compared to Kubernetes secrets, which by default are merely base64 encoded and stored without native rotation. A cleanly configured Vault Agent Injector makes vault management a seamless part of the Kubernetes deployment without developers needing to modify the Symfony application itself.


# k8s/deployment.yaml — annotations for the Vault Agent Injector
apiVersion: apps/v1
kind: Deployment
metadata:
  name: symfony-app
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "symfony-prod"
        vault.hashicorp.com/agent-inject-secret-database.env: "symfony-db/creds/prod-readonly"
        vault.hashicorp.com/agent-inject-template-database.env: |
          {{- with secret "symfony-db/creds/prod-readonly" -}}
          DATABASE_URL="mysql://{{ .Data.username }}:{{ .Data.password }}@db.internal:3306/shop"
          {{- end -}}
    spec:
      serviceAccountName: symfony-app
      containers:
        - name: symfony-app
          image: registry.mironsoft.de/symfony-app:1.4.2

7. Audit log and access control with policies

Every request against Vault, whether allowed or denied, ends up in the audit log, including the identity of the requester, the requested path and a timestamp. For vault management in regulated industries, this gapless log is often a compliance requirement, because it answers precisely who accessed which secret and when, something a .env file cannot deliver by design.

Policies should be scoped as narrowly as possible: a Symfony production environment needs only read access to its own secret paths, never write access and never access to the paths of other environments or applications. This least privilege principle limits the damage if a token is ever compromised, because even a stolen token only carries the narrowly scoped rights of its associated policy, nothing more. Without this consistent principle, vault management loses a substantial part of its security benefit.

8. Local development versus production: Vault dev server

For local development, the full overhead of vault management with cluster setup, unseal process and TLS certificates is usually disproportionate. HashiCorp Vault offers a dev server mode for this, which runs in memory, starts already unsealed, and is ready within seconds. Developers can use exactly the same Vault paths and policies locally as in production, without rebuilding the production infrastructure.

It is important never to accidentally run the dev server in production, since it holds data exclusively in memory and resets completely on every restart. A clean vault management workflow therefore clearly separates the dev server used for local development and continuous integration from a production grade, highly available Vault cluster with a persistent storage backend.


#!/usr/bin/env bash
# enable-audit-log.sh — every request against Vault, allowed or denied,
# is written to this log with identity, path and timestamp
set -euo pipefail

vault audit enable file file_path=/vault/logs/audit.log

# Verify the audit device is active
vault audit list -detailed

echo "[OK] Audit logging enabled — every Vault Management access is now traceable"

9. Vault management compared to .env and Kubernetes secrets

The table below compares three common approaches to secrets in Symfony applications.

Approach Rotation Audit log Storage
.env file manual, error prone none plaintext on the filesystem
Kubernetes secrets manual, no native expiry Kubernetes API audit only base64, unencrypted in etcd by default
HashiCorp Vault automatic, configurable TTL complete, per access encrypted, never permanently in plaintext

The extra effort of operating a Vault cluster clearly pays off for vault management in environments with real compliance requirements or high security needs. For smaller projects with lower risk, a transitional solution using Kubernetes secrets and an external secret store operator can be a sensible intermediate step before building a full Vault setup.

Mironsoft

Symfony DevOps, secrets management and security architecture

Setting up vault management for your Symfony secrets?

We set up HashiCorp Vault for Symfony: dynamic database credentials, the Vault Agent Injector in Kubernetes and a least privilege policy model with a complete audit log.

Secrets audit

Reviewing your existing .env and Kubernetes secrets landscape for risks

Vault migration

Migrating static secrets step by step to dynamic, rotating Vault credentials

Policy design

Defining least privilege policies per environment and application

10. Summary

Vault management with HashiCorp Vault replaces static passwords in .env files with dynamic, short lived credentials that rotate automatically and are logged without gaps. The Vault Agent Injector brings these secrets into Kubernetes without any code change to the Symfony application itself, while the database engine generates database access that becomes invalid automatically once its time to live expires.

Least privilege policies limit the damage from a compromised token to a narrowly defined path, and the complete audit log answers forensic questions a .env file cannot answer by design. The additional operational effort of vault management pays off for any application with genuine security or compliance requirements, because secrets are then truly managed for the first time instead of merely being stored somewhere.

Vault Management for Symfony — The Essentials at a Glance

Dynamic credentials

Database access is generated on demand and expires automatically after a fixed time to live.

Kubernetes integration

Vault Agent Injector delivers secrets through an in memory volume without modifying the application.

Least privilege policies

Every environment gets read only access to its own secret paths.

Complete audit log

Every access is logged, including identity, path and timestamp.

11. FAQ: Symfony Secrets and Vault Management

1Why is .env not enough?
Plaintext, no rotation, no logging. Any server access exposes every secret, unnoticed.
2Benefit of dynamic credentials?
Short lived, uniquely attributable, expire automatically. Traceable exactly to one instance in the audit log.
3Does Symfony need a Vault library?
Not necessarily. Vault Agent or sidecar deliver secrets as a file, the application need not know Vault.
4Rotation without restart?
Vault Agent updates the secrets file live. Recycled FPM workers automatically pick up new values.
5Secrets delivery in Kubernetes?
Vault Agent Injector injects a sidecar, authenticates via service account, writes to an in memory volume.
6What is in the audit log?
Every request with identity, path and timestamp, allowed or denied.
7What does least privilege mean?
Only access to own secret paths, usually read only. Compromised tokens carry limited rights.
8Difference from Kubernetes secrets?
Kubernetes secrets are only base64, no native rotation. Vault encrypts and rotates automatically.
9Is the dev server production suitable?
No, in memory only without persistence. Production needs a highly available cluster.
10Worth it for small projects?
Mainly with compliance needs. Smaller projects can use other secret stores as a transition.