Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/nginx.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: CI

concurrency:
group: nginx-ci-${{ github.ref }}
cancel-in-progress: true

on:
pull_request:
branches: [ master ]
workflow_dispatch:

jobs:
nginx-edge-config:
name: 'Nginx edge config'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Render ERB config
run: PORT=3000 ruby -rerb -e 'File.write("nginx-rendered.conf", ERB.new(File.read("config/nginx.conf.erb")).result)'

- name: Syntax check rendered config
run: docker run --rm --entrypoint nginx -v "$PWD/nginx-rendered.conf:/etc/nginx/nginx.conf:ro" nginx:alpine -t -c /etc/nginx/nginx.conf

- name: Smoke test location matching
run: |
docker run --rm -d --entrypoint nginx --name nginx-smoke -p 3000:3000 -v "$PWD/nginx-rendered.conf:/etc/nginx/nginx.conf:ro" nginx:alpine -c /etc/nginx/nginx.conf
for i in $(seq 1 30); do
curl -s -o /dev/null http://127.0.0.1:3000/ && break
sleep 0.5
done
fail=0
# Scanner junk must 404 at the edge
for path in /.env /.env.local /.env.production /.git/config /.aws/credentials /.ssh/id_rsa /key.pem /id_rsa /rclone.conf /docker-compose.yml /secrets.env /wp-json /backup /Dockerfile /__vite_rsc_findSourceMapURL /nested/whatever.conf; do
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:3000$path")
if [ "$code" != "404" ]; then
echo "FAIL: $path expected 404, got $code"
fail=1
fi
done
# Legitimate paths must not be edge-blocked. There is no Rails
# upstream in this container, so anything other than an edge 404
# (typically 502) proves the request was passed through.
# /api/event is deliberately absent: it is an exact-match proxy to
# plausible.io, which itself answers 404 for GET; /js/script.js
# already proves the = locations win over the regex blocks.
for path in /sitemap.xml.gz /robots.txt /favicon.ico /400.html /london /assets/app.js /tom-select/x.js /uploads/x.jpg /js/script.js; do
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:3000$path")
if [ "$code" == "404" ]; then
echo "FAIL: $path must not be edge-blocked, got 404"
fail=1
fi
done
docker rm -f nginx-smoke > /dev/null
exit $fail
17 changes: 17 additions & 0 deletions config/nginx.conf.erb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ http {
proxy_buffering on;
}

# Scanner junk: 404 at the edge. All of these already 404 in the app
# (the /:id chapter catch-all or the Rails router); blocking here keeps
# the load off Rails. Exact (=) locations above terminate the location
# search before these regex blocks are tried; the regexes in turn beat
# the prefix location below.
# Sensitive or unknown file extensions, with real asset dirs excluded.
# Not anchored to a single segment, so nested probes are caught too.
location ~* "^/(?!(?:assets|uploads|static|tom-select|packs)(?:/|$)|sitemap\.xml\.gz(?:$|/))(?:[^/]+/)*[^/]*\.(?:json|js|yml|yaml|conf|env|pem|key|p12|pfx|crt|bak|sql|sqlite|db|tfstate|swp|old|zip|tar|gz|tgz|7z|php|cgi|asp|aspx|jsp)$|(?:[^/]+/)*\.env(?:\.[^/]*)?(?:$|/)|^/\.git(?:/|$)|^/\.aws(?:/|$)|^/\.ssh(?:/|$)" {
return 404;
}

# Extension-less probes seen in the logs. Anchored to a single segment
# or directory prefix so real chapter pages (/london etc.) are safe.
location ~* "^/(?:__vite_rsc_findSourceMapURL|z9x8c7v6b5-debug-trigger-codebar\.io|debug-trigger|userfiles|wp-(?:json|content|admin|config|login|includes)|id_(?:rsa|dsa|ecdsa|ed25519)|private[-_]?key|backup|Dockerfile|__debug__)(?:$|/)" {
return 404;
}

# Rails: All other requests
location / {
proxy_pass http://app_server;
Expand Down