Stockage objet S3
Le stockage objet remplace le disque local d'une application : images, pièces jointes, vidéos, PDF générés, exports. Les fichiers quittent le serveur applicatif pour un bucket, adressés par une URL et accessibles depuis n'importe quelle instance de l'application.
France Nuage fournit un stockage compatible S3, adossé à Ceph RadosGW et hébergé en France. N'importe quel SDK AWS (boto3, aws-sdk-js, flysystem-s3, minio-go) fonctionne sans adaptation, seuls l'endpoint et les clés changent.
Ce que vous recevez
Vous recevez un bucket, un utilisateur S3 qui en est propriétaire et un quota. Il n'y a rien d'autre à administrer.
| Paramètre | Valeur |
|---|---|
| Endpoint | https://s3.france-nuage.fr |
| Style d'URL | path-style (https://s3.france-nuage.fr/<bucket>/<clé>) |
| Région à signer | default |
| Signature | AWS Signature V4 |
| Bucket | le nom convenu à la commande |
| Quota | la taille convenue, appliquée en dur |
| Stockage sous-jacent | Ceph RadosGW, réplication 3 copies sur 3 datacenters |
Les credentials (access key et secret key) sont transmis séparément, jamais dans un document de référence. La clé secrète ne s'affiche qu'une fois. En cas de perte, nous en régénérons une nouvelle.
L'utilisateur S3 fourni est propriétaire de ce bucket et de lui seul. Il ne peut ni créer d'autres buckets (ce qui contournerait le quota) ni lire ceux des autres clients. Nous vérifions cette isolation à la livraison.
Se connecter
Le SDK doit forcer le path-style et signer sur la région default. Sans le premier, il tente https://<bucket>.s3.france-nuage.fr et ne trouve rien. Sans la seconde, la signature échoue en SignatureDoesNotMatch. Ces deux oublis expliquent la plupart des échecs de première connexion.
Voici la configuration à reprendre, en Python avec boto3 :
import boto3
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url="https://s3.france-nuage.fr",
region_name="default",
aws_access_key_id="...",
aws_secret_access_key="...",
config=Config(s3={"addressing_style": "path"}, signature_version="s3v4"),
)
La même chose avec l'AWS CLI :
aws --endpoint-url https://s3.france-nuage.fr --region default \
s3 ls s3://mon-bucket/
Et pour rclone, dans le fichier ~/.config/rclone/rclone.conf :
[fn]
type = s3
provider = Ceph
endpoint = https://s3.france-nuage.fr
region = default
force_path_style = true
access_key_id = ...
secret_access_key = ...
Les trois modes d'accès
Trois besoins appellent trois mécanismes distincts, qui coexistent sur le même bucket.
| Besoin | Mécanisme | Où stocker le fichier | Qui peut lire |
|---|---|---|---|
| Images, vidéos, pièces publiques | Lien permanent | sous public/ | tout le monde, avec l'URL exacte |
| Documents sensibles | URL pré-signée, expirante | n'importe où sauf public/ | celui qui a le lien, jusqu'à expiration |
| Documents à valeur probante | Object Lock (WORM) | n'importe où sauf public/ | l'application seule |
Lien permanent
Sur demande, nous ouvrons un préfixe de clés en lecture anonyme, public/ par convention. Tout objet dont la clé commence par ce préfixe devient lisible par quiconque connaît son URL, sans signature ni expiration :
https://s3.france-nuage.fr/mon-bucket/public/medias/8f3c1e2a-photo.jpg
Cette URL peut aller dans un <img src>, un e-mail ou un PDF. Elle ne périme jamais.
Deux garde-fous accompagnent cette ouverture. Le listing anonyme du bucket reste refusé, y compris sur le préfixe public, si bien que l'on ne peut pas énumérer les fichiers et qu'il faut connaître l'URL exacte. L'écriture anonyme reste refusée elle aussi, personne ne peut donc déposer ni effacer quoi que ce soit.
Par défaut, un bucket n'a aucun préfixe public, tout y est privé. L'ouverture se demande et le préfixe se choisit.
Lien temporaire (URL pré-signée)
Pour un document sensible, l'application génère une URL signée valable N secondes. Passé le délai, elle renvoie 403. Il n'y a rien à activer, la signature est native et l'opération reste purement locale au SDK, sans appel réseau ni coût à la génération.
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "mon-bucket", "Key": "prive/rapports/2026-09/rapport-1183.pdf"},
ExpiresIn=900, # 15 minutes
)
Signature V4 plafonne la durée de validité à 7 jours. Au-delà, l'URL est rejetée.
Le même mécanisme fonctionne pour l'écriture avec put_object, ce qui permet un upload direct du navigateur vers le stockage, sans faire transiter le fichier par le serveur applicatif. C'est le mode recommandé pour les gros fichiers et les vidéos.
Objets immuables (WORM)
L'Object Lock rend une version d'objet indestructible jusqu'à une échéance. L'application choisit, objet par objet, ce qui devient immuable et pour combien de temps :
s3.put_object(
Bucket="mon-bucket",
Key="rapports/2026-09/rapport-1183.pdf",
Body=pdf,
ObjectLockMode="COMPLIANCE", # ou GOVERNANCE
ObjectLockRetainUntilDate="2031-09-25T00:00:00Z",
)
Deux modes existent, et le choix compte :
GOVERNANCEinterdit la suppression de la version, sauf par un appel explicite portant l'en-têtex-amz-bypass-governance-retention. C'est l'immuabilité qui protège de la fausse manœuvre et du bug applicatif.COMPLIANCEinterdit la suppression à tout le monde avant l'échéance, vous compris, France Nuage comprise, administrateur du cluster compris. C'est l'immuabilité que l'on peut opposer à un tiers.
La contrainte vient du protocole S3 lui-même, et un bucket créé sans Object Lock ne peut plus l'obtenir. Si le WORM est un besoin, même futur, demandez-le dès la commande du bucket. Sinon il faudra créer un second bucket et y recopier les données.
COMPLIANCE est irrévocable dans l'autre sens aussi. Une date de rétention posée trop loin, dix ans sur un fichier de test par exemple, immobilise l'espace jusqu'à l'échéance, et cet espace reste décompté du quota. Validez sur GOVERNANCE avant de basculer en COMPLIANCE en production.
La convention de clés : public/ est public
Si vous faites ouvrir un préfixe public, c'est la seule règle structurante à tenir côté code, et aucun garde-fou automatique ne la protège.
Le préfixe public/ est en lecture anonyme, tout le reste du bucket est privé. Un fichier sensible écrit par erreur sous public/ devient lisible par quiconque connaît son URL, immédiatement et sans trace. Comme les URL de ce type circulent par nature (e-mails, PDF, pages web), une fuite de ce genre se rattrape mal.
Organisation type :
public/medias/<id>.jpg ← lisible par tous
public/videos/<projet>/<id>.mp4
prive/<client>/<id>.pdf ← pré-signé uniquement
rapports/<aaaa-mm>/<id>.pdf ← pré-signé + WORM
Deux recommandations pour l'implémentation :
- Centralisez la construction des clés dans une seule fonction, qui prend la visibilité en paramètre explicite plutôt que de laisser chaque appelant concaténer un chemin.
- Mettez un identifiant non devinable dans la clé, un UUID plutôt que
photo-1.jpg. Le listing anonyme étant fermé, une clé imprévisible rend le contenu depublic/indécouvrable autrement que par le lien que vous avez distribué.
Si la frontière public/ ne colle pas à votre modèle de données, elle se déplace, une ligne de configuration suffit de notre côté. Mieux vaut la fixer avant d'écrire le code.
À savoir avant d'écrire le code
Quatre comportements surprennent quand on ne les a pas anticipés.
Le versioning arrive avec l'Object Lock, et définitivement
Le WORM exige le versioning, et une fois activé il ne se retire plus. Écraser un fichier ne remplace donc pas l'ancien, il l'archive, et les anciennes versions comptent dans le quota. Une règle de cycle de vie les purge au bout de 30 jours, ce qui vous laisse au passage une fenêtre de récupération de 30 jours après un écrasement ou une suppression accidentelle.
Supprimer ne supprime pas
Sur un bucket versionné, un appel delete_object sans numéro de version pose un marqueur de suppression. L'objet disparaît des listings et des GET, mais les octets restent et restent décomptés du quota jusqu'à la purge différée. Pour effacer vraiment et tout de suite, supprimez la version précise via son VersionId.
C'est aussi la limite du WORM, qui empêche la destruction sans empêcher le masquage. Un objet sous rétention peut être rendu invisible par un marqueur de suppression, alors que la version protégée reste intacte et récupérable.
Les gros fichiers passent en multipart
Au-delà de quelques dizaines de Mo, les SDK découpent l'upload. Un upload interrompu laisse des fragments qui consomment le quota sans apparaître dans un listing normal, et c'est la cause classique du « bucket plein alors qu'il est vide ». Une règle de cycle de vie les avorte automatiquement au bout de 7 jours.
Le quota n'est pas appliqué à la seconde près
L'occupation du bucket reste en cache une dizaine de minutes, donc un léger dépassement est possible avant que les écritures ne soient refusées. Quand le plafond est atteint, les PUT renvoient 403 QuotaExceeded. Traitez ce cas explicitement dans l'application plutôt que de le découvrir en production.
Si le navigateur tape directement le stockage, pour un upload pré-signé depuis le front ou la lecture d'une vidéo en fetch, il faut une configuration CORS avec la liste de vos domaines. Elle n'est pas posée par défaut. Communiquez-nous les origines et nous la posons.
Migrer des fichiers existants
Déplacer quelques dizaines de Go depuis un disque local ne pose aucune difficulté technique, le transfert se compte en dizaines de minutes. Tout se joue sur l'ordre des opérations.
Le chemin le plus sûr tient en quatre temps :
- Copier sans basculer. La commande
rclone copyouaws s3 syncpousse les fichiers du serveur de production vers le bucket, application inchangée, en plaçant chaque fichier sous le bon préfixe (public ou non). C'est le moment de trancher. - Faire lire les deux sources à l'application, le stockage objet d'abord et le disque local en repli quand l'objet est absent. Tant que ce repli tient, un fichier oublié par la copie ne casse rien.
- Rejouer la synchronisation pour rattraper ce qui a été écrit sur disque pendant les étapes 1 et 2, puis basculer les écritures vers l'objet.
- Ne libérer le disque qu'après une période d'observation, une fois le repli de l'étape 2 devenu inutile. Sa fréquence d'usage doit tomber à zéro.
La copie initiale se lance via la commande :
rclone copy /var/www/monapp/storage fn:mon-bucket/prive \
--transfers 8 --progress
Deux points méritent attention.
N'activez pas la rétention WORM pendant la migration. Une reprise de copie qui réécrit un objet déjà verrouillé échouerait, et en mode COMPLIANCE la première copie serait indélébile même si elle est mauvaise. Posez la rétention après vérification, sur les objets concernés.
Surveillez aussi le quota pendant la copie. Si le bucket est versionné, une copie relancée depuis zéro archive la précédente au lieu de la remplacer, et deux passes complètes consomment le double jusqu'à la purge différée. La commande rclone copy ne retransfère que ce qui diffère, ce qui évite le problème, alors qu'un rclone sync --delete-before relancé à l'aveugle le provoque.
Si le quota devient juste après la migration, le plafond se relève par une ligne de configuration, sans interruption de service.
Bornage et supervision
| Réglage | Valeur | Rôle |
|---|---|---|
| Quota du bucket | la taille convenue | écritures refusées au-delà |
| Alerte de remplissage | 80 % du quota | prévient l'astreinte France Nuage |
| Purge des anciennes versions | 30 jours (si versioning) | borne le coût du versioning |
| Abandon des uploads incomplets | 7 jours | borne les fragments multipart |
| Création d'autres buckets | interdite | empêche de contourner le quota |
| Redondance | 3 copies, 3 datacenters | survit à la perte d'un site |
L'alerte de remplissage part vers l'astreinte France Nuage. Si vous voulez être prévenus directement, dites-nous où l'envoyer.
Ce qui n'est pas couvert
Autant l'écrire noir sur blanc.
- Le bucket n'est pas sauvegardé ailleurs. Les 3 copies protègent contre une panne matérielle ou la perte d'un datacenter, sans rien pouvoir contre une suppression côté application. Le seul filet est la purge différée des versions, qui laisse 30 jours pour rattraper une suppression accidentelle sur un bucket versionné. Passé ce délai, l'objet est perdu. Si les fichiers le justifient, une sauvegarde s'ajoute explicitement.
- Le chiffrement côté client n'est pas fourni. Les données sont chiffrées en transit par TLS et les disques du cluster le sont aussi, mais les clés appartiennent à la plateforme. Le chiffrement de bout en bout n'est donc pas assuré, un administrateur de l'infrastructure peut techniquement lire les objets. Si certains documents l'exigent, votre application doit chiffrer avant l'envoi.
- Aucun CORS n'est configuré par défaut, à poser dès que le navigateur tape directement le stockage.
- Il n'y a pas de journal d'accès par objet. Nous ne pouvons pas dire aujourd'hui qui a téléchargé quel fichier ni quand. Demandez-le si un besoin de traçabilité apparaît.
Réversibilité
Rien ici ne vous enferme, et cela tient à la conception du service.
L'API est celle de S3, sans extension propriétaire. Le code écrit pour ce bucket fonctionne sans modification sur MinIO auto-hébergé, sur un autre Ceph ou chez un autre fournisseur, seuls l'endpoint et les clés changent. Le lien pré-signé, l'Object Lock, le cycle de vie et la policy sont tous des mécanismes S3 publics.
L'intégralité des données s'exporte avec un outil standard, sans notre intervention :
rclone sync fn:mon-bucket /destination/locale --progress
Deux réserves méritent d'être dites. Les objets sous rétention COMPLIANCE se copient sans problème, mais ils ne peuvent pas être supprimés de la source avant leur échéance, ce qui est exactement ce que cette protection promet et qui se prévoit dans un plan de sortie. Et la configuration du bucket (policy, cycle de vie, Object Lock) ne suit pas automatiquement les données. Elle est versionnée chez nous sous forme de fichiers, que nous fournissons sur demande pour être rejoués tels quels ailleurs.
Prochaines étapes
- Sauvegardes externalisées, pour brancher un NAS sur le même stockage et appliquer la règle 3-2-1
- Transfert de fichiers SFTPGo, pour exposer un bucket en SFTP, FTPS ou WebDAV à des partenaires