Intègre securite/infra : en-têtes nginx, CI épinglée, sauvegardes, docs de contribution

This commit is contained in:
IliesAllali committed 2026-09-30 18:03:11 +02:00
commit a3e69360cd
16 files changed
+303 -42

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
# Mises à jour de dépendances proposées chaque semaine. Les versions mineures et correctives sont
# regroupées en une PR par écosystème et par dossier ; une version majeure arrive seule, pour être lue.
version: 2
updates:
- package-ecosystem: npm
directory: /backend
schedule:
interval: weekly
groups:
backend-minor-patch:
patterns: ["*"]
update-types: [minor, patch]
- package-ecosystem: npm
directory: /frontend
schedule:
interval: weekly
groups:
frontend-minor-patch:
patterns: ["*"]
update-types: [minor, patch]
- package-ecosystem: npm
directory: /landing
schedule:
interval: weekly
groups:
landing-minor-patch:
patterns: ["*"]
update-types: [minor, patch]
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions-minor-patch:
patterns: ["*"]
update-types: [minor, patch]
# Dockerfile.web (racine) et backend/Dockerfile
- package-ecosystem: docker
directories:
- /
- /backend
schedule:
interval: weekly
groups:
docker-minor-patch:
patterns: ["*"]
update-types: [minor, patch]
# Images de docker-compose.yml
- package-ecosystem: docker-compose
directory: /
schedule:
interval: weekly
groups:
compose-minor-patch:
patterns: ["*"]
update-types: [minor, patch]
ignore:
# Changer de version majeure de PostgreSQL demande un pg_dump / restauration du volume db-data
- dependency-name: postgres
update-types: [version-update:semver-major]
+20 -8
View File
@@ -25,11 +25,11 @@ jobs:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 24
cache: npm
@@ -38,6 +38,10 @@ jobs:
- name: Dépendances
run: npm ci --no-audit --no-fund
# Bloque seulement sur une faille critique d'une dépendance de production
- name: Audit des dépendances de production
run: npm audit --omit=dev --audit-level=critical
- name: Lint
run: npm run lint
@@ -55,11 +59,11 @@ jobs:
run:
working-directory: landing
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 24
cache: npm
@@ -68,10 +72,14 @@ jobs:
- name: Dépendances
run: npm ci --no-audit --no-fund
# Bloque seulement sur une faille critique d'une dépendance de production
- name: Audit des dépendances de production
run: npm audit --omit=dev --audit-level=critical
- name: Tests
run: npm test
# Le build lit les polices dans ../frontend/src/assets, présentes dans le dépôt.
# Le build ne lit que landing/ : aucune police n'est prise dans ../frontend.
- name: Build
run: npm run build
@@ -106,11 +114,11 @@ jobs:
CHECKPOINT_DISABLE: '1'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 24
cache: npm
@@ -119,6 +127,10 @@ jobs:
- name: Dépendances
run: npm ci --no-audit --no-fund
# Bloque seulement sur une faille critique d'une dépendance de production
- name: Audit des dépendances de production
run: npm audit --omit=dev --audit-level=critical
- name: Client Prisma
run: npm run prisma:generate
@@ -139,7 +151,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
+15 -5
View File
@@ -48,20 +48,30 @@ jobs:
steps:
- name: Start SSH agent
uses: webfactory/ssh-agent@v0.9.0
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
with:
ssh-private-key: ${{ secrets.HOSTINGER_SSH_PRIVATE_KEY }}
- name: Add Hostinger host key (best-effort)
# Clé d'hôte épinglée par le secret SSH_KNOWN_HOSTS (sortie de ssh-keyscan relue à la main, voir
# deploy/HOSTINGER_GITHUB_SETUP.md). Sans ce secret, repli sur ssh-keyscan au moment du run, qui
# fait confiance à la clé présentée ce jour-là : un avertissement le signale dans le run.
- name: Hostinger host key
env:
HOSTINGER_HOST: ${{ secrets.HOSTINGER_HOST }}
HOSTINGER_PORT: ${{ secrets.HOSTINGER_PORT }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
run: |
set -euo pipefail
PORT="${HOSTINGER_PORT:-22}"
mkdir -p ~/.ssh
# keyscan can fail if the host firewall blocks GitHub runners; make it non-fatal
ssh-keyscan -T 5 -p "$PORT" -H "$HOSTINGER_HOST" >> ~/.ssh/known_hosts || true
chmod 700 ~/.ssh
if [ -n "$SSH_KNOWN_HOSTS" ]; then
printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
else
echo "::warning::Secret SSH_KNOWN_HOSTS absent : clé d'hôte récupérée par ssh-keyscan, sans vérification. Ajoutez le secret."
ssh-keyscan -T 5 -p "$PORT" -H "$HOSTINGER_HOST" >> ~/.ssh/known_hosts || true
fi
chmod 600 ~/.ssh/known_hosts
- name: Deploy on Hostinger
env:
@@ -74,7 +84,7 @@ jobs:
PORT="${HOSTINGER_PORT:-22}"
# backend/.env vit sur le serveur, jamais dans GitHub (dépôt public)
ssh -p "$PORT" -o ServerAliveInterval=30 "$HOSTINGER_USER@$HOSTINGER_HOST" "FORCE_FULL=$FORCE_FULL bash -s" <<'EOF'
ssh -p "$PORT" -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 "$HOSTINGER_USER@$HOSTINGER_HOST" "FORCE_FULL=$FORCE_FULL bash -s" <<'EOF'
set -euo pipefail
cd /var/www/parenthese
# Sous le même verrou que deploy.sh : récupération du code, puis déploiement
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: fresh-install-shots
path: parenthese/shots
+5
View File
@@ -34,3 +34,8 @@ backend/storage/media/
# Clés SSH / certificats
*.pem
id_*
# Outils et notes de travail locaux (méthode BMAD, mémoire d'agent)
_bmad/
_bmad-output/
memory/
+9 -2
View File
@@ -35,6 +35,13 @@ Si vous touchez à Docker ou à nginx, lancez `docker compose up -d --build` pui
- Pas de nouvelle dépendance lourde (framework, bibliothèque d'interface, service externe) sans en avoir discuté dans une issue.
- La promesse du README tient : pas de publicité, et aucune mesure d'audience activée par défaut sur une instance auto-hébergée.
## Licence
## Licence des contributions
Parenthèse est publié sous [PolyForm Noncommercial 1.0.0](LICENSE). En proposant une contribution, vous acceptez qu'elle soit publiée sous cette même licence.
Parenthèse est publié sous [PolyForm Noncommercial 1.0.0](LICENSE) et le reste.
En ouvrant une pull request, vous
- certifiez avoir écrit la contribution ou avoir le droit de la proposer, dans l'esprit du [Developer Certificate of Origin](https://developercertificate.org/). Signez vos commits avec `git commit -s`, qui ajoute une ligne `Signed-off-by` ;
- accordez au mainteneur, Ilies Allali, une licence perpétuelle, irrévocable, mondiale et gratuite pour utiliser, modifier et proposer votre contribution sous d'autres licences, y compris commerciales.
Si vous ne pouvez pas l'accepter, dites-le dans la pull request avant sa fusion.
+9 -2
View File
@@ -35,6 +35,13 @@ If you touch Docker or nginx, run `docker compose up -d --build` then `bash tool
- No new heavy dependency (framework, UI library, external service) without discussing it in an issue.
- The README promise holds: no ads, and no analytics turned on by default on a self-hosted instance.
## License
## License of contributions
Parenthèse is published under [PolyForm Noncommercial 1.0.0](LICENSE). By submitting a contribution, you agree that it is published under the same license.
Parenthèse is published under [PolyForm Noncommercial 1.0.0](LICENSE), and stays under it.
By opening a pull request, you:
- certify that you wrote the contribution or otherwise have the right to submit it, in the spirit of the [Developer Certificate of Origin](https://developercertificate.org/). Sign your commits with `git commit -s`, which adds a `Signed-off-by:` line;
- grant the maintainer, Ilies Allali, a perpetual, irrevocable, worldwide, royalty-free license to use, modify and relicense your contribution under any terms, including commercial ones.
If you can't agree to this, say so in the pull request before it is merged.
+1 -1
View File
@@ -17,7 +17,7 @@ ENV VITE_API_BASE_URL=""
RUN npm run build
FROM nginx:1.27-alpine
FROM nginx:1.30-alpine
COPY deploy/nginx.docker.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
+1 -4
View File
@@ -6,10 +6,7 @@
Ne décrivez jamais une faille dans une issue, une discussion ou une pull request : tout y est public.
Deux canaux privés :
- l'onglet **Security** de ce dépôt GitHub, bouton « Report a vulnerability » ;
- un email à pro.allali.ilies@gmail.com.
Passez de préférence par le signalement privé de GitHub, dans l'onglet **Security** de ce dépôt, bouton « Report a vulnerability ». Si vous ne pouvez pas l'utiliser, écrivez à pro.allali.ilies@gmail.com.
Indiquez si possible ce qui est touché (adresse, route de l'API, fichier), les étapes pour reproduire et l'impact que vous constatez. Un rapport court et précis suffit.
+1 -4
View File
@@ -6,10 +6,7 @@
Never describe a vulnerability in an issue, a discussion or a pull request: everything there is public.
Two private channels:
- the **Security** tab of this GitHub repository, "Report a vulnerability" button;
- an email to pro.allali.ilies@gmail.com.
Report it preferably through GitHub's private vulnerability reporting: the **Security** tab of this repository, "Report a vulnerability" button. If you can't use it, send an email to pro.allali.ilies@gmail.com.
If you can, say what is affected (address, API route, file), the steps to reproduce and the impact you observe. A short, precise report is enough. English or French, as you prefer.
+1
View File
@@ -12,6 +12,7 @@ Chaque push sur `main` lance d'abord la CI (`.github/workflows/ci.yml`). Si elle
- `HOSTINGER_PORT` : port SSH (facultatif, 22 par défaut)
- `HOSTINGER_USER` : utilisateur SSH
- `HOSTINGER_SSH_PRIVATE_KEY` : clé privée dont la clé publique est dans `authorized_keys` sur le serveur
- `SSH_KNOWN_HOSTS` : clé d'hôte du serveur, épinglée. La produire depuis une machine de confiance avec `ssh-keyscan -p PORT HÔTE`, comparer l'empreinte (`ssh-keygen -lf`) à celle affichée sur le serveur par `ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`, puis coller les lignes telles quelles. Sans ce secret, le workflow reprend la clé présentée au moment du run et affiche un avertissement.
## Ce qui vit sur le serveur, hors du dépôt
+44 -2
View File
@@ -5,7 +5,15 @@
# La base est sauvegardée à chaque passage. L'archive des médias n'est refaite que si les médias ont
# changé (sinon une archive identique par jour), et seules les
# BACKUP_MEDIA_KEEP dernières archives sont gardées.
#
# Envoi hors du serveur (facultatif, BACKUP_S3_BUCKET) : si BACKUP_AGE_RECIPIENT est renseigné (une ou
# plusieurs clés publiques séparées par des espaces, « age1... » ou « ssh-ed25519 AAAA... » sans commentaire),
# le dump et l'archive sont chiffrés avec age avant l'envoi et seules les copies chiffrées (.age) partent.
# Les fichiers gardés sur le serveur restent en clair, lisibles par root seulement.
# Restauration d'une copie chiffrée : age -d -i cle-privee.txt fichier.dump.age > fichier.dump
set -euo pipefail
# Dumps, archives et empreintes lisibles par le seul propriétaire
umask 077
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BACKEND_ENV_FILE="$REPO_DIR/backend/.env"
@@ -115,9 +123,43 @@ if [ -n "${BACKUP_S3_BUCKET:-}" ]; then
S3_ARGS+=(--region "$BACKUP_S3_REGION")
fi
aws s3 cp "$DB_BACKUP_FILE" "$S3_BASE_URI/db/$(basename "$DB_BACKUP_FILE")" "${S3_ARGS[@]}"
# Copies à envoyer : chiffrées si une clé age est configurée, sinon les fichiers tels quels (comportement historique)
UPLOAD_DB_FILE="$DB_BACKUP_FILE"
UPLOAD_MEDIA_FILE="$MEDIA_BACKUP_FILE"
if [ -n "${BACKUP_AGE_RECIPIENT:-}" ]; then
if ! command -v age >/dev/null 2>&1; then
echo "ERROR: BACKUP_AGE_RECIPIENT is set but age is not installed (apt install age)."
exit 1
fi
AGE_ARGS=()
read -r -a AGE_KEYS <<< "$BACKUP_AGE_RECIPIENT"
# Une clé ssh s'écrit en deux mots (« ssh-ed25519 AAAA... ») : on recolle type et clé
i=0
while [ "$i" -lt "${#AGE_KEYS[@]}" ]; do
key="${AGE_KEYS[$i]}"
if [[ "$key" == ssh-* ]]; then
i=$((i + 1))
key="$key ${AGE_KEYS[$i]:-}"
fi
AGE_ARGS+=(-r "$key")
i=$((i + 1))
done
ENCRYPT_DIR="$(mktemp -d "$BACKUP_ROOT/.offsite-XXXXXX")"
trap 'rm -rf "$ENCRYPT_DIR"' EXIT
UPLOAD_DB_FILE="$ENCRYPT_DIR/$(basename "$DB_BACKUP_FILE").age"
age "${AGE_ARGS[@]}" -o "$UPLOAD_DB_FILE" "$DB_BACKUP_FILE"
if [ "$MEDIA_IS_NEW" = "1" ]; then
UPLOAD_MEDIA_FILE="$ENCRYPT_DIR/$(basename "$MEDIA_BACKUP_FILE").age"
age "${AGE_ARGS[@]}" -o "$UPLOAD_MEDIA_FILE" "$MEDIA_BACKUP_FILE"
fi
echo "Copies chiffrées avec age avant envoi."
else
echo "WARNING: BACKUP_AGE_RECIPIENT absent, les sauvegardes partent en clair."
fi
aws s3 cp "$UPLOAD_DB_FILE" "$S3_BASE_URI/db/$(basename "$UPLOAD_DB_FILE")" "${S3_ARGS[@]}"
if [ "$MEDIA_IS_NEW" = "1" ]; then
aws s3 cp "$MEDIA_BACKUP_FILE" "$S3_BASE_URI/media/$(basename "$MEDIA_BACKUP_FILE")" "${S3_ARGS[@]}"
aws s3 cp "$UPLOAD_MEDIA_FILE" "$S3_BASE_URI/media/$(basename "$UPLOAD_MEDIA_FILE")" "${S3_ARGS[@]}"
fi
aws s3 cp "$CHECKSUM_FILE" "$S3_BASE_URI/meta/$(basename "$CHECKSUM_FILE")" "${S3_ARGS[@]}"
else
+40 -1
View File
@@ -92,6 +92,43 @@ fix_perms() {
find "$REPO_DIR/$1" -type f -exec chmod 644 {} +
}
# Sauvegarde de la base avant migration, à côté de celles de deploy/backup.sh ($BACKUP_ROOT/pre-migration).
# « prisma migrate status » sort en 0 quand la base est à jour : pas de dump. Tout autre code (migration en
# attente, historique divergent, base injoignable) déclenche le dump, et un dump raté arrête le déploiement.
# Les PREMIGRATION_KEEP derniers dumps sont gardés (10 par défaut).
predeploy_dump() {
if (cd "$REPO_DIR/backend" && npx --no-install prisma migrate status >/dev/null 2>&1); then
log "Aucune migration en attente, pas de sauvegarde avant migration"
return 0
fi
log "Migrations en attente : sauvegarde de la base avant de les appliquer"
(
set -euo pipefail
umask 077
# Sous-shell : DATABASE_URL et les réglages de sauvegarde ne fuient pas dans les builds qui suivent
set -a
# shellcheck disable=SC1090,SC1091
. "$REPO_DIR/backend/.env"
if [ -f "${BACKUP_ENV_FILE:-/etc/genealogy/backup.env}" ]; then
# shellcheck disable=SC1090
. "${BACKUP_ENV_FILE:-/etc/genealogy/backup.env}"
fi
set +a
command -v pg_dump >/dev/null 2>&1 || { echo "ERROR: pg_dump absent, déploiement arrêté avant migration"; exit 1; }
[ -n "${DATABASE_URL:-}" ] || { echo "ERROR: DATABASE_URL absent de backend/.env"; exit 1; }
dir="${BACKUP_ROOT:-/var/backups/genealogy}/pre-migration"
mkdir -p "$dir"
file="$dir/genealogy-premigration-$(date -u +%Y%m%dT%H%M%SZ)-${NEW_SHA:0:12}.dump"
if ! pg_dump "$DATABASE_URL" --format=custom --file "$file"; then
rm -f "$file"
echo "ERROR: pg_dump a échoué, déploiement arrêté avant migration"
exit 1
fi
echo "[deploy] Base sauvegardée : $file"
ls -1t "$dir"/genealogy-premigration-*.dump 2>/dev/null | tail -n +"$((${PREMIGRATION_KEEP:-10} + 1))" | xargs -r rm -f
)
}
# 4. Backend
BACKEND_CHANGED=0
if changed backend ecosystem.config.cjs; then
@@ -101,6 +138,7 @@ if changed backend ecosystem.config.cjs; then
|| { echo "ERROR: dépendances backend manquantes après installation"; exit 1; }
log "Build backend"
(cd "$REPO_DIR/backend" && NODE_ENV='' npm run prisma:generate && NODE_ENV='' npm run build)
predeploy_dump
log "Migrations"
(cd "$REPO_DIR/backend" && npm run prisma:deploy)
else
@@ -117,7 +155,8 @@ else
log "Frontend inchangé"
fi
# 6. Landing (elle utilise les polices du frontend)
# 6. Landing (autonome, plus aucune police lue dans le frontend ; frontend/src/assets reste dans le
# déclencheur par prudence, sans effet tant que rien n'y change)
if changed landing frontend/src/assets; then
install_deps landing
log "Build landing"
+70 -11
View File
@@ -1,3 +1,42 @@
# ─── En-têtes de sécurité ─────────────────────────────────────────────────────
#
# nginx n'hérite des add_header du niveau server que dans les location qui n'en déclarent aucun.
# Toute location qui pose son propre add_header (Cache-Control par exemple) répète donc la liste
# complète. Les politiques longues sont rangées dans des variables (map) pour ne pas les recopier.
#
# CSP de l'application (app.parenthese.io), sources autorisées :
# script-src, connect-src https://*.posthog.com, analytics (VITE_POSTHOG_HOST = us.i.posthog.com,
# extensions chargées depuis us-assets.i.posthog.com ; forme recommandée par PostHog)
# connect-src 'self' appels /api, /auth, /trees
# style-src 'unsafe-inline' styles posés en ligne par React et par posthog-js
# img-src data: blob: aperçu d'une photo avant envoi (URL.createObjectURL), images en data:
# font-src 'self' data: polices servies par le site (Vite peut en inliner une petite en data:)
# media-src blob: lecture locale d'un fichier avant envoi
# worker-src blob: compression des enregistrements posthog-js
# frame-src 'self' pour les PDF servis par /api, YouTube sans cookie, carte OpenStreetMap
# frame-ancestors la landing parenthese.io intègre l'app en iframe
map $host $parenthese_app_csp {
default "default-src 'self'; script-src 'self' https://*.posthog.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://*.posthog.com; media-src 'self' blob:; worker-src 'self' blob:; frame-src 'self' https://www.youtube-nocookie.com https://www.openstreetmap.org; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self' https://parenthese.io https://www.parenthese.io";
}
# CSP de la landing (parenthese.io), sources autorisées :
# script-src 'sha256-...' script en ligne du <head> (animation du logo, dans index.html et en/index.html).
# S'il change, recalculer l'empreinte : base64(sha256(contenu exact entre
# <script> et </script>)), sinon l'animation ne se joue plus (sans autre effet).
# posthog comme pour l'app
# style-src 'unsafe-inline' attributs style du rendu serveur React
# frame-src démonstration de l'app en iframe
# frame-ancestors 'none' équivalent de X-Frame-Options DENY
map $host $parenthese_landing_csp {
default "default-src 'self'; script-src 'self' 'sha256-Av5a1/yoW/tQ2ZQWqH5FnaL896Bubcs9ivdFvsKZBrU=' https://*.posthog.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https://*.posthog.com; worker-src 'self' blob:; frame-src https://app.parenthese.io; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'";
}
# Réponses de l'API relayées (JSON, médias des familles) : intégrables seulement par l'app (PDF en iframe).
map $host $parenthese_api_csp {
default "frame-ancestors 'self' https://parenthese.io https://www.parenthese.io";
}
# ─── app.parenthese.io ────────────────────────────────────────────────────────
server {
@@ -7,18 +46,22 @@ server {
}
server {
# « listen ... http2 » gardé : la directive « http2 on; » demande nginx 1.25.1 ou plus, et la version
# installée sur le VPS n'est pas écrite dans le dépôt (Ubuntu 24.04 livre la 1.24). À passer en
# « listen 443 ssl; » + « http2 on; » une fois « nginx -v » vérifié.
listen 443 ssl http2;
server_name app.parenthese.io;
ssl_certificate /etc/letsencrypt/live/app.parenthese.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.parenthese.io/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Security headers
# frame-ancestors allows the landing page (parenthese.io) to embed the app in an iframe
add_header Content-Security-Policy "frame-ancestors 'self' https://parenthese.io https://www.parenthese.io" always;
# Hérités par les location sans add_header, c'est-à-dire les routes relayées vers l'API
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy $parenthese_api_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
gzip on;
gzip_vary on;
@@ -70,20 +113,21 @@ server {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://parenthese.io https://www.parenthese.io" always;
# Keep embedding control in CSP; override any inherited SAMEORIGIN frame header.
add_header X-Frame-Options "ALLOWALL" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy $parenthese_app_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
add_header Content-Security-Policy "frame-ancestors 'self' https://parenthese.io https://www.parenthese.io" always;
add_header X-Frame-Options "ALLOWALL" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy $parenthese_app_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
}
}
@@ -101,6 +145,8 @@ server {
server_name www.parenthese.io;
ssl_certificate /etc/letsencrypt/live/parenthese.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/parenthese.io/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
return 301 https://parenthese.io$request_uri;
}
@@ -110,10 +156,13 @@ server {
ssl_certificate /etc/letsencrypt/live/parenthese.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/parenthese.io/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy $parenthese_landing_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header X-Frame-Options "DENY" always;
gzip on;
@@ -129,7 +178,11 @@ server {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy $parenthese_landing_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header X-Frame-Options "DENY" always;
}
@@ -137,5 +190,11 @@ server {
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy $parenthese_landing_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header X-Frame-Options "DENY" always;
}
}
+19 -1
View File
@@ -1,6 +1,16 @@
# nginx du service `web` (docker compose). Dérivé de deploy/nginx.conf (prod app.parenthese.io) :
# même origine, /api/* réécrit en /* vers l'API, /auth, /trees et /health relayés tels quels,
# fallback SPA. Pas de TLS ici : placez un reverse proxy HTTPS devant si vous exposez sur Internet.
# fallback SPA. Pas de TLS ici : placez un reverse proxy HTTPS devant si vous exposez sur Internet
# (c'est lui qui posera Strict-Transport-Security).
#
# nginx n'hérite des add_header du niveau server que dans les location qui n'en déclarent aucun :
# les location qui posent leur propre add_header répètent la liste complète.
#
# CSP de l'application : même politique que la production (deploy/nginx.conf, sources expliquées là-bas),
# sans PostHog (l'image ne reçoit pas de clé d'analytics) et sans intégration par un autre site.
map $host $parenthese_app_csp {
default "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; frame-src 'self' https://www.youtube-nocookie.com https://www.openstreetmap.org; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'";
}
server {
listen 80;
@@ -14,9 +24,12 @@ server {
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
# Hérités par les location sans add_header, c'est-à-dire les routes relayées vers l'API
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
location /api/ {
rewrite ^/api(/.*)$ $1 break;
@@ -60,15 +73,20 @@ server {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Content-Security-Policy $parenthese_app_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
add_header Content-Security-Policy $parenthese_app_csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
}
}
+2
View File
@@ -10,6 +10,8 @@ module.exports = {
env_production: {
NODE_ENV: 'production',
PORT: 4000,
// nginx relaie vers 127.0.0.1:4000 (deploy/nginx.conf) : l'API n'écoute pas sur les interfaces publiques
HOST: '127.0.0.1',
},
max_memory_restart: '512M',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',