Cet article déroule la mise en place complète d’une stack Nextcloud durcie sur Debian 13 : stockage chiffré LUKS, socle web, Nextcloud Hub (synchro, partage, ACL), co-édition OnlyOffice, cache et verrouillage Redis, et double authentification TOTP obligatoire. L’objectif : un cloud personnel ou d’équipe, auto-hébergé, performant et sécurisé.

1. Vue d’ensemble de la stack

[ Serveur Nextcloud - Debian 13 ]
 |
 |-- LUKS .............. chiffrement du disque de données (au repos)
 |-- nginx + PHP 8.4-FPM  socle web
 |-- MariaDB ........... base de données
 |-- Nextcloud Hub ..... synchro, partage, ACL (Group folders)
 |-- Redis ............. cache distribué + verrouillage transactionnel (file locking)
 |-- OnlyOffice ....... co-édition de documents (conteneur Docker + JWT)
 |__ MFA TOTP .......... double authentification obligatoire (+ codes de secours)

On vise les versions Nextcloud 34 (Hub) et OnlyOffice Docs récentes, sur Debian 13 (Trixie) avec PHP 8.4 (supporté par Nextcloud 34). Dans tout l’article : domaine cloud.exemple.com pour Nextcloud, office.exemple.com pour OnlyOffice, disque de données /dev/sdb, données montées sur /srv/nextcloud/data.

2. Prérequis

  • Une Debian 13 à jour, avec un accès root/sudo et un nom de domaine pointant vers le serveur.
  • Un second disque (ou partition) dédié aux données, qui sera chiffré.
  • Des certificats TLS (Let’s Encrypt) pour les deux sous-domaines : tout doit être en HTTPS, sous peine de casser l’éditeur OnlyOffice (contenu mixte).
  • Pour OnlyOffice : prévoir au moins 2 vCPU et 4 Go de RAM supplémentaires.
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y curl gnupg unzip cryptsetup

3. Chiffrement du stockage avec LUKS

On chiffre le disque de données avant d’y installer quoi que ce soit. Tout ce qui sera écrit par Nextcloud (fichiers, aperçus, base si on la déplace) reposera ainsi sur un volume chiffré au repos.

# Initialiser LUKS2 sur le disque de données (DÉTRUIT son contenu)
sudo cryptsetup luksFormat /dev/sdb

# Ouvrir le volume -> apparaît en /dev/mapper/ncdata
sudo cryptsetup open /dev/sdb ncdata

# Créer le système de fichiers et le point de montage
sudo mkfs.ext4 /dev/mapper/ncdata
sudo mkdir -p /srv/nextcloud/data
sudo mount /dev/mapper/ncdata /srv/nextcloud/data

Pour un déverrouillage automatique au démarrage (serveur sans clavier), on utilise un fichier-clé stocké sur le système racine. Compromis de sécurité à connaître : si la racine est compromise, le volume devient lisible. Pour un vrai durcissement, préférez une passphrase saisie au boot ou un déverrouillage réseau (clevis + tang).

# Générer un fichier-clé et l'ajouter comme clé LUKS
sudo mkdir -p /etc/luks && sudo chmod 700 /etc/luks
sudo dd if=/dev/urandom of=/etc/luks/ncdata.key bs=4096 count=1
sudo chmod 400 /etc/luks/ncdata.key
sudo cryptsetup luksAddKey /dev/sdb /etc/luks/ncdata.key

# Récupérer l'UUID du disque chiffré
sudo blkid -s UUID -o value /dev/sdb

On déclare ensuite le volume dans /etc/crypttab (déverrouillage au boot) et le montage dans /etc/fstab :

# /etc/crypttab
ncdata  UUID=VOTRE-UUID-LUKS  /etc/luks/ncdata.key  luks,discard

# /etc/fstab
/dev/mapper/ncdata  /srv/nextcloud/data  ext4  defaults  0  2

Pensez à sauvegarder l’en-tête LUKS (sans elle, les données sont irrécupérables) :

sudo cryptsetup luksHeaderBackup /dev/sdb --header-backup-file /root/ncdata-luks-header.img

4. Socle web : nginx, PHP 8.4 et MariaDB

sudo apt install -y nginx mariadb-server redis-server \
  php8.4-fpm php8.4-gd php8.4-mysql php8.4-curl php8.4-mbstring \
  php8.4-intl php8.4-gmp php8.4-bcmath php8.4-xml php8.4-zip \
  php8.4-imagick php8.4-redis php8.4-apcu

On sécurise MariaDB puis on crée la base et l’utilisateur Nextcloud :

sudo mysql_secure_installation

sudo mariadb -e "CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
sudo mariadb -e "CREATE USER 'nc_admin'@'localhost' IDENTIFIED BY 'MotDePasseDB';"
sudo mariadb -e "GRANT ALL PRIVILEGES ON nextcloud.* TO 'nc_admin'@'localhost'; FLUSH PRIVILEGES;"

Quelques réglages PHP utiles dans /etc/php/8.4/fpm/php.ini (et le pool) pour Nextcloud :

memory_limit = 512M
upload_max_filesize = 10G
post_max_size = 10G
max_execution_time = 3600
output_buffering = Off

; Cache d'opcode (recommandé par Nextcloud)
opcache.enable = 1
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.memory_consumption = 128
opcache.save_comments = 1

; APCu disponible aussi en CLI (pour occ)
apc.enable_cli = 1
sudo systemctl restart php8.4-fpm

5. Installation de Nextcloud Hub

cd /tmp
wget https://download.nextcloud.com/server/releases/latest.zip
sudo unzip -q latest.zip -d /var/www/
sudo chown -R www-data:www-data /var/www/nextcloud

On installe Nextcloud en ligne de commande avec occ (toujours exécuté en tant que www-data), en pointant le répertoire de données vers le volume LUKS :

cd /var/www/nextcloud
sudo -u www-data php occ maintenance:install \
  --database "mysql" --database-name "nextcloud" \
  --database-user "nc_admin" --database-pass "MotDePasseDB" \
  --admin-user "admin" --admin-pass "MotDePasseAdmin" \
  --data-dir "/srv/nextcloud/data"

On déclare le domaine de confiance et les URLs publiques :

sudo -u www-data php occ config:system:set trusted_domains 1 --value=cloud.exemple.com
sudo -u www-data php occ config:system:set overwrite.cli.url --value=https://cloud.exemple.com
sudo -u www-data php occ config:system:set overwriteprotocol --value=https
sudo -u www-data php occ config:system:set default_phone_region --value=FR

Voici un vhost nginx minimal pour Nextcloud (TLS géré par Certbot) :

server {
    listen 443 ssl http2;
    server_name cloud.exemple.com;
    root /var/www/nextcloud;
    index index.php;

    ssl_certificate     /etc/letsencrypt/live/cloud.exemple.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.exemple.com/privkey.pem;

    client_max_body_size 10G;
    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains" always;

    location = /.well-known/carddav { return 301 /remote.php/dav; }
    location = /.well-known/caldav  { return 301 /remote.php/dav; }

    location / {
        try_files $uri $uri/ /index.php$request_uri;
    }

    location ~ \.php(?:$|/) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param HTTPS on;
    }
}
sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Enfin, on remplace le cron AJAX par un vrai cron système (recommandé), exécuté toutes les 5 minutes par www-data :

sudo -u www-data php occ background:cron
# crontab de www-data (sudo crontab -u www-data -e)
*/5 * * * * php -f /var/www/nextcloud/cron.php

6. Redis : cache distribué et verrouillage transactionnel

Nextcloud distingue le cache local (rapide, par serveur : APCu) du cache distribué et du verrouillage transactionnel des fichiers (file locking), confiés à Redis. Le file locking via Redis évite les corruptions lors d’accès concurrents.

On fait écouter Redis sur une socket Unix (plus rapide et plus sûr qu’un port TCP local) dans /etc/redis/redis.conf :

port 0
unixsocket /run/redis/redis-server.sock
unixsocketperm 770
# Autoriser www-data à utiliser la socket Redis
sudo usermod -aG redis www-data
sudo systemctl restart redis-server

On déclare APCu + Redis dans la configuration de Nextcloud :

sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu'
sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set redis host --value=/run/redis/redis-server.sock
sudo -u www-data php occ config:system:set redis port --value=0 --type=integer

Le bloc résultant dans config/config.php ressemble à ceci :

'memcache.local' => '\OC\Memcache\APCu',
'memcache.distributed' => '\OC\Memcache\Redis',
'memcache.locking' => '\OC\Memcache\Redis',
'redis' => [
    'host' => '/run/redis/redis-server.sock',
    'port' => 0,
    'timeout' => 1.5,
],
# Vérifier que Redis répond
redis-cli -s /run/redis/redis-server.sock ping

7. OnlyOffice : co-édition de documents

Le serveur de documents OnlyOffice se déploie proprement dans un conteneur Docker. Depuis Docs 7.2, le JWT est activé par défaut : on fixe nous-mêmes le secret pour le reporter côté Nextcloud.

# docker-compose.yml
services:
  onlyoffice:
    image: onlyoffice/documentserver:latest
    container_name: onlyoffice
    restart: unless-stopped
    ports:
      - "127.0.0.1:8081:80"
    environment:
      - JWT_ENABLED=true
      - JWT_SECRET=UnSecretJWTLongEtAleatoire
    volumes:
      - ./data:/var/www/onlyoffice/Data
      - ./logs:/var/log/onlyoffice
sudo docker compose up -d

On expose OnlyOffice derrière nginx en HTTPS sur office.exemple.com (les en-têtes WebSocket sont indispensables pour l’éditeur) :

server {
    listen 443 ssl http2;
    server_name office.exemple.com;

    ssl_certificate     /etc/letsencrypt/live/office.exemple.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/office.exemple.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Côté Nextcloud, on active le connecteur et on renseigne l’URL + le même secret JWT :

sudo -u www-data php occ app:enable onlyoffice
sudo -u www-data php occ config:app:set onlyoffice DocumentServerUrl --value="https://office.exemple.com/"
sudo -u www-data php occ config:app:set onlyoffice jwt_secret --value="UnSecretJWTLongEtAleatoire"

Le secret JWT doit être strictement identique des deux côtés, sinon l’éditeur refuse de s’ouvrir. Alternative plus légère si vous ne voulez pas gérer Docker : l’application Community Document Server (tout-en-un) depuis la boutique d’applications, suffisante pour de petits usages.

8. Partage et permissions avancées (ACL)

Nextcloud offre plusieurs canaux de partage : entre utilisateurs et groupes internes, par lien public (avec mot de passe et expiration), par e-mail, et en fédération avec d’autres instances Nextcloud. La stratégie recommandée : cadrer d’abord ces canaux au niveau global, puis structurer le partage d’équipe avec Group folders et des ACL fines, plutôt que de multiplier les partages individuels (vite ingérables).

8.1 Réglages globaux de partage

Ces paramètres se pilotent dans Administration, Partage ou en ligne de commande. On active le partage et les liens publics, puis on les durcit (mot de passe obligatoire, expiration par défaut) :

# Activer le partage et les liens publics
sudo -u www-data php occ config:app:set core shareapi_enabled --value=yes
sudo -u www-data php occ config:app:set core shareapi_allow_links --value=yes

# Durcir les liens publics : mot de passe obligatoire + expiration par défaut
sudo -u www-data php occ config:app:set core shareapi_enforce_links_password --value=yes
sudo -u www-data php occ config:app:set core shareapi_default_expire_date --value=yes
sudo -u www-data php occ config:app:set core shareapi_expire_after_n_days --value=30

# Expiration aussi pour les partages internes et fédérés
sudo -u www-data php occ config:app:set core shareapi_default_internal_expire_date --value=yes
sudo -u www-data php occ config:app:set core shareapi_internal_expire_after_n_days --value=90

# Cloisonner : un utilisateur ne peut partager qu'avec les membres de ses groupes
sudo -u www-data php occ config:app:set core shareapi_only_share_with_group_members --value=yes

Les permissions par défaut des nouveaux partages sont un masque de bits : 1 = lecture, 2 = mise à jour, 4 = création, 8 = suppression, 16 = repartage. On combine par addition (31 = tout, 1 = lecture seule, 17 = lecture + repartage) :

sudo -u www-data php occ config:app:set core shareapi_default_permissions --value=31

Pour interdire purement et simplement le partage à certains groupes (ex: stagiaires), utilisez la liste Exclure des groupes du partage dans Administration, Partage (plus lisible qu’en ligne de commande).

8.2 Group folders : des dossiers d’équipe

L’application Group folders crée des dossiers partagés rattachés à des groupes (et non à un propriétaire), avec quota dédié : idéal pour des espaces d’équipe pérennes (Projets, Direction, Compta…).

sudo -u www-data php occ app:enable groupfolders

# Créer un dossier d'équipe et le rattacher à un ou plusieurs groupes
sudo -u www-data php occ groupfolders:create "Projets"
sudo -u www-data php occ groupfolders:group 1 equipe-projets write
sudo -u www-data php occ groupfolders:group 1 direction read

# Fixer un quota dédié au dossier (ex: 50 Go)
sudo -u www-data php occ groupfolders:quota 1 50GB

# Lister les Group folders et leur configuration
sudo -u www-data php occ groupfolders:list

8.3 ACL avancées (par sous-dossier)

Par défaut, tous les membres des groupes rattachés ont le même accès sur tout le dossier. Les permissions avancées (ACL) permettent de descendre au niveau du sous-dossier voire du fichier, et d’accorder ou retirer finement lecture, écriture, création, suppression et repartage, par groupe ou par utilisateur.

# Activer les ACL sur le Group folder (id 1)
sudo -u www-data php occ groupfolders:permissions 1 --enable-acl

# Désigner un gestionnaire d'ACL (il éditera les règles depuis l'interface)
sudo -u www-data php occ groupfolders:permissions 1 --manage-add-group equipe-projets

Les règles fines se posent ensuite dans l’application Fichiers : sur le dossier, ouvrir le panneau de détails puis l’onglet des permissions avancées, et ajouter des règles par sous-dossier. Exemple : Projets/Contrats en lecture seule pour equipe-projets mais en écriture pour direction. Le modèle est restrictif et hérité : une permission retirée à un niveau se propage aux sous-dossiers tant qu’on ne la ré-accorde pas explicitement plus bas.

8.4 Règles d’accès par politique (optionnel)

Pour des règles transverses, indépendantes de l’arborescence, l’application File access control ajoute un moteur de règles (conditions ET/OU) : bloquer le téléchargement de certains types de fichiers, interdire l’upload hors d’une plage d’IP, restreindre selon le groupe ou l’agent, etc.

sudo -u www-data php occ app:enable files_accesscontrol

Les règles se définissent ensuite dans Administration, Flux de travail. À manier avec prudence : une règle trop large peut bloquer des accès légitimes.

9. MFA TOTP obligatoire

La double authentification (2FA) ajoute un second facteur au mot de passe. On s’appuie ici sur le TOTP (codes à usage unique basés sur le temps, générés par une appli type Aegis, FreeOTP, Google Authenticator), complété par des codes de secours. Points clés à ne pas rater : l’ordre d’activation (sous peine de se verrouiller dehors) et la compatibilité avec les clients de synchro.

9.1 Applications à installer

sudo -u www-data php occ app:enable twofactor_totp
sudo -u www-data php occ app:enable twofactor_backupcodes

Recommandé en complément : WebAuthn / passkeys (clé matérielle FIDO2 ou biométrie, résistant au hameçonnage) via twofactor_webauthn, et la journalisation des événements de sécurité via admin_audit :

sudo -u www-data php occ app:enable twofactor_webauthn
sudo -u www-data php occ app:enable admin_audit

9.2 Enrôler un compte (AVANT d’imposer la 2FA)

  1. Se connecter à l’interface web avec le compte concerné (commencer par l’admin).
  2. Aller dans Paramètres personnels, Sécurité.
  3. Activer TOTP : scanner le QR code avec l’application d’authentification, puis saisir un premier code pour valider.
  4. Générer les codes de secours et les conserver dans un endroit sûr (gestionnaire de mots de passe, coffre) : ce sont eux qui sauvent en cas de perte du téléphone.

9.3 Rendre la 2FA obligatoire

Une fois l’admin (et idéalement les premiers utilisateurs) enrôlé, on impose la 2FA. Impératif : ne jamais activer l’obligation avant d’avoir configuré le TOTP + les codes de secours d’au moins un compte admin.

# Imposer la 2FA à tous les comptes
sudo -u www-data php occ twofactorauth:enforce --on

# Variante : cibler des groupes précis et exclure les comptes de service
sudo -u www-data php occ twofactorauth:enforce --on --group=staff --exclude=service-accounts

# Lever l'obligation (retour arrière)
sudo -u www-data php occ twofactorauth:enforce --off

# Vérifier l'état 2FA d'un compte (fournisseurs activés)
sudo -u www-data php occ twofactorauth:state admin

9.4 Clients de synchro et WebDAV : mots de passe d’application

Point crucial souvent oublié. La 2FA TOTP s’applique à la connexion web, mais les clients de bureau/mobile et les accès WebDAV utilisent l’authentification basique et ne savent pas saisir un code TOTP. La bonne pratique est le mot de passe d’application : un jeton dédié par appareil, généré dans Paramètres personnels, Sécurité, Appareils et sessions. Il contourne la 2FA, est révocable individuellement, et évite d’exposer le mot de passe principal. À communiquer aux utilisateurs au moment où l’on active l’obligation, pour ne pas casser leurs synchronisations.

9.5 Administration et dépannage

# Activer/désactiver un fournisseur 2FA pour un utilisateur donné
sudo -u www-data php occ twofactorauth:enable utilisateur totp
sudo -u www-data php occ twofactorauth:disable utilisateur totp

# Débloquer un compte verrouillé (téléphone ET codes de secours perdus)
sudo -u www-data php occ twofactorauth:disable admin totp

# Filet de sécurité ultime : lever l'obligation globale puis réactiver après remise en état
sudo -u www-data php occ config:system:set twofactor_enforced --value false

Bonnes pratiques de durcissement :

  • Conserver l’accès occ (donc SSH au serveur) comme unique véritable porte de secours : c’est ce qui évite un verrouillage total.
  • Activer la protection anti-force brute de Nextcloud (active par défaut) et, si possible, un fail2ban sur les échecs de connexion.
  • Proposer WebAuthn/passkeys en second facteur prioritaire : plus résistant au hameçonnage que le TOTP.
  • Auditer les événements 2FA via admin_audit (fichier de log dédié).

10. Vérifications

# État général et avertissements de configuration
sudo -u www-data php occ status
sudo -u www-data php occ config:system:get memcache.locking

# Redis
redis-cli -s /run/redis/redis-server.sock ping

# Réglages recommandés / sécurité : voir Administration > Vue d'ensemble
  • Page Administration, Vue d’ensemble : zéro avertissement de sécurité/configuration.
  • Créer un document et l’ouvrir : l’éditeur OnlyOffice doit s’afficher (sinon, vérifier le secret JWT et le HTTPS des deux côtés).
  • Sur un Group folder avec ACL : vérifier qu’un membre d’un groupe en lecture seule ne peut pas modifier un sous-dossier protégé.
  • Se déconnecter/reconnecter : la demande de code TOTP doit apparaître ; tester aussi un mot de passe d’application sur le client de synchro.
  • Scan externe : scan.nextcloud.com doit donner un bon score.

11. Points de vigilance

  • LUKS : sauvegardez l’en-tête (luksHeaderBackup) et réfléchissez au mode de déverrouillage (fichier-clé pratique mais moins sûr ; passphrase au boot ou clevis/tang pour un vrai durcissement).
  • ACL : modèle restrictif et hérité ; testez toujours avec un compte non-admin après avoir posé des règles.
  • MFA : codes de secours générés avant l’obligation, mots de passe d’application pour les clients de synchro, et occ comme porte de secours.
  • OnlyOffice : secret JWT identique des deux côtés, tout en HTTPS (le moindre contenu mixte casse l’éditeur) ; prévoir CPU/RAM.
  • Redis : APCu en local + Redis pour le distribué/verrouillage ; via socket Unix avec www-data dans le groupe redis.
  • Sauvegardes : base de données + dossier data + config.php + en-tête LUKS ; tester la restauration.
  • Cron : utiliser le cron système (pas AJAX) pour la fiabilité des tâches de fond.

12. Récapitulatif des commandes

# Chiffrement
cryptsetup luksFormat /dev/sdb && cryptsetup open /dev/sdb ncdata
mkfs.ext4 /dev/mapper/ncdata && mount /dev/mapper/ncdata /srv/nextcloud/data

# Installation Nextcloud
sudo -u www-data php occ maintenance:install --database mysql --data-dir /srv/nextcloud/data ...
sudo -u www-data php occ config:system:set trusted_domains 1 --value=cloud.exemple.com

# Redis (cache + verrouillage)
sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis'

# Partage & ACL
sudo -u www-data php occ app:enable groupfolders
sudo -u www-data php occ groupfolders:create "Projets"
sudo -u www-data php occ groupfolders:permissions 1 --enable-acl

# OnlyOffice
sudo -u www-data php occ app:enable onlyoffice
sudo -u www-data php occ config:app:set onlyoffice DocumentServerUrl --value="https://office.exemple.com/"

# MFA TOTP obligatoire
sudo -u www-data php occ app:enable twofactor_totp twofactor_backupcodes
sudo -u www-data php occ twofactorauth:enforce --on

Conclusion

On obtient un Nextcloud complet et durci : données chiffrées au repos avec LUKS, performances et cohérence assurées par Redis, co-édition bureautique via OnlyOffice, partage granulaire par ACL, et accès protégé par une double authentification TOTP obligatoire. Une base solide, entièrement auto-hébergée — et tout à fait scriptable via Ansible pour un déploiement reproductible.