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
}
}