Skip to content

Security Hardening Guide

Security Hardening Guide

Security hardening guide for Cbox containers in production environments.

Built-in Security Features

Cbox PHP Base Images come with security features enabled by default:

Nginx Security (Default Configuration)

Feature Status Description
Server version hidden Enabled server_tokens off - Nginx version not exposed
X-Frame-Options Enabled SAMEORIGIN - Prevents clickjacking
X-Content-Type-Options Enabled nosniff - Prevents MIME sniffing
Referrer-Policy Enabled strict-origin-when-cross-origin
Permissions-Policy Enabled Restricts browser features (camera, microphone, etc.)
X-XSS-Protection Removed Deprecated and can be exploited
Content-Security-Policy Opt-in Disabled by default - too application-specific
Health endpoint restricted Enabled /health only accessible from localhost
Sensitive files blocked Enabled .env, .git, composer.json, artisan, vendor/, etc. return 404
Hidden files blocked Enabled All /. paths return 404
Upload directory protection Enabled PHP execution blocked in upload directories

SSL/TLS Security (When Enabled)

Feature Default Description
Key strength RSA 4096 Strong key generation for self-signed certificates
Protocols TLSv1.2, TLSv1.3 Modern protocols only
Cipher suite Mozilla Modern ECDHE-based ciphers with forward secrecy
HSTS Enabled 1 year max-age with includeSubDomains
Session tickets Disabled Enhanced security for session resumption

For custom TLS configuration, use the Mozilla SSL Configuration Generator to generate nginx TLS directives appropriate for your environment.

Entrypoint Security

Feature Status Description
Input validation Enabled Boolean values and paths validated
Path traversal protection Enabled .. sequences blocked in file paths
Template injection prevention Enabled envsubst used instead of eval
Signal handling Enabled Graceful shutdown on SIGTERM/SIGINT/SIGQUIT

Security Checklist

Before Production

  • Disable PHP error display
  • Restrict dangerous PHP functions
  • Enable HTTPS/TLS
  • Security headers configured
  • Secrets stored securely (not in git)
  • Container runs as non-root
  • File permissions correct
  • Rate limiting enabled
  • CVE scanning enabled

PHP Security Configuration

Disable Error Display

services:
  app:
    environment:
      - PHP_DISPLAY_ERRORS=Off
      - PHP_DISPLAY_STARTUP_ERRORS=Off
      - PHP_LOG_ERRORS=On
      - PHP_ERROR_LOG=/proc/self/fd/2

Restrict Dangerous Functions

Create docker/php/security.ini:

[Security]
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,phpinfo
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
; open_basedir is NOT set here, and since 1.6 it is OFF by default: the
; restriction disables PHP's realpath cache, measured at -39% throughput on
; a real Laravel app (~3% on single-file endpoints, where it hides). It is
; still the right defense-in-depth where the app runs less-trusted code -
; with it on, an LFI in application code cannot read /proc/1/environ (every
; secret in the container). Enable per deployment with `PHP_OPEN_BASEDIR`
; (a `php_admin_value` on the pool - a value in THIS file is silently
; ignored in web requests), and keep the read-only kernel statistics in the
; list or `cboxdk/laravel-telemetry` collects nothing. The copy-paste list
; and the full performance-vs-security note:
; see docs/reference/environment-variables.md.

; Session security
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Strict"
session.use_strict_mode = 1
session.use_only_cookies = 1

Mount in docker-compose.yml:

services:
  app:
    volumes:
      - ./docker/php/security.ini:/usr/local/etc/php/conf.d/zz-security.ini:ro

For a complete reference on PHP security settings, see the PHP Security documentation.

Content Security Policy

CSP is disabled by default because it is too application-specific. Enable it via environment variable:

services:
  app:
    environment:
      # Example for Laravel with Livewire and Google Fonts
      - NGINX_HEADER_CSP=default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' wss:; frame-ancestors 'self'

For strict CSP or per-route CSP via Laravel middleware, configure at the application level. See the MDN CSP documentation for guidance on crafting policies.

Nginx Rate Limiting

# Define rate limit zones
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

server {
    limit_req zone=general burst=20 nodelay;

    location /login {
        limit_req zone=login burst=2 nodelay;
    }

    location /api/ {
        limit_req zone=api burst=50;
    }
}

Secrets Management

Environment Variables (Basic)

Never commit secrets to git. Add to .gitignore:

.env
.env.*
!.env.example
*.key
*.pem

Docker Secrets (Docker Swarm)

services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    secrets:
      - app_key
      - db_password
    environment:
      - APP_KEY_FILE=/run/secrets/app_key
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  app_key:
    external: true
  db_password:
    external: true

Read secrets in your application:

// Laravel - config/database.php
'password' => file_exists(env('DB_PASSWORD_FILE'))
    ? trim(file_get_contents(env('DB_PASSWORD_FILE')))
    : env('DB_PASSWORD'),

For Kubernetes secrets and Vault integration, see the Kubernetes Secrets documentation and your vault provider's docs.

Container Security

Run as Non-Root User

Every image tier ships in two variants:

Variant Tag suffix Runs as Web port Use when
Root (none) — e.g. 8.5-bookworm, latest root (PID 1, nginx master), workers drop to www-data 80 / 443 You need to bind privileged ports or remap PUID/PGID
Rootless -rootless — e.g. 8.5-bookworm-rootless www-data throughout 8080 Recommended — no root anywhere

⚠️ The default/latest tags run their init process and nginx master as root (workers still drop to www-data). If you don't need to bind port 80 or remap ownership, prefer the -rootless variant, or drop capabilities and add no-new-privileges (below) to a root image.

# Rootless variant — nothing runs as root
docker run -p 8080:8080 ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-rootless-v1
docker exec <container> whoami          # -> www-data

For a root image, verify the reduced privileges you've applied:

docker exec <container> ps -o user,comm   # workers should show www-data

Read-Only Root Filesystem

services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
      - /var/cache/nginx
    volumes:
      - ./:/var/www/html:ro
      - app-storage:/var/www/html/storage

Drop Unnecessary Capabilities & Block Privilege Escalation

services:
  app:
    security_opt:
      - no-new-privileges:true   # process can never gain more privileges
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # Only if binding to port <1024 (root image on :80)
      - CHOWN             # Only if using PUID/PGID remap
      - SETGID            # Only if dropping from root to www-data
      - SETUID            # Only if dropping from root to www-data

The rootless variant needs none of these — it binds :8080 and never switches user, so you can run it with cap_drop: [ALL] and no cap_add:

services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-rootless-v1
    ports:
      - "8080:8080"
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL

Network Isolation

services:
  app:
    networks:
      - frontend
      - backend

  mysql:
    networks:
      - backend  # Not exposed to frontend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

Pin Images by Digest

Rolling tags (8.5-bookworm) get weekly security rebuilds — great for staying patched, but the tag moves. For reproducible, tamper-evident deployments, pin the digest and update it deliberately:

services:
  app:
    # Resolve once: docker buildx imagetools inspect ghcr.io/.../php-fpm-nginx:8.5-bookworm
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1@sha256:<digest>

Verify the image signature before trusting a digest (images are cosign-signed with keyless OIDC — see SECURITY.md):

cosign verify \
  --certificate-identity-regexp 'https://github.com/cboxdk/php-baseimages/.github/workflows/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

Protect the cbox-init Management API

The multi-service image bundles the cbox-init process manager, which can expose a REST management API and Prometheus metrics. Both are disabled by default. If you enable them:

services:
  app:
    environment:
      - CBOX_INIT_API_ENABLED=true
      - CBOX_INIT_API_AUTH=${CBOX_INIT_API_TOKEN}   # always set a token
    # Never publish the API/metrics ports to untrusted networks. Prefer a
    # local-only bind (api_host: 127.0.0.1) and scrape metrics via a sidecar.

Never expose the management API on 0.0.0.0 without a bearer token — it can start/stop/scale processes. See the cbox-init docs for api_host/metrics_host and ACL/TLS options.

Supply Chain

Every published image carries, verifiable straight from the registry:

  • Cosign signature (keyless, GitHub OIDC) on the manifest list
  • SLSA provenance attestation (BuildKit mode=max): which workflow, commit, and build steps produced the image
  • SPDX SBOM attestation per platform: the full package inventory
# Inspect provenance and SBOM
docker buildx imagetools inspect \
  ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1 \
  --format '{{ json .Provenance }}'

docker buildx imagetools inspect \
  ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1 \
  --format '{{ json .SBOM }}'

CI vulnerability regression gate: every build fails if a fixable CRITICAL/HIGH CVE appears that is not in the repository's triaged .trivyignore baseline — the security posture cannot silently regress. The full (unfiltered) scan result is always uploaded to the GitHub Security tab, baseline included.

CVE Management

Weekly Security Updates

Cbox images are automatically rebuilt weekly (Mondays 03:00 UTC) with the latest upstream base image patches, PHP security updates, and OS security updates.

# Pull latest image and restart
docker pull ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
docker-compose build --pull
docker-compose up -d

Scanning with Trivy

Trivy detects vulnerabilities in OS packages, application dependencies, and container configuration.

# Scan Cbox image
trivy image ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

# Only HIGH and CRITICAL
trivy image --severity HIGH,CRITICAL ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

# Fail CI on critical vulnerabilities
trivy image --exit-code 1 --severity CRITICAL ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

Cbox CI workflows already include Trivy scanning. For setting up Trivy in your own CI pipeline, see the Trivy GitHub Action documentation.

Ignoring False Positives

Create a .trivyignore file for accepted risks:

# False positive in dev dependency
CVE-2024-12345

# Accepted risk - tracked in JIRA-123
CVE-2024-67890

PHP Dependency Scanning

# Using Symfony CLI
symfony security:check

# Or Enlightn Security Checker
docker-compose exec app composer require --dev enlightn/security-checker
docker-compose exec app php vendor/bin/security-checker security:check

Severity Response Guide

Severity Action
CRITICAL Immediate action - update base image or patch
HIGH Schedule update within 1 week
MEDIUM Address in next regular update cycle
LOW Monitor, address when convenient

Security Best Practices Checklist

Container Security

  • Run as non-root user (default in Cbox)
  • Read-only root filesystem where possible
  • Drop unnecessary capabilities
  • Regular security scanning
  • Minimal base image (use slim tier when possible)
  • No secrets in image layers

Network Security

  • HTTPS/TLS enabled
  • Network isolation configured
  • Rate limiting enabled

For detailed TLS configuration, use the Mozilla SSL Configuration Generator. For monitoring and alerting setup, see your observability platform's documentation.


Questions? Check common issues or ask in GitHub Discussions.