corentin_wakdo/docker/apache/vhost.conf
Corentin JOGUET 6c6a34db9f
All checks were successful
CI / secret-scan (push) Successful in 10s
CI / php-lint (push) Successful in 25s
CI / static-tests (push) Successful in 52s
CI / js-tests (push) Successful in 28s
fix(borne): passerelle /api same-origin sur le vhost kiosk (#62)
2026-06-19 16:15:41 +02:00

183 lines
8 KiB
Text

#
# Wakdo - vhosts applicatifs
#
# Un seul conteneur Apache derriere Traefik sert **les deux** FQDN :
# - APP_HOST_KIOSK -> /var/www/html/public/borne (borne client, Bloc 1)
# - APP_HOST_ADMIN -> /var/www/html/public/admin (back-office + API, Bloc 2)
#
# Comme Traefik termine TLS en amont et communique en HTTP clair avec Apache
# sur le reseau docker, les vhosts ecoutent sur :80 et font confiance aux
# headers X-Forwarded-* fournis par Traefik.
#
# Le SetHandler "proxy:fcgi://wakdo-app:9000" est le coeur du reverse FastCGI :
# toute requete *.php est relayee au pool PHP-FPM via TCP sur le reseau interne
# wakdo_internal. wakdo-app n'est JAMAIS joignable directement depuis l'exterieur.
#
# === Healthcheck interne Docker (non expose publiquement) ===
# Listen sans ServerName = catch-all. Utilise par le HEALTHCHECK du Dockerfile.
# Sert un fichier statique healthz.txt (body = "OK\n") au lieu d'un
# RewriteRule [R=200] qui declenchait le template ErrorDocument generique
# et faisait apparaitre la mention "internal error" dans le body d'un 200.
# Le fichier vit dans /usr/local/apache2/htdocs/ (chemin Apache natif, jamais
# bind-monte) et non dans /var/www/html/ qui est ecrase par le bind-mount
# ./src au runtime.
<VirtualHost *:80>
DocumentRoot "/usr/local/apache2/htdocs"
Alias /healthz /usr/local/apache2/htdocs/healthz.txt
<Directory "/usr/local/apache2/htdocs">
Require all granted
</Directory>
<Files "healthz.txt">
# Pas de cache : la sonde doit toujours toucher Apache.
Header set Cache-Control "no-store"
</Files>
</VirtualHost>
# === Borne client (Bloc 1 - front vanilla HTML/CSS/JS) ===
<VirtualHost *:80>
# Hostname injecte par la var d'env APP_HOST_KIOSK au runtime.
ServerName ${APP_HOST_KIOSK}
DocumentRoot "/var/www/html/public/borne"
# Confiance aux headers X-Forwarded-* de Traefik.
# mod_remoteip pourrait etre active pour restaurer la vraie IP client
# dans les logs, mais en l'etat le header X-Forwarded-For est loggue
# dans combined, ce qui suffit pour un projet RNCP.
<Directory "/var/www/html/public/borne">
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
# SPA-like fallback : toute URL non-fichier -> index.html
# (pour permettre de bookmarker un chemin profond dans la borne).
# Exclusion /api/ : ces requetes sont relayees a l'API (cf. <Location /api>
# plus bas) et ne doivent JAMAIS retomber sur index.html.
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/api/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
</Directory>
# Securite supplementaire : expose uniquement public/borne, pas la racine.
<Directory "/var/www/html/public/borne/..">
Require all denied
</Directory>
# === API en MEME origine (P4 - passerelle same-origin) ===
# La borne consomme l'API publique (/api/*) sur SA PROPRE origine : ce vhost
# relaie ces requetes au front controller admin via PHP-FPM. data.js garde
# donc ses URLs relatives (/api/categories...) -> aucune requete cross-origin
# cote borne -> CORS inutile pour ce parcours (le middleware reste en place
# cote API comme defense en profondeur). SEUL /api est relaye : le back-office
# (/login, /admin/*) n'est PAS joignable depuis l'origine borne.
#
# Le chemin apres host:port dans l'URL fcgi EST le SCRIPT_FILENAME envoye a
# FPM : on le force sur le front controller admin (un .php REEL). Sans ca, FPM
# recevrait un chemin sous le docroot borne sans extension .php et rejetterait
# (security.limit_extensions par defaut = .php -> reponse "Access denied").
# ProxyPassMatch intercepte des la phase translate -> le faux chemin
# /.../borne/api/... n'est jamais calcule. REQUEST_URI (=/api/categories) et la
# query string sont preserves -> le Router (qui lit REQUEST_URI) route correctement.
ProxyPassMatch "^/api(/.*)?$" "fcgi://wakdo-app:9000/var/www/html/public/admin/index.php"
# mod_proxy_fcgi derive un SCRIPT_FILENAME corrompu (prefixe proxy: + chemin
# original colle apres index.php) -> FPM rejette (extension != .php). On force
# la valeur sur le front controller admin (un .php REEL) ; REQUEST_URI reste
# intact, donc le Router route correctement.
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/var/www/html/public/admin/index.php"
# Compression text/html, css, js, json (Cr 1.e.8 temps de chargement).
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript \
application/javascript application/json image/svg+xml
</IfModule>
# Cache long sur les assets versionnes (futur : hash dans le nom de fichier).
<FilesMatch "\.(jpg|jpeg|png|gif|ico|svg|webp|woff2|woff)$">
Header set Cache-Control "public, max-age=604800"
</FilesMatch>
# Borne : pas de code PHP execute cote kiosk (Bloc 1 = front only).
# Si une requete *.php arrive ici, on la refuse pour forcer l'isolation.
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 combined
</VirtualHost>
# === Back-office + API REST (Bloc 2 - PHP from scratch + MVC) ===
<VirtualHost *:80>
ServerName ${APP_HOST_ADMIN}
DocumentRoot "/var/www/html/public/admin"
<Directory "/var/www/html/public/admin">
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
# DirectoryIndex etend a index.php pour que la racine `/` serve
# le front controller PHP sans passer par RewriteRule (qui ne se
# declenche pas sur un repertoire existant a cause du `!-d`).
DirectoryIndex index.php index.html
# Front controller MVC : toute requete non-fichier passe par index.php
# qui dispatche via le Router (src/Core/Router.php a venir en P2).
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</Directory>
# Reverse FastCGI : toute requete *.php est executee par wakdo-app:9000.
# Format proxy:fcgi://<host>:<port><path-to-proxy>
# SetHandler s'applique au chemin courant du <FilesMatch>.
<FilesMatch "\.php$">
SetHandler "proxy:fcgi://wakdo-app:9000"
</FilesMatch>
# Protection des fichiers sensibles du projet si jamais un chemin se
# retrouve sous DocumentRoot par erreur (.env, .git, config/).
<FilesMatch "^\.(env|git|htaccess)">
Require all denied
</FilesMatch>
<DirectoryMatch "/\.(git|env)">
Require all denied
</DirectoryMatch>
# CORS : l'API admin sous /api/* doit accepter les requetes venant
# de la borne kiosk (APP_HOST_KIOSK). Wildcard interdit.
# La vraie valeur vient de CORS_ALLOWED_ORIGIN dans .env, lue cote PHP.
# Ici on pose juste les headers de prealable OPTIONS.
<Location /api>
<IfModule mod_headers.c>
# Les headers definitifs sont poses par le middleware PHP.
# Apache ne fait que relayer le preflight sans le casser.
Header set X-Wakdo-Handled-By "apache-vhost"
</IfModule>
</Location>
# Compression back-office (HTML admin + JSON API)
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript \
application/javascript application/json
</IfModule>
# Securite : cookies httpOnly + secure sont forces cote PHP (voir php.ini).
# Ici on ajoute un header CSP minimal : l'admin n'a pas besoin de charger
# de resources externes pour un MVP (pas de CDN, pas d'analytics).
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>
ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 combined
</VirtualHost>