serveur_nextcloud : rôle codifié depuis le spike (13 gotchas encodés)

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-05 15:00:39 -04:00
parent c98bc8393f
commit 79d0529686
17 changed files with 579 additions and 42 deletions

View file

@ -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

View file

@ -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.<domaine>` 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.<domaine>` ;
- **Exposition edge** : `cloud.<domaine>` → ce nœud:80, avec le FQDN dans les SAN du cert edge ;
- **Plancher** : le nœud doit résoudre `bureau.<domaine>` (→ 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).

View file

@ -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('')) }}"

View file

@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Alliance Boréale">
<defs>
<radialGradient id="sky" cx="50%" cy="36%" r="75%">
<stop offset="0%" stop-color="#10184a"/>
<stop offset="100%" stop-color="#05060f"/>
</radialGradient>
<linearGradient id="aurora" x1="0" y1="1" x2="1" y2="0">
<stop offset="0%" stop-color="#2ec18f"/>
<stop offset="45%" stop-color="#56c2ff"/>
<stop offset="100%" stop-color="#a98bff"/>
</linearGradient>
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#bfe9ff"/>
<stop offset="100%" stop-color="#bfe9ff" stop-opacity="0"/>
</radialGradient>
<radialGradient id="star" cx="50%" cy="45%" r="60%">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="100%" stop-color="#d6ecff"/>
</radialGradient>
</defs>
<!-- ciel nocturne -->
<circle cx="16" cy="16" r="16" fill="url(#sky)"/>
<!-- ruban d'aurore -->
<path d="M3 21 C 9 16, 13 23, 18 18 C 22 14, 26 17, 30 13"
fill="none" stroke="url(#aurora)" stroke-width="2.6"
stroke-linecap="round" opacity="0.85"/>
<!-- petites étoiles (constellation) -->
<circle cx="7" cy="9" r="0.9" fill="#cfe6ff"/>
<circle cx="24" cy="8" r="1.1" fill="#ffffff"/>
<circle cx="26" cy="22" r="0.8" fill="#bfe9ff"/>
<circle cx="9" cy="25" r="0.7" fill="#cfe6ff"/>
<!-- halo de l'étoile centrale -->
<circle cx="16" cy="15" r="9" fill="url(#glow)" opacity="0.55"/>
<!-- étoile centrale à quatre branches -->
<path d="M16 5
C 16.9 12.4, 17.6 13.1, 25 14
C 17.6 14.9, 16.9 15.6, 16 23
C 15.1 15.6, 14.4 14.9, 7 14
C 14.4 13.1, 15.1 12.4, 16 5 Z"
fill="url(#star)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -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} }

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;
}
}