From 79d05296865df8d50c5f57ae4d5457e19b308404 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sun, 5 Jul 2026 15:00:39 -0400 Subject: [PATCH] =?UTF-8?q?serveur=5Fnextcloud=20:=20r=C3=B4le=20codifi?= =?UTF-8?q?=C3=A9=20depuis=20le=20spike=20(13=20gotchas=20encod=C3=A9s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maison numérique souveraine (offres maison-obnl + cabinet). Tâches découpées : paquets → php (512M + TLS PG) → install (occ, PG verify-full) → exposition (vhost .mjs) → configure (overwrite/redis/anti-SSRF/import CA) → OIDC user_oidc → Collabora richdocuments (WOPI split URL) → thème Alliance (ciel étoilé). Chaque réglage encode un piège prouvé sur collab-01 : imagick Debian 13, PGSSLMODE dans php-fpm, MIME .mjs, allow_local_remote_servers (SSRF), import root step-ca, memory_limit 512M, public_wopi_url après activate-config… Version épinglée (robustesse). ansible-lint profil production : 0 échec. Co-Authored-By: Claude Opus 4.8 --- playbooks/groupes/serveur_nextcloud.yml | 15 ++++ roles/serveur_nextcloud/README.md | 49 +++++----- roles/serveur_nextcloud/defaults/main.yml | 90 ++++++++++++++----- .../serveur_nextcloud/files/alliance-logo.svg | 46 ++++++++++ .../files/themes/alliance/core/css/server.css | 19 ++++ roles/serveur_nextcloud/handlers/main.yml | 12 +++ roles/serveur_nextcloud/tasks/collabora.yml | 40 +++++++++ roles/serveur_nextcloud/tasks/configurer.yml | 79 ++++++++++++++++ roles/serveur_nextcloud/tasks/exposition.yml | 22 +++++ roles/serveur_nextcloud/tasks/installer.yml | 36 ++++++++ roles/serveur_nextcloud/tasks/main.yml | 47 ++++++++++ roles/serveur_nextcloud/tasks/oidc.yml | 36 ++++++++ roles/serveur_nextcloud/tasks/paquets.yml | 17 ++++ roles/serveur_nextcloud/tasks/php.yml | 23 +++++ roles/serveur_nextcloud/tasks/theme.yml | 42 +++++++++ .../templates/99-nextcloud.ini.j2 | 12 +++ .../templates/nginx-nextcloud.conf.j2 | 36 ++++++++ 17 files changed, 579 insertions(+), 42 deletions(-) create mode 100644 playbooks/groupes/serveur_nextcloud.yml create mode 100644 roles/serveur_nextcloud/files/alliance-logo.svg create mode 100644 roles/serveur_nextcloud/files/themes/alliance/core/css/server.css create mode 100644 roles/serveur_nextcloud/handlers/main.yml create mode 100644 roles/serveur_nextcloud/tasks/collabora.yml create mode 100644 roles/serveur_nextcloud/tasks/configurer.yml create mode 100644 roles/serveur_nextcloud/tasks/exposition.yml create mode 100644 roles/serveur_nextcloud/tasks/installer.yml create mode 100644 roles/serveur_nextcloud/tasks/main.yml create mode 100644 roles/serveur_nextcloud/tasks/oidc.yml create mode 100644 roles/serveur_nextcloud/tasks/paquets.yml create mode 100644 roles/serveur_nextcloud/tasks/php.yml create mode 100644 roles/serveur_nextcloud/tasks/theme.yml create mode 100644 roles/serveur_nextcloud/templates/99-nextcloud.ini.j2 create mode 100644 roles/serveur_nextcloud/templates/nginx-nextcloud.conf.j2 diff --git a/playbooks/groupes/serveur_nextcloud.yml b/playbooks/groupes/serveur_nextcloud.yml new file mode 100644 index 0000000..aba9211 --- /dev/null +++ b/playbooks/groupes/serveur_nextcloud.yml @@ -0,0 +1,15 @@ +--- +- name: Appliquer le groupe serveur_nextcloud + hosts: serveur_nextcloud + become: true + gather_facts: true + + pre_tasks: + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + + roles: + - serveur_nextcloud diff --git a/roles/serveur_nextcloud/README.md b/roles/serveur_nextcloud/README.md index 699fe20..b853b6b 100644 --- a/roles/serveur_nextcloud/README.md +++ b/roles/serveur_nextcloud/README.md @@ -1,25 +1,32 @@ -# serveur_nextcloud — SQUELETTE (module Collaboration) +# serveur_nextcloud -> ⚠️ **Rôle en cours de construction.** `defaults` + `meta` + architecture sont posés. -> **`tasks/main.yml` attend le SPIKE empirique** — discipline « éprouver l'outil avant le rôle » : -> déployer Nextcloud + Collabora à la main sur un vrai nœud, **prouver** SSO OIDC + WOPI + fichiers, -> **puis** codifier. Écrire les tâches à l'aveugle = les bugs de 1er déploiement qu'on veut éviter. +Maison numérique souveraine : fichiers + collaboration (Nextcloud + Collabora), SSO +Keycloak, base PostgreSQL chiffrée. **Codifié depuis le spike empirique du 2026-07-05** +(collab-01) — chaque réglage encode un gotcha prouvé sur du vrai. Cœur de l'offre +**maison-obnl** (OBNL/coops) et **cabinet** (PME de professionnels). -## Rôle dans l'offre -Cœur de l'offre **maison-obnl** (OBNL/coops) : fichiers, documents, agendas, contacts — le -remplaçant souverain de Google Workspace / Microsoft 365. +## Ce que le rôle fait +1. Paquets PHP-FPM 8.4 + extensions, nginx, redis (imagick tire ses deps — **pas** `libmagickcore-6`, absent de Debian 13) ; +2. tuning PHP **512M** + opcache, et `env[PGSSLMODE]`/`PGSSLROOTCERT` dans le pool php-fpm (PDO pgsql lit libpq) ; +3. tarball **épinglé** → `/var/www/nextcloud` + `occ maintenance:install` sur PG en **verify-full TLS** ; +4. vhost nginx local avec **MIME `.mjs` = text/javascript** (sinon modules ES bloqués → pages blanches) ; +5. `overwrite*` + `trusted_proxies` (edge) + Redis + **`allow_local_remote_servers`** (anti-SSRF, IP privées) + import du **root step-ca** dans le bundle CA propre de Nextcloud ; +6. SSO **user_oidc** (Keycloak) ; **richdocuments** (WOPI : `wopi_url` local + `public_wopi_url` edge, posé *après* activate-config) ; +7. identité **Alliance Boréale** (theming + thème « ciel étoilé ») + whitelist anti-force-brute du sous-réseau d'admin. -## Architecture cible (motifs Set-OPS réutilisés) -- **BD** PostgreSQL via `resoudre_base` (base `nextcloud`, FQDN, secret en voûte) ; -- **Cache** Redis (verrous transactionnels, sessions) ; -- **SSO** OIDC via Keycloak (app `user_oidc`, realm de l'instance = `identite_realm`) ; -- **Édition documents** Collabora Online (rôle `serveur_collabora`, protocole WOPI) ; -- **TLS** cert step-ca + cert-sync, exposé `cloud.` derrière l'edge nginx ; -- **Sauvegarde** `client_backup` (données + BD = Tier 1). +## Dépendances (à fournir avant déploiement) +- **Registre BD** : une base `nextcloud` (consommateur `serveur_nextcloud`) dans `bases-donnees.yml` ; +- **Voûte** : `vault_nextcloud_admin`, `vault_nextcloud_oidc` (secret du client Keycloak) ; +- **Keycloak** : un client `nextcloud` (redirect `…/apps/user_oidc/code`) dans le realm de l'instance ; +- **`serveur_collabora`** actif (le serveur WOPI) + son exposition `bureau.` ; +- **Exposition edge** : `cloud.` → ce nœud:80, avec le FQDN dans les SAN du cert edge ; +- **Plancher** : le nœud doit résoudre `bureau.` (→ edge) ; `client_pki` pour le root_ca. -## Spike à mener (avant les tâches) -1. Un nœud `collab` (fonction à ajouter au plan + cloner). -2. Nextcloud (source/apt) + PHP-FPM + PostgreSQL + Redis ; `occ` config. -3. `user_oidc` branché sur Keycloak → prouver « Se connecter avec … ». -4. Collabora CODE + WOPI ↔ Nextcloud → prouver l'édition d'un .odt. -5. Sauvegarde/restauration prouvée. → **alors** on écrit `tasks/main.yml`. +## Robustesse (hygiène Nextcloud) +- **Version épinglée** (`serveur_nextcloud_version`) — pas de mise à jour surprise ; +- upgrades **en escale** (staging → prod), **sauvegarde d'abord** (restic), surveillance Icinga ; +- l'app riche en config → traiter avec soin (le spike a documenté les pièges, on n'improvise pas). + +## Validation +`ansible-lint` : profil **production** (0 échec, 0 avertissement). `--syntax-check` : OK. +Éprouvé bout-en-bout sur collab-01 (SSO, édition .odt, apps, identité visuelle). diff --git a/roles/serveur_nextcloud/defaults/main.yml b/roles/serveur_nextcloud/defaults/main.yml index 584bb96..5e26447 100644 --- a/roles/serveur_nextcloud/defaults/main.yml +++ b/roles/serveur_nextcloud/defaults/main.yml @@ -1,42 +1,90 @@ --- # ========================================================================= -# serveur_nextcloud — SQUELETTE (surface de config). Les tâches attendent le -# SPIKE empirique (déployer + prouver OIDC/WOPI/fichiers avant de codifier). -# Motifs Set-OPS réutilisés : BD via resoudre_base, SSO via Keycloak, TLS via -# cert-sync + edge, exposition dérivée, secrets en voûte. +# serveur_nextcloud — maison numérique souveraine (fichiers + collaboration). +# Codifié depuis le spike empirique du 2026-07-05 : chaque réglage encode un +# gotcha prouvé (voir README). Motifs Set-OPS : BD via resoudre_base, SSO via +# Keycloak, TLS via cert racine step-ca, exposition derrière l'edge nginx. # ========================================================================= serveur_nextcloud_groupe: "serveur_nextcloud" serveur_nextcloud_utilisateur: "www-data" -# --- Base de données (dérivée par resoudre_base, comme keycloak/forgejo) --- -# Le plan déclare une base 'nextcloud' avec consommateur serveur_nextcloud. -serveur_nextcloud_db_host: "" # dérivé (FQDN) par resoudre_base -serveur_nextcloud_db_port: 5432 +# --- Version ÉPINGLÉE (robustesse : pas de mise à jour surprise ; upgrade en escale) --- +serveur_nextcloud_version: "34.0.1" +serveur_nextcloud_url: "https://download.nextcloud.com/server/releases/nextcloud-{{ serveur_nextcloud_version }}.tar.bz2" +serveur_nextcloud_racine: "/var/www/nextcloud" -# --- Cache Redis (verrous + sessions) --- +# --- PHP (Debian 13 = 8.4) + tuning EXIGÉ par Nextcloud (gotcha : 128M → pages blanches) --- +serveur_nextcloud_php_version: "8.4" +serveur_nextcloud_php_memory_limit: "512M" +serveur_nextcloud_php_upload_max: "512M" +# imagick tire ses deps ; NE PAS lister libmagickcore-6 (absent de Debian 13). +serveur_nextcloud_paquets: + - "php{{ serveur_nextcloud_php_version }}-fpm" + - "php{{ serveur_nextcloud_php_version }}-gd" + - "php{{ serveur_nextcloud_php_version }}-curl" + - "php{{ serveur_nextcloud_php_version }}-mbstring" + - "php{{ serveur_nextcloud_php_version }}-xml" + - "php{{ serveur_nextcloud_php_version }}-zip" + - "php{{ serveur_nextcloud_php_version }}-pgsql" + - "php{{ serveur_nextcloud_php_version }}-intl" + - "php{{ serveur_nextcloud_php_version }}-bcmath" + - "php{{ serveur_nextcloud_php_version }}-gmp" + - "php{{ serveur_nextcloud_php_version }}-imagick" + - "php{{ serveur_nextcloud_php_version }}-redis" + - "php{{ serveur_nextcloud_php_version }}-cli" + - "nginx" + - "redis-server" + - "bzip2" + +# --- Base de données (dérivée par resoudre_base ; PG en verify-full TLS) --- +serveur_nextcloud_db_host: "" # dérivé (FQDN) +serveur_nextcloud_db_port: 5432 +serveur_nextcloud_db_name: "nextcloud" +serveur_nextcloud_db_user: "nextcloud" +# Gotcha : PDO pgsql (libpq) lit PGSSLMODE/PGSSLROOTCERT dans l'env de php-fpm. +serveur_nextcloud_pg_sslmode: "verify-full" +serveur_nextcloud_ca_racine: "/etc/step/certs/root_ca.crt" + +# --- Cache Redis local (verrous + mémoire) --- serveur_nextcloud_redis_host: "localhost" serveur_nextcloud_redis_port: 6379 -# --- SSO OIDC (Keycloak, realm de l'instance) — app 'user_oidc' de Nextcloud --- +# --- Exposition (derrière l'edge nginx ; TLS terminé à l'edge) --- +serveur_nextcloud_hostname: "cloud.{{ domaine_interne }}" +serveur_nextcloud_http_addr: "0.0.0.0" # l'edge (autre hôte) doit joindre :80 +serveur_nextcloud_http_port: 80 +# IP de l'edge → trusted_proxies (pour voir la vraie IP client, pas throttler l'edge). +serveur_nextcloud_edge_ip: "{{ hostvars[(groups['serveur_nginx'] | default([]) | first) | default('')].ansible_host | default('') }}" + +# --- SSO OIDC (Keycloak) — app user_oidc --- serveur_nextcloud_oidc_actif: true +serveur_nextcloud_oidc_nom: "Chezlepro" serveur_nextcloud_oidc_client_id: "nextcloud" serveur_nextcloud_oidc_client_secret: "{{ vault_nextcloud_oidc | default('') }}" serveur_nextcloud_oidc_realm: "{{ identite_realm | default('chezlepro') }}" -serveur_nextcloud_oidc_base: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_nextcloud_oidc_realm }}/protocol/openid-connect" +serveur_nextcloud_oidc_discovery: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_nextcloud_oidc_realm }}/.well-known/openid-configuration" -# --- Exposition (derrière l'edge nginx, TLS terminé à l'edge) --- -serveur_nextcloud_hostname: "cloud.{{ domaine_interne }}" -serveur_nextcloud_http_addr: "127.0.0.1" -serveur_nextcloud_http_port: 8081 - -# --- Collabora Online (édition de documents, protocole WOPI) --- -serveur_nextcloud_collabora_url: "https://bureau.{{ domaine_interne }}" +# --- Collabora Online (WOPI) — app richdocuments --- +serveur_nextcloud_office_actif: true +serveur_nextcloud_wopi_url: "http://127.0.0.1:9980" # serveur → Collabora (local) +serveur_nextcloud_wopi_public_url: "https://bureau.{{ domaine_interne }}" # navigateur → edge # --- Données + admin --- -serveur_nextcloud_data_dir: "/var/lib/nextcloud/data" +serveur_nextcloud_data_dir: "{{ serveur_nextcloud_racine }}/data" serveur_nextcloud_admin_user: "admin" -serveur_nextcloud_admin_password: "{{ vault_nextcloud_admin | default('') }}" # voûte +serveur_nextcloud_admin_password: "{{ vault_nextcloud_admin | default('') }}" -# Domaines de confiance (trusted_domains) : le hostname exposé + localhost. +# Domaines de confiance : hostname exposé + nom court local. serveur_nextcloud_trusted_domains: - "{{ serveur_nextcloud_hostname }}" + - "{{ ansible_hostname | default('localhost') }}" + +# --- Identité visuelle Alliance Boréale --- +serveur_nextcloud_branding: true +serveur_nextcloud_theme_nom: "Chezlepro" +serveur_nextcloud_theme_slogan: "Votre maison numérique souveraine" +serveur_nextcloud_theme_couleur: "#0e1640" +serveur_nextcloud_theme_ciel: true # thème custom « ciel boréal » (login + app) + +# --- Sécurité : whitelister le sous-réseau d'admin (PAS désactiver l'anti-force-brute) --- +serveur_nextcloud_bruteforce_whitelist: "{{ (sous_reseau_admin | default('')) }}" diff --git a/roles/serveur_nextcloud/files/alliance-logo.svg b/roles/serveur_nextcloud/files/alliance-logo.svg new file mode 100644 index 0000000..057b7e0 --- /dev/null +++ b/roles/serveur_nextcloud/files/alliance-logo.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/roles/serveur_nextcloud/files/themes/alliance/core/css/server.css b/roles/serveur_nextcloud/files/themes/alliance/core/css/server.css new file mode 100644 index 0000000..e039a65 --- /dev/null +++ b/roles/serveur_nextcloud/files/themes/alliance/core/css/server.css @@ -0,0 +1,19 @@ +/* Alliance Boréale — ciel étoilé (login + app), 2 couches, mouvements distincts */ +#body-login, #body-user, #body-settings, #body-public { + background: radial-gradient(1200px 600px at 75% -10%, rgba(79,227,193,.10), transparent 60%), + radial-gradient(1000px 500px at 15% 5%, rgba(169,139,255,.13), transparent 55%), + linear-gradient(180deg,#05060f 0%,#0e1640 60%,#131a44 100%) !important; + overflow: hidden; +} +#body-login, #body-user, #body-settings, #body-public { position: relative; } +#body-login::before, #body-login::after, #body-user::before, #body-user::after { + content:''; position:fixed; top:0; left:0; width:1px; height:1px; border-radius:50%; + background:transparent; pointer-events:none; z-index:0; +} +#body-login::before, #body-user::before { box-shadow:1852px 1146px #ffffff, 1907px 925px #ffffff, 2080px 1202px #ffffff, 777px 378px #ffffff, 2096px 974px #ffffff, 2514px 1624px #ffffff, 762px 192px #ffffff, 1829px 621px #ffffff, 580px 185px #ffffff, 2206px 1658px #ffffff, 171px 1219px #ffffff, 1622px 927px #ffffff, 2521px 1331px #ffffff, 645px 1276px #ffffff, 61px 1082px #ffffff, 258px 121px #ffffff, 146px 389px #ffffff, 990px 1228px #ffffff, 123px 1593px #ffffff, 1900px 668px #ffffff, 1804px 1210px #ffffff, 800px 1063px #ffffff, 957px 1311px #ffffff, 1204px 1023px #ffffff, 18px 1356px #ffffff, 348px 936px #ffffff, 1139px 833px #ffffff, 2257px 170px #ffffff, 1040px 645px #ffffff, 940px 1050px #ffffff, 1183px 60px #ffffff, 287px 1153px #ffffff, 442px 820px #ffffff, 441px 595px #ffffff, 1583px 136px #ffffff, 69px 1402px #ffffff, 2px 437px #ffffff, 859px 107px #ffffff, 1925px 768px #ffffff, 1627px 859px #ffffff, 299px 1159px #ffffff, 813px 1594px #ffffff, 1105px 689px #ffffff, 356px 637px #ffffff, 1362px 31px #ffffff, 1679px 1552px #ffffff, 483px 275px #ffffff, 1009px 1447px #ffffff, 413px 22px #ffffff, 245px 952px #ffffff, 1994px 363px #ffffff, 2290px 385px #ffffff, 1832px 1042px #ffffff, 781px 1498px #ffffff, 536px 858px #ffffff, 1571px 238px #ffffff, 1617px 861px #ffffff, 871px 0px #ffffff, 1105px 1645px #ffffff, 2427px 622px #ffffff, 80px 431px #ffffff, 767px 807px #ffffff, 2465px 1314px #ffffff, 2363px 205px #ffffff, 172px 299px #ffffff, 873px 904px #ffffff, 1057px 19px #ffffff, 2499px 673px #ffffff, 1213px 790px #ffffff, 300px 152px #ffffff, 369px 427px #ffffff, 2386px 1303px #ffffff, 995px 31px #ffffff, 2462px 755px #ffffff, 1522px 1274px #ffffff, 1856px 260px #ffffff, 2405px 990px #ffffff, 2353px 277px #ffffff, 1581px 374px #ffffff, 631px 636px #ffffff, 935px 1674px #ffffff, 2500px 510px #ffffff, 777px 324px #ffffff, 2268px 402px #ffffff, 1590px 988px #ffffff, 2472px 160px #ffffff, 1726px 97px #ffffff, 425px 223px #ffffff, 158px 1049px #ffffff, 1045px 488px #ffffff, 1604px 526px #ffffff, 1723px 1686px #ffffff, 2443px 1005px #ffffff, 1201px 1065px #ffffff, 718px 1475px #ffffff, 281px 258px #ffffff, 935px 981px #ffffff, 2291px 1338px #ffffff, 2522px 1258px #ffffff, 303px 573px #ffffff, 869px 417px #ffffff, 67px 141px #ffffff, 1102px 842px #ffffff, 1825px 510px #ffffff, 247px 95px #ffffff, 721px 577px #ffffff, 1510px 1087px #ffffff, 2343px 269px #ffffff, 377px 741px #ffffff, 566px 922px #ffffff, 1355px 1345px #ffffff, 2137px 1196px #ffffff, 574px 1208px #ffffff, 143px 36px #ffffff, 1944px 732px #ffffff, 1277px 68px #ffffff, 87px 1225px #ffffff, 306px 987px #ffffff, 275px 1496px #ffffff, 1274px 653px #ffffff, 559px 148px #ffffff, 308px 927px #ffffff, 2236px 753px #ffffff, 182px 1509px #ffffff, 530px 1624px #ffffff, 1399px 720px #ffffff, 347px 1403px #ffffff, 1938px 159px #ffffff, 1708px 1619px #ffffff, 123px 1023px #ffffff, 2346px 29px #ffffff, 2559px 1356px #ffffff, 1566px 776px #ffffff, 2386px 25px #ffffff, 2493px 147px #ffffff, 328px 185px #ffffff, 473px 526px #ffffff, 1704px 1491px #ffffff, 1352px 795px #ffffff, 2379px 937px #ffffff, 1804px 947px #ffffff, 2216px 171px #ffffff, 2124px 1536px #ffffff, 2107px 61px #ffffff, 1270px 1231px #ffffff, 359px 984px #ffffff, 91px 471px #ffffff, 462px 1018px #ffffff, 2517px 1351px #ffffff, 1991px 523px #ffffff, 46px 753px #ffffff, 1233px 293px #ffffff, 2505px 414px #ffffff, 2123px 347px #ffffff, 1402px 1351px #ffffff, 1810px 1020px #ffffff, 989px 669px #ffffff, 1657px 1363px #ffffff, 1026px 406px #ffffff, 1764px 1646px #ffffff, 820px 438px #ffffff, 1574px 449px #ffffff, 2388px 648px #ffffff, 859px 278px #ffffff, 551px 1016px #ffffff, 1436px 83px #ffffff, 262px 566px #ffffff, 692px 231px #ffffff, 1845px 965px #ffffff, 1127px 438px #ffffff; animation: deriveA 165s linear infinite, scintA 3.5s ease-in-out infinite alternate; } +#body-login::after, #body-user::after { width:2px; height:2px; box-shadow:1695px 783px #ffffff, 2129px 1011px #ffffff, 1291px 1466px #ffffff, 2559px 926px #ffffff, 1312px 152px #ffffff, 128px 569px #ffffff, 2488px 84px #ffffff, 1151px 1168px #ffffff, 1450px 632px #ffffff, 2310px 39px #ffffff, 556px 829px #ffffff, 1862px 388px #ffffff, 101px 1575px #ffffff, 1091px 486px #ffffff, 577px 1631px #ffffff, 192px 1288px #ffffff, 472px 914px #ffffff, 446px 1290px #ffffff, 2192px 1341px #ffffff, 1510px 159px #ffffff, 811px 408px #ffffff, 1946px 524px #ffffff, 731px 1462px #ffffff, 44px 1546px #ffffff, 1934px 1095px #ffffff, 148px 366px #ffffff, 927px 557px #ffffff, 1416px 1105px #ffffff, 2131px 1024px #ffffff, 2516px 1548px #ffffff, 652px 805px #ffffff, 917px 178px #ffffff, 1681px 1479px #ffffff, 1589px 266px #ffffff, 1845px 928px #ffffff, 806px 1281px #ffffff, 27px 771px #ffffff, 2252px 1164px #ffffff, 2058px 1631px #ffffff, 1405px 949px #ffffff, 1337px 1333px #ffffff, 839px 202px #ffffff, 505px 436px #ffffff, 992px 799px #ffffff, 359px 634px #ffffff, 2199px 1619px #ffffff, 1312px 536px #ffffff, 64px 713px #ffffff, 2067px 169px #ffffff, 152px 902px #ffffff, 1401px 1127px #ffffff, 1726px 1573px #ffffff, 1127px 998px #ffffff, 116px 447px #ffffff, 262px 878px #ffffff; animation: deriveB 95s linear infinite, scintB 5s ease-in-out infinite alternate; opacity:.9; } +@keyframes deriveA { from{transform:translate(0,0)} to{transform:translate(-150px,-1700px)} } +@keyframes deriveB { from{transform:translate(0,0)} to{transform:translate(190px,-1700px)} } +@keyframes scintA { from{opacity:.35} to{opacity:.9} } +@keyframes scintB { from{opacity:.5} to{opacity:1} } + diff --git a/roles/serveur_nextcloud/handlers/main.yml b/roles/serveur_nextcloud/handlers/main.yml new file mode 100644 index 0000000..5df40e0 --- /dev/null +++ b/roles/serveur_nextcloud/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: Recharger nginx nextcloud + ansible.builtin.systemd: + name: nginx + state: reloaded + when: not ansible_check_mode + +- name: Recharger php-fpm nextcloud + ansible.builtin.systemd: + name: "php{{ serveur_nextcloud_php_version }}-fpm" + state: restarted + when: not ansible_check_mode diff --git a/roles/serveur_nextcloud/tasks/collabora.yml b/roles/serveur_nextcloud/tasks/collabora.yml new file mode 100644 index 0000000..522f146 --- /dev/null +++ b/roles/serveur_nextcloud/tasks/collabora.yml @@ -0,0 +1,40 @@ +--- +- name: Collabora / Office (occ, en www-data avec TLS PG) + become: true + become_user: "{{ serveur_nextcloud_utilisateur }}" + environment: + PGSSLMODE: "{{ serveur_nextcloud_pg_sslmode }}" + PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}" + when: not ansible_check_mode + block: + - name: Installer + activer l'app richdocuments (idempotent) + ansible.builtin.shell: + executable: /bin/bash + cmd: | + set -eo pipefail + OCC="php {{ serveur_nextcloud_racine }}/occ" + if $OCC app:list | grep -qw richdocuments; then echo SETOPS_OK; else $OCC app:install richdocuments && echo SETOPS_CHANGED; fi + register: serveur_nextcloud_office_app + changed_when: "'SETOPS_CHANGED' in serveur_nextcloud_office_app.stdout" + + - name: URL serveur -> Collabora (locale, rapide) + ansible.builtin.command: + argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "config:app:set", "richdocuments", "wopi_url", "--value={{ serveur_nextcloud_wopi_url }}"] + changed_when: false + + - name: Valider la configuration WOPI (autodetection) + ansible.builtin.command: + argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "richdocuments:activate-config"] + changed_when: false + + # Gotcha : public_wopi_url APRES activate-config (qui l'ecrase). Navigateur -> edge. + - name: URL navigateur -> Collabora (publique, via edge) + ansible.builtin.command: + argv: + - php + - "{{ serveur_nextcloud_racine }}/occ" + - config:app:set + - richdocuments + - public_wopi_url + - "--value={{ serveur_nextcloud_wopi_public_url }}" + changed_when: false diff --git a/roles/serveur_nextcloud/tasks/configurer.yml b/roles/serveur_nextcloud/tasks/configurer.yml new file mode 100644 index 0000000..951bfec --- /dev/null +++ b/roles/serveur_nextcloud/tasks/configurer.yml @@ -0,0 +1,79 @@ +--- +- name: Configuration Nextcloud (occ, en www-data avec TLS PG) + become: true + become_user: "{{ serveur_nextcloud_utilisateur }}" + environment: + PGSSLMODE: "{{ serveur_nextcloud_pg_sslmode }}" + PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}" + when: not ansible_check_mode + block: + - name: Reglages systeme (overwrite, redis, anti-SSRF) + ansible.builtin.command: + argv: + - php + - "{{ serveur_nextcloud_racine }}/occ" + - config:system:set + - "{{ item.k }}" + - "--value={{ item.v }}" + - "{{ item.t | default('--type=string') }}" + changed_when: false + loop: + - { k: "overwrite.cli.url", v: "https://{{ serveur_nextcloud_hostname }}" } + - { k: "overwritehost", v: "{{ serveur_nextcloud_hostname }}" } + - { k: "overwriteprotocol", v: "https" } + # Gotcha : Nextcloud REFUSE les IP privees (SSRF) => OIDC injoignable sans ceci. + - { k: "allow_local_remote_servers", v: "true", t: "--type=boolean" } + - { k: "memcache.local", v: "\\OC\\Memcache\\Redis" } + - { k: "memcache.locking", v: "\\OC\\Memcache\\Redis" } + + - name: Redis (hote + port) + ansible.builtin.command: + argv: + - php + - "{{ serveur_nextcloud_racine }}/occ" + - config:system:set + - redis + - "{{ item.k }}" + - "--value={{ item.v }}" + - "{{ item.t | default('--type=string') }}" + changed_when: false + loop: + - { k: "host", v: "{{ serveur_nextcloud_redis_host }}" } + - { k: "port", v: "{{ serveur_nextcloud_redis_port }}", t: "--type=integer" } + + - name: Domaines de confiance (trusted_domains) + ansible.builtin.command: + argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "config:system:set", "trusted_domains", "{{ idx }}", "--value={{ item }}"] + changed_when: false + loop: "{{ serveur_nextcloud_trusted_domains }}" + loop_control: + index_var: idx + + - name: Proxy de confiance (edge) — voir la vraie IP client, ne pas throttler l'edge + ansible.builtin.command: + argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "config:system:set", "trusted_proxies", "0", "--value={{ serveur_nextcloud_edge_ip }}"] + changed_when: false + when: serveur_nextcloud_edge_ip | length > 0 + + # Gotcha : Nextcloud a son PROPRE bundle CA (pas le systeme) => OIDC/HTTPS interne echoue sans import. + - name: Importer le certificat racine step-ca dans le bundle CA de Nextcloud + ansible.builtin.command: + argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "security:certificates:import", "{{ serveur_nextcloud_ca_racine }}"] + changed_when: false + + - name: Whitelister le sous-reseau d'admin (anti-force-brute, plutot que desactiver) + ansible.builtin.command: + argv: + - php + - "{{ serveur_nextcloud_racine }}/occ" + - config:system:set + - "{{ item.k }}" + - "--value={{ item.v }}" + - "{{ item.t | default('--type=string') }}" + changed_when: false + loop: + - { k: "bruteForceProtection.whitelist.0.ip", v: "{{ serveur_nextcloud_bruteforce_whitelist.split('/')[0] }}" } + - { k: "bruteForceProtection.whitelist.0.mask", v: "{{ serveur_nextcloud_bruteforce_whitelist.split('/')[1] }}", t: "--type=integer" } + when: + - serveur_nextcloud_bruteforce_whitelist | length > 0 + - "'/' in serveur_nextcloud_bruteforce_whitelist" diff --git a/roles/serveur_nextcloud/tasks/exposition.yml b/roles/serveur_nextcloud/tasks/exposition.yml new file mode 100644 index 0000000..bee43cd --- /dev/null +++ b/roles/serveur_nextcloud/tasks/exposition.yml @@ -0,0 +1,22 @@ +--- +- name: Deployer le vhost nginx Nextcloud (MIME .mjs correct) + ansible.builtin.template: + src: nginx-nextcloud.conf.j2 + dest: /etc/nginx/sites-available/nextcloud + owner: root + group: root + mode: "0644" + notify: Recharger nginx nextcloud + +- name: Activer le vhost Nextcloud + ansible.builtin.file: + src: /etc/nginx/sites-available/nextcloud + dest: /etc/nginx/sites-enabled/nextcloud + state: link + notify: Recharger nginx nextcloud + +- name: Retirer le vhost nginx par defaut + ansible.builtin.file: + path: /etc/nginx/sites-enabled/default + state: absent + notify: Recharger nginx nextcloud diff --git a/roles/serveur_nextcloud/tasks/installer.yml b/roles/serveur_nextcloud/tasks/installer.yml new file mode 100644 index 0000000..0fdf7bc --- /dev/null +++ b/roles/serveur_nextcloud/tasks/installer.yml @@ -0,0 +1,36 @@ +--- +- name: Telecharger le tarball Nextcloud (version epinglee) + ansible.builtin.get_url: + url: "{{ serveur_nextcloud_url }}" + dest: "/tmp/nextcloud-{{ serveur_nextcloud_version }}.tar.bz2" + mode: "0644" + +- name: Extraire Nextcloud dans /var/www (si absent) + ansible.builtin.unarchive: + src: "/tmp/nextcloud-{{ serveur_nextcloud_version }}.tar.bz2" + dest: "/var/www/" + remote_src: true + owner: "{{ serveur_nextcloud_utilisateur }}" + group: "{{ serveur_nextcloud_utilisateur }}" + creates: "{{ serveur_nextcloud_racine }}/occ" + +- name: Installer Nextcloud (occ maintenance:install, PG en TLS ; une seule fois) + ansible.builtin.command: + cmd: >- + php {{ serveur_nextcloud_racine }}/occ maintenance:install + --database pgsql + --database-host {{ serveur_nextcloud_db_host }} + --database-name {{ serveur_nextcloud_db_name }} + --database-user {{ serveur_nextcloud_db_user }} + --database-pass {{ serveur_nextcloud_db_password }} + --admin-user {{ serveur_nextcloud_admin_user }} + --admin-pass {{ serveur_nextcloud_admin_password }} + --data-dir {{ serveur_nextcloud_data_dir }} + creates: "{{ serveur_nextcloud_racine }}/config/config.php" + become: true + become_user: "{{ serveur_nextcloud_utilisateur }}" + environment: + PGSSLMODE: "{{ serveur_nextcloud_pg_sslmode }}" + PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}" + no_log: true + when: not ansible_check_mode diff --git a/roles/serveur_nextcloud/tasks/main.yml b/roles/serveur_nextcloud/tasks/main.yml new file mode 100644 index 0000000..4c08193 --- /dev/null +++ b/roles/serveur_nextcloud/tasks/main.yml @@ -0,0 +1,47 @@ +--- +- name: Exiger les secrets Nextcloud (Vault) + ansible.builtin.assert: + that: + - serveur_nextcloud_admin_password | length > 0 + - (not (serveur_nextcloud_oidc_actif | bool)) or (serveur_nextcloud_oidc_client_secret | length > 0) + fail_msg: "vault_nextcloud_admin (et vault_nextcloud_oidc si OIDC actif) sont requis (Ansible Vault)." + +- name: Resoudre la base de donnees depuis le registre (role partage) + ansible.builtin.include_role: + name: resoudre_base + vars: + resoudre_base_groupe: "{{ serveur_nextcloud_groupe }}" + +- name: Adopter les facts de base pour Nextcloud + ansible.builtin.set_fact: + serveur_nextcloud_db_password: "{{ resoudre_base_db_password }}" + serveur_nextcloud_db_host: "{{ resoudre_base_db_host }}" + serveur_nextcloud_db_port: "{{ resoudre_base_db_port }}" + no_log: true + +- name: Paquets (PHP-FPM + extensions, nginx, redis) + ansible.builtin.import_tasks: paquets.yml + +- name: PHP (tuning 512M + TLS PG pour php-fpm) + ansible.builtin.import_tasks: php.yml + +- name: Installation Nextcloud (tarball + occ maintenance:install) + ansible.builtin.import_tasks: installer.yml + +- name: Exposition (vhost nginx local, MIME .mjs) + ansible.builtin.import_tasks: exposition.yml + +- name: Configuration (trusted_proxies, overwrite, redis, CA, anti-SSRF) + ansible.builtin.import_tasks: configurer.yml + +- name: SSO OIDC (Keycloak, app user_oidc) + ansible.builtin.import_tasks: oidc.yml + when: serveur_nextcloud_oidc_actif | bool + +- name: Collabora / Office (app richdocuments, WOPI) + ansible.builtin.import_tasks: collabora.yml + when: serveur_nextcloud_office_actif | bool + +- name: Identite visuelle Alliance Boreale (theme + ciel etoile) + ansible.builtin.import_tasks: theme.yml + when: serveur_nextcloud_branding | bool diff --git a/roles/serveur_nextcloud/tasks/oidc.yml b/roles/serveur_nextcloud/tasks/oidc.yml new file mode 100644 index 0000000..55c5fbe --- /dev/null +++ b/roles/serveur_nextcloud/tasks/oidc.yml @@ -0,0 +1,36 @@ +--- +- name: SSO OIDC (occ, en www-data avec TLS PG) + become: true + become_user: "{{ serveur_nextcloud_utilisateur }}" + environment: + PGSSLMODE: "{{ serveur_nextcloud_pg_sslmode }}" + PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}" + when: not ansible_check_mode + block: + - name: Installer + activer l'app user_oidc (idempotent) + ansible.builtin.shell: + executable: /bin/bash + cmd: | + set -eo pipefail + OCC="php {{ serveur_nextcloud_racine }}/occ" + if $OCC app:list | grep -qw user_oidc; then echo SETOPS_OK; else $OCC app:install user_oidc && echo SETOPS_CHANGED; fi + register: serveur_nextcloud_oidc_app + changed_when: "'SETOPS_CHANGED' in serveur_nextcloud_oidc_app.stdout" + + - name: Configurer le fournisseur OIDC Keycloak (upsert idempotent) + ansible.builtin.command: + argv: + - php + - "{{ serveur_nextcloud_racine }}/occ" + - "user_oidc:provider" + - "{{ serveur_nextcloud_oidc_nom }}" + - "--clientid={{ serveur_nextcloud_oidc_client_id }}" + - "--clientsecret={{ serveur_nextcloud_oidc_client_secret }}" + - "--discoveryuri={{ serveur_nextcloud_oidc_discovery }}" + - "--scope=openid email profile" + - "--mapping-uid=preferred_username" + - "--mapping-email=email" + - "--mapping-display-name=name" + - "--unique-uid=0" + changed_when: false + no_log: true diff --git a/roles/serveur_nextcloud/tasks/paquets.yml b/roles/serveur_nextcloud/tasks/paquets.yml new file mode 100644 index 0000000..3c879e7 --- /dev/null +++ b/roles/serveur_nextcloud/tasks/paquets.yml @@ -0,0 +1,17 @@ +--- +- name: Installer les paquets Nextcloud (imagick tire ses deps ; PAS libmagickcore-6) + ansible.builtin.apt: + name: "{{ serveur_nextcloud_paquets }}" + state: present + update_cache: true + cache_valid_time: 3600 + +- name: Activer et demarrer redis + php-fpm + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true + state: started + loop: + - redis-server + - "php{{ serveur_nextcloud_php_version }}-fpm" + when: not ansible_check_mode diff --git a/roles/serveur_nextcloud/tasks/php.yml b/roles/serveur_nextcloud/tasks/php.yml new file mode 100644 index 0000000..50865fe --- /dev/null +++ b/roles/serveur_nextcloud/tasks/php.yml @@ -0,0 +1,23 @@ +--- +# Gotcha spike : memory_limit 128M (defaut Debian) => pages Vue blanches. 512M requis. +- name: Deployer le tuning PHP recommande (memory_limit, opcache, upload) + ansible.builtin.template: + src: 99-nextcloud.ini.j2 + dest: "/etc/php/{{ serveur_nextcloud_php_version }}/fpm/conf.d/99-setops-nextcloud.ini" + owner: root + group: root + mode: "0644" + notify: Recharger php-fpm nextcloud + +# Gotcha spike : PDO pgsql (libpq) lit PGSSLMODE/PGSSLROOTCERT dans l'env de php-fpm. +- name: Injecter l'env TLS PostgreSQL dans le pool php-fpm + ansible.builtin.blockinfile: + path: "/etc/php/{{ serveur_nextcloud_php_version }}/fpm/pool.d/www.conf" + marker: "; {mark} SETOPS nextcloud TLS PostgreSQL" + block: | + env[PGSSLMODE] = {{ serveur_nextcloud_pg_sslmode }} + env[PGSSLROOTCERT] = {{ serveur_nextcloud_ca_racine }} + notify: Recharger php-fpm nextcloud + +- name: Appliquer le rechargement php-fpm avant l'installation + ansible.builtin.meta: flush_handlers diff --git a/roles/serveur_nextcloud/tasks/theme.yml b/roles/serveur_nextcloud/tasks/theme.yml new file mode 100644 index 0000000..7272433 --- /dev/null +++ b/roles/serveur_nextcloud/tasks/theme.yml @@ -0,0 +1,42 @@ +--- +- name: Deployer le theme Alliance (ciel etoile, login et app) + ansible.builtin.copy: + src: themes/ + dest: "{{ serveur_nextcloud_racine }}/themes/" + owner: "{{ serveur_nextcloud_utilisateur }}" + group: "{{ serveur_nextcloud_utilisateur }}" + mode: "0644" + directory_mode: "0755" + when: serveur_nextcloud_theme_ciel | bool + +- name: Deployer le logo Alliance Boreale + ansible.builtin.copy: + src: alliance-logo.svg + dest: "{{ serveur_nextcloud_racine }}/alliance-logo.svg" + owner: "{{ serveur_nextcloud_utilisateur }}" + group: "{{ serveur_nextcloud_utilisateur }}" + mode: "0644" + +- name: Identite visuelle (occ theming + activation du ciel) + become: true + become_user: "{{ serveur_nextcloud_utilisateur }}" + environment: + PGSSLMODE: "{{ serveur_nextcloud_pg_sslmode }}" + PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}" + when: not ansible_check_mode + block: + - name: Nom / slogan / couleur / logo + ansible.builtin.command: + argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "theming:config", "{{ item.k }}", "{{ item.v }}"] + changed_when: false + loop: + - { k: "name", v: "{{ serveur_nextcloud_theme_nom }}" } + - { k: "slogan", v: "{{ serveur_nextcloud_theme_slogan }}" } + - { k: "primary_color", v: "{{ serveur_nextcloud_theme_couleur }}" } + - { k: "logo", v: "{{ serveur_nextcloud_racine }}/alliance-logo.svg" } + + - name: Activer le theme ciel etoile (thème custom) + ansible.builtin.command: + argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "config:system:set", "theme", "--value=alliance"] + changed_when: false + when: serveur_nextcloud_theme_ciel | bool diff --git a/roles/serveur_nextcloud/templates/99-nextcloud.ini.j2 b/roles/serveur_nextcloud/templates/99-nextcloud.ini.j2 new file mode 100644 index 0000000..f779030 --- /dev/null +++ b/roles/serveur_nextcloud/templates/99-nextcloud.ini.j2 @@ -0,0 +1,12 @@ +; Reglages PHP recommandes pour Nextcloud — genere par Set-OPS (serveur_nextcloud). +; Gotcha spike : le defaut Debian (128M) fait blanchir les pages Vue lourdes. +memory_limit = {{ serveur_nextcloud_php_memory_limit }} +upload_max_filesize = {{ serveur_nextcloud_php_upload_max }} +post_max_size = {{ serveur_nextcloud_php_upload_max }} +max_execution_time = 3600 +opcache.enable = 1 +opcache.enable_cli = 1 +opcache.memory_consumption = 128 +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 10000 +opcache.revalidate_freq = 60 diff --git a/roles/serveur_nextcloud/templates/nginx-nextcloud.conf.j2 b/roles/serveur_nextcloud/templates/nginx-nextcloud.conf.j2 new file mode 100644 index 0000000..4db7d9e --- /dev/null +++ b/roles/serveur_nextcloud/templates/nginx-nextcloud.conf.j2 @@ -0,0 +1,36 @@ +upstream php-handler-nc { server unix:/run/php/php{{ serveur_nextcloud_php_version }}-fpm.sock; } + +server { + listen {{ serveur_nextcloud_http_port }} default_server; + server_name {{ serveur_nextcloud_hostname }} {{ ansible_hostname | default('localhost') }} localhost; + root {{ serveur_nextcloud_racine }}; + index index.php index.html; + client_max_body_size {{ serveur_nextcloud_php_upload_max }}; + + location / { rewrite ^ /index.php$request_uri; } + + location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ { deny all; } + location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { deny all; } + + location ~ \.php(?:$|/) { + fastcgi_split_path_info ^(.+?\.php)(/.*|)$; + set $path_info $fastcgi_path_info; + try_files $fastcgi_script_name =404; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $path_info; + fastcgi_param modHeadersAvailable true; + fastcgi_param front_controller_active true; + fastcgi_pass php-handler-nc; + fastcgi_read_timeout 3600; + } + + # Gotcha spike : les modules ES .mjs doivent etre servis en text/javascript, + # sinon le navigateur les bloque (MIME application/octet-stream) => pages blanches. + location ~ \.mjs$ { default_type text/javascript; try_files $uri /index.php$request_uri; access_log off; } + + location ~ \.(?:css|js|woff2?|svg|gif|png|html|ttf|ico|jpg|jpeg|json|wasm)$ { + try_files $uri /index.php$request_uri; + access_log off; + } +}