Secure PHP Deployment: Moving Beyond .env Files in Production

Modern PHP applications have embraced the convenience of .env files for managing configuration during development. Frameworks like Laravel, Symfony, and Slim have made environment-based configuration a cornerstone of application setup, allowing developers to easily switch between local, staging, and production settings. However, this convenience comes with significant security implications when improperly extended to production environments.

While .env files serve developers well during the development phase, they represent a substantial security vulnerability in production deployments. The practice of storing sensitive credentials in plain text files on production servers creates multiple attack vectors, from unauthorized file access to accidental exposure through version control systems.

This article explores why production PHP applications should abandon .env files in favor of secure environment variable injection, and provides comprehensive guidance on implementing robust deployment practices that enhance security without sacrificing operational efficiency.

The Security Problem with Production .env Files

Understanding the Risks

The fundamental issue with .env files in production stems from their nature as persistent, readable files stored on the filesystem. Unlike temporary environment variables that exist only in process memory, .env files create permanent security liabilities that can be exploited through various means.

File System Vulnerabilities: Production servers often host multiple applications or services, potentially with different permission levels. A misconfigured file permission, a compromised service account, or a local privilege escalation vulnerability can expose .env files to unauthorized access. Unlike database credentials stored in encrypted key management systems, .env files typically contain secrets in plain text.

Version Control Exposure: Despite best practices encouraging .env to be included in .gitignore, production-specific environment files like .env.production or .env.prod sometimes find their way into repositories. This exposure can persist indefinitely in git history, even after files are subsequently removed.

Backup and Logging Risks: System administrators often implement comprehensive backup strategies that include entire application directories. These backups may inadvertently capture .env files, creating additional copies of sensitive credentials that may be stored with less stringent security controls than the production environment itself.

Limited Audit Capabilities: File-based configuration provides minimal insight into who accessed what information and when. Modern security compliance requirements often mandate detailed audit trails for sensitive credential access, something that traditional file-based approaches cannot provide.

Real-World Impact Scenarios

Consider a typical PHP application running on a shared hosting environment or a misconfigured cloud instance. If an attacker gains read access to the web directory through a file inclusion vulnerability, directory traversal attack, or compromised adjacent service, they immediately gain access to all application secrets stored in .env files.

In containerized environments, the risks multiply. Container images that include .env files effectively distribute secrets to every environment where the image is pulled, including developer workstations, CI/CD runners, and potentially compromised systems.

The Development-Production Divide

Maintaining Development Efficiency

The transition away from production .env files doesn’t require abandoning their convenience during development. The key is establishing a clear boundary between development and production practices while maintaining workflow efficiency.

During development, .env files remain the optimal solution for several reasons:

Local Environment Consistency: Developers need consistent, reproducible environments that can be quickly set up and torn down. .env files provide this consistency while allowing for easy customization of local settings like database connections, debug flags, and external service endpoints.

Team Collaboration: When properly managed (excluded from version control but documented through .env.example files), environment files enable teams to maintain consistent local development environments without sharing actual credentials.

Rapid Iteration: Development workflows benefit from the ability to quickly modify configuration values without restarting services or modifying system-level environment variables.

Production Environment Requirements

Production environments, however, have fundamentally different requirements that make .env files inappropriate:

Security by Design: Production systems should minimize the attack surface by avoiding persistent storage of secrets in readable files. Environment variables injected at process startup provide secrets only to the specific process that needs them, without creating persistent copies on disk.

Credential Rotation: Modern security practices require regular rotation of sensitive credentials. Environment variable injection enables automated credential rotation without requiring file system modifications or application downtime.

Compliance and Auditing: Enterprise environments often require detailed audit trails showing who accessed what credentials and when. Proper secret management systems provide these capabilities, while file-based approaches do not.

Multi-Environment Deployment: Production deployments often involve multiple environments (staging, production, disaster recovery) with different credential sets. Environment variable injection allows the same application code to work across all environments without environment-specific configuration files.

Framework-Specific Considerations

Laravel Applications

Laravel’s configuration system provides excellent support for environment variable injection, but developers must understand how the framework handles configuration caching to avoid common pitfalls.

Configuration Caching Implications: Laravel’s config:cache command compiles all configuration values into a single cached file for performance optimization. This process evaluates all env() calls at cache time, meaning that subsequent changes to environment variables won’t take effect until the cache is cleared and regenerated.

# After deploying with new environment variables
php artisan config:cache
php artisan route:cache
php artisan view:cache

Service Provider Considerations: Custom service providers that directly call env() functions can cause issues in cached configurations. The Laravel documentation strongly recommends using configuration files as intermediaries:

// Problematic: Direct env() call in service provider
$apiKey = env('EXTERNAL_API_KEY');

// Correct: Use configuration file
$apiKey = config('services.external_api.key');

Queue Worker Implications: Long-running queue workers don’t automatically pick up environment variable changes. In production deployments with environment variable updates, queue workers must be restarted to recognize new values.

Symfony Applications

Symfony’s environment variable handling through the DotEnv component provides robust support for production environment variable injection, with some framework-specific considerations.

Environment Variable Processing: Symfony processes environment variables during the container compilation phase. The framework supports complex environment variable resolution, including default values and variable interpolation:

# config/packages/doctrine.yaml
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
        server_version: '%env(default:mariadb_version:DATABASE_SERVER_VERSION)%'

Cache Warming: Symfony’s cache warming process evaluates environment variables during deployment. Production deployments should ensure environment variables are available during the cache warming phase:

# Deployment script with environment variable injection
export DATABASE_URL="mysql://user:pass@host/db"
export APP_SECRET="production-secret"
php bin/console cache:warmup --env=prod

Secrets Management Integration: Symfony provides built-in integration with various secret management systems through its Secrets component, enabling seamless transition from .env files to proper secret management.

Secure Deployment Strategies

CI/CD-Based Environment Variable Injection

Modern deployment practices center around CI/CD pipelines that inject environment variables at deployment time, ensuring secrets never persist on disk while maintaining automated deployment capabilities.

GitHub Actions Implementation:

name: Secure PHP Deployment

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: 
      name: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, xml, bcmath, pdo_mysql

      - name: Install Dependencies
        run: composer install --no-dev --optimize-autoloader --no-interaction

      - name: Run Security Audit
        run: composer audit

      - name: Deploy Application
        env:
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}
          APP_KEY: ${{ secrets.APP_KEY }}
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
          REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
          MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
        run: |
          echo "$DEPLOY_KEY" > deploy_key
          chmod 600 deploy_key

          # Create deployment script with environment variables
          cat > deploy.sh << 'EOF'
          #!/bin/bash
          set -e

          export APP_ENV=production
          export APP_DEBUG=false
          export APP_KEY="${{ env.APP_KEY }}"
          export DB_CONNECTION=mysql
          export DB_HOST=localhost
          export DB_DATABASE=production_db
          export DB_USERNAME=app_user
          export DB_PASSWORD="${{ env.DB_PASSWORD }}"
          export REDIS_HOST=localhost
          export REDIS_PASSWORD="${{ env.REDIS_PASSWORD }}"
          export MAIL_MAILER=smtp
          export MAIL_HOST=smtp.mailgun.org
          export [email protected]
          export MAIL_PASSWORD="${{ env.MAIL_PASSWORD }}"

          cd /var/www/production-app
          git pull origin main
          composer install --no-dev --optimize-autoloader
          php artisan migrate --force
          php artisan config:cache
          php artisan route:cache
          php artisan view:cache

          # Restart application services
          sudo systemctl reload php8.2-fpm
          sudo systemctl reload nginx
          EOF

          # Execute deployment on remote server
          scp -i deploy_key -o StrictHostKeyChecking=no deploy.sh $DEPLOY_USER@$DEPLOY_HOST:/tmp/
          ssh -i deploy_key -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST 'bash /tmp/deploy.sh && rm /tmp/deploy.sh'

          rm deploy_key

This approach ensures that secrets are injected only during the deployment process and never stored permanently on the target server.

GitLab CI/CD Alternative:

stages:
  - test
  - deploy

variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"

cache:
  paths:
    - .composer-cache/

test:
  stage: test
  image: php:8.2-cli
  before_script:
    - apt-get update && apt-get install -y git unzip
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
  script:
    - composer install
    - ./vendor/bin/phpunit

deploy_production:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts
  script:
    - |
      ssh $DEPLOY_USER@$DEPLOY_HOST << EOF
        export APP_ENV=production
        export APP_DEBUG=false
        export APP_KEY="$APP_KEY"
        export DB_PASSWORD="$DB_PASSWORD"
        export REDIS_PASSWORD="$REDIS_PASSWORD"
        export MAIL_PASSWORD="$MAIL_PASSWORD"

        cd /var/www/production-app
        git pull origin main
        composer install --no-dev --optimize-autoloader
        php artisan migrate --force
        php artisan config:cache
        sudo systemctl reload php-fpm
      EOF
  only:
    - main
  environment:
    name: production

Container-Based Deployments

Containerized PHP applications benefit significantly from environment variable injection, as containers provide natural isolation boundaries for process-specific environment variables.

Docker Deployment with Secret Management:

# Dockerfile
FROM php:8.2-fpm-alpine

# Install system dependencies
RUN apk add --no-cache \
    git \
    curl \
    libpng-dev \
    oniguruma-dev \
    libxml2-dev \
    zip \
    unzip

# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Set working directory
WORKDIR /var/www

# Copy application files
COPY . .

# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction

# Set permissions
RUN chown -R www-data:www-data /var/www

# Expose port
EXPOSE 9000

CMD ["php-fpm"]

Docker Compose with Secret Injection:

version: '3.8'

services:
  app:
    build: .
    environment:
      - APP_ENV=production
      - APP_DEBUG=false
      - APP_KEY=${APP_KEY}
      - DB_CONNECTION=mysql
      - DB_HOST=db
      - DB_DATABASE=${DB_DATABASE}
      - DB_USERNAME=${DB_USERNAME}
      - DB_PASSWORD=${DB_PASSWORD}
      - REDIS_HOST=redis
      - REDIS_PASSWORD=${REDIS_PASSWORD}
    depends_on:
      - db
      - redis
    networks:
      - app-network

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    networks:
      - app-network

volumes:
  db_data:

networks:
  app-network:
    driver: bridge

Kubernetes Deployment:

apiVersion: v1
kind: Secret
metadata:
  name: php-app-secrets
type: Opaque
stringData:
  app-key: "base64:generated-laravel-key"
  db-password: "secure-database-password"
  redis-password: "secure-redis-password"
  mail-password: "smtp-mail-password"

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: php-app
  template:
    metadata:
      labels:
        app: php-app
    spec:
      containers:
      - name: php-app
        image: your-registry/php-app:latest
        env:
        - name: APP_ENV
          value: "production"
        - name: APP_DEBUG
          value: "false"
        - name: APP_KEY
          valueFrom:
            secretKeyRef:
              name: php-app-secrets
              key: app-key
        - name: DB_CONNECTION
          value: "mysql"
        - name: DB_HOST
          value: "mysql-service"
        - name: DB_DATABASE
          value: "production_db"
        - name: DB_USERNAME
          value: "app_user"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: php-app-secrets
              key: db-password
        - name: REDIS_HOST
          value: "redis-service"
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: php-app-secrets
              key: redis-password
        - name: MAIL_MAILER
          value: "smtp"
        - name: MAIL_HOST
          value: "smtp.mailgun.org"
        - name: MAIL_USERNAME
          value: "[email protected]"
        - name: MAIL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: php-app-secrets
              key: mail-password
        ports:
        - containerPort: 9000
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Secret Management System Integration

Enterprise-grade deployments benefit from integration with dedicated secret management systems that provide additional security features like automatic rotation, detailed audit logging, and fine-grained access control.

AWS Systems Manager Parameter Store Integration:

#!/bin/bash
# Deployment script with AWS Parameter Store

# Fetch secrets from Parameter Store
export APP_KEY=$(aws ssm get-parameter --name "/production/app/key" --with-decryption --query 'Parameter.Value' --output text)
export DB_PASSWORD=$(aws ssm get-parameter --name "/production/db/password" --with-decryption --query 'Parameter.Value' --output text)
export REDIS_PASSWORD=$(aws ssm get-parameter --name "/production/redis/password" --with-decryption --query 'Parameter.Value' --output text)

# Set non-secret environment variables
export APP_ENV=production
export APP_DEBUG=false
export DB_CONNECTION=mysql
export DB_HOST=production-db.cluster-xxx.us-east-1.rds.amazonaws.com
export DB_DATABASE=production_app

# Deploy application
cd /var/www/production-app
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
systemctl reload php-fpm

HashiCorp Vault Integration:

<?php
// config/vault.php - Custom configuration provider

use Vault\AuthenticationStrategies\AppRoleAuthenticationStrategy;
use Vault\Client;

class VaultConfigProvider
{
    private Client $vault;

    public function __construct()
    {
        $this->vault = new Client(env('VAULT_ADDR', 'https://vault.example.com'));

        $strategy = new AppRoleAuthenticationStrategy(
            env('VAULT_ROLE_ID'),
            env('VAULT_SECRET_ID')
        );

        $this->vault->setAuthenticationStrategy($strategy);
    }

    public function getDatabasePassword(): string
    {
        $response = $this->vault->read('secret/data/production/database');
        return $response->getData()['data']['password'];
    }

    public function getAppKey(): string
    {
        $response = $this->vault->read('secret/data/production/app');
        return $response->getData()['data']['key'];
    }
}

Performance and Operational Considerations

Configuration Caching Impact

The transition from .env files to environment variables can have performance implications, particularly in how PHP frameworks handle configuration caching.

Laravel Configuration Caching: Laravel’s configuration caching provides significant performance benefits by eliminating the need to parse configuration files and evaluate env() calls on every request. However, this caching behavior requires careful consideration in deployment pipelines:

# Recommended Laravel deployment sequence
php artisan config:clear    # Clear existing cache
php artisan config:cache    # Rebuild with current environment variables
php artisan route:cache     # Cache routes for performance
php artisan view:cache      # Cache Blade templates
php artisan event:cache     # Cache event listeners

Cache Invalidation Strategy: Environment variable changes require cache invalidation to take effect. Deployment scripts should include cache clearing and regeneration steps:

#!/bin/bash
# Production deployment with proper cache management

set -e

# Set environment variables
export APP_ENV=production
export APP_KEY="$VAULT_APP_KEY"
export DB_PASSWORD="$VAULT_DB_PASSWORD"

# Update application code
git pull origin main
composer install --no-dev --optimize-autoloader

# Run database migrations
php artisan migrate --force

# Clear and rebuild all caches
php artisan optimize:clear
php artisan optimize

# Restart services to ensure clean state
systemctl reload php-fpm
systemctl reload nginx

Process Management

Environment variable injection requires careful consideration of process lifecycle management, particularly for long-running processes like queue workers and scheduled tasks.

Queue Worker Management: PHP queue workers are long-running processes that don’t automatically pick up environment variable changes. Production deployments must include worker restart procedures:

# Deployment script with queue worker management
php artisan queue:restart  # Signal workers to restart after current job
php artisan horizon:terminate  # For Laravel Horizon users

# Wait for workers to restart
sleep 10

# Verify workers are running with new configuration
php artisan queue:work --once --verbose

Supervisor Configuration: For non-containerized deployments using Supervisor, configuration files should reference environment variables:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/production-app/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-worker.log
environment=APP_ENV=production,APP_KEY=%(ENV_APP_KEY)s,DB_PASSWORD=%(ENV_DB_PASSWORD)s

Monitoring and Debugging

Production deployments using environment variable injection benefit from comprehensive monitoring to ensure proper configuration and identify issues quickly.

Configuration Validation: Implement configuration validation during deployment to catch missing or invalid environment variables before they cause runtime failures:

<?php
// app/Console/Commands/ValidateConfiguration.php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class ValidateConfiguration extends Command
{
    protected $signature = 'config:validate';
    protected $description = 'Validate critical configuration values';

    public function handle()
    {
        $errors = [];

        // Validate required configuration
        $required = [
            'APP_KEY' => config('app.key'),
            'DB_PASSWORD' => config('database.connections.mysql.password'),
            'REDIS_PASSWORD' => config('database.redis.default.password'),
        ];

        foreach ($required as $key => $value) {
            if (empty($value)) {
                $errors[] = "Missing required configuration: {$key}";
            }
        }

        // Validate configuration format
        if (!str_starts_with(config('app.key'), 'base64:')) {
            $errors[] = 'APP_KEY must be a valid Laravel application key';
        }

        if (strlen(config('database.connections.mysql.password')) < 12) {
            $errors[] = 'Database password does not meet minimum length requirements';
        }

        if (!empty($errors)) {
            $this->error('Configuration validation failed:');
            foreach ($errors as $error) {
                $this->line("  - {$error}");
            }
            return 1;
        }

        $this->info('Configuration validation passed');
        return 0;
    }
}

Health Check Endpoints: Implement health check endpoints that verify critical configuration without exposing sensitive values:

<?php
// routes/web.php

Route::get('/health', function () {
    $checks = [
        'database' => DB::connection()->getPdo() !== null,
        'redis' => Redis::connection()->ping(),
        'cache' => Cache::put('health_check', true, 10) && Cache::get('health_check'),
        'app_key' => !empty(config('app.key')),
    ];

    $healthy = !in_array(false, $checks, true);

    return response()->json([
        'status' => $healthy ? 'healthy' : 'unhealthy',
        'checks' => $checks,
        'timestamp' => now()->toISOString(),
    ], $healthy ? 200 : 503);
});

Common Pitfalls and Troubleshooting

Environment Variable Scope Issues

One of the most common issues when transitioning to environment variable injection involves understanding variable scope across different process types and deployment methods.

SSH Session vs. Service Context: Environment variables set in SSH sessions don’t automatically propagate to system services. This distinction is crucial for deployments that use systemd, supervisor, or other process managers:

# This works for the current SSH session but not for services
export APP_KEY="base64:..."
php artisan config:cache  # Works in SSH session

# This does NOT work for systemd services
systemctl restart my-app  # Service doesn't see the variable

# Correct approach: Set variables in service configuration
echo "Environment=APP_KEY=base64:..." >> /etc/systemd/system/my-app.service
systemctl daemon-reload
systemctl restart my-app

Container Environment Inheritance: In containerized environments, environment variables must be explicitly passed to containers. Variables available to the host or orchestration system don’t automatically become available inside containers:

# This doesn't work - variable not passed to container
export APP_KEY="base64:..."
docker run my-php-app

# Correct approach - explicitly pass environment variables
docker run -e APP_KEY="base64:..." my-php-app

Framework-Specific Configuration Issues

Laravel Artisan Commands: Some Laravel Artisan commands behave differently when configuration is cached vs. loaded from environment variables. This can cause confusion during deployment:

# If config is cached, this command uses cached values
php artisan config:show

# To see current environment variables (before caching)
php artisan tinker
>>> env('APP_KEY')

Symfony Environment Resolution: Symfony’s environment variable resolution can fail silently if variables aren’t available during container compilation:

# This will fail silently if DATABASE_URL isn't set during compilation
doctrine:
    dbal:
        url: '%env(DATABASE_URL)%'

# Better approach with defaults and validation
doctrine:
    dbal:
        url: '%env(default:mysql://localhost/app:DATABASE_URL)%'

Debugging Missing Variables

Variable Visibility Testing: Create debugging tools to verify environment variable availability across different contexts:

<?php
// Create a temporary debug endpoint or command

Route::get('/debug-env', function () {
    if (app()->environment('production')) {
        abort(404); // Never expose in production
    }

    $envVars = [
        'APP_ENV' => env('APP_ENV'),
        'APP_KEY' => env('APP_KEY') ? '[SET]' : '[MISSING]',
        'DB_PASSWORD' => env('DB_PASSWORD') ? '[SET]' : '[MISSING]',
        'REDIS_PASSWORD' => env('REDIS_PASSWORD') ? '[SET]' : '[MISSING]',
    ];

    return response()->json($envVars);
});

Process Environment Inspection: For system-level debugging, inspect environment variables of running processes:

# Find PHP-FPM process ID
pgrep php-fpm

# Inspect environment variables of specific process
cat /proc/[PID]/environ | tr '\0' '\n' | grep -E '^(APP_|DB_|REDIS_)'

# Alternative using ps
ps eww [PID] | tr ' ' '\n' | grep -E '^(APP_|DB_|REDIS_)'

Security Best Practices

Principle of Least Privilege

Environment variable injection should follow security principles that minimize exposure and limit access to sensitive information.

Process-Specific Variables: Different application components should receive only the environment variables they need:

# Kubernetes example with selective variable injection
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-web
spec:
  template:
    spec:
      containers:
      - name: web
        env:
        - name: APP_KEY
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: app-key
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secrets
              key: password
        # Web processes don't need queue-specific variables

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-queue
spec:
  template:
    spec:
      containers:
      - name: queue
        env:
        - name: APP_KEY
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: app-key
        - name: QUEUE_CONNECTION
          value: "redis"
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: redis-secrets
              key: password
        # Queue processes don't need database credentials

Temporal Access Control: For deployment scripts, implement just-in-time secret access that minimizes credential exposure duration:

#!/bin/bash
# Deployment script with temporary credential access

set -e

# Function to clean up credentials on exit
cleanup() {
    unset APP_KEY DB_PASSWORD REDIS_PASSWORD
    echo "Credentials cleared from environment"
}
trap cleanup EXIT

# Fetch credentials just before use
echo "Fetching deployment credentials..."
export APP_KEY=$(vault kv get -field=key secret/production/app)
export DB_PASSWORD=$(vault kv get -field=password secret/production/database)
export REDIS_PASSWORD=$(vault kv get -field=password secret/production/redis)

# Perform deployment
echo "Deploying application..."
php artisan migrate --force
php artisan config:cache

# Credentials automatically cleared by trap
echo "Deployment complete"

Credential Rotation

Environment variable injection facilitates automated credential rotation, a critical security practice for production systems.

Automated Rotation Strategy: Implement rotation procedures that update secrets and restart affected services:

#!/bin/bash
# Automated credential rotation script

VAULT_ADDR="https://vault.example.com"
SERVICE_NAME="php-app"

# Generate new database password
NEW_PASSWORD=$(openssl rand -base64 32)

# Update password in database
mysql -h "$DB_HOST" -u root -p"$DB_ROOT_PASSWORD" -e "
    ALTER USER 'app_user'@'%' IDENTIFIED BY '$NEW_PASSWORD';
    FLUSH PRIVILEGES;
"

# Store new password in Vault
vault kv put secret/production/database password="$NEW_PASSWORD"

# Update Kubernetes secret
kubectl create secret generic db-secrets \
    --from-literal=password="$NEW_PASSWORD" \
    --dry-run=client -o yaml | kubectl apply -f -

# Rolling restart to pick up new credentials
kubectl rollout restart deployment/"$SERVICE_NAME"

# Verify deployment health
kubectl rollout status deployment/"$SERVICE_NAME"

echo "Database password rotation completed successfully"

Audit and Compliance

Environment variable injection enables comprehensive audit trails when integrated with proper secret management systems.

Access Logging: Configure secret management systems to log all credential access:

{
  "timestamp": "2024-07-12T10:30:00Z",
  "user": "deployment-service-account",
  "action": "secret_read",
  "resource": "/production/database/password",
  "source_ip": "10.0.1.100",
  "user_agent": "vault-cli/1.14.0",
  "success": true
}

Compliance Validation: Implement automated compliance checks that verify deployment security practices:

#!/usr/bin/env python3
# Compliance validation script

import subprocess
import json
import sys

def check_no_env_files():
    """Verify no .env files exist in production directories"""
    result = subprocess.run([
        'find', '/var/www', '-name', '.env*', '-type', 'f'
    ], capture_output=True, text=True)

    if result.stdout.strip():
        print("FAIL: Found .env files in production:")
        print(result.stdout)
        return False

    print("PASS: No .env files found in production")
    return True

def check_secret_injection():
    """Verify environment variables are properly injected"""
    required_vars = ['APP_KEY', 'DB_PASSWORD', 'REDIS_PASSWORD']
    missing_vars = []

    for var in required_vars:
        if not os.environ.get(var):
            missing_vars.append(var)

    if missing_vars:
        print(f"FAIL: Missing required environment variables: {missing_vars}")
        return False

    print("PASS: All required environment variables present")
    return True

def check_file_permissions():
    """Verify application files have appropriate permissions"""
    result = subprocess.run([
        'find', '/var/www', '-name', '*.php', '-perm', '/o+w'
    ], capture_output=True, text=True)

    if result.stdout.strip():
        print("FAIL: Found world-writable PHP files:")
        print(result.stdout)
        return False

    print("PASS: No world-writable PHP files found")
    return True

def main():
    checks = [
        check_no_env_files,
        check_secret_injection,
        check_file_permissions,
    ]

    passed = 0
    total = len(checks)

    for check in checks:
        if check():
            passed += 1

    print(f"\nCompliance Check Results: {passed}/{total} passed")

    if passed == total:
        print("All compliance checks passed")
        sys.exit(0)
    else:
        print("Compliance violations detected")
        sys.exit(1)

if __name__ == '__main__':
    main()

Migration Strategy

Phased Transition Approach

Migrating from .env files to environment variable injection should follow a phased approach that minimizes disruption while ensuring security improvements.

Phase 1: Environment Preparation

  • Set up secret management infrastructure (Vault, AWS Parameter Store, etc.)
  • Create CI/CD pipeline modifications for secret injection
  • Implement configuration validation tools
  • Test deployment procedures in staging environments

Phase 2: Parallel Configuration

  • Maintain .env files while adding environment variable support
  • Implement fallback mechanisms that prefer environment variables over file-based configuration
  • Validate that all configuration paths work correctly with both methods

Phase 3: Production Deployment

  • Deploy applications with environment variable injection
  • Remove .env files from production servers
  • Implement monitoring to ensure proper configuration
  • Document rollback procedures in case of issues

Phase 4: Cleanup and Optimization

  • Remove .env fallback mechanisms from code
  • Optimize configuration loading for environment variables only
  • Implement automated compliance checking
  • Update documentation and team training

Code Migration Examples

Laravel Migration Example:

<?php
// Before: Direct env() usage
class DatabaseConfiguration
{
    public function getPassword()
    {
        return env('DB_PASSWORD', 'default_password');
    }
}

// After: Configuration-based approach with validation
class DatabaseConfiguration
{
    public function getPassword()
    {
        $password = config('database.connections.mysql.password');

        if (empty($password)) {
            throw new RuntimeException('Database password not configured');
        }

        return $password;
    }
}

// config/database.php
'mysql' => [
    'driver' => 'mysql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'database' => env('DB_DATABASE', 'forge'),
    'username' => env('DB_USERNAME', 'forge'),
    'password' => env('DB_PASSWORD', ''),
    // ... other configuration
],

Symfony Migration Example:

# Before: Simple environment variable usage
# config/packages/doctrine.yaml
doctrine:
    dbal:
        url: '%env(DATABASE_URL)%'

# After: Enhanced configuration with validation
# config/packages/doctrine.yaml
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
        charset: utf8mb4
        default_table_options:
            charset: utf8mb4
            collate: utf8mb4_unicode_ci

# config/services.yaml
parameters:
    env(DATABASE_URL): 'mysql://app:[email protected]:3306/app'

services:
    app.database_connection_validator:
        class: App\Service\DatabaseConnectionValidator
        arguments:
            $databaseUrl: '%env(resolve:DATABASE_URL)%'
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: validateConnection }

Conclusion

The transition from .env files to secure environment variable injection represents a fundamental shift in how PHP applications handle configuration in production environments. While .env files serve developers well during development, their persistence, visibility, and audit limitations make them unsuitable for production deployments where security, compliance, and operational excellence are paramount.

Modern deployment practices that embrace CI/CD-driven environment variable injection provide significant advantages: enhanced security through elimination of persistent credential files, improved audit capabilities through integration with secret management systems, simplified credential rotation procedures, and better alignment with cloud-native and containerized deployment patterns.

The implementation of secure deployment practices requires careful consideration of framework-specific behaviors, particularly around configuration caching, process lifecycle management, and environment variable scope. However, the operational and security benefits far outweigh the initial implementation complexity.

Organizations making this transition should adopt a phased approach that validates each step thoroughly, maintains fallback capabilities during migration, and implements comprehensive monitoring to ensure proper configuration management. The result is a more secure, auditable, and operationally robust application deployment process that aligns with modern security best practices and compliance requirements.

As PHP applications continue to evolve toward cloud-native architectures and enterprise deployment patterns, the adoption of secure environment variable injection practices becomes not just a best practice, but a necessity for maintaining competitive and compliant application delivery capabilities.