Add nginx vhosts, so hardening is not Apache-only - #97
Merged
Conversation
Web server hardening lived entirely in the two .htaccess files. nginx reads
neither: served from an nginx webroot with no configuration of its own, the
app hands out /.env and /.git/config to anyone who asks, and there was
nothing in the repo to copy from.
deploy/nginx/ adds the equivalent for both deployment shapes: a document root
at public/ (preferred, and safe by construction) and a document root fixed at
the repo root, which is what the Hostinger deploy uses and where the deny
rules are load-bearing. README.md maps each .htaccess directive to what
replaces it, and notes the nginx location-precedence rule that decides
whether a deny fires at all.
Three rules go beyond the .htaccess, and mapping the rules surfaced the gaps:
- vendor/ is denied. It is not in the .htaccess list and public/vendor/ does
not exist, so every request to that prefix reached Composer package source
under the webroot.
- packages/*.{php,json,lock,md,env,yml,sh} is denied. public/packages is a
tree of symlinks into vendor/zerp/<pkg>, so each module's own composer.json
sits behind it; the .htaccess rule covering those filenames is anchored at
the root and never reached them. Module images still serve.
- Only the front controller executes PHP, so a stray .php under the webroot
cannot be run.
deploy/ was missing from both deny lists and is added to the .htaccess too,
so Apache and nginx stay in step.
verify-hardening.sh runs the RELEASE_README checklist against a live host and
exits non-zero if anything is exposed, for either web server.
Both vhosts were checked with nginx -t and then exercised against a running
nginx over a representative tree: every sensitive path 403s, module images,
uploads and the build manifest still serve, and the front controller still
routes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #88.
Hardening lived entirely in the two
.htaccessfiles. nginx reads neither, and there was nothing in the repo to copy from, so an nginx deploy served/.envand/.git/configto anyone who asked.What's here
deploy/nginx/with one vhost per deployment shape:zerp-public-docroot.conf<repo>/publiczerp-repo-docroot.conf<repo>.htaccessis.Plus
README.mdwith a directive-by-directive mapping from.htaccess, andverify-hardening.sh, which runs the RELEASE_README checklist against a live host and exits non-zero if anything is exposed, so it can gate a deploy. It works against Apache too.README.mdanddeploy/RELEASE_README.mdnow say plainly that the.htaccesscheck does not apply on nginx, which was the trap.Gaps found while mapping the rules
Three rules go beyond what the
.htaccessdoes today:vendor/is denied. Not in the.htaccesslist, andpublic/vendor/does not exist, so nothing legitimate is served from that prefix while every request to it reached Composer package source under the webroot.packages/*.{php,json,lock,md,env,yml,sh}is denied.public/packagesis a tree of symlinks intovendor/zerp/<pkg>, so each module's owncomposer.jsonand source sit behind that prefix. The.htaccessrule covering those filenames is anchored at the root (^(composer\.(json|lock)|...)$) and never reached them. Confirmed:/packages/Pos/composer.jsonreturned 200 before this rule. Module images still serve.\.php$returns 403, so a stray.phpunder the webroot (an upload, a leftover installer) cannot run. Under Apache any such file executes.deploy/was in neither deny list, so it is added to the.htaccesstoo and Apache and nginx stay in step.publish.shstripsdeploy/from the release tree, so Hostinger is unaffected either way; a deploy straight from this repo was not.The vhosts are deliberately not published into
zerp-release: a.confsitting inpublic_htmlwould itself be fetchable. RELEASE_README links to them here instead.Verification
Not just
nginx -t. Both configs were run under a real nginx (1.31.3) against a tree representing the deployed layout, including thepublic/storagesymlink intostorage/app/publicand thepublic/packagesmodule symlinks. Every path was requested against both vhosts:*these are not under the webroot on the public-docroot vhost, so the app answers 404, which is as private as a deny.Worth noting from that table:
/index.phpreaches the front controller rather than being served as source (the naivetry_files $uri ...would have leaked the repo-root shim), directory URLs fall through to Laravel's 404 rather than 403-ing on autoindex and confirming the directory exists, and.well-knownstays reachable so ACME renewals keep working.No application code is touched.