diff --git a/.github/workflows/action_update-dockerhub-readme.yml b/.github/workflows/action_update-dockerhub-readme.yml index b7ebb6c98..9f848712a 100644 --- a/.github/workflows/action_update-dockerhub-readme.yml +++ b/.github/workflows/action_update-dockerhub-readme.yml @@ -14,7 +14,7 @@ jobs: name: Push README to Docker Hub steps: - name: git checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: main diff --git a/.github/workflows/scheduled-task_update-sponsors.yml b/.github/workflows/scheduled-task_update-sponsors.yml index 6461482da..3d1d233b1 100644 --- a/.github/workflows/scheduled-task_update-sponsors.yml +++ b/.github/workflows/scheduled-task_update-sponsors.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout đŸ›Žī¸ - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Generate Sponsors 💖 uses: JamesIves/github-sponsors-readme-action@v1 diff --git a/.github/workflows/service_docker-build-and-publish.yml b/.github/workflows/service_docker-build-and-publish.yml index dbd4d7fa5..7c3a36dd1 100644 --- a/.github/workflows/service_docker-build-and-publish.yml +++ b/.github/workflows/service_docker-build-and-publish.yml @@ -39,7 +39,7 @@ jobs: php-version-map-json: ${{ steps.get-php-versions.outputs.php-version-map-json }} steps: - name: Check out code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} @@ -67,25 +67,25 @@ jobs: echo "${MATRIX_JSON}" | jq '.' - name: Upload the php-versions.yml file - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: php-versions.yml path: ${{ inputs.php-versions-file }} docker-publish: needs: setup-matrix - runs-on: depot-ubuntu-24.04-4 + runs-on: depot-ubuntu-24.04-8 strategy: matrix: ${{fromJson(needs.setup-matrix.outputs.php-version-map-json)}} steps: - name: Check out code. - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - name: Download PHP Versions file - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: php-versions.yml path: ./artifacts diff --git a/scripts/conf/php-versions-base-config.yml b/scripts/conf/php-versions-base-config.yml index 373f0897d..03394995a 100644 --- a/scripts/conf/php-versions-base-config.yml +++ b/scripts/conf/php-versions-base-config.yml @@ -35,42 +35,43 @@ php_versions: - minor: "8.1" base_os: - name: alpine3.21 + - name: alpine3.22 - name: bookworm - name: trixie patch_versions: - # - 8.1.28 # Pull latest from Official PHP source + # - 8.1.34 # Pull latest from Official PHP source - minor: "8.2" base_os: - - name: alpine3.21 - name: alpine3.22 + - name: alpine3.23 - name: bookworm - name: trixie patch_versions: - # - 8.2.18 # Pull latest from Official PHP source + # - 8.2.30 # Pull latest from Official PHP source - minor: "8.3" base_os: - - name: alpine3.21 - name: alpine3.22 + - name: alpine3.23 - name: bookworm - name: trixie patch_versions: - # - 8.3.6 # Pull latest from Official PHP source + # - 8.3.29 # Pull latest from Official PHP source - minor: "8.4" base_os: - - name: alpine3.21 - name: alpine3.22 + - name: alpine3.23 - name: bookworm - name: trixie patch_versions: - # - 8.4.1 # Pull latest from Official PHP source + # - 8.4.16 # Pull latest from Official PHP source - minor: "8.5" base_os: - - name: alpine3.21 - name: alpine3.22 + - name: alpine3.23 - name: bookworm - name: trixie patch_versions: - # - 8.5.0 # Pull latest from Official PHP source + # - 8.5.1 # Pull latest from Official PHP source operating_systems: - family: alpine @@ -94,27 +95,31 @@ operating_systems: - name: "Alpine 3.20" version: alpine3.20 number: 3.20 - nginx_version: 1.28.0-r1 + nginx_version: 1.28.1-r1 - name: "Alpine 3.21" version: alpine3.21 number: 3.21 - nginx_version: 1.28.0-r1 + nginx_version: 1.28.1-r1 - name: "Alpine 3.22" version: alpine3.22 number: 3.22 - nginx_version: 1.28.0-r1 + nginx_version: 1.28.1-r1 + - name: "Alpine 3.23" + version: alpine3.23 + number: 3.23 + nginx_version: 1.28.1-r1 - family: debian default: true versions: - name: "Debian Bullseye" version: bullseye number: 11 - nginx_version: 1.28.0-1~bullseye + nginx_version: 1.28.1-1~bullseye - name: "Debian Bookworm" version: bookworm number: 12 - nginx_version: 1.28.0-1~bookworm + nginx_version: 1.28.1-1~bookworm - name: "Debian Trixie" version: trixie number: 13 - nginx_version: 1.28.0-1~trixie + nginx_version: 1.28.1-1~trixie diff --git a/src/common/etc/entrypoint.d/0-container-info.sh b/src/common/etc/entrypoint.d/0-container-info.sh index a71de57ca..b92ee2bdb 100644 --- a/src/common/etc/entrypoint.d/0-container-info.sh +++ b/src/common/etc/entrypoint.d/0-container-info.sh @@ -55,7 +55,8 @@ Brought to you by serversideup.net â€ĸ Upload Limit: '"$UPLOAD_LIMIT"' 🔄 Runtime -â€ĸ Docker CMD: '"$DOCKER_CMD"' +â€ĸ Automations: '"$AUTORUN_ENABLED"' +â€ĸ Docker CMD: '"$DOCKER_CMD"' ' if [ "$PHP_OPCACHE_STATUS" = "0" ]; then diff --git a/src/common/etc/entrypoint.d/50-laravel-automations.sh b/src/common/etc/entrypoint.d/50-laravel-automations.sh index 1e86e0348..3634c0a12 100644 --- a/src/common/etc/entrypoint.d/50-laravel-automations.sh +++ b/src/common/etc/entrypoint.d/50-laravel-automations.sh @@ -53,10 +53,6 @@ fi ############################################################################ artisan_migrate() { - migrate_flags="" - - debug_log "Starting migrations (isolation: $AUTORUN_LARAVEL_MIGRATION_ISOLATION)" - echo "🚀 Clearing Laravel cache before attempting migrations..." php "$APP_BASE_DIR/artisan" config:clear @@ -73,7 +69,8 @@ artisan_migrate() { ;; esac - # Build migration flags (used for all databases) + # Determine if isolation is intended to be used + isolation_enabled="false" if [ "$AUTORUN_LARAVEL_MIGRATION_ISOLATION" = "true" ]; then # Isolation only works in default mode if [ "$AUTORUN_LARAVEL_MIGRATION_MODE" != "default" ]; then @@ -82,14 +79,18 @@ artisan_migrate() { fi # Isolation requires Laravel 9.38.0+ - if ! laravel_version_is_at_least "9.38.0"; then - echo "❌ $script_name: Isolated migrations require Laravel v9.38.0 or above. Detected version: $(get_laravel_version)" - return 1 + if laravel_version_is_at_least "9.38.0"; then + isolation_enabled="true" + debug_log "Isolation mode enabled (Laravel version check passed)" + else + echo "âš ī¸ $script_name: Isolated migrations require Laravel v9.38.0 or above. Detected version: $(get_laravel_version)" + echo " Continuing without isolation mode..." fi - - migrate_flags="$migrate_flags --isolated" fi + # Start assembling migration flags + migrate_flags="" + if [ "$AUTORUN_LARAVEL_MIGRATION_FORCE" = "true" ]; then migrate_flags="$migrate_flags --force" fi @@ -98,30 +99,55 @@ artisan_migrate() { migrate_flags="$migrate_flags --seed" fi - # Determine if multiple databases are specified + # Helper function to run migrations for a specific database + run_migration_for_db() { + db_name="${1:-}" + + # Build display name and database flag for messages/commands + if [ -n "$db_name" ]; then + db_display_name="'$db_name'" + db_flag="--database=$db_name" + else + db_display_name="default database" + db_flag="" + fi + + # Wait for database connection + if ! wait_for_database_connection $db_name; then + echo "❌ $script_name: Failed to connect to $db_display_name" + return 1 + fi + + # Determine if --isolated can be used for this database + db_migrate_flags="$migrate_flags" + if [ "$isolation_enabled" = "true" ]; then + if db_has_migrations_table $db_name; then + db_migrate_flags="$db_migrate_flags --isolated" + debug_log "Using --isolated flag for $db_display_name" + else + echo "â„šī¸ Skipping --isolated flag for $db_display_name: migrations table not ready (normal for first deployment)" + echo " The --isolated flag will be used on subsequent deployments." + fi + fi + + echo "🚀 Running migrations for $db_display_name" + php "$APP_BASE_DIR/artisan" $migration_command $db_flag $db_migrate_flags + } + + # Run migrations for specified database(s) if [ -n "$AUTORUN_LARAVEL_MIGRATION_DATABASE" ]; then databases=$(convert_comma_delimited_to_space_separated "$AUTORUN_LARAVEL_MIGRATION_DATABASE") database_list=$(echo "$databases" | tr ',' ' ') for db in $database_list; do - # Wait for this specific database to be ready - if ! wait_for_database_connection "$db"; then - echo "❌ $script_name: Failed to connect to database: $db" + if ! run_migration_for_db "$db"; then return 1 fi - - echo "🚀 Running migrations for database: $db" - php "$APP_BASE_DIR/artisan" $migration_command --database=$db $migrate_flags done else - # Wait for default database connection - if ! wait_for_database_connection; then - echo "❌ $script_name: Failed to connect to default database" + if ! run_migration_for_db; then return 1 fi - - # Run migration with default database connection - php "$APP_BASE_DIR/artisan" $migration_command $migrate_flags fi } @@ -241,17 +267,16 @@ get_laravel_version() { fi debug_log "Detecting Laravel version..." - # Use 2>/dev/null to handle potential PHP warnings - artisan_version_output=$(php "$APP_BASE_DIR/artisan" --version 2>/dev/null) - # Check if command was successful - if [ $? -ne 0 ]; then + # Capture artisan output + if ! artisan_version_output=$(php "$APP_BASE_DIR/artisan" --version 2>/dev/null); then echo "❌ $script_name: Failed to execute artisan command" >&2 return 1 fi + debug_log "Raw artisan output: $artisan_version_output" + # Extract version number using sed (POSIX compliant) - # Using a more strict pattern that matches "Laravel Framework X.Y.Z" laravel_version=$(echo "$artisan_version_output" | sed -e 's/^Laravel Framework \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*$/\1/') # Validate that we got a version number (POSIX compliant regex) @@ -261,7 +286,7 @@ get_laravel_version() { echo "$laravel_version" return 0 else - echo "❌ $script_name: Failed to determine Laravel version" >&2 + echo "❌ $script_name: Failed to determine Laravel version from: $artisan_version_output" >&2 return 1 fi } @@ -286,33 +311,55 @@ laravel_version_is_at_least() { return 1 fi - # Validate required version format - if ! echo "$required_version" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then - echo "❌ $script_name - Invalid version requirement format: $required_version" >&2 - return 1 - fi - current_version=$(get_laravel_version) if [ $? -ne 0 ]; then echo "❌ $script_name: Failed to get Laravel version" >&2 return 1 fi - # normalize_version() takes a version string and ensures it has 3 parts - normalize_version() { - echo "$1" | awk -F. '{ print $1"."$2"."(NF>2?$3:0) }' - } + # Extract version components using cut (POSIX compliant) + cur_major=$(echo "$current_version" | cut -d. -f1) + cur_minor=$(echo "$current_version" | cut -d. -f2) + cur_patch=$(echo "$current_version" | cut -d. -f3) + + req_major=$(echo "$required_version" | cut -d. -f1) + req_minor=$(echo "$required_version" | cut -d. -f2) + req_patch=$(echo "$required_version" | cut -d. -f3) - normalized_current=$(normalize_version "$current_version") - normalized_required=$(normalize_version "$required_version") + # Default patch to 0 if not specified + : "${cur_patch:=0}" + : "${req_patch:=0}" - # Use sort -V to get the lower version, then compare it with required version - # This works in BusyBox because we only need to check the first line of output - lowest_version=$(printf '%s\n%s\n' "$normalized_required" "$normalized_current" | sort -V | head -n1) - if [ "$lowest_version" = "$normalized_required" ]; then - return 0 # Success: current version is >= required version + # Numeric comparison (POSIX arithmetic expansion handles this correctly) + # Compare major version + if [ "$cur_major" -gt "$req_major" ]; then + return 0 + elif [ "$cur_major" -lt "$req_major" ]; then + return 1 + fi + + # Major versions equal, compare minor + if [ "$cur_minor" -gt "$req_minor" ]; then + return 0 + elif [ "$cur_minor" -lt "$req_minor" ]; then + return 1 + fi + + # Minor versions equal, compare patch + if [ "$cur_patch" -ge "$req_patch" ]; then + return 0 + fi + + return 1 +} + +db_has_migrations_table() { + database_arg="${1:-}" + + if [ -n "$database_arg" ]; then + php "$APP_BASE_DIR/artisan" migrate:status --database="$database_arg" > /dev/null 2>&1 else - return 1 # Failure: current version is < required version + php "$APP_BASE_DIR/artisan" migrate:status > /dev/null 2>&1 fi } @@ -324,9 +371,9 @@ test_db_connection() { # Pass database connection name only if specified (not empty) database_arg="${1:-}" if [ -n "$database_arg" ]; then - php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" "$AUTORUN_LARAVEL_MIGRATION_ISOLATION" "$database_arg" + php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" "$database_arg" else - php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" "$AUTORUN_LARAVEL_MIGRATION_ISOLATION" + php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" fi } diff --git a/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php b/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php index e4817fd3b..0c0b52259 100644 --- a/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php +++ b/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php @@ -5,12 +5,11 @@ * This script tests if the Laravel application can connect to its configured database. * It's designed to be called from shell scripts during container initialization. * - * Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [migration_isolation] [database_connection] + * Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [database_connection] * * Arguments: - * app_base_dir - Path to Laravel application root - * migration_mode - Migration mode: 'default', 'fresh', or 'refresh' (optional, defaults to 'default') - * migration_isolation - Whether to run migrations in isolation (optional, defaults to 'false') + * app_base_dir - Path to Laravel application root + * migration_mode - Migration mode: 'default', 'fresh', or 'refresh' (optional, defaults to 'default') * database_connection - Name of the database connection to test (optional, defaults to 'default') * * Exit codes: @@ -21,15 +20,14 @@ */ // Validate arguments -if ($argc < 2 || $argc > 5) { - fwrite(STDERR, "Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [migration_isolation] [database_connection]\n"); +if ($argc < 2 || $argc > 4) { + fwrite(STDERR, "Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [database_connection]\n"); exit(1); } $appBaseDir = $argv[1]; $migrationMode = $argc >= 3 ? $argv[2] : 'default'; -$migrationIsolation = $argc >= 4 ? $argv[3] : 'false'; -$databaseConnection = $argc >= 5 ? $argv[4] : null; +$databaseConnection = $argc >= 4 ? $argv[3] : null; // Validate migration mode $validModes = ['default', 'fresh', 'refresh']; @@ -38,13 +36,6 @@ exit(1); } -// Validate migration isolation -$validIsolations = ['true', 'false']; -if (!in_array($migrationIsolation, $validIsolations)) { - fwrite(STDERR, "Error: Invalid migration isolation '{$migrationIsolation}'. Must be one of: " . implode(', ', $validIsolations) . "\n"); - exit(1); -} - // Validate that the app base directory exists if (!is_dir($appBaseDir)) { fwrite(STDERR, "Error: App base directory does not exist: {$appBaseDir}\n"); @@ -126,17 +117,7 @@ exit(1); } - // For isolated migrations, the database file must exist (even in default mode) - if ($migrationIsolation === 'true') { - fwrite(STDERR, "SQLite database file does not exist: {$dbPath}\n"); - fwrite(STDERR, "Isolated migrations require the database file to exist before running.\n"); - fwrite(STDERR, "Either:\n"); - fwrite(STDERR, " 1. Create the database (ensure it has read and write permissions for your user): touch {$dbPath}\n"); - fwrite(STDERR, " 2. Set AUTORUN_LARAVEL_MIGRATION_ISOLATION=false to let migrations create it\n"); - exit(1); - } - - // Directory exists and is writable - migrations can create the database file (default mode only) + // Directory exists and is writable - migrations can create the database file fwrite(STDOUT, "SQLite database directory is ready - migrations will create database\n"); exit(0); } diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer index 2d7ddac48..a3c3cfc04 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer @@ -11,7 +11,7 @@ script_name="docker-php-serversideup-install-php-ext-installer" ############ # Environment variables ############ -PHP_EXT_INSTALLER_VERSION="2.9.18" +PHP_EXT_INSTALLER_VERSION="2.9.27" ############ # Main diff --git a/src/variations/fpm-apache/etc/apache2/conf-available/security.conf b/src/variations/fpm-apache/etc/apache2/conf-available/security.conf index 43957f476..f572f2501 100644 --- a/src/variations/fpm-apache/etc/apache2/conf-available/security.conf +++ b/src/variations/fpm-apache/etc/apache2/conf-available/security.conf @@ -1,98 +1,83 @@ +## +# Security Configuration +## + +# This configuration follows security best practices from: # -# Disable access to the entire file system except for the directories that -# are explicitly allowed later. +# H5BP Server Configs (Apache) +# https://github.com/h5bp/server-configs-apache # -# This currently breaks the configurations that come with some web application -# Debian packages. +# OWASP Secure Headers Project +# https://owasp.org/www-project-secure-headers/ # -# -# AllowOverride None -# Require all denied -# - +# RFC 8615 - Well-Known URIs +# https://www.rfc-editor.org/rfc/rfc8615 +# +# ############################################################################## -# Changing the following options will not really affect the security of the -# server, but might make attacks slightly more difficult in some cases. +# ------------------------------------------------------------------------------ +# | Server Software Information | +# ------------------------------------------------------------------------------ -# -# ServerTokens -# This directive configures what you return as the Server HTTP response -# Header. The default is 'Full' which sends information about the OS-Type -# and compiled in modules. -# Set to one of: Full | OS | Minimal | Minor | Major | Prod -# where Full conveys the most information, and Prod the least. -#ServerTokens Minimal -# ServerTokens OS -# #ServerTokens Full +# Minimize information sent about the server +# https://httpd.apache.org/docs/current/mod/core.html#servertokens ServerTokens Prod -# -# Optionally add a line containing the server version and virtual host -# name to server-generated pages (internal error documents, FTP directory -# listings, mod_status and mod_info output etc., but not CGI generated -# documents or custom error documents). -# Set to "EMail" to also include a mailto: link to the ServerAdmin. -# Set to one of: On | Off | EMail +# Disable server signature on error pages +# https://httpd.apache.org/docs/current/mod/core.html#serversignature ServerSignature Off -# ServerSignature On -# -# Allow TRACE method -# -# Set to "extended" to also reflect the request body (only for testing and -# diagnostic purposes). -# -# Set to one of: On | Off | extended +# Disable TRACE HTTP method to prevent XST attacks +# https://owasp.org/www-community/attacks/Cross_Site_Tracing TraceEnable Off -#TraceEnable On -# -# Forbid access to version control directories -# -# If you use version control systems in your document root, you should -# probably deny access to their directories. For example, for subversion: -# - - Require all denied +# ------------------------------------------------------------------------------ +# | Security Headers | +# ------------------------------------------------------------------------------ + +# Prevent clickjacking attacks by disabling iframe embedding +# https://owasp.org/www-project-secure-headers/#x-frame-options +Header always set X-Frame-Options "SAMEORIGIN" + +# Prevent MIME type sniffing attacks +# https://owasp.org/www-project-secure-headers/#x-content-type-options +Header always set X-Content-Type-Options "nosniff" + +# Control referrer information sent with requests +# https://owasp.org/www-project-secure-headers/#referrer-policy +Header always set Referrer-Policy "strict-origin-when-cross-origin" + +# Enable HTTP Strict Transport Security (HSTS) +# https://owasp.org/www-project-secure-headers/#strict-transport-security +Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" + +# ------------------------------------------------------------------------------ +# | File Access Restrictions | +# ------------------------------------------------------------------------------ + +# Block PHP execution in storage directory to prevent uploaded malicious PHP files from running +# Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) + + Require all denied + + +# Block access to all hidden files and directories (dotfiles) +# EXCEPT for the "/.well-known/" directory which is required by RFC 8615 +# for ACME challenges, security.txt, and other standardized endpoints. +# https://www.rfc-editor.org/rfc/rfc8615 +# https://github.com/h5bp/server-configs-apache + + Require all denied -# Prevent Apache from serving Gitlab files - - Require all denied +# Block access to files that may expose sensitive information +# Based on H5BP server configs: https://github.com/h5bp/server-configs-apache + + Require all denied # Disable XML-RPC on all wordpress sites Require all denied # allow from xxx.xxx.xxx.xxx - - -# -# Setting this header will prevent MSIE from interpreting files as something -# else than declared by the content type in the HTTP headers. -# Requires mod_headers to be enabled. -# -Header always set X-Content-Type-Options: "nosniff" - -# -# Setting this header will prevent other sites from embedding pages from this -# site as frames. This defends against clickjacking attacks. -# Requires mod_headers to be enabled. -# -Header always set X-Frame-Options: "sameorigin" - -# -# Referrer policy -# -Header always set Referrer-Policy "no-referrer-when-downgrade" - -# -# Content Security Policy -# UPDATE - September 2020: Commenting this out until we grasp better security requirements -# -#Header always set Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" - -# -# Strict-Transport-Security Policy (set HSTS) -# -Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" \ No newline at end of file + \ No newline at end of file diff --git a/src/variations/fpm-nginx/etc/nginx/server-opts.d/security.conf b/src/variations/fpm-nginx/etc/nginx/server-opts.d/security.conf index e90562983..19986ee33 100644 --- a/src/variations/fpm-nginx/etc/nginx/server-opts.d/security.conf +++ b/src/variations/fpm-nginx/etc/nginx/server-opts.d/security.conf @@ -1,24 +1,51 @@ +## +# Security Configuration +## + +# This configuration follows security best practices from: +# +# H5BP Server Configs (nginx) +# https://github.com/h5bp/server-configs-nginx # -# Security Headers +# OWASP Secure Headers Project +# https://owasp.org/www-project-secure-headers/ # +# RFC 8615 - Well-Known URIs +# https://www.rfc-editor.org/rfc/rfc8615 +# +# ############################################################################## -# Prevent IFRAME spoofing attacks +# Prevent clickjacking attacks by disabling iframe embedding +# https://owasp.org/www-project-secure-headers/#x-frame-options add_header X-Frame-Options "SAMEORIGIN" always; -# Prevent MIME attacks +# Prevent MIME type sniffing attacks +# https://owasp.org/www-project-secure-headers/#x-content-type-options add_header X-Content-Type-Options "nosniff" always; -# Prevent Referrer URL from being leaked -add_header Referrer-Policy "no-referrer-when-downgrade" always; - -# Configure Content Security Policy -# UPDATE - September 2020: Commenting this out until we grasp better security requirements -#add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; +# Control referrer information sent with requests +# https://owasp.org/www-project-secure-headers/#referrer-policy +add_header Referrer-Policy "strict-origin-when-cross-origin" always; -# Enable HSTS +# Enable HTTP Strict Transport Security (HSTS) +# https://owasp.org/www-project-secure-headers/#strict-transport-security add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; -# Prevent access to . files (the well-known directory) +# ------------------------------------------------------------------------------ +# | File Access Restrictions | +# ------------------------------------------------------------------------------ + +# Block access to hidden files and directories (dotfiles) +# EXCEPT for the "/.well-known/" directory which is required by RFC 8615 +# for ACME challenges, security.txt, and other standardized endpoints. +# https://www.rfc-editor.org/rfc/rfc8615 +# https://github.com/h5bp/server-configs-nginx location ~ /\.(?!well-known) { deny all; +} + +# Block access to files that may expose sensitive information +# Based on H5BP server configs: https://github.com/h5bp/server-configs-nginx +location ~* (?:#.*#|\.(?:bak|conf|config|dist|inc|ini|log|sh|sql|sw[op])|~)$ { + deny all; } \ No newline at end of file diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template index 1d6ee8e6d..08a90ff96 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template @@ -30,6 +30,12 @@ location / { try_files $uri $uri/ /index.php?$query_string; } +# Block PHP execution in storage directory to prevent uploaded malicious PHP files from running +# Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) +location ~* ^/storage/.*\.php$ { + deny all; +} + # Pass "*.php" files to PHP-FPM location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template index 0685ac17c..810ff0747 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template @@ -36,6 +36,12 @@ location / { try_files $uri $uri/ /index.php?$query_string; } +# Block PHP execution in storage directory to prevent uploaded malicious PHP files from running +# Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) +location ~* ^/storage/.*\.php$ { + deny all; +} + # Pass "*.php" files to PHP-FPM location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 50813be46..f9c1ed677 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -2,7 +2,7 @@ ARG BASE_OS_VERSION='trixie' ARG PHP_VERSION='8.5' ARG BASE_IMAGE="php:${PHP_VERSION}-zts-${BASE_OS_VERSION}" -ARG FRANKENPHP_VERSION='1.10.1' +ARG FRANKENPHP_VERSION='1.11.1' ARG GOLANG_VERSION='1.25' ######################## diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index 566911126..50b2158be 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -127,24 +127,43 @@ fd00::/8 \ } (security) { - # Reject dot files and certain file extensions - @rejected path *.bak *.conf *.dist *.fla *.ini *.inc *.inci *.log *.orig *.psd *.sh *.sql *.swo *.swp *.swop */.* - - # Return 403 Forbidden for rejected files + # This configuration follows security best practices from: + # + # H5BP Server Configs (nginx) - Adapted for Caddy + # https://github.com/h5bp/server-configs-nginx + # + # OWASP Secure Headers Project + # https://owasp.org/www-project-secure-headers/ + # + # RFC 8615 - Well-Known URIs + # https://www.rfc-editor.org/rfc/rfc8615 + + # Block PHP execution in storage directory to prevent uploaded malicious PHP files from running + # Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) + @storage-php path_regexp ^/storage/.*\.php$ + respond @storage-php 403 + + # Block access to files that may expose sensitive information + @rejected { + path *.bak *.conf *.config *.dist *.inc *.ini *.log *.sh *.sql *.swp *.swo *~ */.* + # EXCEPTION: /.well-known/* is allowed per RFC 8615 for ACME challenges + # https://www.rfc-editor.org/rfc/rfc8615 + not path /.well-known/* + } respond @rejected 403 - # Security headers + # Security Headers + # https://owasp.org/www-project-secure-headers/ header { defer - # Prevent IFRAME spoofing attacks + # Prevent clickjacking attacks by disabling iframe embedding X-Frame-Options "SAMEORIGIN" - # Prevent MIME type sniffing + # Prevent MIME type sniffing attacks X-Content-Type-Options "nosniff" - # Prevent referrer leakage + # Control referrer information sent with requests Referrer-Policy "strict-origin-when-cross-origin" - # Prevent server header leakage + # Remove server identification headers -Server - # Prevent powered by header leakage -X-Powered-By } }