diff --git a/7.4/README.md b/7.4/README.md index c05f602e1..915f722a3 100644 --- a/7.4/README.md +++ b/7.4/README.md @@ -248,6 +248,11 @@ yourself: * The [MaxKeepAliveRequests](http://httpd.apache.org/docs/current/mod/core.html#maxkeepaliverequests) directive limits the number of requests allowed per connection when `KeepAlive` is on. If it is set to 0, unlimited requests will be allowed. * Default: 100 +* **HTTPD_ENABLE_REMOTEIP** + * When set to 1, enables [mod_remoteip](https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html) + to properly handle `X-Forwarded-For` headers from trusted reverse proxies (e.g., when running behind a load balancer or in Kubernetes). + This configures Apache to trust private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8). + * Default: not set (disabled) You can use a custom composer repository mirror URL to download packages instead of the default 'packagist.org': diff --git a/7.4/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh b/7.4/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh new file mode 100755 index 000000000..4a1215571 --- /dev/null +++ b/7.4/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh @@ -0,0 +1,19 @@ +# Enable mod_remoteip for X-Forwarded-For handling behind reverse proxies +# Activated by setting HTTPD_ENABLE_REMOTEIP=1 + +if [ "${HTTPD_ENABLE_REMOTEIP:-}" == "1" ]; then + log_info 'Enabling mod_remoteip for X-Forwarded-For handling...' + cat > "${HTTPD_CONFIGURATION_PATH}/remoteip.conf" <<'EOF' +# mod_remoteip - Handle X-Forwarded-For from trusted proxies +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html +LoadModule remoteip_module modules/mod_remoteip.so + +RemoteIPHeader X-Forwarded-For +# Private IP ranges - safe for Docker/Kubernetes internal networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 +RemoteIPTrustedProxy 169.254.0.0/16 +RemoteIPTrustedProxy 127.0.0.0/8 +EOF +fi diff --git a/8.0/README.md b/8.0/README.md index f905ec25e..e9f978af9 100644 --- a/8.0/README.md +++ b/8.0/README.md @@ -249,6 +249,11 @@ yourself: * The [MaxKeepAliveRequests](http://httpd.apache.org/docs/current/mod/core.html#maxkeepaliverequests) directive limits the number of requests allowed per connection when `KeepAlive` is on. If it is set to 0, unlimited requests will be allowed. * Default: 100 +* **HTTPD_ENABLE_REMOTEIP** + * When set to 1, enables [mod_remoteip](https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html) + to properly handle `X-Forwarded-For` headers from trusted reverse proxies (e.g., when running behind a load balancer or in Kubernetes). + This configures Apache to trust private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8). + * Default: not set (disabled) You can use a custom composer repository mirror URL to download packages instead of the default 'packagist.org': diff --git a/8.0/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh b/8.0/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh new file mode 100755 index 000000000..4a1215571 --- /dev/null +++ b/8.0/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh @@ -0,0 +1,19 @@ +# Enable mod_remoteip for X-Forwarded-For handling behind reverse proxies +# Activated by setting HTTPD_ENABLE_REMOTEIP=1 + +if [ "${HTTPD_ENABLE_REMOTEIP:-}" == "1" ]; then + log_info 'Enabling mod_remoteip for X-Forwarded-For handling...' + cat > "${HTTPD_CONFIGURATION_PATH}/remoteip.conf" <<'EOF' +# mod_remoteip - Handle X-Forwarded-For from trusted proxies +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html +LoadModule remoteip_module modules/mod_remoteip.so + +RemoteIPHeader X-Forwarded-For +# Private IP ranges - safe for Docker/Kubernetes internal networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 +RemoteIPTrustedProxy 169.254.0.0/16 +RemoteIPTrustedProxy 127.0.0.0/8 +EOF +fi diff --git a/8.1/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh b/8.1/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh new file mode 100755 index 000000000..4a1215571 --- /dev/null +++ b/8.1/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh @@ -0,0 +1,19 @@ +# Enable mod_remoteip for X-Forwarded-For handling behind reverse proxies +# Activated by setting HTTPD_ENABLE_REMOTEIP=1 + +if [ "${HTTPD_ENABLE_REMOTEIP:-}" == "1" ]; then + log_info 'Enabling mod_remoteip for X-Forwarded-For handling...' + cat > "${HTTPD_CONFIGURATION_PATH}/remoteip.conf" <<'EOF' +# mod_remoteip - Handle X-Forwarded-For from trusted proxies +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html +LoadModule remoteip_module modules/mod_remoteip.so + +RemoteIPHeader X-Forwarded-For +# Private IP ranges - safe for Docker/Kubernetes internal networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 +RemoteIPTrustedProxy 169.254.0.0/16 +RemoteIPTrustedProxy 127.0.0.0/8 +EOF +fi diff --git a/8.2/README.md b/8.2/README.md index 2db8551c5..dfd2a2330 100644 --- a/8.2/README.md +++ b/8.2/README.md @@ -249,6 +249,11 @@ yourself: * The [MaxKeepAliveRequests](http://httpd.apache.org/docs/current/mod/core.html#maxkeepaliverequests) directive limits the number of requests allowed per connection when `KeepAlive` is on. If it is set to 0, unlimited requests will be allowed. * Default: 100 +* **HTTPD_ENABLE_REMOTEIP** + * When set to 1, enables [mod_remoteip](https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html) + to properly handle `X-Forwarded-For` headers from trusted reverse proxies (e.g., when running behind a load balancer or in Kubernetes). + This configures Apache to trust private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8). + * Default: not set (disabled) You can use a custom composer repository mirror URL to download packages instead of the default 'packagist.org': diff --git a/8.2/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh b/8.2/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh new file mode 100755 index 000000000..4a1215571 --- /dev/null +++ b/8.2/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh @@ -0,0 +1,19 @@ +# Enable mod_remoteip for X-Forwarded-For handling behind reverse proxies +# Activated by setting HTTPD_ENABLE_REMOTEIP=1 + +if [ "${HTTPD_ENABLE_REMOTEIP:-}" == "1" ]; then + log_info 'Enabling mod_remoteip for X-Forwarded-For handling...' + cat > "${HTTPD_CONFIGURATION_PATH}/remoteip.conf" <<'EOF' +# mod_remoteip - Handle X-Forwarded-For from trusted proxies +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html +LoadModule remoteip_module modules/mod_remoteip.so + +RemoteIPHeader X-Forwarded-For +# Private IP ranges - safe for Docker/Kubernetes internal networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 +RemoteIPTrustedProxy 169.254.0.0/16 +RemoteIPTrustedProxy 127.0.0.0/8 +EOF +fi diff --git a/8.3/README.md b/8.3/README.md index b87ab74be..1dbacf050 100644 --- a/8.3/README.md +++ b/8.3/README.md @@ -252,6 +252,11 @@ yourself: * The [MaxKeepAliveRequests](http://httpd.apache.org/docs/current/mod/core.html#maxkeepaliverequests) directive limits the number of requests allowed per connection when `KeepAlive` is on. If it is set to 0, unlimited requests will be allowed. * Default: 100 +* **HTTPD_ENABLE_REMOTEIP** + * When set to 1, enables [mod_remoteip](https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html) + to properly handle `X-Forwarded-For` headers from trusted reverse proxies (e.g., when running behind a load balancer or in Kubernetes). + This configures Apache to trust private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8). + * Default: not set (disabled) You can use a custom composer repository mirror URL to download packages instead of the default 'packagist.org': diff --git a/8.3/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh b/8.3/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh new file mode 100755 index 000000000..4a1215571 --- /dev/null +++ b/8.3/root/usr/share/container-scripts/php/pre-start/40-remoteip.sh @@ -0,0 +1,19 @@ +# Enable mod_remoteip for X-Forwarded-For handling behind reverse proxies +# Activated by setting HTTPD_ENABLE_REMOTEIP=1 + +if [ "${HTTPD_ENABLE_REMOTEIP:-}" == "1" ]; then + log_info 'Enabling mod_remoteip for X-Forwarded-For handling...' + cat > "${HTTPD_CONFIGURATION_PATH}/remoteip.conf" <<'EOF' +# mod_remoteip - Handle X-Forwarded-For from trusted proxies +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html +LoadModule remoteip_module modules/mod_remoteip.so + +RemoteIPHeader X-Forwarded-For +# Private IP ranges - safe for Docker/Kubernetes internal networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 +RemoteIPTrustedProxy 169.254.0.0/16 +RemoteIPTrustedProxy 127.0.0.0/8 +EOF +fi diff --git a/test/run-pytest b/test/run-pytest index 35fca6794..43bf9635f 100755 --- a/test/run-pytest +++ b/test/run-pytest @@ -12,4 +12,7 @@ PYTHON_VERSION="3.12" if [[ ! -f "/usr/bin/python$PYTHON_VERSION" ]]; then PYTHON_VERSION="3.13" fi +if [[ ! -f "/usr/bin/python$PYTHON_VERSION" ]]; then + PYTHON_VERSION="3.14" +fi cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/test/test_container_application.py b/test/test_container_application.py index 396f160fc..387a3a76c 100644 --- a/test/test_container_application.py +++ b/test/test_container_application.py @@ -192,3 +192,113 @@ def test_npm_works(self): Test checks if NPM is valid and works properly """ assert self.s2i_app.npm_works(image_name=VARS.IMAGE_NAME) + + +class TestHTTPDRemoteIPContainer: + def setup_method(self): + container_args = "-e HTTPD_ENABLE_REMOTEIP=1" + self.s2i_app = build_s2i_app(test_app, container_args=container_args) + + def teardown_method(self): + self.s2i_app.cleanup() + + def test_remoteip_config_created(self): + """ + Test checks if remoteip.conf is created when HTTPD_ENABLE_REMOTEIP=1 + and contains the expected Apache mod_remoteip configuration. + """ + cid_file_name = self.s2i_app.app_name + assert self.s2i_app.create_container(cid_file_name=cid_file_name, container_args="--user=100001") + assert ContainerImage.wait_for_cid(cid_file_name=cid_file_name) + cid = self.s2i_app.get_cid(cid_file_name=cid_file_name) + assert cid + + # Wait for container to fully start and run pre-start scripts + import time + time.sleep(5) + + # Verify remoteip.conf exists and contains expected directives + file_content = PodmanCLIWrapper.podman_get_file_content( + cid_file_name=cid, + filename="/opt/app-root/etc/conf.d/remoteip.conf" + ) + assert file_content, "remoteip.conf file should exist" + + # Check for required mod_remoteip directives + required_directives = [ + "LoadModule remoteip_module modules/mod_remoteip.so", + "RemoteIPHeader X-Forwarded-For", + "RemoteIPTrustedProxy 10.0.0.0/8", + "RemoteIPTrustedProxy 172.16.0.0/12", + "RemoteIPTrustedProxy 192.168.0.0/16", + "RemoteIPTrustedProxy 169.254.0.0/16", + "RemoteIPTrustedProxy 127.0.0.0/8", + ] + + for directive in required_directives: + assert directive in file_content, f"Missing directive: {directive}" + + def test_remoteip_container_runs(self): + """ + Test checks if container with HTTPD_ENABLE_REMOTEIP=1 starts successfully + and serves content properly. + """ + cid_file_name = self.s2i_app.app_name + assert self.s2i_app.create_container(cid_file_name=cid_file_name, container_args="--user=100001") + assert ContainerImage.wait_for_cid(cid_file_name=cid_file_name) + cid = self.s2i_app.get_cid(cid_file_name=cid_file_name) + assert cid + + # Wait for container to fully start + import time + time.sleep(5) + + cip = self.s2i_app.get_cip(cid_file_name=cid_file_name) + + # Only test HTTP responses if container has an IP (may not work in some rootless environments) + if cip: + # Verify container is serving content properly + assert self.s2i_app.test_response(url=cip) + + # Verify HTTPS also works + assert self.s2i_app.test_response( + url=f"https://{cip}", port=8443 + ) + + +class TestHTTPDRemoteIPDisabledContainer: + def setup_method(self): + # Build without HTTPD_ENABLE_REMOTEIP environment variable + self.s2i_app = build_s2i_app(test_app) + + def teardown_method(self): + self.s2i_app.cleanup() + + def test_remoteip_disabled_by_default(self): + """ + Test checks if mod_remoteip is NOT enabled by default + when HTTPD_ENABLE_REMOTEIP is not set. + """ + cid_file_name = self.s2i_app.app_name + assert self.s2i_app.create_container(cid_file_name=cid_file_name, container_args="--user=100001") + assert ContainerImage.wait_for_cid(cid_file_name=cid_file_name) + cid = self.s2i_app.get_cid(cid_file_name=cid_file_name) + assert cid + + # Wait for container to fully start + import time + time.sleep(5) + + # Verify remoteip.conf does NOT exist + result = PodmanCLIWrapper.podman_exec_shell_command( + cid_file_name=cid, + cmd="test -f /opt/app-root/etc/conf.d/remoteip.conf && echo exists || echo missing" + ) + assert "missing" in result, "remoteip.conf should not exist when HTTPD_ENABLE_REMOTEIP is not set" + + # Verify container still runs properly without remoteip + cip = self.s2i_app.get_cip(cid_file_name=cid_file_name) + + # Only test HTTP response if container has an IP (may not work in some rootless environments) + if cip: + assert self.s2i_app.test_response(url=cip)