mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-09 07:50:58 +00:00
Intègre securite/infra : en-têtes nginx, CI épinglée, sauvegardes, docs de contribution
This commit is contained in:
commit
a3e69360cd
16 files changed
+303
-42
No files matched your search
@@ -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]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user