Skip to content

fix(apache): harden the default configuration - #49

Merged
zebby76 merged 1 commit into
Smals-Webtech:mainfrom
zebby76:fix/apache-harden-default-configuration
Sep 3, 2026
Merged

fix(apache): harden the default configuration#49
zebby76 merged 1 commit into
Smals-Webtech:mainfrom
zebby76:fix/apache-harden-default-configuration

Conversation

@zebby76

@zebby76 zebby76 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Six defaults in the apache variant, all measured on smalswebtech/base-php:8.5.9-apache with an
index.php, a .env, a config.yml, a .git/config and a .well-known/probe.txt in the
docroot.

Note the image renders its own httpd.conf from config/apache2/httpd.conf.tmpl; the
distribution file is not used, so everything below is ours.

request before after
GET / 200Index of / 200 — front controller
GET /.env 200APP_SECRET=very-secret 404
GET /.git/config 200[remote "origin"] url = git@internal:app.git 404
GET /.well-known/probe.txt 200 200
GET /index.php 200 200
TRACE / 200 405
error page footer <address>Apache Server at … Port …</address> none
AH00558 at startup 2 0
SCRIPT_FILENAME //app/var/www/html/index.php /app/var/www/html/index.php

What changed and why

Directory listing was on, and the PHP front controller was not a DirectoryIndex candidate.
GET / returned a browsable listing of the docroot rather than running the application. Options
loses Indexes, and index.php joins the list — which is what the nginx variant already does
with index index.php.

Dotfiles were served. The rule is a RedirectMatch, not a Require, for two reasons:

  • it matches any path component, so a file inside a dot directory is covered — .git/config
    slips through a FilesMatch, its own name being an ordinary one;
  • it answers 404, the same as the nginx variant now does. Require all denied gives 403,
    which confirms the file exists and leaves the two images disagreeing about the same request.

/.well-known keeps its normal handling. A FilesMatch stays behind the redirect as an
authorisation-layer backstop, and as the broader form of the .ht* rule it replaces.

TRACE answered 200. It echoes the request back and has no use here.

ServerSignature defaulted to On — the server version and port in the footer of every error
page, while ServerTokens was already Prod. The two settings disagreed about the same
disclosure.

Every start logged AH00558, apache having no ServerName to work from; it then guesses one
from the container address, which lands in self-referential redirects. New APACHE_SERVER_NAME,
default default.localhost, matching the nginx server_name.

The FastCGI handler URL carried a trailing slash, so SCRIPT_FILENAME reached php-fpm as
//app/var/www/html/index.php. PHP realpath()s it, but it is in every access log line and some
frameworks compare that string.

Not broken by this

/status, /server-status and /real-time-status still answer, and the rendered configuration
passes httpd -t (Syntax OK).

/server-info answers 404 before and aftermod_info is not among the modules the
template loads, so its <IfModule> block has never been active. Left alone here; it belongs with
the module list, which is a separate change.

Six defaults, all measured on smalswebtech/base-php:8.5.9-apache with an
index.php, a .env, a config.yml, a .git/config and a .well-known/probe.txt in
the docroot.

Directory listing was on and the PHP front controller was not a DirectoryIndex
candidate, so GET / returned "Index of /" listing every one of those files as a
link rather than running the application. Options loses Indexes and index.php
joins the DirectoryIndex list, which is what the nginx variant already does.

Dotfiles were served: .env answered 200 with its contents, and so did
.git/config. The rule is a RedirectMatch rather than a Require, for two reasons.
It matches any path component, so a file inside a dot directory is covered --
.git/config would otherwise slip through a FilesMatch, its own name being an
ordinary one. And it answers 404, the same as the nginx variant now does, which
neither confirms what exists nor leaves the two images disagreeing about the
same request. /.well-known keeps its normal handling. A FilesMatch stays behind
it as an authorisation-layer backstop and as the broader form of the .ht* rule
it replaces.

TRACE answered 200. It echoes the request back and has no use here.

ServerSignature defaulted to On, printing the server version and port in the
footer of every error page while ServerTokens was already Prod -- the two
settings disagreed about the same disclosure.

Every start logged AH00558, apache having no ServerName to work from; it then
guesses one from the container address, which lands in self-referential
redirects. There is now an APACHE_SERVER_NAME variable, default
default.localhost, matching the nginx server_name.

The FastCGI handler URL carried a trailing slash, so SCRIPT_FILENAME reached
php-fpm as //app/var/www/html/index.php. PHP realpath()s it, but it is in every
access log line and some frameworks compare that string.

  request                     before   after
  GET /                       200 listing        200 front controller
  GET /.env                   200 contents       404
  GET /.git/config            200 contents       404
  GET /.well-known/probe.txt  200                200
  GET /index.php              200                200
  TRACE /                     200                405
  error page footer           <address>Apache Server at ... Port ...</address>   none
  AH00558 at startup          2                  0
  SCRIPT_FILENAME             //app/var/...      /app/var/...

/status, /server-status and /real-time-status still answer, and the rendered
configuration passes httpd -t. /server-info answers 404 before and after this
change: mod_info is not among the modules the template loads, so its IfModule
block has never been active. Left alone here, it belongs with the module list.
@zebby76

zebby76 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on main so the new bats job (#47) runs against it — the apache jobs now execute tests.web.bats, which confirms the hardening breaks nothing the suite already covers.

The assertions for this change (docroot not browsable, dotfiles 404, TRACE) are held back deliberately: #48 introduces the shared test plumbing they need — the web_put/web_status helpers and the docroot fixtures — and adding them here in parallel would collide in the same file. They land here as a follow-up commit once #48 is merged, and the same commit flips #48's skip guard so the dotfile assertions cover the apache variant too.

@zebby76
zebby76 force-pushed the fix/apache-harden-default-configuration branch from fb07adf to 7e430aa Compare September 3, 2026 09:22
@zebby76
zebby76 merged commit 33c1dc8 into Smals-Webtech:main Sep 3, 2026
19 checks passed
zebby76 added a commit that referenced this pull request Sep 3, 2026
Six defaults, all measured on smalswebtech/base-php:8.5.9-apache with an
index.php, a .env, a config.yml, a .git/config and a .well-known/probe.txt in
the docroot.

Directory listing was on and the PHP front controller was not a DirectoryIndex
candidate, so GET / returned "Index of /" listing every one of those files as a
link rather than running the application. Options loses Indexes and index.php
joins the DirectoryIndex list, which is what the nginx variant already does.

Dotfiles were served: .env answered 200 with its contents, and so did
.git/config. The rule is a RedirectMatch rather than a Require, for two reasons.
It matches any path component, so a file inside a dot directory is covered --
.git/config would otherwise slip through a FilesMatch, its own name being an
ordinary one. And it answers 404, the same as the nginx variant now does, which
neither confirms what exists nor leaves the two images disagreeing about the
same request. /.well-known keeps its normal handling. A FilesMatch stays behind
it as an authorisation-layer backstop and as the broader form of the .ht* rule
it replaces.

TRACE answered 200. It echoes the request back and has no use here.

ServerSignature defaulted to On, printing the server version and port in the
footer of every error page while ServerTokens was already Prod -- the two
settings disagreed about the same disclosure.

Every start logged AH00558, apache having no ServerName to work from; it then
guesses one from the container address, which lands in self-referential
redirects. There is now an APACHE_SERVER_NAME variable, default
default.localhost, matching the nginx server_name.

The FastCGI handler URL carried a trailing slash, so SCRIPT_FILENAME reached
php-fpm as //app/var/www/html/index.php. PHP realpath()s it, but it is in every
access log line and some frameworks compare that string.

  request                     before   after
  GET /                       200 listing        200 front controller
  GET /.env                   200 contents       404
  GET /.git/config            200 contents       404
  GET /.well-known/probe.txt  200                200
  GET /index.php              200                200
  TRACE /                     200                405
  error page footer           <address>Apache Server at ... Port ...</address>   none
  AH00558 at startup          2                  0
  SCRIPT_FILENAME             //app/var/...      /app/var/...

/status, /server-status and /real-time-status still answer, and the rendered
configuration passes httpd -t. /server-info answers 404 before and after this
change: mod_info is not among the modules the template loads, so its IfModule
block has never been active. Left alone here, it belongs with the module list.
@zebby76
zebby76 deleted the fix/apache-harden-default-configuration branch September 4, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant