generated from bisco/codex-bootstrap
merge: security hardening into wp
This commit is contained in:
@@ -81,6 +81,7 @@ docker compose run --rm --no-deps proxy nginx -t
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build -d db wordpress proxy
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --rm security-tests
|
||||
docker compose config --quiet
|
||||
```
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ browser tests.
|
||||
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
||||
- `wp-cli`: opt-in bootstrap and maintenance service.
|
||||
- `tests/functional`: Playwright tests running only through the public virtual host.
|
||||
- `tests/security`: containerized static checks for security-sensitive Compose and
|
||||
Apache assumptions.
|
||||
|
||||
See [architecture](docs/architecture.md) and
|
||||
[ADR-0001](docs/adr/0001-wordpress-single-page.md).
|
||||
@@ -224,6 +226,7 @@ docker compose run --rm --no-deps proxy nginx -t
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build -d db wordpress proxy
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --rm security-tests
|
||||
docker compose config --quiet
|
||||
```
|
||||
|
||||
|
||||
@@ -42,6 +42,21 @@ services:
|
||||
- no-new-privileges:true
|
||||
pids_limit: 200
|
||||
|
||||
security-tests:
|
||||
image: wordpress:cli-2.12.0-php8.3
|
||||
init: true
|
||||
user: "33:33"
|
||||
command: ["/bin/sh", "/workspace/tests/security/check-compose.sh"]
|
||||
working_dir: /workspace
|
||||
volumes:
|
||||
- .:/workspace:ro
|
||||
profiles: ["test"]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
pids_limit: 50
|
||||
|
||||
volumes:
|
||||
test_db_data:
|
||||
test_wordpress_data:
|
||||
|
||||
@@ -67,8 +67,6 @@ services:
|
||||
networks:
|
||||
- web
|
||||
- data
|
||||
ports:
|
||||
- 8000:80
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
NGINX is the only public entry point for `azionelab.org`. It proxies HTTP to the
|
||||
official WordPress 7.0/PHP 8.3 Apache image over the private `web` network. WordPress
|
||||
connects to MariaDB 11.8 LTS over a separate internal `data` network. Neither WordPress
|
||||
nor MariaDB publishes a host port.
|
||||
nor MariaDB publishes a host port; automated security checks guard this assumption.
|
||||
|
||||
The custom `azionelab` classic theme renders the public single page. Theme modifications
|
||||
store the hero, manifesto, laboratory, teacher, lesson, and contact fields. The
|
||||
@@ -16,6 +16,10 @@ activates the theme, configures the site, and creates realistic demo content. Ce
|
||||
another optional service, enabled only for direct deployments. It shares challenge and
|
||||
certificate volumes with NGINX but has no container-control access.
|
||||
|
||||
Apache includes a small defense-in-depth hardening file that denies uploaded PHP files,
|
||||
direct `wp-config.php` requests, and direct access to selected internal WordPress PHP
|
||||
paths even if a future routing mistake bypasses NGINX.
|
||||
|
||||
Persistent state lives in host-based bind mounts configured by `DB_DATA_PATH`,
|
||||
`WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and `CERTBOT_CHALLENGES_PATH`.
|
||||
Functional tests replace the database and WordPress mounts with isolated Docker test
|
||||
|
||||
+5
-3
@@ -9,8 +9,9 @@
|
||||
- File editing is always disabled. Production also disables web-based core, theme, and
|
||||
plugin changes; patched images are rebuilt and redeployed instead.
|
||||
- XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
|
||||
and direct `wp-config.php` requests, and rate-limits login/public requests. Public
|
||||
REST user enumeration and author archives are disabled.
|
||||
and direct `wp-config.php` requests, and rate-limits login/public requests. Apache
|
||||
also denies uploaded PHP files and direct access to sensitive WordPress internals as
|
||||
defense in depth. Public REST user enumeration and author archives are disabled.
|
||||
- Security headers include CSP, same-origin framing, content-type protection, a strict
|
||||
referrer policy, and a restrictive Permissions Policy. WordPress compatibility still
|
||||
requires inline style/script CSP allowances; do not treat this CSP as an XSS sanitizer.
|
||||
@@ -20,7 +21,8 @@
|
||||
no Docker socket, dropped capabilities, and narrowly scoped volumes.
|
||||
- Containers are not privileged and do not use host networking or the Docker socket.
|
||||
WordPress/Apache retains the capabilities needed by the official image internally,
|
||||
but no WordPress port is published.
|
||||
but no WordPress port is published. A containerized security test fails if the
|
||||
WordPress service is configured with host-published ports.
|
||||
- Persistent state uses host-based bind mounts. Keep those paths outside the public web
|
||||
root, restrict host access, never make them world-writable, and run
|
||||
`./scripts/prepare-host-volumes.sh` when paths or image user IDs change.
|
||||
|
||||
+4
-1
@@ -10,6 +10,7 @@ docker compose run --rm --no-deps proxy nginx -t
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build -d db wordpress proxy
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --rm security-tests
|
||||
docker compose config --quiet
|
||||
LETSENCRYPT_ENABLED=1 docker compose config --quiet
|
||||
```
|
||||
@@ -19,7 +20,9 @@ override replaces database and WordPress state with isolated Docker volumes, map
|
||||
`azionelab.org` to the internal proxy, and never publishes an extra port. Browser tests
|
||||
cover content and section order, contact actions, mobile overflow/navigation, semantic
|
||||
landmarks, image alternatives, admin routing, security headers, blocked sensitive
|
||||
routes, and unknown virtual hosts.
|
||||
routes, blocked uploaded PHP requests, and unknown virtual hosts. Security checks also
|
||||
assert that WordPress does not publish host ports and that Apache hardening remains
|
||||
installed in the WordPress image.
|
||||
|
||||
Subjective visual review and a real-device accessibility audit remain manual release
|
||||
checks.
|
||||
|
||||
@@ -101,6 +101,7 @@ test("protects the edge and exposes the WordPress admin", async ({ page, request
|
||||
expect((await request.get("/xmlrpc.php")).status()).toBe(403);
|
||||
expect((await request.get("/.env")).status()).toBe(404);
|
||||
expect((await request.get("/wp-config.php")).status()).toBe(404);
|
||||
expect((await request.get("/wp-content/uploads/probe.php")).status()).toBe(403);
|
||||
expect((await request.get("/wp-json/wp/v2/users")).status()).toBe(404);
|
||||
|
||||
await page.goto("/wp-admin/");
|
||||
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
compose_file="${1:-/workspace/docker-compose.yml}"
|
||||
|
||||
awk '
|
||||
/^[[:space:]]{2}wordpress:/ {
|
||||
in_wordpress = 1
|
||||
next
|
||||
}
|
||||
in_wordpress && /^[[:space:]]{2}[A-Za-z0-9_-]+:/ {
|
||||
in_wordpress = 0
|
||||
}
|
||||
in_wordpress && /^[[:space:]]{4}ports:/ {
|
||||
print "The wordpress service must not publish host ports; route traffic through proxy only." > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
' "$compose_file"
|
||||
|
||||
grep -q 'azionelab-apache-hardening.conf' /workspace/wordpress/Dockerfile || {
|
||||
echo "The WordPress image must install Apache hardening rules." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q 'wp-content/(uploads|files)' /workspace/wordpress/apache-hardening.conf || {
|
||||
echo "Apache hardening must block PHP execution below uploads/files." >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -4,10 +4,12 @@ COPY php.ini /usr/local/etc/php/conf.d/azionelab.ini
|
||||
COPY .htaccess /opt/azionelab/.htaccess
|
||||
COPY entrypoint-wrapper.sh /usr/local/bin/azionelab-entrypoint
|
||||
COPY healthcheck.php /usr/local/bin/azionelab-healthcheck.php
|
||||
COPY apache-hardening.conf /etc/apache2/conf-available/azionelab-apache-hardening.conf
|
||||
COPY theme/azionelab /opt/azionelab/theme
|
||||
COPY mu-plugins/azionelab-content.php /opt/azionelab/azionelab-content.php
|
||||
|
||||
RUN sed -ri 's!^[[:space:]]*CustomLog .*!CustomLog /dev/null combined!' /etc/apache2/sites-available/000-default.conf \
|
||||
&& a2enconf azionelab-apache-hardening \
|
||||
&& chmod 755 /usr/local/bin/azionelab-entrypoint \
|
||||
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \
|
||||
&& chown -R www-data:www-data /opt/azionelab
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
ServerTokens Prod
|
||||
ServerSignature Off
|
||||
TraceEnable Off
|
||||
|
||||
<LocationMatch "^/wp-content/(uploads|files)/.*\.php$">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/wp-config\.php$">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/wp-admin/includes/">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/wp-includes/[^/]+\.php$">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/wp-includes/js/tinymce/langs/.+\.php$">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/wp-includes/theme-compat/">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
Reference in New Issue
Block a user