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 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 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 --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
|
docker compose config --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ browser tests.
|
|||||||
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
||||||
- `wp-cli`: opt-in bootstrap and maintenance service.
|
- `wp-cli`: opt-in bootstrap and maintenance service.
|
||||||
- `tests/functional`: Playwright tests running only through the public virtual host.
|
- `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
|
See [architecture](docs/architecture.md) and
|
||||||
[ADR-0001](docs/adr/0001-wordpress-single-page.md).
|
[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 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 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 --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
|
docker compose config --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,21 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
pids_limit: 200
|
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:
|
volumes:
|
||||||
test_db_data:
|
test_db_data:
|
||||||
test_wordpress_data:
|
test_wordpress_data:
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
- data
|
- data
|
||||||
ports:
|
|
||||||
- 8000:80
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
NGINX is the only public entry point for `azionelab.org`. It proxies HTTP to the
|
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
|
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
|
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
|
The custom `azionelab` classic theme renders the public single page. Theme modifications
|
||||||
store the hero, manifesto, laboratory, teacher, lesson, and contact fields. The
|
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
|
another optional service, enabled only for direct deployments. It shares challenge and
|
||||||
certificate volumes with NGINX but has no container-control access.
|
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`,
|
Persistent state lives in host-based bind mounts configured by `DB_DATA_PATH`,
|
||||||
`WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and `CERTBOT_CHALLENGES_PATH`.
|
`WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and `CERTBOT_CHALLENGES_PATH`.
|
||||||
Functional tests replace the database and WordPress mounts with isolated Docker test
|
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
|
- File editing is always disabled. Production also disables web-based core, theme, and
|
||||||
plugin changes; patched images are rebuilt and redeployed instead.
|
plugin changes; patched images are rebuilt and redeployed instead.
|
||||||
- XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
|
- 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
|
and direct `wp-config.php` requests, and rate-limits login/public requests. Apache
|
||||||
REST user enumeration and author archives are disabled.
|
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
|
- Security headers include CSP, same-origin framing, content-type protection, a strict
|
||||||
referrer policy, and a restrictive Permissions Policy. WordPress compatibility still
|
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.
|
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.
|
no Docker socket, dropped capabilities, and narrowly scoped volumes.
|
||||||
- Containers are not privileged and do not use host networking or the Docker socket.
|
- 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,
|
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
|
- 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
|
root, restrict host access, never make them world-writable, and run
|
||||||
`./scripts/prepare-host-volumes.sh` when paths or image user IDs change.
|
`./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 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 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 --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
|
docker compose config --quiet
|
||||||
LETSENCRYPT_ENABLED=1 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
|
`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
|
cover content and section order, contact actions, mobile overflow/navigation, semantic
|
||||||
landmarks, image alternatives, admin routing, security headers, blocked sensitive
|
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
|
Subjective visual review and a real-device accessibility audit remain manual release
|
||||||
checks.
|
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("/xmlrpc.php")).status()).toBe(403);
|
||||||
expect((await request.get("/.env")).status()).toBe(404);
|
expect((await request.get("/.env")).status()).toBe(404);
|
||||||
expect((await request.get("/wp-config.php")).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);
|
expect((await request.get("/wp-json/wp/v2/users")).status()).toBe(404);
|
||||||
|
|
||||||
await page.goto("/wp-admin/");
|
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 .htaccess /opt/azionelab/.htaccess
|
||||||
COPY entrypoint-wrapper.sh /usr/local/bin/azionelab-entrypoint
|
COPY entrypoint-wrapper.sh /usr/local/bin/azionelab-entrypoint
|
||||||
COPY healthcheck.php /usr/local/bin/azionelab-healthcheck.php
|
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 theme/azionelab /opt/azionelab/theme
|
||||||
COPY mu-plugins/azionelab-content.php /opt/azionelab/azionelab-content.php
|
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 \
|
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 755 /usr/local/bin/azionelab-entrypoint \
|
||||||
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \
|
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \
|
||||||
&& chown -R www-data:www-data /opt/azionelab
|
&& 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