Apache

Voici la configuration de reverse proxy réellement utilisée par Aetheus en production, avec les jetons propres au déploiement remplacés par des valeurs d'exemple. En cas de doute sur le proxy à choisir, prenez celui-ci.

Activer les modules

sudo a2enmod ssl proxy proxy_http proxy_wstunnel rewrite headers
sudo systemctl restart apache2

proxy_wstunnel n'est pas facultatif : sans lui, l'hôte API ne peut pas passer en WebSocket, et la sortie des exécutions en direct cesse de se mettre à jour alors que tout le reste semble fonctionner.

Obtenir un certificat

Un seul certificat couvrant les deux noms d'hôte garde la configuration simple :

sudo certbot certonly --apache \
    -d app.example.com \
    -d api.example.com

Hôte applicatif

Il sert le frontend. Que les fichiers viennent d'un conteneur ou d'un wwwroot publié, les deux règles qui comptent sont les mêmes : ne jamais mettre en cache les fichiers d'amorçage, et laisser fonctionner le routage côté client.

<VirtualHost *:443>
    ServerName app.example.com

    SSLEngine On
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Forwarded-Proto "https"

    # Une application Blazor WebAssembly s'amorce depuis index.html et _framework/. Les mettre en
    # cache fait qu'un navigateur continue d'exécuter la version précédente après une mise à jour.
    <LocationMatch "^/(index\.html|service-worker(\.published)?\.js|_framework/|_content/Radzen\.Blazor/)">
        Header always set Cache-Control "no-cache, no-store, must-revalidate"
        Header always set Pragma "no-cache"
        Header always set Expires "0"
    </LocationMatch>

    ProxyPass / http://127.0.0.1:10025/
    ProxyPassReverse / http://127.0.0.1:10025/

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    ErrorLog ${APACHE_LOG_DIR}/app.example.com_ssl_error.log
    CustomLog ${APACHE_LOG_DIR}/app.example.com_ssl_access.log combined
</VirtualHost>

Servir les fichiers directement plutôt que les relayer

Si vous avez publié le frontend sur disque plutôt que d'exécuter le conteneur, remplacez les deux lignes ProxyPass par une racine documentaire et un repli pour que les chemins inconnus atteignent index.html :

    DocumentRoot /var/www/aetheus-app

    <Directory /var/www/aetheus-app>
        Require all granted
        Options -Indexes
        FallbackResource /index.html
    </Directory>

Hôte API

Il sert le backend. Le bloc de réécriture est ce qui transforme une demande de mise à niveau en connexion WebSocket relayée ; il doit précéder ProxyPass.

<VirtualHost *:443>
    ServerName api.example.com

    SSLEngine On
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Forwarded-Proto "https"

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) ws://127.0.0.1:10026/$1 [P,L]

    ProxyPass / http://127.0.0.1:10026/
    ProxyPassReverse / http://127.0.0.1:10026/

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"

    ErrorLog ${APACHE_LOG_DIR}/api.example.com_ssl_error.log
    CustomLog ${APACHE_LOG_DIR}/api.example.com_ssl_access.log combined
</VirtualHost>

Rediriger le HTTP en clair

Un hôte virtuel par nom, sur le port 80, redirigeant vers HTTPS :

<VirtualHost *:80>
    ServerName app.example.com
    RewriteEngine On
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

<VirtualHost *:80>
    ServerName api.example.com
    RewriteEngine On
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Appliquer

sudo apachectl configtest
sudo systemctl reload apache2

Lancez toujours configtest d'abord : un rechargement avec une configuration cassée coupe le site au lieu d'être ignoré.

Vérifier

# L'API répond, et sa base est joignable.
curl -sf https://api.example.com/health/ready

# Le frontend est servi et n'est pas mis en cache.
curl -sI https://app.example.com/ | grep -i cache-control

Ouvrez ensuite l'hôte applicatif dans un navigateur et connectez-vous. Si la page se charge mais que chaque appel échoue, les deux réglages à vérifier sont le ApiBaseUrl du frontend et le Cors__Origins__0 du backend.

Pannes courantes

SymptômeCause habituelle
L'application se charge, chaque appel d'API échoue avec une erreur CORS.Cors__Origins__0 ne correspond pas exactement à l'hôte applicatif, schéma compris.
L'application se charge mais les appels partent vers la mauvaise adresse.ApiBaseUrl dans le appsettings.json du frontend a été écrasé par un redéploiement.
Tout fonctionne mais la sortie en direct ne se rafraîchit jamais.proxy_wstunnel n'est pas activé, ou le bloc de réécriture manque sur l'hôte API.
Un rafraîchissement sur un lien profond renvoie 404.Pas de repli vers index.html quand les fichiers sont servis directement.
Le navigateur exécute encore la version précédente après une mise à jour.Les règles no-store sur index.html et _framework/ manquent.