Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CraftFileGate

CraftFileGate est un serveur de fichiers : un client SFTP, un script HTTP ou un navigateur y deposent et y prennent des fichiers, que le serveur range sur un ou plusieurs stockages. Il remplace un serveur SFTP classique (ProFTPD, OpenSSH internal-sftp) quand les fichiers ne vivent pas tous sur un disque local, ou quand les comptes viennent d’un fournisseur d’identite.

Ce qu’il fait

BesoinReponse
Deposer et prendre des fichiersSFTP, API REST de fichiers, explorateur web
Ranger les fichiersdisque local, serveur SFTP amont, S3 et compatibles, HDFS via Knox (lecture seule)
Authentifiermot de passe, cle publique SSH, jeton JWT (secret, cle publique ou JWKS)
Donner des droitsdes roles, qui montent des stockages et y donnent des droits par chemin ; refus par defaut
Surveillerpiste d’audit, journaux JSON, metriques Prometheus, traces OpenTelemetry
AdministrerAPI d’administration et console web : sessions, bans, configuration en vigueur
Deployerun binaire statique, des images Docker, un chart Helm

Les mots du guide

MotSens
backendun stockage nomme, decrit dans [[backends]]
roleun ensemble de montages ; un utilisateur en recoit un ou plusieurs
montageun backend vu par l’utilisateur a un chemin (mount_path), a partir d’un sous-repertoire (home_dir), avec son ACL
ACLles droits (read, write, list, delete, rename) par chemin, dans un montage
porteune facon d’entrer : SFTP, API REST, explorateur, console d’administration

Le chapitre Utilisateurs, roles et montages les explique avec des exemples.

Demarrage rapide

Un serveur sur un disque local, un utilisateur alice qui ne voit que son repertoire.

1. Generer une cle d’hote

ssh-keygen -t ed25519 -f /etc/craft-file-gate/host_ed25519 -N ""

Une cle listee dans host_keys mais absente refuse le demarrage.

2. Ecrire la configuration

Creer /etc/craft-file-gate/config.toml :

#:schema ./config.schema.json
[server]
shutdown_grace_period_secs = 30
# max_sessions_per_user = 10  # optionnel, illimite par defaut

[sftp]
listen = "0.0.0.0:2222"
host_keys = ["/etc/craft-file-gate/host_ed25519"]
# login_grace_secs = 120      # connexion non authentifiee coupee au-dela (0 = jamais)

[auth]
jwt_sentinel_username = "jwt"
timeout_secs = 5
# roles_file = "/etc/craft-file-gate/roles.toml"  # optionnel, sinon [[roles]] inline
# authz_base_url = "https://authz.internal"  # optionnel, service de mapping distant

[auth.jwt]
secret = "changez-moi-en-production"
# public_key_file = "/etc/craft-file-gate/jwt_public.pem"  # alternative au secret
# jwks_url = "https://idp.interne/.well-known/jwks.json"   # alternative : cles du fournisseur d'identite
# jwks_refresh_interval_secs = 3600                        # defaut, minimum 1
# username_path = "/sub"       # defaut
# authorities_path = "/groups" # defaut

[auth.methods]
jwt   = { enabled = true }
# Mot de passe et cle publique sont les preuves du magasin local, pas des
# methodes a part : elles se declarent sous `local`.
local = { enabled = true, password = true, pubkey = true }

[admin]
listen = "127.0.0.1:8080"
bearer_token = "changez-moi-en-production"

[log]
level = "info"
format = "json"

# ── Stockage et droits ──
[[backends]]
name = "local"
type = "local"
root = "/srv/sftp"

[[roles]]
name = "utilisateurs"

[[roles.mounts]]
backend = "local"
home_dir = "/{username}"   # alice voit /srv/sftp/alice comme sa racine /
create_home = true

[[roles.mounts.acl]]
path = "/"
rights = ["read", "write", "list", "delete", "rename"]
recursive = true

# ── Utilisateurs locaux ──
[[users]]
username = "alice"
# mot de passe : changez-moi-en-production
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$QzIrOEdtBZI4UL4ddXDbkg$yudDYsZSLamFZHWeKlFpFWtkP4kX6ALV8GNKs3pEXBk"
# authorized_keys = ["ssh-ed25519 AAAA... alice@poste"]
authorities = ["utilisateurs"]

Le secret JWT, le jeton admin et le mot de passe d’alice (changez-moi-en-production) sont publics. Le serveur demarre avec, mais emet un WARN par valeur d’exemple encore en place. Pour remplacer le bloc [[users]] d’alice :

echo -n "mot-de-passe" | craft-file-gate hash-password --user alice

Le processus doit pouvoir ecrire dans /srv/sftp : create_home y cree alice/ a sa premiere connexion.

3. Lancer le serveur

craft-file-gate --config /etc/craft-file-gate/config.toml

Pour Docker et Kubernetes, voir le guide (« Exploiter »).

4. Se connecter

sftp -P 2222 alice@votre-serveur

ls montre le contenu de /srv/sftp/alice, et rien au-dessus.

Et ensuite

PourLire
donner a chacun ce qu’il doit voirUtilisateurs, roles et montages, Recettes
brancher S3 ou un serveur SFTP amontLes backends
brancher un fournisseur d’identiteAuthentification
deployer en conteneurDocker, Kubernetes
comprendre un refusDepannage, Audit
connaitre une cleReference de la configuration

Utilisateurs, roles et montages

utilisateur ──authorities──▶ role ──▶ montage ──▶ backend (stockage)
                                       │
                                       └── ACL (droits par chemin)

Aucun droit n’est implicite : un backend que nul role ne monte n’est visible par personne, et le jeton d’administration ne donne acces a aucun fichier.

Un exemple complet

[[backends]]                 # un stockage, decrit une fois
name = "disque"
type = "local"
root = "/srv/sftp"

[[roles]]
name = "utilisateurs"

[[roles.mounts]]             # au moins un par role
backend = "disque"           # un nom de [[backends]]
home_dir = "/{username}"     # ou le montage commence sur le stockage
create_home = true

[[roles.mounts.acl]]         # les droits, relatifs au montage
path = "/"
rights = ["read", "write", "list", "delete", "rename"]
recursive = true

[[users]]
username = "alice"
password_hash = "$argon2id$..."   # craft-file-gate hash-password
authorities = ["utilisateurs"]    # les roles d'alice

alice voit / ; ce / est /srv/sftp/alice sur le disque.

Les cles d’un role et d’un montage

Toutes les cles, avec leur rechargement : Reference [[roles]], [[roles.mounts]].

Ou vont les fichiers

Un chemin client va au montage dont le mount_path le prefixe le plus longuement ; le reste s’ajoute a home_dir, sur le stockage (ce que home_dir designe sur chaque type). .. ne remonte jamais au-dessus de home_dir.

{username} : un repertoire par utilisateur

home_dir = "/{username}" donne a chaque utilisateur son repertoire, avec un seul role. Le nom, jamais nettoye, est fait de A-Z a-z 0-9 . _ -, 1 a 64 caracteres, sans point en tete.

Un montage ou plusieurs

MontagesCe que voit l’utilisateur
un seul, a /l’arborescence du backend, a partir de home_dir
plusieurs, sous des noms (/disque, /archives)/ liste les montages ; chacun montre son backend
a profondeur (/partenaires/acme)/ et /partenaires sont des repertoires intermediaires

Les chemins entre les montages sont des repertoires synthetiques. Un renommage reste dans son montage.

Cumuler des roles

Un utilisateur a souvent plusieurs roles. Sa session a les montages de tous.

Les montages de deux rolesDans la session
des mount_path dont aucun n’est le prefixe d’un autre (/a, /b)ils coexistent
identiques : meme mount_path, backend, home_dir, et memes max_file_mb, create_home, hidden_storesun seul montage, ACL unies

Deux montages differents d’un meme backend ont des home_dir disjoints (/a et /b, pas / et /a) : chaque fichier n’est atteint que sous une ACL.

La doctrine : un role a / est complet

  • un role a / est complet : il donne a lui seul tout ce que voit son utilisateur, et ne se cumule avec aucun autre ;
  • un role fait pour se cumuler monte sous un nom (/archives).

Voir les recettes.

D’ou viennent les roles

SourceCommentLes roles
utilisateur localauthorities de son entree [[users]]des noms de [[roles]]
jeton JWTla claim authorities_path (defaut /groups)des noms de [[roles]], ou des authorities a traduire
service d’autorisationauthz_base_url traduit les authorities inconnuesdes noms de [[roles]]

Les roles sont toujours definis dans [[roles]] ou roles_file. Seules les authorities qui ne sont pas des noms de roles vont au service.

Le contrat du service d’autorisation

POST <authz_base_url>/authz/resolve
Content-Type: application/json

{"authorities": ["CN=ACME-Partners,OU=Groups"]}

Reponse attendue : 200 et

{"roles": ["partenaire-acme"]}

authz_base_url et timeout_secs : Reference [auth].

Les roles nommes s’ajoutent a ceux trouves localement ; un nom que [[roles]] ne definit pas est ignore, et un service en panne laisse les roles locaux.

Une session SFTP construit tous ses backends a l’authentification et garde ses montages jusqu’a sa fin, meme apres un rechargement ; une requete REST construit celui du montage qu’elle atteint.

Ce que vous verrez

QuandLigne
demarrage, role a {username}INFO role gives each user their own home directory
demarrage, role qui monte plusieurs backendsINFO role mounts several backends: its users hold the credentials of all of them ..., champs role, backends, writable
create_home cree un repertoireINFO created missing home directory

Un role qui monte plusieurs backends detient leurs credentials : gardez-le en lecture seule, pour un utilisateur dedie authentifie par cle.

Recettes de montages

Chaque recette suppose ce backend :

[[backends]]
name = "disque"
type = "local"
root = "/srv/sftp"

1. Un repertoire par utilisateur

[[roles]]
name = "utilisateurs"

[[roles.mounts]]
backend = "disque"
home_dir = "/{username}"
create_home = true
acl = [{ path = "/", rights = ["read", "write", "list", "delete", "rename"], recursive = true }]

alice voit / = /srv/sftp/alice, cree a sa premiere connexion. Un role a / ne se cumule pas.

2. Un partenaire : depot entrant, releve sortante

Le partenaire depose dans /in sans pouvoir relire, et releve dans /out.

[[roles]]
name = "partenaire-acme"

[[roles.mounts]]
backend = "disque"
home_dir = "/partenaires/acme"
max_file_mb = 500
hidden_stores = { enabled = true, prefix = ".in.", extension = "" }
acl = [
  { path = "/in", rights = ["write", "list"], recursive = true },
  { path = "/out", rights = ["read", "list", "delete"], recursive = true },
]

/in est /srv/sftp/partenaires/acme/in ; un envoi y est publie a la fin du transfert.

Pour que ls / montre in et out, ajoutez une entree non recursive : path = "/", rights = ["list"].

3. Le compte d’exploitation : tous les stockages, en lecture

Un compte qui voit chaque backend a la racine et ne peut rien modifier.

[[backends]]
name = "archives"
type = "s3"
bucket = "archives"
region = "eu-west-3"
prefix = "sftp/"
credentials = { type = "iam_role" }

[[backends]]
name = "acme-amont"
type = "sftp"
host = "sftp.acme.example"
host_key_fingerprint = "SHA256:2hZbXq1b5Xb3vN2mQ6w7l3Fv0tXk4yJ8aUeYp9rS0cE"   # ssh-keygen -lf, verifie hors bande
auth = { type = "password", username = "relais", password = "changez-moi" }

[[roles]]
name = "exploitant"

[[roles.mounts]]
backend = "disque"
mount_path = "/disque"
acl = [{ path = "/", rights = ["read", "list"], recursive = true }]

[[roles.mounts]]
backend = "archives"
mount_path = "/archives"
acl = [{ path = "/", rights = ["read", "list"], recursive = true }]

[[roles.mounts]]
backend = "acme-amont"
mount_path = "/acme-amont"
home_dir = "/depot"
acl = [{ path = "/", rights = ["read", "list"], recursive = true }]

[[users]]
username = "exploitation"
password_hash = "$argon2id$..."   # d'un mot de passe aleatoire, jete : seule la cle sert
authorized_keys = ["ssh-ed25519 AAAA... exploitation@poste"]
authorities = ["exploitant"]

ls / montre acme-amont archives disque ; /archives/2024/x est la cle S3 sftp/2024/x.

Au demarrage, INFO role mounts several backends: ... donne role=exploitant et writable=[] : verifiez-le apres chaque modification du role.

4. Des roles qui se cumulent

[[roles]]
name = "equipe-a"
[[roles.mounts]]
backend = "disque"
mount_path = "/equipe-a"
home_dir = "/equipes/a"
acl = [{ path = "/", rights = ["read", "list"], recursive = true }]

[[roles]]
name = "equipe-b"
[[roles.mounts]]
backend = "disque"
mount_path = "/equipe-b"
home_dir = "/equipes/b"
acl = [{ path = "/", rights = ["read", "write", "list"], recursive = true }]

Un jeton dont la claim groups vaut ["equipe-a", "equipe-b"] ouvre une session avec /equipe-a (lecture) et /equipe-b (lecture et ecriture).

Deux roles sur le meme montage unissent leurs ACL : acme-depot (/in) et acme-releve (/out) sur le montage du cas 2 donnent /in et /out.

5. Des montages ranges : /partenaires/<nom>

Avec acme-amont du cas 3 :

[[roles]]
name = "gestion-partenaires"

[[roles.mounts]]
backend = "disque"
mount_path = "/partenaires/acme"
home_dir = "/partenaires/acme"
acl = [{ path = "/", rights = ["read", "list"], recursive = true }]   # "/" : la racine du montage

[[roles.mounts]]
backend = "acme-amont"
mount_path = "/partenaires/amont"
home_dir = "/depot"
acl = [{ path = "/", rights = ["read", "list"], recursive = true }]

/ et /partenaires sont synthetiques ; /partenaires/amont est le /depot de l’amont.

ACL

L’ACL d’un montage donne des droits, chemin par chemin ; tout le reste est refuse. Elle s’ecrit sous [[roles.mounts.acl]], et son path est relatif au montage.

Ecrire une ACL

[[roles.mounts]]
backend = "disque"
mount_path = "/partenaire"
home_dir = "/partenaires/acme"

[[roles.mounts.acl]]
path = "/"                    # /partenaire pour l'utilisateur
rights = ["list"]             # non recursive : ce dossier seulement

[[roles.mounts.acl]]
path = "/in"                  # /partenaire/in
rights = ["write", "list"]
recursive = true              # et tout ce qui est dessous

Toutes les cles : Reference [[roles.mounts.acl]].

Une ACL ne gouverne que son montage, meme sur un backend monte deux fois.

Les droits

Chaque operation demande son droit sur chaque chemin qu’elle nomme :

OperationDroit exige
envoi, mkdirwrite sur le chemin, et sur chaque parent qu’il faut creer
telechargementread
listagelist sur le repertoire liste
stat (SFTP), HEAD (REST)read ou list
suppression d’un fichier, d’un repertoire videdelete
suppression d’un repertoire non videdelete sur tout l’arbre
renommagerename sur la source et la destination, et write la ou le contenu deplace atterrit

Un listage montre tout le repertoire, sans filtrer par l’ACL. list sur /in ne donne pas list sur /.

Quelle entree decide

  1. Refus par defaut : un chemin qu’aucune entree ne gouverne est refuse.
  2. L’entree la plus specifique decide : celle du chemin exact, sinon l’entree recursive la plus proche au-dessus.
  3. Une entree plus specifique qui n’accorde pas le droit refuse, meme sous une entree plus large qui l’accorde.
[[roles.mounts.acl]]
path = "/"
rights = ["read", "write", "list", "delete", "rename"]
recursive = true

[[roles.mounts.acl]]
path = "/archives"
rights = ["read", "list"]
recursive = true

Ici tout est modifiable, sauf /archives et ce qu’il contient, en lecture.

Un deplacement est rejuge sur ce qu’il emporte : un dossier qui contient un sous-dossier protege ne se deplace pas vers un endroit gouverne par une entree plus large, ou le sous-arbre perdrait sa protection.

Plusieurs roles sur un montage

Deux roles sur le meme montage mettent leurs ACL ensemble :

  • deux entrees au meme chemin unissent leurs droits, et une seule recursive rend l’union recursive ;
  • a des chemins differents, l’entree la plus specifique decide, de quelque role qu’elle vienne.

Une entree etroite d’un role peut donc restreindre ce qu’une entree large d’un autre accordait. Le resultat se lit dans la console, onglet des roles d’une session (GET /admin/sessions/<id>/roles).

Repertoires synthetiques

Avec plusieurs montages sous des noms, un chemin sous aucun montage (/, /partenaires) est synthetique : il n’appartient a aucun backend.

OperationSur un chemin synthetique
listageles montages et les repertoires synthetiques qu’il contient
statun repertoire, date du debut de la session
?rights (REST)["list"]
toute autre operationrefusee
chemin sous lequel aucun montage n’est (/inconnu)introuvable

Un point de montage (/partenaires/acme) se liste selon son ACL, mais ne s’ecrit, ne se supprime ni ne se renomme.

Casse des noms

Quand l’ACL d’un backend plie la casse (case_insensitive), /Archives et /archives sont un seul chemin : ecrivez-le une fois par montage.

Portes : SFTP, API REST, explorateur, console

Les portes de fichiers (SFTP, API REST, explorateur) partagent utilisateurs, roles, montages et ACL : un droit accorde l’est partout. La console d’administration n’ouvre aucun fichier. SFTP tient des sessions ; l’API REST n’en a pas, la console montre ses transferts longs en cours (son activite).

PortePour quiEcouteCredentialSection
SFTPclients SFTP (OpenSSH sftp, WinSCP, FileZilla, rclone), sshfs[sftp] listenmot de passe, cle SSH, JWT (nom sentinel)[sftp]
API REST de fichiersscripts HTTP, applications[admin] listen, sous [api] prefixBasic, Bearer (JWT), ticket[api]
Explorateur webutilisateurs dans un navigateur[admin] listen, a [api.ui] pathcelle de l’API, gardee en memoire[api.ui]
Console et API d’administrationexploitants[admin] listen, ou [admin] control_listencompte local (mot de passe) ou JWT avec un role de [[admin.roles]], bearer_token de secours[admin]

SFTP

[sftp]
listen = "0.0.0.0:2222"
host_keys = ["/etc/craft-file-gate/host_ed25519"]
  • Seul le sous-systeme sftp est servi : ni shell, ni scp, ni exec, ni renvoi de ports ou d’agent.
  • Par JWT : le nom sentinel (jwt par defaut) et le jeton en mot de passe, sftp -P 2222 jwt@serveur.
  • Les cles : Porte SFTP et algorithmes SSH.

API REST de fichiers

[admin]
listen = "0.0.0.0:8080"
bearer_token = "changez-moi-en-production"

[api]
enabled = true
prefix = "/api/v1/files"
# lister un repertoire
curl -u alice:mot-de-passe 'http://serveur:8080/api/v1/files/in?list'
# deposer un fichier
curl -u alice:mot-de-passe -T rapport.csv http://serveur:8080/api/v1/files/in/rapport.csv
# telecharger
curl -u alice:mot-de-passe -o rapport.csv http://serveur:8080/api/v1/files/in/rapport.csv
RequeteEffet
GET <chemin>?list&offset=&limit=listage pagine ; limit 100 par defaut, 10000 au plus
GET <chemin>telechargement ; un Range d’un seul intervalle le reprend (206)
HEAD <chemin>metadonnees
PUT <chemin>depot du fichier entier
PUT <chemin>?mkdircreation d’un repertoire
DELETE <chemin>suppression
POST <chemin>?rename=<destination>renommage
GET <chemin>?rights{"path", "rights", "home"} : les droits de l’appelant sur ce chemin, sans toucher au stockage
POST <fichier>?ticket201 {"url": "<prefix>/<fichier>?ticket=<jeton>", "expires_in": 600} ; droit read ; 32 tickets vivants par utilisateur, et un ticket qui a encore 60 s est rendu de nouveau
GET <fichier>?ticket=<jeton>le fichier, sans Authorization, autant de fois que voulu pendant 600 s (moins si le JWT de la demande expire avant), l’ACL rejugee a chaque fois

Toutes les cles : Reference [api].

  • Une credential par requete ; ni cookie, ni session.
  • Un fichier telecharge part en attachment, Cache-Control: no-store, Content-Security-Policy: sandbox, Accept-Ranges: bytes. Une URL de ticket qui fuit telecharge ce fichier jusqu’a son expiration.

Explorateur web

[api.ui]
enabled = true
path = "/files"

Une page sur http://serveur:8080/files, cliente de l’API : memes identifiants, memes ACL. Voir Explorateur de fichiers.

Console et API d’administration

[admin]
listen = "127.0.0.1:8080"
bearer_token = "changez-moi-en-production"
# control_listen = "127.0.0.1:8081"   # une porte a part pour l'administration

La console est sur / de l’ecouteur admin : sessions et montages, bans, configuration sans secret, journaux, metriques, revocations. On s’y connecte par identifiant et mot de passe, ou avec un jeton. /metrics et les sondes sont sur le meme ecouteur, ou sur control_listen, ou sur [server] probes_listen : voir Une porte ou deux.

Ce que vous verrez

LigneSens
INFO SFTP server listeningla porte SFTP est ouverte
INFO starting admin API server (HTTP) (ou (HTTPS))l’ecouteur admin est ouvert
INFO every configured door startedtoutes les portes configurees servent (champ doors)
audit connection_acceptedune authentification reussie, sur toute porte (a chaque requete en REST)

Les backends

Un backend est un stockage nomme, decrit une fois dans [[backends]] ; les montages le citent par son nom.

[[backends]]
name = "disque"        # le nom que citent les montages
type = "local"         # local, sftp, s3 ou webhdfs
root = "/srv/sftp"     # les cles propres au type

[[roles]]
name = "utilisateurs"

[[roles.mounts]]
backend = "disque"
home_dir = "/{username}"
acl = [{ path = "/", rights = ["read", "write", "list"], recursive = true }]

Les [[backends]] s’ecrivent dans config.toml ou dans roles_file, ou ils se rechargent a chaud avec les roles.

Les types

Typetype =Le stockageCe que home_dir y designeEcritures
Local"local"un repertoire du serveurun sous-repertoire de rootoui
Proxy SFTP"sftp"un serveur SFTP amont, sous un compte de serviceun repertoire de l’amontoui
S3 et compatibles"s3"un bucket AWS S3, MinIO, Garage, Scaleway…un prefixe de cle, apres prefixoui
WebHDFS (Knox)"webhdfs"HDFS a travers Apache Knoxun repertoire HDFSlecture seule

Les binaires et images publies portent les quatre types (Les features du binaire).

Les cles de tout backend

Toutes les cles : Reference [[backends]].

Une sous-table ([backends.auth], [backends.credentials]) se rattache au [[backends]] qui la precede : ecrivez-la juste apres lui. GET /admin/config et l’onglet Configuration montrent chaque backend, secrets remplaces par ***.

Cles communes a tous les types

Ce qu’elles font sur chaque type est dans la page du type ; leur defaut et leur effet, dans la meme Reference.

hidden_stores se resout montage, puis backend, puis [server.hidden_stores] ; stale_partials backend, puis [uploads.stale_partials] ; cle par cle (Uploads).

Reservation entre instances

Le premier envoi vers une destination la garde jusqu’a sa fin (pourquoi). Entre instances, c’est un verrou <lock_prefix><nom> pose a cote de la destination. lock_prefix :

  • fait de 8 caracteres a 64 octets, sans / ni caractere de controle ;
  • nomme aussi les noms jetables du serveur (PPnew.<jeton>, PPprobe., PPstale. pour un prefixe P) ;
  • reserve au serveur tout nom qui commence par lui, sans egard a la casse, comme .craftfilegate-upload... : choisissez-en un qu’aucun fichier des utilisateurs ne porte.

Disponibilite

Chaque instance visite chaque backend en arriere-plan ([server.backend_probe]) : up, down apres failures_before_down visites en echec, unknown avant la premiere ; un WARN a chaque changement (Depannage), l’etat dans GET /admin/health, la vue Instances et craftfilegate_backend_up (Metriques). Sur S3, une visite est un ListObjectsV2 facture, toutes les 15 s par instance par defaut.

Local

Le backend local sert un repertoire du systeme de fichiers du serveur, sa racine (root). Chaque montage y a son sous-repertoire, home_dir.

[[backends]]
name = "disque"
type = "local"
root = "/srv/sftp"

[[roles]]
name = "utilisateurs"

[[roles.mounts]]
backend = "disque"
home_dir = "/{username}"
create_home = true
acl = [{ path = "/", rights = ["read", "write", "list", "delete", "rename"], recursive = true }]

alice envoie /rapport.txt : le fichier est /srv/sftp/alice/rapport.txt.

Les cles

Toutes les cles : Reference [[backends]] type = “local” ; cles communes : Les backends.

La racine et les liens symboliques

Aucune operation ne sort de root + home_dir du montage, quels que soient les liens qui s’y trouvent, y compris ce que le serveur fait lui-meme (fichier en cours, verrou, balayage). Un lien vers le home_dir d’un autre montage sort autant qu’un lien hors de la racine.

ReglageUn lien dans le montageUn lien qui en sort
follow_symlinks = false (defaut)jamais suivijamais suivi
follow_symlinks = truesuivi s’il reste sous root + home_dirjamais suivi
  • Un lien se liste comme un lien (type l ; "is_symlink": true en REST), ou, suivi, comme sa cible. Supprimer ou renommer agit sur le lien.
  • Avec hidden_stores, un envoi sur un lien de fichier remplace le lien ; la cible reste intacte.
  • Aucune porte ne cree de lien : ils viennent d’un autre processus ou d’une restauration.
  • La racine et ses ancetres sont de confiance (une racine a travers un lien est suivie) : inscriptibles par l’administrateur seul.
  • Sur un volume partage (NFS…), montez la racine nosymfollow (Linux 5.10 et plus).

Casse des chemins de l’ACL

Sur un stockage qui confond les majuscules (NTFS, APFS et HFS+ par defaut, SMB/CIFS, ext4 ou tmpfs casefold), l’ACL compare les chemins plies : NFD, retrait des points de code ignorables (U+200B…), pliage de casse complet (straße = strasse), NFC.

ReglageComparaison des chemins de l’ACL
case_insensitive = truepliee
case_insensitive = falseoctet a octet, sans normalisation
cle absenteune sonde decide, au demarrage et a chaque rechargement des roles

La sonde cree un fichier en minuscules et le cherche en majuscules dans chaque repertoire ou une entree se resout (le pliage se regle par repertoire, chattr +F) : un repertoire qui plie fait plier le backend, ce qui ne tranche pas plie. Un repertoire absent ou sous {username} n’est pas sonde, et un volume APFS sensible a la casse reste insensible a la normalisation : dans le doute, case_insensitive = true.

La ou l’ACL plie, et sur un serveur Windows, un nom court 8.3 (PROTEG~1) ou fini par . ou une espace n’est pas un chemin valide : desactivez les noms courts (fsutil 8dot3name).

Les cles communes sur un backend local

CleSur un backend local
home_dir (montage)un sous-repertoire de root : /partenaire/in/x d’un montage a /partenaire de home_dir = "/partenaires/acme" est /srv/sftp/partenaires/acme/in/x
create_home (montage)true : cree home_dir s’il manque, a l’ouverture de la session, un niveau a la fois, jamais a travers un lien
hidden_storesun fichier en cours de transfert a cote de la destination, publie par renommage
stale_partialsle balayage de ces fichiers, sur l’horloge du systeme de fichiers
cross_instance_reservationun fichier verrou a cote de la destination ; false par defaut sous Windows : une instance par racine
lock_prefixle nom de ces verrous

Le processus agit sous son propre uid ; le mode des fichiers suit son umask.

Sur Linux 5.6 et plus, le noyau impose le confinement (openat2(2), RESOLVE_BENEATH) ; sans openat2, chaque composante est ouverte O_NOFOLLOW (dit une fois en INFO) ; sous Windows, le parent est verifie juste avant l’appel.

Performances et limites

  • Un renommage et la publication d’un envoi utilisent RENAME_NOREPLACE. La ou le systeme de fichiers ne l’a pas (NFS, FUSE dont sshfs, 9p), ils retombent sur rename(2) : un ecrasement concurrent n’y est pas detecte, et l’audit d’un envoi qui ecrase dit replaced=unknown.
  • Aucun fsync : un succes ne promet pas la durabilite sur disque.
  • mkdir cree un niveau ; les parents manquants d’un envoi sont crees un par un, chacun juge par l’ACL.
  • La suppression d’un arbre n’est pas comptee : l’audit dit removed=unknown.

Ce que vous verrez

QuandLigne
demarrage, sonde de la casseINFO avec backend, acl_paths = folded ou exact
create_home cree un repertoireINFO created missing home directory, champ home_dir
demarrage, balayage des restesINFO stale in-flight files: ..., champs backend, grace_secs, age_check

Proxy SFTP

Le backend sftp range les fichiers sur un serveur SFTP amont, joint en SSH sous un compte de service que les utilisateurs ne connaissent pas.

[[backends]]
name = "acme-amont"
type = "sftp"
host = "sftp.acme.example"
host_key_fingerprint = "SHA256:2hZbXq1b5Xb3vN2mQ6w7l3Fv0tXk4yJ8aUeYp9rS0cE"   # verifiee hors bande

[backends.auth]
type = "password"
username = "relais"
password = "changez-moi"

[[roles]]
name = "acme"

[[roles.mounts]]
backend = "acme-amont"
home_dir = "/depot"
acl = [{ path = "/", rights = ["read", "write", "list"], recursive = true }]

Un client du role acme envoie /f.txt : le fichier est /depot/f.txt sur l’amont.

Les cles

Toutes les cles : Reference [[backends]] type = “sftp” ; cles communes : Les backends. Authentification par cle :

[backends.auth]
type = "private_key"
username = "relais"
private_key_pem = """
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
"""

Preferez une cle ed25519 ou ECDSA. Une cle RSA signe en rsa-sha2-512 ou rsa-sha2-256 selon server-sig-algs, ssh-rsa (SHA-1) en dernier recours.

La cle d’hote de l’amont

Le proxy verifie la cle d’hote de l’amont avant de s’authentifier : sans cela, un intermediaire recevrait les identifiants du compte de service. L’empreinte :

ssh-keyscan -p 22 sftp.acme.example | ssh-keygen -lf -
  • Epinglez l’empreinte ED25519, a defaut ECDSA, a defaut RSA : c’est l’ordre dans lequel le proxy negocie.
  • Verifiez-la hors bande : sur l’amont, ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub.
  • sha256: en minuscules et le bourrage = final sont acceptes.
  • Une cle presentee sous forme de certificat est jugee sur la cle qu’il certifie.
  • Rotation : une empreinte par backend ; changez-la a la bascule, le rechargement des roles l’applique aux connexions suivantes.

Les envois

Avec hidden_stores.enabled = true, un envoi ecrit dans un fichier en cours de transfert sur l’amont, a cote de la destination, puis le publie :

EtapeRequete a l’amontreplaced
1SSH_FXP_RENAME du fichier en cours sur la destination ; reussi si elle est libreno
2destination prise : posix-rename@openssh.com, atomique, sur un second canal SFTP ouvert au premier ecrasementyes
3sans cette extension ni second canal : suppression de la destination, puis renommageyes

A l’etape 3, la destination manque le temps d’un aller-retour. Sans hidden_stores, l’envoi ecrit dans la destination, et la reprise et l’ajout sont servis (Uploads).

Le compte de service peut creer, renommer et supprimer dans le repertoire de destination. Un ecrasement remplace l’inode (mode, proprietaire, ACL, attributs etendus perdus) et occupe deux fois la place jusqu’a la publication.

Les cles communes sur un proxy SFTP

CleSur un proxy SFTP
home_dir (montage)un chemin absolu sur l’amont
create_home (montage)sans effet : home_dir existe sur l’amont
hidden_storesun fichier en cours de transfert sur l’amont, publie par renommage
stale_partialsle balayage de ces fichiers sur l’amont, juge sur l’horloge de l’amont par un fichier sonde ; au demarrage, il attend la connexion 30 s au plus
cross_instance_reservationun fichier verrou chez l’amont, a cote de la destination ; true par defaut, Windows compris
lock_prefixle nom de ces verrous ; un autre prefixe, sans point en tete, convient a un amont qui refuse le nom par defaut
case_insensitivefalse par defaut : le systeme de fichiers de l’amont n’est pas visible d’ici ; un amont Windows ou macOS veut true

Performances et limites

  • Lecture : une requete READ en vol par handle ; une lecture a la suite de la precedente est servie depuis le bloc deja lu. Un telechargement dans l’ordre lit le fichier environ une fois sur l’amont.
  • Ecriture : des paquets aussi grands que l’amont les accepte, huit en vol.
  • Une connexion SSH par session SFTP et par requete REST : la poignee de main pese sur les requetes REST courtes.
  • Le proxy parle SFTP v3 (statuts 0 a 8), comme OpenSSH.
  • Les liens de l’amont ne sont pas distingues : stat suit un lien, un listage ne marque aucune entree comme lien.

Ce que vous verrez

QuandLigne
ouverture d’une sessionINFO SFTP proxy: connecting, champs host, port
cle d’hote verifieeINFO SFTP proxy: host key verified, champs upstream, fingerprint
session preteINFO SFTP proxy: connected and ready, champs host, home_dir

S3 et compatibles

Le backend s3 range les fichiers comme objets d’un bucket AWS S3 ou d’un stockage compatible (MinIO, Garage, Scaleway…).

[[backends]]
name = "archives"
type = "s3"
bucket = "archives"
region = "eu-west-3"
prefix = "sftp/"
endpoint_url = "https://minio.interne.example:9000"   # sans la cle : AWS

[backends.credentials]
type = "static"
access_key_id = "AKIAEXEMPLE"
secret_access_key = "changez-moi"

[[roles]]
name = "archivistes"

[[roles.mounts]]
backend = "archives"
home_dir = "/{username}"
acl = [{ path = "/", rights = ["read", "write", "list"], recursive = true }]

alice envoie /in/x.txt : l’objet est la cle sftp/alice/in/x.txt.

Les cles

Toutes les cles : Reference [[backends]] type = “s3”.

iam_role : variables AWS_*, profil, role de l’instance ou du pod. Cles communes : Les backends.

Fichiers et repertoires

Ce que voit le clientSur le stockage
un fichierune cle
un repertoireun marqueur <cle>/, ou toute cle sous <cle>/
un mkdirecrit le marqueur
parents manquants d’un envoiun marqueur par niveau, chacun juge par l’ACL comme un mkdir
un repertoire sans marqueurtaille 0, sans date

Une cle et un repertoire du meme nom peuvent coexister, sauf avec refuse_upload_over_directory = true.

OperationRequetes S3
lecture SFTPun HeadObject a l’ouverture, puis un GetObject avec Range par lecture
telechargement RESTun GetObject lu en flux
envoi de moins de 8 Mioun PutObject avec If-None-Match: *, a la fin
envoi de 8 Mio ou plusun upload multipart, une part de 8 Mio a la fois
renommage d’un fichierCopyObject avec If-None-Match: *, puis DeleteObject de la source
listageListObjectsV2 avec le delimiteur /, page apres page
suppression d’un arbreListObjectsV2 par 1 000 cles, puis DeleteObjects par lot

Un objet s’ecrit en entier : ni reprise (reput), ni ajout. Seul un fichier se renomme.

Les droits du bucket

Sur le bucket et <prefix>/* :

ActionPour
s3:ListBucketlister, distinguer un repertoire, trouver les parents manquants, mkdir, renommer, supprimer un arbre
s3:GetObjecttelecharger, stat, et l’examen qui precede une suppression ou un renommage
s3:PutObjectenvoyer, mkdir, renommer, l’auto-test des ecritures conditionnelles
s3:DeleteObjectsupprimer, renommer, nettoyer la cle de l’auto-test
s3:AbortMultipartUploadannuler un upload multipart qui ne sera pas complete
s3:ListBucketMultipartUploadsbalayer les uploads multipart abandonnes
s3:ListMultipartUploadPartsdater la derniere part d’un candidat au balayage

Un backend en lecture seule n’a besoin que de s3:ListBucket et s3:GetObject.

Les uploads multipart

Un upload multipart jamais complete reste facture jusqu’a son annulation :

QuandAnnulation
un envoi finit sans completeraussitot, quelle qu’en soit la cause
arret propreceux encore en vol, 5 s au plus
un processus tuele balayage sous <prefix>/ : au demarrage, toutes les grace_secs et a chaque envoi, quand la derniere part a plus de grace_secs sur l’horloge du service
  • Donnez un prefix a chaque backend : le balayage ne travaille que sous lui.
  • Un upload lent envoie une part vide toutes les 15 s, qui le date pour les autres instances ; celles-ci ont le meme uploads.idle_timeout_secs.
  • Une regle de cycle de vie reste conseillee :
{"Rules": [{"ID": "abort-incomplete-multipart", "Status": "Enabled", "Filter": {"Prefix": ""},
            "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 2}}]}

Son delai, compte depuis l’initiation, depasse le plus long envoi. MinIO expire de lui-meme les uploads incomplets (stale_uploads_expiry).

Les ecritures conditionnelles

replaced dans l’audit et le marqueur de reservation reposent sur If-None-Match: * et If-Match de PutObject. Derriere un endpoint_url (et sur AWS avec cross_instance_reservation), le serveur les mesure au demarrage, sur la cle <prefix>/.craftfilegate-upload-precondition.<16 hex> ensuite supprimee ; le verdict tient jusqu’au redemarrage. MinIO : honoured.

Les cles communes sur S3

CleSur S3
home_dir (montage)un prefixe : la cle est <prefix>/<home_dir>/<chemin>, parties vides omises
create_home (montage)sans effet : un prefixe existe des qu’une cle est dessous
refuse_upload_over_directorytrue : un envoi sur un nom qui est aussi un repertoire est refuse ; cout : un listage d’une cle a l’ouverture de chaque envoi
cross_instance_reservationun objet marqueur <lock_prefix><nom>, pose avec If-None-Match: *
lock_prefixle nom de ces marqueurs
stale_partialsle balayage des uploads multipart abandonnes
case_insensitivefalse par defaut : les cles sont sensibles a la casse
hidden_storessans objet : un objet n’apparait qu’une fois complet

Performances et limites

  • Chaque envoi en cours retient un tampon de 8 Mio (Memoire) ; ses ecritures sont sequentielles.
  • Un envoi hors de la racine paie un listage par niveau parent.
  • Le client adresse le bucket dans le chemin de l’URL (path-style), AWS compris.
  • 10 000 parts au plus par objet, soit environ 78 Gio a 8 Mio la part.
  • Sur un bucket versionne, l’auto-test laisse deux versions vides et un marqueur de suppression par demarrage.

Ce que vous verrez

QuandLigne
demarrageINFO S3 backend initialized, champs bucket, prefix, endpoint
demarrage, auto-testINFO S3 upload precondition self-test, champs backend, verdict = honoured, probe_key
balayageINFO aborted an abandoned multipart upload: ..., champs bucket, key, upload_id, age_secs
arretINFO uncompleted S3 multipart uploads aborted before the shutdown completes, champ aborted

WebHDFS (Knox)

Le backend webhdfs lit HDFS, en lecture seule, a travers WebHDFS d’Apache Knox : HTTPS, un compte de service en Basic, au nom de l’utilisateur (doAs). Knox porte Kerberos vers le cluster. Sert aussi un HttpFS ou un WebHDFS sans Kerberos ; Hadoop 2.8 et plus.

[[backends]]
name = "datalake"
type = "webhdfs"
url = "https://knox.example.com:8443/gateway/default"
auth = { type = "basic", username = "svc-sftp", password_file = "/run/secrets/knox-password" }
ca_bundle = "/etc/craft-file-gate/knox-ca.pem"   # la CA privee de Knox

[[roles]]
name = "analystes"

[[roles.mounts]]
backend = "datalake"
mount_path = "/datalake"
home_dir = "/data/projets"
acl = [{ path = "/", rights = ["read", "list"], recursive = true }]

alice lit /datalake/2026/x.csv : la requete vise <url>/webhdfs/v1/data/projets/2026/x.csv?op=OPEN&doAs=alice.

Les cles

Toutes les cles : Reference [[backends]] type = “webhdfs”.

La connexion a Knox s’ouvre en 5 s au plus ; HTTPS_PROXY n’est pas suivie. Cles communes : Les backends.

Le montage sur WebHDFS

CleSur WebHDFS
home_dir (montage)un repertoire HDFS ; . et .. ne sont jamais resolus
create_home (montage)sans effet
acl (montage)read et list : le reste n’a pas d’effet

Avec doAs, le nom d’utilisateur est fait de A-Z a-z 0-9 . _ -, 64 caracteres au plus.

hidden_stores, refuse_upload_over_directory, stale_partials, cross_instance_reservation et lock_prefix sont sans objet : rien n’est ecrit. case_insensitive : laissez-la absente, HDFS compare octet a octet.

Prerequis

Le droit de nommer un utilisateur (doAs), avec impersonate = true. Qui le donne depend de ce qui est derriere url :

Derriere urlLe reglage
Knox (le cas vise)cote Knox : dans la topologie, l’assertion d’identite avec l’impersonation active et hadoop.proxyuser.svc-sftp.users, .groups, .hosts ; la topologie expose WEBHDFS et authentifie svc-sftp en Basic (ShiroProvider sur LDAP/AD). Cote cluster, hadoop.proxyuser.knox.*, pose en general par l’installation de Knox
WebHDFS en directsvc-sftp declare mandataire dans core-site.xml (ci-dessous)
HttpFSles memes droits, sous httpfs.proxyuser.svc-sftp. dans httpfs-site.xml
<property>
  <name>hadoop.proxyuser.svc-sftp.hosts</name>
  <value>sftp-gateway.example.com</value>  <!-- d'ou il se presente -->
</property>
<property>
  <name>hadoop.proxyuser.svc-sftp.groups</name>
  <value>sftp-users</value>                <!-- pour qui il agit -->
</property>

Au demarrage, le serveur fait un GETFILESTATUS de la racine HDFS sous le compte de service, sans doAs, 5 s au plus.

Ce que fait le backend

OperationRequete WebHDFS
stat, existence d’un parentGETFILESTATUS, une requete par chemin
listageLISTSTATUS_BATCH, page par page (startAfter) ; LISTSTATUS en une reponse sur un cluster plus ancien que 2.8
telechargement, reprise, lecture a un offsetOPEN?offset=&length=, par plages
envoi, mkdir, renommage, suppressionaucune : lecture seule
  • Lecture anticipee : 10 Mio lus par paquets SFTP de 32 Kio coutent 3 OPEN avec le defaut ; une lecture qui saute ne demande que sa taille.
  • Listages : lus jusqu’au bout, plafonnes a 10 000 pages, 1 000 000 d’entrees et 64 Mio par reponse.
  • Redirections : un 307 de Knox est suivi une fois, vers la meme origine (schema, hote, port) que url seulement.
  • Une entree SYMLINK est montree comme un lien, et ne se lit pas ; HDFS compare les noms octet a octet, l’ACL aussi.

Performances et limites

  • Knox est le goulot : chaque stat, page et plage est une requete HTTPS qu’il relaie au cluster. Les sessions d’un backend partagent un client HTTP.
  • timeout_secs couvre une plage entiere : sur un lien lent, montez-le plutot que de baisser read_ahead_bytes.
  • Les petits fichiers coutent chacun un GETFILESTATUS et un OPEN : ce backend sert a parcourir HDFS, pas de stockage courant.
  • Le corps d’une erreur de Knox n’est jamais recopie dans un journal : seuls le nom de l’exception et le code.

Le fichier de configuration

Un fichier, nomme au lancement

craft-file-gate --config /etc/craft-file-gate/config.toml

-c est la forme courte. Seule [auth] est obligatoire, avec au moins un role et une porte : [sftp], ou [api] avec [admin]. Toutes les cles : la reference.

SectionCe qu’elle reglePage
[server]ce que toutes les portes partagent : arret, sessions par utilisateur, ecritures atomiques, port des sondesArret, Uploads, Console et API
[sftp]la porte SSH, ses algorithmes, ses bansPorte SFTP, Bans
[auth]qui entre, et sous quel nomAuthentification
[[users]], [[roles]]les comptes locaux, les roles et leurs montagesUtilisateurs, roles et montages
[[backends]]les stockagesLes backends
[uploads], [tcp_keepalive]les delaisDelais, Uploads
[log], [telemetry]journaux, piste d’audit, tracesJournaux, Telemetrie
[admin], [api]la console, l’API d’administration, l’API de fichiers, leurs bansConsole et API, Bans
[reload]le rechargement a chaudRechargement
[security]le jugement des fichiers de confianceFichiers de confiance et TLS

Decouper : users_file et roles_file

Les utilisateurs, les roles et les backends peuvent vivre a part. Ces fichiers se rechargent a chaud ; config.toml presque pas.

[auth]
users_file = "users.toml"   # les [[users]]
roles_file = "roles.toml"   # les [[roles]] et les [[backends]]
Ou ecrireEffet
[[users]], [[roles]], [[backends]] a la racine de config.tomllus au demarrage
les memes sous [auth] ([[auth.users]], …)idem ; la racine l’emporte si les deux existent
auth.users_file, auth.roles_filerelus a chaque modification du fichier
rien de tout celausers.toml et roles.toml a cote de config.toml sont pris s’ils existent

users_file et des [[users]] en ligne ne se cumulent pas : quand users_file est donne, lui seul est lu.

Les chemins relatifs

Un chemin relatif des fichiers que le serveur lit (host_keys, users_file, roles_file, public_key_file, les persist_file, la paire et le repertoire de [admin.tls]) part du repertoire de config.toml, pas du repertoire de lancement : une configuration et ses fichiers voyagent ensemble, y compris dans un conteneur. Les autres chemins (root d’un backend local, log.dir, les fichiers d’un backend WebHDFS) partent du repertoire de lancement : ecrivez-les absolus.

L’environnement

Une variable CRAFT_FILE_GATE_* remplace la cle qu’elle surcharge, apres la lecture du fichier, au demarrage comme a chaque rechargement : la liste est dans Variables d’environnement. Pour un secret, preferez la forme _FILE : un fichier lu une fois, juge comme un fichier de confiance.

Les outils du binaire

hash-password et verify-password ne lisent ni la configuration ni les secrets ; healthcheck lit les ecouteurs de --config, rien d’autre.

CommandeEffet
craft-file-gate hash-passwordlit un mot de passe sur stdin, ecrit son hash argon2id ; --user <nom> ecrit un bloc [[users]] ; --profile choisit le cout (voir Authentification)
craft-file-gate verify-password '<hash>'lit un mot de passe sur stdin ; sortie 0 s’il produit ce hash, 1 sinon, 2 si le hash est illisible
craft-file-gate config check <fichier>, config schemaverifier une configuration sans demarrer, ecrire son schema JSON : Verifier une configuration
craft-file-gate healthcheckinterroge /livez la ou la configuration le sert : probes_listen (HTTP), sinon control_listen, sinon admin.listen (HTTPS sous [admin.tls]), lus dans --config (par defaut /config/config.toml) et l’environnement, une adresse 0.0.0.0 interrogee sur la boucle locale ; sans ce fichier, http://localhost:8080/livez ; --url pour une autre adresse ; sortie 0 sur 200 ; pour le HEALTHCHECK d’une image

Ce que vous verrez

QuandLigne
un fichier declare est luINFO auth source loaded, champs kind (roles, users) et path
un fichier non declare est trouve a coteINFO found next to the config and used; declare it explicitly to pin it
une variable remplace une cleINFO config override from env, champs env et value ; (value hidden) pour un secret

Verifier une configuration

config check applique a un fichier la validation du demarrage, sans demarrer : ni port ouvert, ni fichier cree, ni secret lu, ni reseau joint.

craft-file-gate config check /etc/craft-file-gate/config.toml
craft-file-gate config check config.toml --format json
craft-file-gate config schema > config.schema.json
craft-file-gate config explain sftp.ban.max_failures
craft-file-gate config effective /etc/craft-file-gate/config.toml

config check

OptionEffet
<fichier>la configuration ; ses users_file, roles_file et fichiers de confiance sont lus et juges comme au demarrage
--format textpar defaut : une ligne par constat, <level> [<cle>] <message> (see <lien>)
--format json{"valid": ..., "findings": [{"level", "key", "message", "rule", "doc"}]}
ChampSens
levelerror : le demarrage refuserait ; warn : un WARN du demarrage ; info : une ligne du demarrage, ou une etape laissee au demarrage, not checked offline: ...
keyla cle, ecrite comme dans la reference (roles[].mounts[].backend)
rulela regle de spec citee par le message
docla section de la cle dans ce guide
SortieSens
0aucune erreur, des warn possibles
1une erreur, la premiere, comme au demarrage
2fichier illisible, ou commande mal ecrite

Les variables d’environnement comptent comme au demarrage. Un secret (cle d’hote, _FILE, password_file, variable) est juge present, avec son proprietaire et son mode ; son contenu n’est pas lu. Restent au demarrage, dits en info : le JWKS, l’API Kubernetes, la sonde d’un stockage, l’amont SFTP, les fichiers de log, l’exporteur OTLP.

Une CI ou un assistant IA ecrit la configuration, lance config check --format json, corrige chaque error a l’aide de key et doc, recommence jusqu’a la sortie 0.

config schema

Le schema JSON (draft 2020-12) de config.toml, de users_file et de roles_file : types, defauts, bornes, valeurs permises, description de chaque cle, x-reload (rechargement), x-env (variables), x-doc (lien). Une cle inconnue y est refusee ; aucune n’y est exigee.

Une premiere ligne #:schema ./config.schema.json le donne a Taplo (Even Better TOML) : completion et soulignement dans l’editeur, comme dans config.example.toml. Le schema est a la racine du depot, joint a chaque release, et dans les images a /usr/share/craft-file-gate/config.schema.json ; config schema > config.schema.json l’ecrit a cote de la configuration.

config explain <cle>

Une cle : type, defaut, valeurs permises, rechargement (⟳), variables, effet, spec, lien vers sa section. --format json pour une machine ; une cle inconnue sort en 2 avec les plus proches.

config effective <fichier>

La configuration appliquee : le fichier, ses fichiers lies, les variables d’environnement, les defauts de la reference ; --format toml (par defaut) ou json. Chaque secret et chaque empreinte de mot de passe s’affichent ***, comme l’identifiant qui va avec un secret (access_key_id) et toute valeur d’une cle nommee comme un secret ou sous headers. Hors ligne comme config check, lance d’abord : une configuration refusee sort en 1 avec son constat.

Authentification

Deux sources, trois portes

PorteCredentialSource
SFTPmot de passecompte local, ou jeton JWT sous le nom sentinel
SFTPcle publiquecompte local
API de fichiersAuthorization: Basiccompte local, mot de passe
API de fichiersAuthorization: Bearerjeton JWT
Console et API d’administrationAuthorization: Bearerjeton de session (compte local, mot de passe), jeton JWT, ou jeton statique (voir Console et API)

Une connexion est authentifiee par une seule source, dont viennent ses roles. Un compte local alice et le sub alice d’un jeton sont deux utilisateurs distincts.

Choisir les methodes

[auth.methods]
jwt   = { enabled = false }
local = { enabled = true, password = true, pubkey = true }

Un deploiement en cles seules coupe le mot de passe :

local = { enabled = true, password = false, pubkey = true }

Les cles : [auth.methods].

Les comptes locaux

[[users]]
username = "alice"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."   # craft-file-gate hash-password
authorized_keys = ["ssh-ed25519 AAAA... alice@poste"]
authorities = ["utilisateurs"]                         # ses roles

Les cles : [[users]]. Dans users_file, ils se modifient sans redemarrer.

Les hashes

echo -n "mot-de-passe" | craft-file-gate hash-password              # le hash seul
echo -n "mot-de-passe" | craft-file-gate hash-password --user alice # un bloc [[users]]

Le mot de passe se lit sur stdin, jamais en argument. Le hash sort sur stdout, ses parametres sur stderr. L’onglet Hash tools de la console fait de meme dans le navigateur, sans envoyer le mot de passe.

Profil (--profile)Parametres argon2idMemoire par verification
owasp-min (defaut)m=19456,t=2,p=119 Mio
rfc9106-low-memm=65536,t=3,p=464 Mio

Une verification coute ce que dit le hash stocke : le serveur ne re-hache jamais. Gardez un seul profil par fichier : un nom inconnu coute alors exactement le temps d’un mauvais mot de passe, et ne se devine pas au temps de reponse.

Les formats argon2 ($argon2id$, $argon2i$, $argon2d$) sont acceptes. Pour migrer d’un autre serveur sans connaitre les mots de passe, allow_sha512_crypt = true ou allow_bcrypt = true sous [auth.methods] local accepte aussi ces formats : Reference [auth.methods].

A chaque chargement du fichier, la ligne password hashes in use compte les comptes par format, et la metrique craftfilegate_local_users{hash_format} aussi : quand un format tombe a zero, retirez son drapeau. local.max_argon2_memory_kib (19456 : le m du profil owasp-min) nomme au chargement tout compte dont le hash demande plus, qui se connecte toujours : le re-hacher au profil par defaut le ramene sous le budget.

La verification se fait sur un pool de fils borne, hors du runtime : voir Verification des mots de passe.

La cle publique

Le client propose une cle, puis signe si elle est dans ses authorized_keys. Les cles proposees ne comptent pas : un agent SSH qui en essaie cinq avant la bonne ne se fait pas bannir ; une connexion qui finit sans s’authentifier, apres une cle declinee, compte un echec.

Les algorithmes de signature d’une cle

[sftp.algorithms] user_key dit les algorithmes permis a tous (voir Porte SFTP) ; ssh-rsa (SHA-1) n’y est pas par defaut. Un role peut en permettre d’autres a ses utilisateurs :

[[roles]]
name = "partenaire-ancien"
user_key_algorithms = ["ssh-rsa"]   # en plus de user_key

[[roles.mounts]]
backend = "disque"
home_dir = "/partenaire"

L’exception vaut pour tout utilisateur qui porte ce role, jamais pour un utilisateur seul. Seuls les roles des fichiers du serveur la donnent, pas ceux qu’atteint le service d’autorisation.

JWT

La methode jwt est active par defaut.

[auth.jwt]
secret = "remplacez-moi"     # ou CRAFT_FILE_GATE_JWT_SECRET_FILE
algorithm = "HS256"
username_path = "/sub"       # le nom de l'utilisateur
authorities_path = "/groups" # ses roles
PorteOu mettre le jeton
SFTPnom de login jwt (auth.jwt_sentinel_username), le jeton en mot de passe
API de fichiers, API d’administrationAuthorization: Bearer <jeton>

Une source de cle, au choix : secret (HS256, HS384, HS512), public_key_file (la cle publique PEM de l’emetteur : RS256, RS384, RS512, ES256, ES384) ou jwks_url (le jeu de cles d’un fournisseur d’identite, chaque cle disant son algorithme). Les cles : [auth.jwt].

Un jeton est accepte si sa signature verifie, si exp est present et a venir, et si nbf, quand il est la, est passe (60 s de tolerance pour les deux). Avec issuer ou audience, la claim iss ou aud doit etre presente et egale. Le nom de l’utilisateur est lu a username_path, ses roles a authorities_path (un tableau de chaines).

Brancher un fournisseur d’identite (jwks_url)

[auth.jwt]
jwks_url = "https://idp.example.com/.well-known/jwks.json"
algorithm = "RS256"
jwks_refresh_interval_secs = 3600
issuer = "https://idp.example.com/"
MomentCe que fait le serveur
demarragevalide le fichier, ouvre le journal, puis charge le jeu de cles, avant d’ouvrir les portes
chaque requeteverifie le jeton contre la cle que son en-tete kid designe
toutes les jwks_refresh_interval_secsrecharge le jeu ; une cle ajoutee par le fournisseur est apprise a ce moment
rafraichissement sans reponsegarde les cles en cache, qui continuent de verifier les jetons

L’age du cache est la jauge craftfilegate_jwks_cache_age_seconds : alertez dessus (voir Metriques).

Ce que vous verrez

QuandLigne
chaque chargement des comptesINFO password hashes in use, un champ par format
jeu JWKS chargeINFO JWKS cache refreshed, champ keys ; puis JWKS background refresh started
connexion accepteeaudit connection_accepted, auth_method = password, pubkey, jwt, basic ou bearer ; par cle, signature_algorithm

Porte SFTP et algorithmes SSH

Un exemple

[sftp]
listen = "0.0.0.0:2222"
host_keys = ["/etc/craft-file-gate/host_ed25519"]

[server]
max_sessions_per_user = 10

[sftp] est optionnelle : sans elle, la porte SFTP est eteinte.

La cle d’hote se cree une fois, avant le premier demarrage :

ssh-keygen -t ed25519 -f /etc/craft-file-gate/host_ed25519 -N ""

Les cles de [sftp]

Toutes les cles : Reference [sftp] ; les delais : Delais.

[sftp.ban], [sftp.rate_limit] : Bans. Dans [server], communes a toutes les portes : max_sessions_per_user (sessions simultanees d’un meme nom, de toutes les instances avec [cluster]), shutdown_grace_period_secs (Arret), [server.hidden_stores] (Uploads).

La porte annonce les methodes password et publickey. Elle sert le seul sous-systeme sftp, une fois par connexion : ni shell, ni exec, ni renvoi de port.

Cles d’hote

TypeFormat
ed25519, ECDSA (P-256, P-384, P-521), RSAOpenSSH (ssh-keygen) ou PEM (ssh-keygen -m PEM, PKCS#8)

Chaque cle chargee est annoncee avec son empreinte SHA256:..., celle que donne ssh-keygen -l et que voit un client a sa premiere connexion. GET /admin/config et l’onglet Configuration de la console donnent les memes.

generate_host_key cree la cle seulement si son fichier est absent, en 0600, et donne son empreinte. Un fichier present est relu tel quel : la cle ne change pas d’un demarrage a l’autre. Reservez-le a une instance unique sur un stockage persistant ; en Kubernetes, montez un Secret (voir Secrets).

Une entree en table choisit les algorithmes qu’une cle annonce, avec la syntaxe de [sftp.algorithms] appliquee a la liste host_key :

[sftp]
listen = "0.0.0.0:2222"
host_keys = [
  "/etc/craft-file-gate/host_ed25519",
  { path = "/etc/craft-file-gate/host_rsa", algorithms = ["rsa-sha2-512", "rsa-sha2-256"] },
]

Un algorithme n’est annonce que si une cle chargee sait le signer.

Algorithmes

[sftp.algorithms] regle les algorithmes proposes, par categorie. Chaque liste part d’un defaut moderne et s’edite comme dans OpenSSH :

EcritureEffet
["+nom"]ajoute nom a la fin du defaut
["-nom"]retire nom du defaut
["a", "b"]remplace le defaut par cette liste

Les defauts, liste par liste : Reference [sftp.algorithms].

  • kex, ciphers, macs et host_key se negocient avant que l’utilisateur soit connu : ils valent pour tout le serveur.
  • user_key dit les algorithmes avec lesquels une cle d’utilisateur peut signer sa connexion (le PubkeyAcceptedAlgorithms d’OpenSSH). Un role peut en permettre d’autres a ses seuls utilisateurs (user_key_algorithms, voir Authentification).
  • Les marqueurs ext-info-* et kex-strict-* sont toujours ajoutes a kex.
  • L’extension server-sig-algs annonce au client la liste host_key en vigueur, puis les algorithmes de user_key qu’aucune cle d’hote ne signe.

Les algorithmes faibles

diffie-hellman-group1-sha1, diffie-hellman-group14-sha1, diffie-hellman-group-exchange-sha1, aes*-cbc, hmac-sha1, hmac-sha1-etm@openssh.com et ssh-rsa (RSA signe en SHA-1) ne sont dans aucun defaut. Ajoutes avec +, ils sont proposes. diffie-hellman-group-exchange-sha256 s’ajoute aussi, hors defaut.

Un vieux client

# Un client JSch ancien : ni curve25519 ni ML-KEM
[sftp.algorithms]
kex = ["+diffie-hellman-group14-sha1"]
Le client ne connait pasAjouter
curve25519, ML-KEMkex = ["+diffie-hellman-group14-sha1"]
ssh-ed25519 en cle d’hoteune cle d’hote ECDSA P-256 a host_keys (ssh-keygen -t ecdsa -b 256 -f host_ecdsa -N ""), plutot qu’une RSA en ssh-rsa
rsa-sha2-* pour sa cle d’utilisateuruser_key_algorithms = ["ssh-rsa"] sur son role seulement

La liste des sessions de la console montre, pour chaque session, les algorithmes negocies et ceux qui sont faibles (weak_algorithms).

Ce que vous verrez

QuandLigne
demarrage, chaque cle d’hoteINFO loaded host key, champs path, key_type, fingerprint
demarrageINFO SSH algorithms offered, champs kex, ciphers, macs, host_key, user_key
connexion accepteeINFO new SSH connection
session ouverteINFO session established and registered, champs session_id, auth_method, signature_algorithm, total_sessions
fin ordinaire d’une connexionINFO SSH session ended: <raison>, champs peer, username, client_version

Uploads et ecritures atomiques

Ecritures atomiques (hidden stores)

Par defaut, un upload ecrit dans le fichier de destination, comme HiddenStores off dans ProFTPD : un lecteur peut y voir un fichier partiel tant que le transfert dure.

Avec enabled = true, l’upload ecrit dans un fichier en cours de transfert du meme repertoire, puis le renomme en destination a la fin. La destination n’apparait, ou ne change, qu’une fois le fichier complet.

[server.hidden_stores]
enabled = true
prefix = ".in."
extension = "."

Toutes les cles : Reference [server.hidden_stores].

Le fichier en cours de transfert s’appelle <prefix><nom>.<jeton><extension> : .in.rapport.csv.3f9a1c04b7e25d68. avec les defauts. Le jeton, 16 caracteres hexadecimaux, est propre a chaque upload. Un motif .in.* les reconnait tous.

enabledPendant le transfertUpload qui echoue
falsele fichier grossit sous son vrai nomle fichier partiel reste sous son vrai nom
truele fichier en cours de transfert grossit a cote ; la destination est intactele fichier en cours de transfert est supprime ; la destination est intacte

Les fichiers en cours de transfert se listent et se lisent comme les autres. Ils concernent les backends local et sftp (proxy) ; un objet S3 n’apparait qu’une fois son upload termine, sans fichier a cote.

Par backend, par montage

La meme table se pose a trois niveaux. Chaque cle se resout a part : le montage l’emporte sur le backend, qui l’emporte sur [server.hidden_stores], qui l’emporte sur les defauts. SFTP et REST resolvent la meme valeur.

[server.hidden_stores]            # tout le serveur
enabled = true

[[backends]]
name = "disque"
type = "local"
root = "/srv/sftp"
hidden_stores = { enabled = false }   # ce backend ecrit en place

[[roles]]
name = "depot"

[[roles.mounts]]
backend = "disque"
mount_path = "/depot"
home_dir = "/depot"
hidden_stores = { enabled = true }    # sauf ce montage
acl = [{ path = "/", rights = ["write", "list"], recursive = true }]

Restes d’un transfert interrompu

Un processus tue en plein upload laisse son fichier en cours de transfert. Le serveur le supprime plus tard, quand un upload passe dans le meme repertoire, une fois le fichier plus vieux que grace_secs.

[uploads.stale_partials]
grace_secs = 900
age_check = true

Toutes les cles : Reference [uploads.stale_partials].

La meme table se pose par backend (stale_partials = { grace_secs = 1800 } dans son [[backends]]), cle par cle au-dessus de [uploads.stale_partials]. Sur un backend S3, elle regle le menage des uploads multipart abandonnes (voir S3).

Un reste n’est supprime que si son nom a la forme exacte que le serveur ecrit, si personne ne tient son verrou, et si son age depasse grace_secs sur l’horloge du stockage. Au moindre doute, il reste. Chaque suppression laisse une ligne INFO.

Les instances qui partagent un stockage ont le meme uploads.idle_timeout_secs
le seuil se calcule sur celui de l’instance qui balaie.

Deux uploads vers la meme destination

Le premier upload a s’ouvrir garde la destination jusqu’a sa fin. Un second, du meme compte ou d’un autre, par la meme porte ou une autre, est refuse des son ouverture, avant d’envoyer un octet.

Un upload bloque (client suspendu par Ctrl+Z) est repris par une relance du meme compte sur la meme instance, passe uploads.takeover_idle_secs sans donnees.

Entre plusieurs instances, un verrou pose sur le stockage, a cote de la destination, porte la meme regle. Il est rafraichi toutes les 20 s ; celui d’un processus disparu est repris passe 2 x idle_timeout_secs + 60 s (120 s par defaut). cross_instance_reservation et lock_prefix : Reference [[backends]].

Ce qu’est le verrou sur chaque type : Local, S3, Proxy SFTP. Les noms qui commencent par .craftfilegate-upload ou par le lock_prefix appartiennent au serveur : on les liste et on les lit, on n’y ecrit pas. Avec une seule instance, cross_instance_reservation = false suffit.

Plafonner la taille d’un fichier

max_file_mb, sur un montage, plafonne la taille de chaque fichier envoye (1 Mo = 1 048 576 octets). Ce n’est pas un compteur d’espace.

[[roles.mounts]]
backend = "disque"
home_dir = "/{username}"
max_file_mb = 500
ValeurEffet
absenteaucun plafond
Nchaque fichier envoye fait au plus N Mo, juge avant le stockage (en REST, sur Content-Length avant le corps) ; ce qu’un envoi qui depasse avait ecrit est retire
0aucun envoi sous ce montage, lecture seule de fait

Ce que vous verrez

QuandLigne
demarrage et rechargement, par backendINFO stale in-flight files: ..., champs backend, grace_secs, grace_from, age_check, idle_timeout_secs
un reste supprimeINFO removed a stale in-flight file: ..., champs backend, path, age_secs, clock, lock
verrou d’un processus disparu reprisINFO took over a stale upload reservation: ... (local), took over the marker of an upload ... (S3), took over the lock file of an upload ... (proxy)
ecart d’horloge du stockagemetrique craftfilegate_stale_partials_clock_skew_seconds{backend}

Bans et limites de debit

Un exemple

[sftp.ban]
max_failures = 5
window_secs = 300
ban_duration_secs = 600
whitelist_ips = ["10.0.0.0/8"]

[admin]
listen = "0.0.0.0:8080"

[admin.ban]
max_failures = 10
trusted_proxies = ["10.0.0.5"]   # le reverse proxy devant l'API

[api]
enabled = true

[api.ban]
max_failures = 5
trusted_proxies = ["10.0.0.5"]   # le meme : un seul ecouteur

Sans la section, la porte ne bannit rien.

Une liste par porte

ListeSectionPortes
sftp[sftp.ban]SFTP
api[api.ban]API de fichiers, tickets de telechargement, explorateur
admin[admin.ban]API d’administration et console

Les trois listes sont independantes : une adresse bannie sur l’API de fichiers ouvre toujours la console, et inversement.

Les cles d’un ban

Les memes sous [sftp.ban], [api.ban] et [admin.ban], lues au demarrage.

Toutes les cles : Reference [sftp.ban], [api.ban], [admin.ban].

Ce qui compte comme un echec

Un echec est une credential presentee et jugee fausse, quel que soit le compte essaye.

PorteCompte
SFTPmot de passe faux ou nom inconnu ; jeton JWT juge faux ; signature d’une cle refusee ; une connexion finie sans s’authentifier apres des cles declinees (un echec par connexion, pas par cle)
API de fichiersBasic faux ou mal forme ; Bearer vide ou jeton juge faux
Console et API d’administrationjeton statique faux ; Bearer vide, JWT ou jeton de session juge faux ; POST /admin/login : mot de passe faux, nom inconnu ou compte sans role admin

Ne comptent pas : une methode coupee, un jeton expire ou pas encore valide, un ticket de telechargement refuse, quel qu’il soit, un refus apres une credential acceptee (aucun role, limite de sessions), un refus du limiteur de debit, le refus d’une adresse deja bannie. Une connexion reussie ne remet pas le compteur a zero.

Avec [api] cors_origins = ["*"], toute page web peut faire envoyer par le navigateur d’un visiteur des credentials fausses, qui comptent pour le ban de son adresse : listez les origines de vos applications.

La vie d’un ban

MomentEffet
max_failures echecs dans window_secsl’adresse est bannie pour ban_duration_secs ; une ligne d’audit ip_banned
avec [cluster]les echecs que les autres instances tiennent dans leur fenetre s’ajoutent au seuil, lus chaque seconde (≈ 1 s de retard) ; une instance injoignable ne compte plus apres 5 s (2 × peer_timeout_ms + 1 s si plus long)
sur SFTP, au verdictles sessions SFTP ouvertes de l’adresse sont coupees, transferts compris
un echec de plus pendant le banl’echeance recule a ban_duration_secs apres lui
l’echeancele ban finit, sans ligne
DELETE /admin/bans/{protocol}/{ip}le ban est leve

Une adresse IPv4 mappee (::ffff:10.1.2.3) est la meme que 10.1.2.3, pour le ban, la liste blanche et les proxies.

Lever un ban

GET /admin/bans (onglet Bans de la console) les liste ; la levee demande la permission unban, {protocol} est sftp, api ou admin :

curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/admin/bans/sftp/203.0.113.7

Une levee est une decision datee : elle annule les verdicts decides avant elle, jamais un verdict posterieur. Elle ne remet pas le compteur a zero.

Partager les bans entre instances

BackendClesPartage
memoireaucunechaque instance a ses bans, perdus au redemarrage ; avec [cluster], une levee est relayee a chaque instance, comme pour un fichier
fichierpersist_filerelu au demarrage, publie toutes les 30 s, relu sur un changement et toutes les reread_interval_secs ; ecrit de facon atomique, sur un volume partage (NFS, EFS) la relecture suffit
ConfigMapbackend = "configmap", ban_configmap_namesuivi par l’API Kubernetes (feature k8s, un Role) : Kubernetes

Les trois listes peuvent partager un fichier ou un ConfigMap. La fusion garde, par adresse, le ban le plus long et le verdict le plus recent ; un ban appris d’un pair coupe les sessions SFTP de l’adresse, sans ligne ip_banned (celle de l’instance qui l’a decide suffit). A l’arret, chaque liste est publiee, 5 s au plus chacune.

L’adresse d’un client HTTP : trusted_proxies

Les portes HTTP prennent le pair TCP pour adresse. Quand ce pair est dans le trusted_proxies de leur liste ([api.ban] pour l’API de fichiers, [admin.ban] pour l’API d’administration), elles prennent l’adresse la plus a droite de X-Forwarded-For qui n’est pas un proxy de confiance, a defaut X-Real-IP. Le ban, le limiteur, la part de la file de hachage et l’audit utilisent cette adresse. La porte SFTP ne lit que le pair TCP.

Sans [admin] control_listen, l’API de fichiers et l’API d’administration partagent un ecouteur : [api.ban] et [admin.ban] portent alors les memes trusted_proxies.

Limites de debit

Un seau a jetons par adresse : burst connexions ou requetes d’affilee, puis le debit par minute. Sans la section, aucune limite.

[sftp.rate_limit]
connections_per_minute = 30
burst = 10

[api.rate_limit]
requests_per_minute = 600

Toutes les cles : Reference [sftp.rate_limit], [api.rate_limit].

L’API d’administration n’a pas de limite de debit. Un refus de debit ne compte pas pour le ban.

Ce que vous verrez

QuandLigne
un ban decideaudit ip_banned ; INFO IP banned after auth failures
sessions coupees par un banINFO cut the sessions of a banned address ; audit session_end reason=banned
un pair publie des bansINFO merged persisted bans from file

Delais

Ce que chaque delai detecte

DelaiDetecteDefaut
sftp.login_grace_secsune connexion SSH qui ne s’authentifie pas120 s
uploads.idle_timeout_secsun client vivant qui n’envoie plus rien pendant un upload (suspendu, bloque)30 s
uploads.min_rate_bytes_per_secun upload au goutte-a-gouttedesactive
[tcp_keepalive]un pair disparu sans rien en vol (machine eteinte, NAT qui a oublie la connexion)~60 s
tcp_keepalive.user_timeout_secsun pair disparu alors que le serveur lui envoyait quelque chose ; un client qui ne lit plus65 s (SFTP), 35 s (HTTP)
sftp.inactivity_timeout_secsune connexion SSH sans aucun paquet600 s
sftp.keepalive_interval_secsun client SSH fige tout entierdesactive
admin.header_read_timeout_secsune requete HTTP dont les en-tetes n’arrivent pas10 s

Uploads : [uploads]

[uploads]
idle_timeout_secs = 30
min_rate_bytes_per_sec = 1024   # 1 Kio/s en moyenne ; absent ou 0 : desactive
min_rate_grace_secs = 60

Toutes les cles : Reference [uploads].

Le delai d’inactivite

Le chronometre ne court que quand le serveur attend le client. Le temps passe par le serveur sur son stockage (S3 lent, NFS qui cale) ne compte jamais.

PorteLe chronometre
RESTrepart a chaque octet du corps
SFTPun par handle d’ecriture : part a l’OPEN, repart a chaque WRITE complet de ce handle, quoi que fasse la session par ailleurs ; un paquet doit arriver en entier dans le delai qui suit son premier octet

Au bout du delai, le partiel est jete (fichier en cours de transfert supprime, multipart S3 annule) et rien n’est publie.

Un upload vivant atteint son fichier au moins toutes les 2 x idle_timeout_secs : le menage des restes et la reprise des verrous s’y fient, donnez la meme valeur a toutes les instances qui partagent un stockage. Relevez-la pour les clients qui gardent un fichier ouvert sans ecrire (sshfs, client graphique qui demande une confirmation, lien tres lent : au defaut, un paquet de 32 Kio demande ~1,1 Ko/s).

Le debit minimal

Passe min_rate_grace_secs d’attente, un upload doit avoir recu en moyenne au moins min_rate_bytes_per_sec, comme RequestReadTimeout ... MinRate d’Apache. La moyenne court depuis le debut : un client en rafales garde le credit de ses rafales, un goutte-a-goutte est coupe a la fin de la grace. Les octets comptes sont ceux des WRITE acceptes, pas l’offset atteint.

En dessous, l’upload est coupe comme pour l’inactivite. 1 Kio/s coupe un goutte-a-goutte et laisse passer un lien de 10 Kio/s qui cale.

Keepalive TCP : [tcp_keepalive]

Pose sur chaque connexion acceptee, sur le port SFTP et sur le port HTTP.

Toutes les cles : Reference [tcp_keepalive].

Coupure du reseau en plein transfert : TCP_USER_TIMEOUT

Le keepalive ne sonde qu’une connexion sans rien en vol. Quand le serveur envoie quelque chose a un client disparu (les reponses aux WRITE d’un upload SFTP, un telechargement), c’est TCP_USER_TIMEOUT qui coupe.

user_timeout_secsPort SFTPPort HTTP
0 ou absent2 x idle_timeout_secs + 5 s (65 s)idle_timeout_secs + 5 s (35 s)
NNN

La connexion est ainsi coupee 5 s apres l’abandon de l’upload. Sous Linux, ce delai remplace count : une connexion inoccupee dont le pair a disparu est coupee a la premiere sonde qui le depasse (vers 70 s en SFTP). Il vaut aussi pour un client qui ne lit plus : un telechargement REST dont le client est suspendu est coupe a 35 s. Une coupure reseau plus longue (perte de couverture, VPN qui se reconnecte) tue le transfert ; relevez user_timeout_secs si vos clients en traversent.

Hors Linux, rien n’est pose.

SSH : [sftp]

login_grace_secs et inactivity_timeout_secs coupent la connexion (0 : jamais) ; keepalive_interval_secs (0 : aucun) sonde le client, keepalive_max (3) coupe apres autant de sondes sans reponse. Les cles : [sftp].

La reponse a un keepalive SSH compte comme de l’activite : avec un keepalive plus court que inactivity_timeout_secs, une session vivante mais inoccupee n’est plus coupee par l’inactivite, seul un client qui ne repond plus l’est. C’est pourquoi le keepalive SSH est desactive par defaut.

HTTP : delai de lecture des en-tetes

admin.header_read_timeout_secs (10 s, de 1 a 300, CRAFT_FILE_GATE_ADMIN_HEADER_READ_TIMEOUT_SECS) borne, sur le port de l’API d’administration et de l’API de fichiers, l’attente des en-tetes d’une requete : depuis la connexion, la poignee de main TLS ou la reponse precedente en keep-alive. Au-dela, la connexion est fermee sans reponse. Il ne borne ni un corps ni une reponse.

Le port ne sert que HTTP/1.1 ; en HTTPS, l’ALPN n’offre que http/1.1, et les clients HTTP/2 s’y rabattent.

Ce que vous verrez

QuandLigne
demarrageINFO idle and liveness timeouts in force: ..., une valeur par champ, et from_config : les cles fixees par le fichier
pair disparuINFO SSH session ended: the connection timed out: ...

Journaux

Deux flux

FluxTargetContenu
journal applicatiftout sauf auditdemarrage, connexions, avertissements, erreurs
piste d’auditauditune ligne par operation, connexion, refus, ban, action d’administration

Les deux vont sur stdout. Avec log.dir, ils vont aussi dans deux fichiers separes.

[log]

[log]
level = "info"
format = "json"
audit = "all"
dir = "/var/log/craft-file-gate"

Toutes les cles : Reference [log].

Le volume de la piste : log.audit

log.audit ne retire que des succes. Un refus, une erreur ou une issue inconnue est toujours ecrit.

log.auditSucces d’operation ecritsconnection_accepted, session_end ordinaires
alltousoui
changesupload, delete, delete_recursive, rename, mkdir, rmdir, rmdir_recursivenon
failuresaucunnon

Toujours ecrits, quel que soit log.audit : connection_rejected, connection_rejected_summary, ip_banned, tout ce qu’ecrit l’API d’administration, et les session_end d’une session coupee (administrateur, arret, ban, erreur interne).

Le niveau de log ne touche pas la piste : le target audit reste a info quel que soit log.level.

Fichiers de log : log.dir

FichierContenuRetention
craft-file-gate.logle journal applicatifretention_days (7 jours)
craft-file-gate-audit.logla piste d’audit, et elle seuleaudit_retention_days (90 jours)
  • Rotation : au premier ecrit d’un nouveau jour UTC, et avant une ecriture qui depasserait max_file_size_mb. L’archive porte l’instant de son ouverture, craft-file-gate.2026-09-27T00-00-00Z.log.gz, et ne contient que des lignes de ce jour UTC.
  • Retention : une archive part une fois son jour plus la retention ecoule, jugee sur son nom. Les autres fichiers du repertoire ne sont pas touches.
  • Fichiers en 0640, archives .gz toujours completes ; un repertoire par processus.

En conteneur, stdout et la rotation de la plateforme (kubelet, journald, Docker) restent la voie normale ; dir y demande un volume. Le chart Helm le propose : logFiles.enabled: true.

Refus repetes : une ligne resumee

Un client anonyme qui insiste (scanner, client mal configure) produirait une ligne par tentative. Par porte, adresse, reason et result, les refusal_summary_threshold premiers refus d’une fenetre de refusal_summary_window_secs gardent leur ligne ; les suivants sont comptes, puis resumes en une ligne connection_rejected_summary a la fin de la fenetre, avec suppressed, threshold et window_secs.

Seuls les refus qui n’ont juge aucune credential sont resumes : missing credential, empty credential, malformed credential, method disabled, verifier unavailable, banned, rate limit, password checks saturated, password checks saturated for address, shutting down. Un mot de passe faux garde toujours sa ligne.

Au-dela de refusal_summary_max_addresses cles suivies, les refus de plus sont resumes ensemble (overflow=true).

Fins de session SFTP

La ligne applicative de fin d’une session dit sa cause :

Ligne INFOCause
connection ended without a channel close — unregistering sessionle client est parti
SFTP channel closed — unregistering sessionle client a ferme le canal
session cut by an administrator — unregistering sessionDELETE /admin/sessions/{id}
session cut by the ban of its address — unregistering sessionl’adresse vient d’etre bannie
idle session cut by the server shutting down — unregistering sessionarret, session sans transfert
session cut by the server shutting down — unregistering sessionarret, fin de la periode de grace

La piste porte une ligne session_end par session, avec la meme cause dans reason.

Une connexion qui finit avant la session (client qui ne parle pas SSH, negociation sans algorithme commun) ecrit INFO SSH session ended: <raison>, avec peer et client_version. Ce sont des fins ordinaires : alertez sur leur nombre, pas sur chacune.

Ce que vous verrez

QuandLigne
demarrageINFO log filter in force, champs filter et source
demarrageINFO audit trail filter in force (refusals and errors are always recorded), champ audit
demarrage avec dirINFO log files in force, les deux chemins, la taille et les retentions
refusal_summary_* modifiesINFO refusal summary settings reloaded
ligne impossible a ecrire (disque plein)metrique craftfilegate_log_write_errors_total ; une ligne sur stderr au plus par minute

Niveau de log et diagnostic

Changer le niveau a chaud

[log]
level = "debug"

Enregistrer le fichier suffit : le niveau change en quelques instants, sans redemarrage, comme pour les fichiers d’utilisateurs et de roles. Sous Unix, kill -HUP applique l’edition sans attendre la surveillance du fichier.

levelEffet
trace, debugplus de detail, pour CraftFileGate seul ; les dependances (russh, hyper, le SDK AWS) restent a info
info (defaut)le fonctionnement normal
warn, error, offmoins de lignes, pour tout le processus, dependances comprises

level prend une seule valeur, pas une chaine de directives. La piste d’audit n’en depend pas : elle reste a info.

Dans [log], seuls level, audit et les refusal_summary_* s’appliquent a chaud ; le reste attend un redemarrage (voir Rechargement).

Qui decide du niveau

SourcePrioriteA chaud
RUST_LOGla plus forte : remplace tout le filtrenon : le niveau reste le sien pour la vie du processus
CRAFT_FILE_GATE_LOG_LEVELpasse avant le fichierle fichier n’est plus suivi tant qu’elle est posee
[log] levelle defautoui

La premiere ligne du journal dit le filtre en vigueur et sa source :

INFO log filter in force  filter="info,craft_file_gate=info,audit=info,opentelemetry_sdk=off" source="config [log] level"

RUST_LOG, pour aller plus loin

RUST_LOG donne le detail d’une dependance, par exemple le trace de russh. Il remplace la chaine entiere, y compris le audit=info que le serveur ajoute toujours : ecrivez-le vous-meme.

RUST_LOG=warn,russh=trace,audit=info
DirectiveCible
craft_file_gate=debugle serveur (le nom du crate, avec des _)
audit=infola piste d’audit ; audit=warn n’en garde que les refus et les erreurs
russh=tracele protocole SSH

Comprendre un refus d’authentification

La ligne d’audit d’un mot de passe refuse ne dit pas pourquoi : distinguer « utilisateur inconnu » de « mot de passe faux » revelerait quels comptes existent, et les journaux voyagent loin. Le detail est au niveau debug, pour l’exploitant seul.

DEBUG local password authentication rejected  username=alice reason="password does not match the stored hash"
DEBUG local password authentication rejected  username=bob reason="no such user in the users file"

debug donne aussi les comptes charges (local user store contents, champ usernames) ; info n’en dit que le nombre (local user store loaded).

Pour verifier un hash sans serveur :

echo -n "mot-de-passe" | craft-file-gate verify-password '$argon2id$v=19$...'

Revenez a info une fois le diagnostic fait. Les autres refus portent leur cause dans reason : Depannage.

Un changement a chaud ecrit INFO log level reloaded (level, source, filter). Sous level = "warn" ou plus severe, ces annonces partent sur stderr, prefixees craft-file-gate: : l’annonce d’un filtre n’est jamais etouffee par lui.

Telemetrie

Un exemple

[telemetry]
enabled = true
otlp_endpoint = "http://otel-collector:4318"
protocol = "http"
service_name = "craft-file-gate"
metrics = true

[telemetry]

Toutes les cles : Reference [telemetry].

Le point d’acces

protocolotlp_endpointSpans envoyes a
httphttp://collector:4318http://collector:4318/v1/traces (et /v1/metrics)
httphttp://gw/otel/v1/tracestel quel
grpchttp://collector:4317tel quel
  • https:// est chiffre dans les deux protocoles ; le certificat du collecteur est verifie contre les racines du systeme (voir Racines de confiance TLS).
  • protocol fait autorite : OTEL_EXPORTER_OTLP_PROTOCOL et OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, qu’injectent certains operateurs, ne le changent pas.
  • L’exporteur lit OTEL_EXPORTER_OTLP_HEADERS (en-tetes d’authentification) et OTEL_EXPORTER_OTLP_TIMEOUT (10 s par defaut).

Ce qui est exporte

L’export recoit les spans et les evenements de CraftFileGate et de la piste d’audit a partir de info, quel que soit le niveau du journal : un serveur regle en warn exporte toujours ses spans.

SpanAttributsParent
ssh_connectionpeer, username-
auth_password, auth_publickeyusernamessh_connection
sftp_sessionsession_id, usernamela connexion
sftp_operationoperation, session_id, username, path, bytessftp_session
api_requestmethod, path (tel que recu, encode en pourcent), username-
api_operationoperation, path (decode), usernameapi_request
authz_service_callhttp.method, http.url, http.status_codel’appelant

La ligne d’audit d’une operation est un evenement de son span ; un echec passe le span en status=error.

Pour un panneau Tempo ou Jaeger :

  • un transfert qui va jusqu’a close a deux spans sftp_operation du meme operation (upload, download) : l’ouverture, puis la validation, qui seule porte bytes. Comptez les spans qui portent bytes ;
  • un rmdir d’un repertoire non vide a un span rmdir puis un span rmdir_recursive, pour une seule ligne d’audit rmdir_recursive ;
  • les lectures et ecritures n’ont pas de span par paquet.

Un path ou un username qui contient un caractere de controle ou un separateur Unicode (U+2028, controles bidirectionnels) arrive echappe, sous une forme visible (\u{2028}). La valeur exacte est dans la ligne d’audit.

Les metriques en OTLP

Avec metrics = true, le serveur pousse ses metriques toutes les metrics_interval_secs, en plus de GET /metrics, qui reste servi ; la correspondance des noms : Metriques.

Envois, relances et arret

MomentComportement
refus transitoire (http : 429, 502, 503, 504, pas de reponse ; grpc : UNAVAILABLE, DEADLINE_EXCEEDED, …)jusqu’a 4 tentatives dans le delai de 10 s ; Retry-After respecte
autre refusune seule tentative
collecteur injoignablele lot est perdu et compte, rien ne s’arrete ; une ligne a la panne, une au retour
arretdernier envoi des metriques et des spans, 5 s au plus

Les spans perdus se comptent sur /metrics :

MetriqueCompte
craftfilegate_otel_spans_ended_totalspans remis a l’export
craftfilegate_otel_spans_exported_totalspans acceptes par le collecteur
craftfilegate_otel_spans_export_failed_totalspans d’un lot perdu apres sa derniere tentative

ended - exported - export_failed est ce qui est encore en route (jusqu’a 2560 spans : la file et le lot en cours), plus ce qu’une file pleine a rejete. Un ecart qui croit au-dela, ce sont des spans perdus.

increase(craftfilegate_otel_spans_ended_total[5m])
  - increase(craftfilegate_otel_spans_exported_total[5m])

Ce que vous verrez

QuandLigne
demarrage, export coupeINFO OpenTelemetry tracing disabled ([telemetry] absent or enabled = false); no spans are exported
demarrage, export actifINFO OpenTelemetry tracing enabled, champs endpoint (complete en http), protocol, service_name
demarrage, metriquesINFO OpenTelemetry metrics export enabled, champs endpoint, interval_secs
collecteur revenu apres une panneINFO OTLP collector recovered — telemetry export resumed
arretINFO flushing OpenTelemetry spans, puis OpenTelemetry shutdown complete

Fichiers de confiance et TLS

Les fichiers de confiance

Ces fichiers decident de qui entre et de ce qu’il peut faire : quiconque peut les ecrire peut se donner l’acces. Le serveur les juge au demarrage, et au rechargement pour ceux qui se rechargent.

FichierJuge
le fichier passe a --configdemarrage, rechargement
sftp.host_keysdemarrage
auth.users_file, auth.methods.local.users_file, ou users.toml adoptedemarrage, rechargement
auth.roles_file, ou roles.toml adoptedemarrage, rechargement
auth.jwt.public_key_filedemarrage
admin.tls.cert_file, admin.tls.key_filedemarrage, rechargement
admin.session.key_filedemarrage
CRAFT_FILE_GATE_JWT_SECRET_FILE, CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN_FILEdemarrage
auth.password_file et ca_bundle d’un backend webhdfsconstruction du backend
SSL_CERT_FILE, SSL_CERT_DIR, s’ils sont posesdemarrage

Le fichier, et chaque repertoire traverse depuis / pour l’atteindre (liens symboliques compris), est juge sur ses bits de mode, son proprietaire et son groupe. Le serveur lit ensuite le fichier par le descripteur qu’il a juge.

Les poser

PoseVerdict
appartient a root, ecrit par son seul proprietaire, repertoires de memeaccepte sans un mot
appartient a l’uid du serveur, ecrit par lui seulaccepte, dit au demarrage
sur un montage en lecture seule (Secret ou ConfigMap Kubernetes, volume :ro)accepte ; les repertoires ne sont pas juges
/, /tmp (root, bit sticky) sur le cheminaccepte

La pose recommandee :

chown root:root /etc/craft-file-gate /etc/craft-file-gate/*
chmod 755 /etc/craft-file-gate
chmod 644 /etc/craft-file-gate/config.toml /etc/craft-file-gate/roles.toml
chmod 640 /etc/craft-file-gate/users.toml /etc/craft-file-gate/host_ed25519
chgrp craft-file-gate /etc/craft-file-gate/users.toml /etc/craft-file-gate/host_ed25519
En conteneur, montez la configuration en lecture seule (readOnly: true, :ro)
un Secret Kubernetes en 0440 avec le fsGroup du pod convient tel quel.

[security]

Toutes les cles : Reference [security].

Lue au demarrage. A reserver a un deploiement ou le groupe du serveur ne contient que lui.

Ce que le serveur cree lui-meme

Le processus pose umask(077) au demarrage.

Cree par le serveurMode
cle d’hote generee, cle TLS admin generee0600 (certificat genere : 0644)
repertoire d’une cle generee, fichier de bans, fichiers temporaires0700 / 0600
fichiers deposes par les clients, repertoires d’accueil (backend local)0666 / 0777 masques par l’umask herite du lancement
fichiers de log et d’audit0640 masque par l’umask herite

Les instances qui partagent un volume de bans tournent sous le meme uid.

Les chemins des clients

Un chemin client est juge sur sa forme virtuelle, puis le backend le resout sous sa racine. Sur un backend local, un lien symbolique ne sort jamais de la racine : voir Local.

Racines de confiance TLS

Il s’agit du TLS sortant : backend S3 en HTTPS, jwks_url, service d’autorisation, collecteur OTLP en https://, backend WebHDFS. Le TLS entrant de la console se regle dans [admin.tls] (voir Console et API).

Le serveur n’embarque aucune racine : il lit le magasin du systeme.

VariableContenu
SSL_CERT_FILEun fichier PEM, un nombre quelconque de certificats
SSL_CERT_DIRdes repertoires, separes par :

Posee, l’une de ces variables remplace le magasin du systeme, elle ne s’y ajoute pas. Pour faire confiance a une autorite interne et aux racines publiques, concatenez :

cat /etc/ssl/certs/ca-certificates.crt ca-interne.pem > bundle.pem

Sans variable, le premier fichier trouve parmi les chemins usuels l’emporte (/etc/ssl/certs/ca-certificates.crt, /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem, /etc/pki/tls/certs/ca-bundle.crt, /etc/ssl/ca-bundle.pem, /etc/ssl/cert.pem), puis les repertoires /etc/ssl/certs et /etc/pki/tls/certs. Les images :latest et :alpine portent un magasin ; l’image :scratch n’en a pas.

Monter un jeu de racines

Le bundle Mozilla que maintient le projet curl convient :

curl -fsSLo bundle.pem https://curl.se/ca/cacert.pem

Montez-le au chemin par defaut (/etc/ssl/certs/ca-certificates.crt, en lecture seule), ou n’importe ou avec SSL_CERT_FILE ; en Kubernetes, un ConfigMap suffit, ce n’est pas un secret.

Un deploiement sans connexion TLS sortante (backend local ou proxy SFTP, comptes locaux ou cle JWT statique, pas d’OTLP) n’a besoin d’aucune racine.

Recommandations de deploiement

SujetRecommandation
fichiers de confianceen lecture seule, a root ; un Secret ou un ConfigMap monte readOnly: true
secretsles formes _FILE plutot que les variables, lisibles dans /proc/<pid>/environ
uidun uid dedie par instance, NoNewPrivileges=yes, un filtre seccomp (SystemCallFilter=@system-service, seccompProfile: RuntimeDefault)
podle chart Helm applique le profil restricted : voir Kubernetes
port d’administrationcontrol_listen sur une interface interne ; en conteneur, derriere une NetworkPolicy (le chart Helm en pose une, et un Service ClusterIP a part)
description OpenAPI[api] openapi, eteinte par defaut : sans authentification, elle decrit toute l’API
TLS de la console[admin.tls], certificat fourni ou genere
force bruteles trois listes de bans et les deux limiteurs (voir Bans)
piste d’auditexportee en entier hors de l’hote ; les refus d’authentification sont en INFO, un filtre WARN les perd
JWTrotation reguliere des cles de signature ; issuer et audience poses

Exploiter

Je veuxPage
installer : image, Compose, binaireDocker
deployer dans un cluster, a plusieurs podsKubernetes, Secrets
voir les sessions, couper, lever un ban, lire les journauxConsole et API d’administration
donner un acces web aux fichiersExplorateur de fichiers
savoir qui a fait quoiAudit : lire la piste
surveiller et alerterMetriques
changer la configuration sans redemarrerRechargement
arreter sans couper les transfertsArret
dimensionner memoire et CPUPerformances et memoire
comprendre une erreurDepannage

Docker

ssh-keygen -t ed25519 -f ./host_ed25519 -N ""   # la cle d'hote
sudo chown 65532 host_ed25519                   # lisible par l'utilisateur de l'image
docker run -d --name craft-file-gate \
  -v ./config.toml:/config/config.toml:ro \
  -v ./host_ed25519:/config/host_ed25519:ro \
  -v ./roles.toml:/config/roles.toml:ro -v ./users.toml:/config/users.toml:ro \
  -v data:/data \
  -p 2222:2222 -p 8080:8080 -p 8081:8081 \
  craftogether/craft-file-gate:latest

config.toml reprend le demarrage rapide avec ce qu’un conteneur change : des ecoutes sur 0.0.0.0, un chemin de cle relatif au repertoire de config.toml, la console par compte et mot de passe (les portes sont publiees), stockage et comptes dans leurs propres fichiers. docker run --rm craftogether/craft-file-gate:latest config schema > config.schema.json ecrit a cote le schema que nomme #:schema (Verifier une configuration) ; l’image le porte a /usr/share/craft-file-gate/config.schema.json.

#:schema ./config.schema.json
[sftp]
listen = "0.0.0.0:2222"
host_keys = ["host_ed25519"]        # /config/host_ed25519

[auth.methods]
local = { enabled = true }          # les comptes de users.toml
jwt = { enabled = false }           # sans fournisseur d'identite

[admin]
listen = "0.0.0.0:8080"             # API de fichiers, explorateur
control_listen = "0.0.0.0:8081"     # console, /admin, sondes, /metrics

[[admin.roles]]                     # un compte [[users]] avec authorities = ["admins"]
name = "admins"
permissions = ["overview", "sessions", "kick", "bans", "unban", "config", "logs", "audit", "revoke", "grant"]

[admin.ban]
max_failures = 5

[api]
enabled = true

[api.ban]
max_failures = 5

[api.ui]
enabled = true

Le premier montage et son ACL : roles.toml ([[backends]], [[roles]]) et users.toml ([[users]]), a cote de config.toml, sont pris sans etre declares.

#:schema ./config.schema.json
[[backends]]                        # roles.toml
name = "donnees"
type = "local"
root = "/data"                      # le volume data:/data

[[roles]]
name = "utilisateurs"

[[roles.mounts]]
backend = "donnees"
home_dir = "/{username}"            # alice voit /data/alice comme sa racine /
create_home = true

[[roles.mounts.acl]]
path = "/"
rights = ["read", "write", "list", "delete", "rename"]
recursive = true
#:schema ./config.schema.json
[[users]]                           # users.toml
username = "alice"
password_hash = "$argon2id$..."     # sortie de hash-password (plus bas)
authorities = ["utilisateurs", "admins"]   # ses fichiers, et la console

La console : http://hote:8081/, l’explorateur : http://hote:8080/files (Session de console, Explorateur).

ImageContenuUtilisateurHEALTHCHECK
:latestdistroless statique, sans shell65532 (nonroot)oui
:alpineAlpine, avec un shell pour le diagnostic65534 (nobody)oui
:scratchle binaire seul, sans magasin de certificatsaucun declare : posez --user 65532non

L’image expose 2222 (SFTP) et 8080 ; control_listen en publie un troisieme. Le HEALTHCHECK lance craft-file-gate healthcheck sur /config/config.toml. ENTRYPOINT est le binaire seul, la commande --config /config/config.toml ; le binaire traite SIGTERM en PID 1.

  • Montez en lecture seule (:ro) les fichiers auxquels le serveur se fie : configuration, utilisateurs, roles, cle d’hote, paire TLS (Securite).
  • /data appartient dans l’image a son utilisateur, et un volume nomme en herite ; un repertoire de l’hote monte a sa place doit lui appartenir : sudo chown 65532 data (65534 pour :alpine).
  • :scratch n’a aucune racine de certificats : une connexion TLS sortante (S3, JWKS, OTLP, Knox) demande un bundle monte et SSL_CERT_FILE.
  • Les sous-commandes passent telles quelles : echo -n 'mot-de-passe' | docker run -i --rm craftogether/craft-file-gate hash-password.
  • Gardez le delai d’arret de Docker au-dessus de celui du serveur : docker stop -t 90. Voir Arret.

Docker Compose

services:
  craft-file-gate:
    image: craftogether/craft-file-gate:latest
    restart: unless-stopped
    stop_grace_period: 90s
    ports:
      - "2222:2222"   # SFTP
      - "8080:8080"   # API de fichiers, explorateur
      - "8081:8081"   # console, /admin, sondes, metriques
    volumes:
      - ./config.toml:/config/config.toml:ro
      - ./roles.toml:/config/roles.toml:ro
      - ./users.toml:/config/users.toml:ro
      - ./host_ed25519:/config/host_ed25519:ro
      - data:/data

volumes:
  data:

Les features du binaire

Chaque type de backend et chaque porte est une feature Cargo, toutes actives par defaut. Les binaires et les images publies les portent toutes, et k8s. Un binaire construit avec moins de features refuse au demarrage la table d’une porte ou le type d’un backend qu’il ne porte pas, en nommant la feature.

FeatureBackend ou porte
backend-locallocal ; requise (etat, verrous et bans du serveur vivent sur le disque local)
backend-sftpsftp (proxy)
backend-s3s3
backend-webhdfswebhdfs
door-sftpla porte SFTP, [sftp]
door-restl’API REST de fichiers et l’explorateur, [api]
k8sbans et revocations partages par ConfigMap, cle de session dans un Secret ; hors defaut dans une construction sur mesure

La console, les sondes et /metrics n’ont pas de feature : tout binaire les porte.

Sous-commandes (hash-password, verify-password, healthcheck) : Le fichier de configuration.

Kubernetes

Le parcours de Docker sous le chart : le stockage local sur un volume, les comptes dans un Secret, les roles dans config.toml.

kubectl create secret generic sftp-host-keys --from-file=host_ed25519
kubectl create secret generic sftp-users --from-file=users.toml
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: sftp-data }
spec: { accessModes: [ReadWriteOnce], resources: { requests: { storage: 10Gi } } }
EOF
helm install sftp deploy/helm/craft-file-gate -f sftp-values.yaml \
  --set-file config.inline=config.toml
# sftp-values.yaml
hostKeys:
  existingSecret: sftp-host-keys                         # /keys/host_ed25519
extraVolumes:
  - name: data
    persistentVolumeClaim: { claimName: sftp-data }
  - name: users
    secret: { secretName: sftp-users, defaultMode: 0440 }
extraVolumeMounts:
  - { name: data, mountPath: /data }                     # root = "/data" du backend
  - { name: users, mountPath: /secrets/users, readOnly: true }

config.toml est celui de Docker suivi du contenu de son roles.toml (le chart ne monte que config.toml), avec host_keys = ["/keys/host_ed25519"] sous [sftp] et users_file = "/secrets/users/users.toml" sous [auth]. Les ecoutes restent sur 0.0.0.0 (les sondes joignent le pod par son IP) et [[admin.roles]] ouvre la console, puisque le chart ecarte le jeton statique. Le volume est au groupe du pod (fsGroup 65532) : create_home y cree alice/. Au-dela d’un pod, le volume est ReadWriteMany.

Le chart

ValeurDefautEffet
replicaCount1nombre de pods ; au-dela de 1, voir Plusieurs pods
config.inline""le contenu de config.toml, rendu dans le ConfigMap <release>-config ; un changement fait rouler les pods
config.existingConfigMap""ou un ConfigMap a vous, cle config.toml ; exactement une des deux sources
config.allowStaticTokenfalseallow_static_token, ecrit sous [admin] dans config.inline sauf si la configuration le pose ; avec existingConfigMap, a ecrire vous-meme. Voir S’authentifier
extraVolumes, extraVolumeMounts, extraEnv, extraEnvFrom[]ajoutes tels quels au pod et a son conteneur, apres ceux du chart : un volume pour la racine d’un backend local, un Secret
image.repository, image.tag, image.pullPolicycraftogether/craft-file-gate, la version du chart, IfNotPresentl’image
hostKeys.existingSecret""le Secret des cles d’hote, monte dans hostKeys.mountPath (/keys), fichiers en 0440 avec le fsGroup du pod ; voir Secrets
service.sftp.port, service.sftp.type2222, ClusterIPle Service <release>, SFTP seul
service.admin.port, service.admin.type8080, ClusterIPle Service <release>-api : API de fichiers, explorateur ; absent avec un config.inline sans [admin]
service.control.port8081control_listen : /admin, /metrics, sondes ; "" pour une seule porte ; ignore avec un config.inline sans [admin]
service.control.typeClusterIPle type du Service <release>-control : port de controle et port des sondes expose
service.probes.port""probes_listen : /livez, /readyz, /health et /metrics sur leur port, en HTTP ; les sondes y vont
service.probes.exposefalsece port aussi sur le Service <release>-control : sans lui, sondes et collecte passent par l’IP du pod
networkPolicy.enabledtrueune NetworkPolicy d’entree sur le pod
networkPolicy.publicFrom[] : toute sourcesources admises sur SFTP et le port admin
networkPolicy.controlFrom- namespaceSelector: {} : tout pod du clustersources admises sur le port de controle et celui des sondes ; au moins une
ban.backendfileconfigmap pour partager les bans entre pods
ban.configmapName"" : <release>-bansla ConfigMap des bans, creee par le chart et donnee au serveur par CRAFT_FILE_GATE_{SFTP,API,ADMIN}_BAN_CONFIGMAP_NAME
stateDir.enabled, stateDir.pathtrue, /var/lib/craft-file-gateun emptyDir pour le fichier de bans
logFiles.enabled, logFiles.dirfalse, /var/log/craft-file-gatefichiers de log, dans un emptyDir ou logFiles.existingClaim
passwordHashing.workers"" : requests.cpufils de hachage ; voir Verification des mots de passe
passwordHashing.queue"" : 1024file de hachage
resourcesrequests 100m, 64Mi ; limits 500m, 256Mivoir Memoire
tls.enabledfalsesondes en HTTPS, sauf sur service.probes.port ; [admin.tls] se regle dans config.toml
terminationGracePeriodSeconds"" : le shutdown_grace_period_secs de config.inline plus 60 s, sinon 90 sle delai d’arret du pod, au-dessus du pire cas d’un arret ; a poser avec un existingConfigMap qui allonge le delai
clusterSecret.*, adminRevocations.*, adminGrants.*, access.*la cle de session et les revocations partagees entre pods : Secrets
cluster.enabled, cluster.port"" : des que replicaCount depasse 1 ; 8083le canal entre pods : Plusieurs instances
cluster.minPeers, cluster.unreadyWhen"" : floor(replicaCount / 2) ; "" : alerter seulementles detecteurs ; unreadyWhen : une liste, ou auto (storage_alone, isolated_and_storage_down)
  • Un Service par exposition : publier SFTP par un LoadBalancer (service.sftp.type) ne publie ni la console, ni /metrics, ni les sondes.
  • Les sondes du kubelet viennent du noeud du pod, que la plupart des CNI laissent passer malgre la NetworkPolicy ; sinon, ajoutez le CIDR des noeuds en ipBlock a networkPolicy.controlFrom. Pour restreindre Prometheus et l’ingress, listez leurs namespaces dans networkPolicy.controlFrom.

Les sondes

/livez et /readyz (Sondes et sante) sont sondes sur le port des sondes quand il est donne, sinon sur le port de controle, sinon sur le port admin.

Un port pour les sondes

service.probes.port: 9090 pose [server] probes_listen : les sondes et /metrics ont un port a eux, en HTTP simple, meme sous [admin.tls] (Une porte ou deux). Le kubelet et Prometheus (par pod : PodMonitor ou annotations) n’ont plus besoin du certificat ni du port de la console ; le port n’est sur le Service <release>-control qu’avec service.probes.expose: true. Un pod SFTP sans [admin] a ainsi ses sondes, /readyz pret des que la porte SFTP ecoute.

Durcissement du pod

Le chart applique le profil restricted des Pod Security Standards. Chaque champ se surcharge dans podSecurityContext et securityContext ; null retire un champ ou un bloc.

ReglageValeur
utilisateurrunAsNonRoot: true, runAsUser, runAsGroup, fsGroup : 65532
privilegesallowPrivilegeEscalation: false, privileged: false, capabilities.drop: [ALL]
seccompRuntimeDefault
racinereadOnlyRootFilesystem: true : le serveur n’ecrit que dans un volume
jeton de service accountmonte avec ban.backend: configmap, le Secret partage (clusterSecret) ou la ConfigMap d’acces (adminRevocations, adminGrants)
configuration, cles d’hotemontees readOnly: true

La racine d’un backend local est un volume de extraVolumes. Cles d’hote et paire TLS viennent d’un Secret (cert-manager pour le TLS), jamais d’un emptyDir : auto_generate de [admin.tls] ne sert pas sous ce chart.

Plusieurs pods

Ce qui se partageCommentVoir
les pods se trouventdes que replicaCount depasse 1 : Service headless <release>-cluster, port cluster.port reserve aux pods de la release (app.kubernetes.io/instance), certificat dans le Secret partagePlusieurs instances
cle de session de la consolele Secret partage (clusterSecret), entree admin-session.keyLe Secret partage des pods
revocations de la console, acces temporairesla ConfigMap <release>-access (access.configmapName)Partager les revocations
bansConfigMap (ban.backend: configmap), suivie par un watch : un peu plus de 100 ms ; ou fichier sur un volume ReadWriteMany (persist_file), relu toutes les 5 s, convergence eventuelleBans
sessions, metriques, limites de debitrien : propres a chaque pod ; GET /admin/sessions liste celles du pod qui repond, chacune avec son pod_name (HOSTNAME) ; scope=cluster (la console) celles de tous

Chaque Role porte sur un seul nom, sans create : get, update sur le Secret ; get, update, patch sur chaque ConfigMap, watch sur leur collection pour les bans (helm template ... --show-only templates/rbac.yaml).

Le partage par ConfigMap

Feature k8s (dans les images publiees). Le pod lit la ConfigMap avant de servir ; chaque publication est conditionnee a la resourceVersion lue, et un data.bans efface par kubectl edit est republie a la suivante. Sans watch, la propagation tombe a une relecture toutes les 30 s ; un deploiement qui ne cree pas la ConfigMap ajoute create sur la collection.

[sftp.ban]           # de meme sous [api.ban] et [admin.ban] ; le chart en donne le nom
backend = "configmap"

inotify sur un noeud partage

fs.inotify.max_user_instances (128 par defaut) se compte par UID, sur tout le noeud. Le serveur n’en prend qu’une. Quand d’autres pods l’ont epuise et que vous ne pouvez pas regler le noeud :

[reload]
watch = "poll"   # aucune instance inotify ; une mise a jour de Secret est vue en 5 s

Voir Rechargement.

Secrets en Kubernetes

SecretPar fichierPar variable d’environnement
configuration entiereconfig.toml monte dans /config-
utilisateurs, rolesusers_file, roles_file-
cles d’hote SSHhost_keys-
paire TLS admin[admin.tls] cert_file, key_fileCRAFT_FILE_GATE_ADMIN_TLS_CERT, CRAFT_FILE_GATE_ADMIN_TLS_KEY
secret JWTCRAFT_FILE_GATE_JWT_SECRET_FILECRAFT_FILE_GATE_JWT_SECRET
jeton d’administration (secours ; interdit par allow_static_token = false, que le chart pose)CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN_FILECRAFT_FILE_GATE_ADMIN_BEARER_TOKEN
cles S3credentials = { type = "static", ... }AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY avec type = "iam_role"
cle des sessions de la console[admin.session] key_fileCRAFT_FILE_GATE_ADMIN_SESSION_SECRET_NAME : le Secret partage, rempli par les pods (ci-dessous)
  • Un Secret monte en volume est mis a jour par Kubernetes, qui repointe le lien ..data : configuration, utilisateurs, roles et paire TLS sont recharges a chaud. Voir Rechargement.
  • Un Secret monte avec subPath n’est jamais mis a jour : evitez subPath.
  • Montez Secrets et ConfigMaps readOnly: true, comme le chart, jamais sur un emptyDir ou un hostPath inscriptible (Fichiers de confiance et TLS).
  • Un Secret passe par envFrom (extraEnvFrom) n’est lu qu’au demarrage : kubectl rollout restart pour une nouvelle valeur.
  • Le serveur ne lit pas les Secrets par l’API Kubernetes : montez-les. Seule exception, le Secret partage des pods (ci-dessous).

Un fichier plutot qu’une variable

Montez le Secret en fichier par les valeurs du chart, et nommez-le par la variable _FILE (le jeton d’administration de meme, avec config.allowStaticToken: true) :

extraVolumes:
  - name: jwt
    secret: { secretName: sftp-jwt, defaultMode: 0440 }
extraVolumeMounts:
  - { name: jwt, mountPath: /secrets/jwt, readOnly: true }
extraEnv:
  - { name: CRAFT_FILE_GATE_JWT_SECRET_FILE, value: /secrets/jwt/jwt-secret }

Le fichier est lu une fois, au demarrage ; /proc/<pid>/environ ne montre qu’un chemin. Voir Variables d’environnement. users.toml se monte de meme, nomme par users_file : voir Kubernetes.

Le Secret partage des pods

Les pods d’une meme release partagent la cle qui signe les sessions de la console, pour qu’un pod accepte le jeton qu’un autre a emis. Le chart cree un Secret vide et donne aux pods get et update sur ce seul nom ; le premier pod qui n’y trouve pas l’entree admin-session.key la genere et l’ecrit, les autres la lisent. Chaque entree a sa propre vie : le certificat du canal entre pods y vit aussi (tls.crt, tls.key).

Valeur du chartEffet
clusterSecret.enabledtrue par defaut, avec [admin] ou le canal entre pods : le Secret, le Role (get, update sur ce nom), le jeton du compte de service ; avec [admin], CRAFT_FILE_GATE_ADMIN_SESSION_SECRET_NAME
clusterSecret.name<release>-cluster par defaut
adminRevocations.backendvide par defaut : configmap avec le Secret partage, memory sinon ; configmap cree la ConfigMap d’acces, ou vivent les revocations, son Role (get, update, patch sur ce nom), et pose CRAFT_FILE_GATE_ADMIN_SESSION_BACKEND, CRAFT_FILE_GATE_ADMIN_SESSION_REVOCATION_CONFIGMAP_NAME
adminGrants.backendvide par defaut : configmap avec [admin] et le Secret partage ou plusieurs replicas, memory sinon ; les acces temporaires dans la meme ConfigMap, CRAFT_FILE_GATE_ADMIN_GRANTS_BACKEND, CRAFT_FILE_GATE_ADMIN_GRANTS_GRANT_CONFIGMAP_NAME ; memory, ou file sans persist_file sous [admin.grants], avec plusieurs replicas fait echouer le rendu
access.configmapNamela ConfigMap d’acces, <release>-access par defaut

Avec replicaCount au-dela de 1, clusterSecret.enabled: false demande un key_file sous [admin.session] dans config.inline, et une cle partagee demande des revocations partagees (adminRevocations.backend: configmap, ou persist_file sous [admin.session]).

Pour changer la cle : supprimer l’entree (kubectl patch secret ... --type=json -p '[{"op":"remove","path":"/data/admin-session.key"}]') puis redemarrer les pods ; les sessions ouvertes sont perdues.

Cle d’hote SSH

Creez la cle une fois, gardez-la dans un Secret, montez-la. Ne laissez pas le serveur la generer (generate_host_key) dans un pod : chaque pod, ou chaque redemarrage sans volume, aurait sa cle, et les clients verraient « host key changed ».

ssh-keygen -t ed25519 -f host_ed25519 -N ""
ssh-keygen -l -f host_ed25519.pub          # l'empreinte a donner aux clients
kubectl create secret generic sftp-host-keys --from-file=host_ed25519

hostKeys.existingSecret: sftp-host-keys la monte dans /keys (Le chart), la configuration pointe dessus : host_keys = ["/keys/host_ed25519"]. L’empreinte du demarrage (loaded host key, champ fingerprint) et de GET /admin/config est celle de ssh-keygen -l.

Plusieurs instances

Les instances d’un deploiement se trouvent et se parlent sur un port a elles, en TLS 1.3 dans les deux sens, admises par le certificat qu’elles partagent. Le chart Kubernetes pose tout cela des que replicaCount depasse 1.

[cluster]
listen = "0.0.0.0:8083"
peers = "dns:craft-file-gate-cluster:8083"
cert_file = "/var/lib/craft-file-gate/cluster/tls.crt"
key_file = "/var/lib/craft-file-gate/cluster/tls.key"

Toutes les cles : Reference [cluster].

Le certificat partage

Son empreinte SHA-256 est l’identite du cluster : ni nom ni date ne sont verifies, l’expiration est dite par craftfilegate_cluster_cert_not_after_seconds. Pour le changer, retirez la paire et redemarrez toutes les instances ; pendant le roulement, un pair a l’ancien certificat est certificate_mismatch.

kubectl patch secret sftp-cluster --type=json \
  -p '[{"op":"remove","path":"/data/tls.crt"},{"op":"remove","path":"/data/tls.key"}]'
kubectl rollout restart deployment/sftp

Detecteurs et retrait du service

Apres chaque tour, chaque instance juge trois detecteurs : storage_alone (un backend down ici qu’un pair joignable voit up : une panne que tous voient ne compte pas), isolated (moins de min_peers pairs joignables depuis isolated_after_secs) et isolated_and_storage_down (isolee, et chaque backend non local down ici ; un backend local, le disque du pod, ne compte pas : sans backend non local, il ne se declenche jamais). Par defaut ils alertent seulement : WARN cluster detector active, craftfilegate_unready_detector{detector}, craftfilegate_cluster_isolated, checks.cluster de /admin/health.

Nommes dans unready_when, ils retirent l’instance du service : /readyz non pret apres unready_after_secs d’un detecteur actif, pret de nouveau apres ready_after_secs sans aucun ; /livez ne bouge pas. storage_alone ne retire un pod que si un autre pod, en service et sans aucun backend en panne, voit chacun de ses backends en panne disponible, et un seul pod a la fois : si chaque pod a une panne quelque part, aucun ne part.

[cluster]
min_peers = 1                    # le chart : floor(replicaCount / 2)
unready_when = ["storage_alone", "isolated_and_storage_down"]  # le chart : auto

isolated seul peut retirer chaque pod : coupure du port du cluster, perte d’une majorite de noeuds, partage en deux moities egales, reduction manuelle du nombre de replicas. Ne le nommez qu’en connaissance de cause, et ne descendez jamais sous 2 × min_peers + 1 replicas sans helm upgrade.

Avec les defauts, une panne de stockage est vue en 35 s au pire, le retrait suit 60 s plus tard, et Kubernetes sort le pod du Service apres 3 sondes de readiness a 10 s : environ 2 min.

Ce que vous verrez

Au demarrage, INFO cluster channel listening (TLS 1.3, pinned on the shared certificate) ; a chaque pair qui repond, INFO cluster peer reachable ; par statut : craftfilegate_cluster_peers{status}. Les onglets Instances, Sessions et Bans de la console lisent chaque pair avec votre credential, qu’il juge lui-meme (scope=cluster) ; un kick va au seul pair qui tient la session, une levee a chaque pair, sauf sur une liste ConfigMap ; par appel : craftfilegate_cluster_relay_total{route,result}.

Un ban y est un garde contre les essais, pas un retrait d’acces : une adresse bannie sur un pair le lit encore a travers une autre instance avec une credential valide (une session revoquee reste refusee).

Console et API d’administration

[admin]
listen = "0.0.0.0:8080"           # API de fichiers, explorateur
control_listen = "0.0.0.0:8081"   # console, /admin, /metrics, sondes
bearer_token = "changez-moi-en-production"

[api]
enabled = true
prefix = "/api/v1/files"

Les cles : reference.

Une porte ou deux

ConfigurationEcouteurCe qu’il sertRuntime
sans APIlistenconsole, /admin/*, /metrics, sondesadmin (deux fils admin-rt)
API, sans control_listenlistentout, et l’APIprincipal, partage avec les transferts
control_listencontrol_listen/admin/*, /, /ui/*, /metrics, /health, /livez, /readyzadmin
listenAPI de fichiers, explorateur, /api/docs et /api/openapi.jsonprincipal
[server] probes_listenprobes_listen/livez, /readyz, /health et /metrics, en HTTP simple, et rien d’autre ; ils quittent listen et control_listenadmin

Avec l’API de fichiers, posez control_listen : bans, kicks, /metrics et sondes restent joignables quand les transferts saturent le serveur. Le chart Helm le fait (service.control.port). probes_listen donne aux sondes et a Prometheus un port sans TLS ni console ; sans [admin], c’est le seul ecouteur HTTP (Kubernetes). La console lit sa sante sur /admin/health, servi a cote d’elle dans tous les cas.

S’authentifier

Chaque route protegee demande Authorization: Bearer <jeton>.

JetonIdentitePermissions
un jeton de session, rendu par POST /admin/login (Session de console)le compte localcelles des roles admin que nomment ses authorities, relues a chaque requete
un JWT verifie par [auth.jwt]son nomcelles des roles admin que nomme sa revendication de roles (authorities_path)
egal a bearer_token<static-token>toutes ; un secours, que allow_static_token = false ecarte (le chart Helm le pose)

Un role admin nomme des permissions ; une authority qui porte son nom les donne.

[[admin.roles]]
name = "support"
permissions = ["overview", "sessions", "logs"]

[[admin.roles]]
name = "admins"
permissions = ["overview", "sessions", "kick", "bans", "unban", "config", "logs", "audit", "revoke", "grant"]

Les noms des roles admin et ceux de [[roles]] sont distincts. Un compte qui porte les deux sortes ouvre la console et les portes de fichiers ; un compte qui ne porte que des roles admin n’ouvre que la console. Le service d’autorisation ne recoit jamais un nom de role admin, et aucun jeton d’administration n’ouvre un fichier. Les echecs comptent dans [admin.ban] (Bans).

PermissionRoutes
aucune (tout jeton admis)GET /admin/me, POST /admin/logout ; POST /admin/session/renew (jeton de session)
overviewGET /admin/resources, GET /admin/status, GET /admin/health, GET /admin/doors
sessionsGET /admin/sessions ; GET /admin/events (evenements de session)
kickDELETE /admin/sessions/{id}
configGET /admin/config, GET /admin/sessions/{id}/roles
bansGET /admin/bans ; GET /admin/events (evenements de ban)
unbanDELETE /admin/bans/{protocol}/{ip}
logsGET /admin/logs/app, GET /admin/logs/app/stream
auditGET /admin/logs/audit, GET /admin/logs/audit/stream (chaque lecture est auditee)
revokeGET /admin/revocations, POST /admin/revocations
grantGET, POST /admin/grants, DELETE /admin/grants/{id}, GET /admin/grants/candidates (les comptes et les montages demandent aussi config, les noms vus sessions) : un role de fichiers accorde pour un temps ([admin.grants], reference) ; a until ou a la revocation, les sessions SFTP ouvertes par l’acces et les transferts REST en cours sous son montage sont coupes (sur une autre instance : a sa relecture suivante) ; GET /admin/events (evenements d’acces)
publiquesPOST /admin/login, GET /admin/login, /, /ui/*, /health, /livez, /readyz, /metrics ; /api/docs, /api/openapi.json avec api.openapi

Sessions

{"total": 1, "offset": 0, "limit": 50, "server_time": "2026-10-07T14:42:10.125Z", "inactivity_timeout_secs": 600,
 "sessions": [{"session_id": "550e8400-e29b-41d4-a716-446655440000", "username": "alice", "remote_addr": "192.168.1.42:54321", "auth_method": "password",
   "mounts": [{"mount_path": "/", "backend": "disque", "home_dir": "/alice", "acl": [{"path": "/", "rights": ["read", "write", "list", "delete", "rename"], "recursive": true}]}],
   "connected_at": "2026-10-07T14:30:00Z", "idle_since": null, "last_sftp_op_at": "2026-10-07T14:41:25.402Z", "last_traffic_at": "2026-10-07T14:42:05.871Z",
   "bytes_read": 1048576, "bytes_written": 524288, "sftp_ops": 412, "has_active_transfer": false, "pod_name": "craft-file-gate-0", "authorities": ["utilisateurs"],
   "client_version": "SSH-2.0-OpenSSH_9.6", "algorithms": {"kex": "curve25519-sha256", "host_key": "ssh-ed25519", "cipher": "chacha20-poly1305@openssh.com",
     "mac_client_to_server": "hmac-sha2-256", "mac_server_to_client": "hmac-sha2-256"}, "user_key": null, "weak_algorithms": [], "type": "sftp"}]}
ChampSens
mountsun objet par montage : mount_path, backend (son nom), home_dir developpe, acl relative au montage
last_sftp_op_atla derniere requete SFTP, refus compris, keepalive non compris
last_traffic_atles derniers octets du client, keepalives compris ; la coupure par inactivity_timeout_secs part de la
has_active_transferun fichier ouvert ; retient l’arret pendant le delai de grace
user_keyla cle SSH : key_type, fingerprint, signature_algorithm
weak_algorithmsles algorithmes negocies juges faibles

La liste porte aussi les transferts REST (upload, telechargement) en cours depuis long_request_threshold_secs (30 s par defaut, 0 : tous, applique a chaud) : "type": "rest" (une session SFTP a "sftp"), session_id, username, remote_addr, method (PUT, GET), direction (upload, download), path (le chemin vu par l’utilisateur), mounts (mount_path, backend), connected_at (son debut), bytes, bytes_read, bytes_written, bytes_per_sec (la moyenne depuis le debut).

RequeteEffet
GET /admin/sessions?offset=&limit=&scope=limit 50 par defaut, 1000 au plus ; scope=cluster : aussi les sessions de chaque instance, champ instance, et peers (ce que chaque pair a repondu) ; local par defaut
GET /admin/sessions/{id}/rolesles montages tels que resolus a la connexion (effective.mounts, avec roles, max_file_mb, create_home, hidden_stores) et la definition actuelle de chaque role
DELETE /admin/sessions/{id}coupe la session ({"status": "disconnected"}) et la retire de la liste et du compte de max_sessions_per_user ; un transfert REST est coupe comme par un client parti, ligne d’audit reason="session ended: admin_kick" ; tenue par une autre instance, c’est elle qui la coupe (instance)
GET /admin/doorsles portes configurees : name, readiness (accepting, not_accepting, hosted), started, sessions_open, activity

Journaux et flux

Avec [log] dir, GET /admin/logs/{app|audit} rend les dernieres entrees du fichier courant, filtrees par le serveur. Voir Journaux.

Parametres : lines (500, 5000 au plus ; 4 Mio parcourus au plus), level (ERROR a TRACE), since et until (RFC 3339), q (texte, 256 octets), field=cle:valeur (16 au plus). 3 lectures simultanees au plus.

Les flux SSE (text/event-stream) suivent l’activite en direct :

FluxEvenements
GET /admin/eventssession.opened, session.closed, session.kicked, session.revoked, ban.added, ban.lifted, lagged
GET /admin/logs/{app|audit}/streamentry, une par nouvelle entree retenue ; suit une rotation

Au plus 8 flux ouverts, 2 par identite ; un flux finit a l’exp de son jeton ou a sa revocation, au plus tard apres 15 minutes.

Sondes et sante

RouteRepondUsage
/livez200 {"status": "alive"} tant que le processus vitsonde de vie ; craft-file-gate healthcheck
/readyz200 si le port SFTP accepte et que le runtime principal prend une tache en 500 ms, 503 sinon et des le signal d’arretsonde de disponibilite
/health200 toujours ; status ok ou degraded (service d’autorisation injoignable), checks.auth_service, checks.data_plane (verdict de /readyz), rien d’autresupervision
/admin/health/health, avec la version, les sessions actives et les backends configures ; permission overviewla console
/admin/statusready, degraded ou not_ready, avec chaque verification (sftp, data_runtime, auth_service, jwks) et son detaille bandeau de la console

La console

http://serveur:8081/ : une page qui appelle /admin/*. Elle ne charge rien d’un tiers. Sa connexion : Session de console.

OngletContenuPermission
Overviewstatut, version, sessions, service d’autorisationoverview
Metricsbandeau de /admin/status, tuiles sur cinq minutes, backends, top usersoverview (top users : sessions)
Instancesavec [cluster], chaque instance : statut, version, sessions, bans, fin du certificat, santeoverview
Sessionsune ligne par session SFTP et par transfert REST long, colonne Type (sftp, rest) ; montages, activite, kick ; avec un pair, chaque instance (colonne Instance)sessions, kick
Configurationbackends (secrets masques, horloge du stockage), roles, cles d’hoteconfig
Bansbans en cours, levee ; avec un pair, chaque instance (colonne Instance)bans, unban
Logsjournal et piste d’audit, filtres, suivi en direct ; avec [log] dirlogs, audit
Revocationsrevoquer les sessions d’un compte, celles en vigueur ; quand la porte connecte des comptesrevoke
Temporary accessaccorder un role de fichiers pour un temps, les acces vivants et finis depuis un jour, revocation ; Acces temporaire ; la vue Sessions montre les acces d’une sessiongrant
Hash toolshash-password et verify-password en WebAssembly dans la page : le mot de passe ne quitte pas le navigateuraucune
  • Les filtres et tris (ip:, user:, role:, backend:, type:) vivent dans l’URL : /#sessions?q=user:bob&sort=-last_op. Le jeton n’y figure jamais.
  • La page et ses fichiers (/ui/*) portent une CSP stricte (default-src 'none', tout de la meme origine, 'wasm-unsafe-eval') ; un reverse proxy qui pose la sienne permet au moins autant.

Ce que vous verrez

QuandLigne
demarrageINFO starting admin API server (HTTP) (ou (HTTPS))
demarrage avec [admin.tls]INFO admin TLS certificate is valid, champs not_after, remaining_days ; /metrics publie la date

Session de console et revocations

Un compte local dont les authorities nomment un role admin se connecte a la console par identifiant et mot de passe ; le serveur lui rend un jeton de session qu’il signe lui-meme.

[auth.methods]
local = { enabled = true, password = true }

[[users]]
username = "eric"
password_hash = "$argon2id$..."
authorities = ["admins"]

[[admin.roles]]
name = "admins"
permissions = ["overview", "sessions", "kick", "bans", "unban", "config", "logs", "audit", "revoke", "grant"]

[admin.ban]
max_failures = 5

[admin.session]
key_file = "/secrets/admin-session.key"   # 32 octets au moins, la meme sur toutes les instances
ttl_secs = 3600
max_age_secs = 43200

La connexion compte ses echecs dans [admin.ban], qu’elle exige (Bans), et passe par la file de hachage des mots de passe (Verification des mots de passe).

Se connecter

RequeteReponse
POST /admin/login {"username": "...", "password": "..."}token, a envoyer en Bearer ; username, expires_at, expires_in, auth_time, roles, permissions
GET /admin/login{"password": true} quand la porte connecte des comptes
POST /admin/session/renew (jeton de session)un nouveau jeton, tant que max_age_secs depuis la connexion n’est pas passe
GET /admin/me (tout jeton)nom, methode, permissions, roles admin, expires_at, expires_in, auth_time

Le jeton ne porte que le compte : chaque requete relit le compte en vigueur ([[users]] ou users_file) et recalcule ses permissions. Un rechargement de users_file qui retire un role, change le mot de passe ou retire le compte s’applique a la requete suivante.

La cle de session

La cle qui signe les jetons est la meme sur toutes les instances : key_file, ou sous Kubernetes secret_name (Secrets). Sans l’une ni l’autre, la cle est tiree au demarrage : les sessions vivent avec le processus, sur cette instance seule. Toutes les cles : Reference [admin.session].

Revoquer, se deconnecter

RequeteEffet
POST /admin/revocations {"username": "eric"} (permission revoke)tout jeton de session du compte emis jusque-la est refuse, sur tous ses onglets et appareils ; une nouvelle connexion passe. La reponse dit lift : held (l’etat partage porte la revocation) ou local_only (cette instance seulement)
POST /admin/logoutde meme pour le compte de l’appelant ; sous un JWT du fournisseur ou le jeton statique, rien n’est revoque (204) et la console oublie le jeton
GET /admin/revocationsles revocations en vigueur ; chacune disparait max_age_secs apres son issued_before

Les flux ouverts sous un jeton revoque (/admin/events, suivis de journaux) se ferment.

Partager les revocations

[admin.session]
persist_file = "/var/lib/craft-file-gate/admin-revocations.json"  # un fichier a elles
# ou, sous Kubernetes :
# backend = "configmap"                                  # le chart le pose avec le Secret partage
# revocation_configmap_name = "craft-file-gate-access"
reread_interval_secs = 5

Toutes les cles : Reference [admin.session].

Une cle partagee va avec des revocations partagees, et des horloges synchronisees (NTP) : une revocation est datee, et une instance dont l’horloge avance ou retarde l’applique avec cet ecart.

Dans la console

  • Identifiant et mot de passe quand la porte connecte des comptes ; « Sign in with a token » prend le jeton statique ou un JWT du fournisseur, seul formulaire sinon.
  • Le jeton vit dans la page : recharger, ouvrir un onglet ou fermer le navigateur redemande la connexion. Le navigateur ne garde que le theme et les vues des tableaux.
  • La session est renouvelee tant que la page est ouverte, jusqu’a max_age_secs apres la connexion ; un onglet ferme la laisse finir a ttl_secs. L’en-tete dit le compte, ses roles admin et la fin du jeton.
  • Logout appelle POST /admin/logout : une session deconnecte tous les onglets et appareils du compte.

Acces temporaire

Un acces temporaire donne un role de fichiers a un nom d’utilisateur jusqu’a une date, en plus des roles qu’il tient deja : un prestataire pour une livraison, un support pour une journee. Le nom peut etre un compte local ou une identite qu’apporte un JWT ou le service d’autorisation.

[[admin.roles]]
name = "support"
permissions = ["overview", "sessions", "grant"]

[admin.grants]
persist_file = "/var/lib/craft-file-gate/grants.json"   # ou backend = "configmap"
max_duration_secs = 604800                              # 7 jours

Toutes les cles : Reference [admin.grants].

  • Qui accorde : un role admin qui tient la permission grant, depuis l’onglet « Temporary access » de la console ou POST /admin/grants (S’authentifier). Tout role de [[roles]] peut etre accorde, jamais un role admin. Un motif est obligatoire ; l’accord et sa revocation sont dans la piste (grant_add, grant_revoke).
  • Le nom : la console propose les comptes locaux et les noms qu’une porte de fichiers a authentifies dans les 7 derniers jours, sur cette instance. Un nom jamais vu est accepte avec un avertissement : il n’ouvre rien tant que personne ne se connecte sous ce nom.
  • La duree : 1 h, 8 h, 24 h, 48 h, 7 jours ou une date de fin, au plus max_duration_secs.
  • A la fin (date atteinte ou revocation) : les sessions SFTP que l’acces a servies et les transferts REST en cours sous ses montages sont coupes, un upload en cours est abandonne ; chaque requete REST suivante est jugee sans lui. Une session ouverte avant l’accord ne le voit pas.
  • Plusieurs instances : l’acces vit dans un fichier ou la ConfigMap <release>-access (l’un des deux avec [cluster]), relu toutes les reread_interval_secs ; une autre instance l’applique et le coupe a sa relecture suivante. Les horloges sont supposees synchronisees (NTP).

Explorateur de fichiers

[admin]
listen = "0.0.0.0:8080"
bearer_token = "changez-moi-en-production"

[api]
enabled = true
prefix = "/api/v1/files"

[api.ui]
enabled = true
path = "/files"

La page est sur http://serveur:8080/files.

Les options

Les deux cles sont lues au demarrage. Toutes les cles : Reference [api.ui].

Ce que fait la page

La page est un client de l’API REST de fichiers, et de rien d’autre. Elle n’a aucun droit propre : ce que l’ACL refuse, l’API le refuse, et la page affiche le detail du serveur.

GesteRequeteDroit
se connecterGET <prefix>?rights-
ouvrir un dossierGET <dossier>?list&offset=&limit=200, puis ?rightslist
telechargerPOST <fichier>?ticket, puis l’URL rendue confiee au navigateurread
deposerHEAD <fichier> (« Replace file? » s’il existe), puis PUT en fluxwrite
nouveau dossierPUT <dossier>?mkdirwrite
renommerPOST <chemin>?rename=<destination>rename
supprimerDELETE <chemin>delete
  • Un bouton dont le droit manque sur le dossier courant est grise.
  • Les requetes partent une a une, plus le depot en cours, pour rester sous [auth] hash_per_address en Basic.
  • Les depots partent un fichier apres l’autre, avec un panneau de progression (nom, pourcentage, debit, annulation).
  • La liste est paginee par 200, triable par nom, taille ou date, dossiers d’abord.
  • Le dossier courant est dans l’URL (#/rapports/2026) : precedent, suivant et liens fonctionnent.

Se connecter

Identifiant et mot de passe (Basic, utilisateur local), ou « Use a token instead » (Bearer, un JWT). La credential reste en memoire seulement : recharger la page deconnecte. Seul le theme est garde (craft-admin-theme). Les echecs comptent pour [api.ban].

Montages

La page commence a /, le home que rend GET ?rights. Un montage a / y montre son home_dir ; des montages sous des noms y montrent la racine synthetique qui les liste.

EntreeAffichage
point de montageicone de stockage, etiquette mount ; s’ouvre comme un dossier
repertoire synthetiquedossier ordinaire ; deposer, renommer, supprimer grises

Telecharger : le ticket

Un lien du navigateur ne porte pas d’en-tete Authorization : la page demande un ticket (ce fichier, cet utilisateur, dix minutes ; voir L’API de fichiers), puis le navigateur telecharge en flux. Pendant ces dix minutes, « Reessayer » et la reprise du navigateur reutilisent la meme URL.

Les fichiers de la page sont publics, sous la meme CSP que la console ; les donnees passent par l’API.

Limites

LimiteEffet
pas de reprise de depotun depot interrompu est a refaire ; un telechargement relance apres dix minutes demande un nouveau clic
pas d’apercu ni d’editionni lien de partage, ni archive d’un dossier
Basic derriere une adresse partagee (NAT)les utilisateurs partagent hash_per_address ; declarez le proxy dans api.ban.trusted_proxies, ou connectez-vous par JWT

Interface en anglais ou en francais (?lang=fr, sinon la langue du navigateur) ; sous 720 px, les actions passent dans un menu.

Audit : lire la piste

La piste d’audit dit qui a fait quoi, sur quel fichier, et si ca a marche ; refus et erreurs compris. Le vocabulaire des lignes (actions, champs, reason) est dans la reference.

{"timestamp":"2026-10-07T14:41:25.402Z","level":"INFO","target":"audit","fields":{"message":"audit: operation succeeded","source":"sftp","action":"upload","session_id":"550e8400-e29b-41d4-a716-446655440000","remote_addr":"192.168.1.42","username":"alice","path":"/in/rapport.csv","backend":"disque","new_path":"","count":524288,"replaced":"no","removed":"","result":"success"}}

Ou elle va

ReglageOu va la piste
defautstdout, melee au journal applicatif, une ligne JSON par evenement, target = audit
[log] diren plus, craft-file-gate-audit.log (la piste seule) a cote de craft-file-gate.log
[log] stdout = falseles fichiers seulement
[telemetry]chaque ligne d’operation est aussi un evenement du span de l’operation

Rotation, retention et niveau : Journaux.

Ce qu’elle garde

log.audit (all, changes, failures) ne retire que des succes ; les refus anonymes repetes sont resumes en connection_rejected_summary. Voir Le volume de la piste et Refus repetes.

Severite

NiveauLignes
WARNce que seul un porteur de credential produit : une operation denied, error ou unknown ; un refus apres une credential acceptee ; un refus d’action admin ; ip_banned
INFOce que n’importe quel pair produit : les autres connection_rejected ; connection_accepted, session_end, les resumes, les succes

Une alerte sur les WARN de la piste ne se declenche donc pas pour un scanner anonyme.

Filtrer

Dans la console, onglet Logs, source audit (permission audit) : filtres par niveau, periode, texte et champ:valeur ; chaque lecture ecrit une ligne audit_read. Voir Console et API d’administration.

# l'API de la console
curl -H "Authorization: Bearer $TOKEN" \
  'http://serveur:8081/admin/logs/audit?field=username:alice&level=WARN&since=2026-10-07T00:00:00Z'

# les fichiers ; sans [log] dir : docker logs ... | jq -c 'select(.target == "audit")'
jq -c 'select(.fields.username == "alice")' /var/log/craft-file-gate/craft-file-gate-audit.log
QuestionFiltre
tout ce qu’a fait une session SFTP.fields.session_id == "<id>"
les fichiers ecrases.fields.action == "upload" and .fields.replaced == "yes"
les refus d’ACL.fields.reason == "acl"
les pannes du stockage.fields.result == "error" and .fields.backend == "<nom>"
les refus et erreurs.fields.result != "success"

Le texte d’une erreur du stockage n’est pas dans la piste : cherchez-le dans le journal applicatif, a la meme heure, avec le meme path.

Une piste incomplete

Une ligne que le serveur ne peut pas ecrire (pipe ferme, disque plein) est perdue : craftfilegate_log_write_errors_total le compte, stderr le dit au plus une fois par minute, et le serveur continue de servir. Alertez sur increase(craftfilegate_log_write_errors_total[5m]) > 0.

Metriques

curl http://serveur:8081/metrics          # texte Prometheus, sans credential
# prometheus.yml
scrape_configs:
  - job_name: craftfilegate
    static_configs:
      - targets: ['serveur:8081']   # probes_listen, sinon control_listen, sinon admin.listen
SortieFormeAcces
GET /metricstexte Prometheus 0.0.4public ; sur [server] probes_listen ou control_listen seulement s’ils sont poses
GET /admin/resourcesle meme instantane en JSON, plus sessions, bans actifs, backends, seuilspermission overview
export OTLPles memes nombres, un instantane par intervalle[telemetry] metrics = true, voir Telemetrie
  • Valeurs cumulees depuis le demarrage, par pod ; un debit se calcule entre deux lectures (rate()).
  • Une valeur inconnue est absente (null en JSON, aucun point en OTLP), jamais 0 : la colonne « Absente quand » dit quand.
  • Le nom OTLP est le nom Prometheus sans _total.

Catalogue

Serie PrometheusInstrument OTLPTypeLabelsAbsente quandSens
craftfilegate_connections_totalcraftfilegate_connectionscountersessions SSH ouvertes apres une authentification reussie
craftfilegate_connections_rejected_totalcraftfilegate_connections_rejectedcounterrefus de la porte SFTP, un par tentative (lignes connection_rejected de source=sftp, resumes compris)
craftfilegate_sftp_operations_totalcraftfilegate_sftp_operationscounterop (21 valeurs)paquets SFTP recus, refus compris
craftfilegate_sftp_bytes_read_totalcraftfilegate_sftp_bytes_readcounteroctets servis par SFTP
craftfilegate_sftp_bytes_written_totalcraftfilegate_sftp_bytes_writtencounteroctets acceptes par SFTP
craftfilegate_api_requests_totalcraftfilegate_api_requestscounterrequetes recues par l’API de fichiers, 429 et refus compris
craftfilegate_banned_ips_totalcraftfilegate_banned_ipscounterverdicts de ban de ce pod, toutes portes
craftfilegate_sftp_accept_errors_totalcraftfilegate_sftp_accept_errorscounteraccept() en echec sur le port SFTP
craftfilegate_log_write_errors_totalcraftfilegate_log_write_errorscounterlignes de journal ou d’audit non ecrites
craftfilegate_audit_refusals_suppressed_totalcraftfilegate_audit_refusals_suppressedcountersource (sftp, api, admin)refus anonymes resumes au lieu d’etre ecrits
craftfilegate_audit_refusal_summary_overflow_totalcraftfilegate_audit_refusal_summary_overflowcounterrefus arrives table des resumes pleine
craftfilegate_otel_spans_ended_totalcraftfilegate_otel_spans_endedcountertelemetrie coupeespans termines
craftfilegate_otel_spans_exported_totalcraftfilegate_otel_spans_exportedcountertelemetrie coupeespans acceptes par le collecteur
craftfilegate_otel_spans_export_failed_totalcraftfilegate_otel_spans_export_failedcountertelemetrie coupeespans perdus
craftfilegate_admin_tls_cert_not_after_secondscraftfilegate_admin_tls_cert_not_after_secondsgaugesans TLS, date illisibleexpiration du certificat servi, secondes Unix
craftfilegate_admin_tls_cert_expiry_unreadablecraftfilegate_admin_tls_cert_expiry_unreadablegaugesans TLS1 date illisible, 0 lue
craftfilegate_sftp_rate_limit_tracked_addressescraftfilegate_sftp_rate_limit_tracked_addressesgaugesans [sftp.rate_limit], ou avant son premier balayageadresses suivies par le limiteur
craftfilegate_api_rate_limit_tracked_addressescraftfilegate_api_rate_limit_tracked_addressesgaugesans [api.rate_limit], ou avant son premier balayageidem pour l’API
craftfilegate_local_userscraftfilegate_local_usersgaugehash_formatsans magasin localcomptes locaux par format de hash
craftfilegate_reload_watch_modecraftfilegate_reload_watch_modegaugemode (inotify, poll, sighup)avant l’armement1 pour le mode en vigueur
craftfilegate_jwks_cache_age_secondscraftfilegate_jwks_cache_age_secondsgaugesans JWKS, aucun chargement reussiage du cache JWKS
craftfilegate_jwks_refresh_failures_totalcraftfilegate_jwks_refresh_failurescountersans JWKSrafraichissements en echec
craftfilegate_jwks_cached_keyscraftfilegate_jwks_cached_keysgaugesans JWKScles en cache
craftfilegate_grants_activecraftfilegate_grants_activegaugesans [admin]acces temporaires qui donnent leur role maintenant
craftfilegate_grant_sessions_cut_totalcraftfilegate_grant_sessions_cutcounterreason (expired, revoked)sans [admin]sessions SFTP et transferts REST coupes a la fin d’un acces temporaire qu’ils utilisaient
craftfilegate_grants_store_local_onlycraftfilegate_grants_store_local_onlygaugesans [admin]1 : acces temporaires en memoire alors qu’un pair du cluster repond
craftfilegate_cluster_peerscraftfilegate_cluster_peersgaugestatussans [cluster]pairs du canal entre instances par statut : reachable, unreachable, certificate_mismatch
craftfilegate_cluster_relay_totalcraftfilegate_cluster_relaycounterroute (health, sessions, bans, kick, unban), result (answered, refused, failed)sans [cluster]requetes d’operateur relayees a un pair
craftfilegate_cluster_cert_not_after_secondscraftfilegate_cluster_cert_not_after_secondsgaugesans [cluster], date illisibleexpiration du certificat partage, secondes Unix ; jamais verifiee
craftfilegate_cluster_reachable_peerscraftfilegate_cluster_reachable_peersgaugesans [cluster]pairs joignables au dernier tour
craftfilegate_cluster_isolatedcraftfilegate_cluster_isolatedgaugesans [cluster]1 : moins de min_peers pairs joignables depuis isolated_after_secs (detecteurs)
craftfilegate_unready_detectorcraftfilegate_unready_detectorgaugedetector (storage_alone, isolated, isolated_and_storage_down)sans [cluster]1 tant que le detecteur est actif
craftfilegate_withdrawncraftfilegate_withdrawngaugesans [cluster]1 : /readyz non pret a cause d’un detecteur de unready_when
craftfilegate_stale_partials_clock_skew_secondscraftfilegate_stale_partials_clock_skew_secondsgaugebackendrien mesurehorloge du stockage moins celle du serveur
craftfilegate_password_hash_pool_placescraftfilegate_password_hash_pool_placesgaugesans magasin localfils plus places de file du pool de hachage
craftfilegate_password_hash_pool_in_usecraftfilegate_password_hash_pool_in_usegaugesans magasin localplaces prises
craftfilegate_password_hash_pool_full_totalcraftfilegate_password_hash_pool_fullcountersans magasin localverifications refusees pool plein
craftfilegate_jwt_refused_totalcraftfilegate_jwt_refusedcountersans JWTjetons refuses par le verificateur
craftfilegate_backend_operations_totalcraftfilegate_backend_operationscounterbackendbackend inutiliseappels au stockage
craftfilegate_backend_errors_totalcraftfilegate_backend_errorscounterbackendbackend inutilisepannes du stockage (entree-sortie, injoignable, identifiants refuses) ; pas les refus du client
craftfilegate_backend_bytes_read_totalcraftfilegate_backend_bytes_readcounterbackendbackend inutiliseoctets lus du stockage
craftfilegate_backend_bytes_written_totalcraftfilegate_backend_bytes_writtencounterbackendbackend inutiliseoctets ecrits vers le stockage
craftfilegate_backend_upcraftfilegate_backend_upgaugebackendavant la premiere visite, sonde coupee1 le stockage repond a la sonde, 0 il ne repond plus
craftfilegate_backend_probe_secondscraftfilegate_backend_probe_secondsgaugebackendidemduree de la derniere visite
craftfilegate_local_symlink_refusals_totalcraftfilegate_local_symlink_refusalscounterbackendbackend non local ou inutilisechemins refuses symlink escape
process_resident_memory_bytesprocess_resident_memory_bytesgaugehors Linux, /proc muetmemoire residente
process_peak_resident_memory_bytesprocess_peak_resident_memory_bytesgaugeidempic de memoire residente
process_cpu_seconds_totalprocess_cpu_secondscounteridemtemps CPU user + system
process_threadsprocess_threadsgaugeidemthreads

craftfilegate_connections_rejected_total compte par tentative, plusieurs par connexion : son rapport a craftfilegate_connections_total n’est pas un taux d’echec par connexion.

Seuils de la console

[admin.metrics_thresholds] decide seulement ou l’onglet Metrics marque une tuile ; le serveur n’alerte pas. Les cles et leurs defauts : reference.

Alertes

groups:
  - name: craftfilegate
    rules:
      - alert: CraftFileGateAdminCertExpiringSoon
        expr: craftfilegate_admin_tls_cert_not_after_seconds - time() < 30 * 86400
        for: 1h
      - alert: CraftFileGateAdminCertExpiryUnknown
        expr: craftfilegate_admin_tls_cert_expiry_unreadable == 1
      - alert: CraftFileGateBackendErrors
        expr: increase(craftfilegate_backend_errors_total[5m]) > 0
      - alert: CraftFileGateBackendDown
        expr: craftfilegate_backend_up == 0
      - alert: CraftFileGateStorageAlone
        expr: craftfilegate_unready_detector{detector="storage_alone"} == 1
      - alert: CraftFileGateIsolated
        expr: craftfilegate_cluster_isolated == 1

Alerter sur la peremption du cache JWKS

      - alert: CraftFileGateJwksStale
        expr: craftfilegate_jwks_cache_age_seconds > 2 * 3600   # 2 x jwks_refresh_interval_secs
        for: 5m
      - alert: CraftFileGateJwksNeverLoaded
        expr: absent(craftfilegate_jwks_cache_age_seconds) and on() craftfilegate_jwks_cached_keys >= 0
        for: 5m

Un cache perime garde ses cles : les jetons signes par une cle tournee depuis sont refuses. jwks_age_secs fait le meme jugement pour /admin/status.

Rechargement

vi /etc/craft-file-gate/users.toml          # le serveur voit l'edition et recharge
kill -HUP $(pidof craft-file-gate)          # ou : recharger tout de suite
[reload]
watch = "auto"            # auto | inotify | poll
poll_interval_secs = 5    # periode de la relecture periodique

Ce qui est surveille

config.toml toujours ; users_file, roles_file et la paire [admin.tls] quand ils sont configures. Le serveur surveille le repertoire de chaque fichier : un remplacement atomique (sed -i, mv) et la bascule du lien ..data d’un volume ConfigMap ou Secret sont vus. Les evenements sont regroupes par rafale de 200 ms, et une seule tache relit. SIGHUP declenche un rechargement, meme sans surveillance.

Ce qu’un rechargement fait

Il relit tous les fichiers surveilles, quel que soit celui qui a change.

Il appliqueIl garde
les cles marquees ⟳ dans la referenceles sessions SFTP etablies, avec ce qu’elles ont resolu a leur authentification
users_file : utilisateurs, hashs, cles, authoritiesun fichier refuse : rien n’en est applique
roles_file : roles, montages, ACL, backends, client auth.authz_base_urlles autres cles : une edition est nommee, jamais appliquee
la paire TLS admin, si ses octets ont change

Une authentification, ou une requete REST, lit les utilisateurs en vigueur a son arrivee. Un fichier relu est juge comme au demarrage. Une variable d’environnement definie garde le dernier mot.

Ce qu’un rechargement applique, cle par cle

Les cles marquees ⟳ dans la reference ; une cle sans marque est lue au demarrage. Les entrees [[users]], [[roles]] et [[backends]] sont rechargees quand elles vivent dans users_file ou roles_file ; ecrites dans config.toml, elles sont lues au demarrage. Les chemins de [admin.tls] demandent un redemarrage ; le contenu de la paire, lui, est relu a chaque rechargement.

inotify ou relecture periodique

Toutes les cles : Reference [reload].

Les deux cles sont lues au demarrage.

  • Une seule instance inotify par processus, partagee avec les fichiers de bans ; poll convient la ou elles sont rares (fs.inotify.max_user_instances, par UID et pour tout le noeud).
  • La relecture periodique compare date, taille, inode, ctime, proprietaire et mode ; un intervalle sans changement n’ecrit rien.
  • Quand la file d’evenements du noyau deborde (fs.inotify.max_queued_events), tous les fichiers sont relus ; des debordements repetes disent qu’un voisin agite le repertoire de configuration : deplacez-la, ou montez la limite.

Ce que vous verrez

Ligne INFOSens
hot reload armed (file watch + SIGHUP), ... (periodic re-read + SIGHUP)le mode en vigueur ; champ watching
file change detected, reloading configuration (ou ... by the periodic re-read ..., SIGHUP received, ...)un rechargement commence
hot-reloaded users file, hot-reloaded roles file, log level reloaded, audit trail filter reloadedce qui est applique
inline config — roles not hot-reloadableau demarrage : roles et backends dans config.toml

Arret

[server]
shutdown_grace_period_secs = 30   # attente du travail en cours, toutes portes
docker stop -t 90 craft-file-gate                  # Docker
# Kubernetes : terminationGracePeriodSeconds: 90

Les etapes

SIGTERM ou SIGINT (Ctrl-C) declenchent l’arret, le serveur fut-il PID 1.

EtapeCe qui se passeBorne
1la porte SFTP n’accepte plus ; /readyz repond 503 ; plus aucune verification de mot de passe admise-
2les listes de bans ([sftp.ban], [api.ban], [admin.ban]) sont ecrites dans leur persist_file5 s par liste
3server shutting down envoye a toutes les sessions SFTP ; la porte admin/API n’accepte plus et ferme ses flux-
4les sessions sans transfert en cours sont coupees-
5attente des transferts en cours, s’il y en a ; finit avec le derniershutdown_grace_period_secs
6toute session encore la est coupee-
7attente de la fin des sessions coupees (spans, dernieres lignes d’audit)reste du delai
8attente des requetes REST et admin en coursreste du delai
9uploads multipart S3 encore ouverts annules5 s
10nettoyages de quota et retraits des verrous d’upload5 s
11dernier envoi OTLP (metriques, spans)5 s
12graceful shutdown complete, sortie 0-

Les etapes 5, 7 et 8 partagent un seul delai, compte depuis l’annonce. Sans transfert en cours, l’arret ne l’attend pas.

Regler l’orchestrateur

Le pire cas d’un arret :

ComposanteDefaut
shutdown_grace_period_secs30 s
ecriture des bans5 s par liste ([sftp.ban], [api.ban], [admin.ban])
travaux des backends5 s
nettoyages et verrous5 s
envoi OTLP5 s
total50 s avec une liste de bans, 60 s avec les trois

Le delai d’arret de l’orchestrateur doit le depasser : docker stop -t 90, terminationGracePeriodSeconds: 90, que le chart pose (delai de grace plus 60 s). Un SIGKILL avant la fin coupe les transferts sans ligne d’audit, laisse les multipart S3 ouverts et perd les spans non exportes.

Ce que vous verrez

Chaque etape ecrit sa ligne INFO : shutdown signal received, ban files written out before the shutdown sequence, idle sessions disconnected (idle_kicked), waiting for active transfers to complete (seulement s’il y en a), remaining sessions disconnected (force_kicked, 0 compris), graceful shutdown complete. Les sessions coupees finissent en session_end reason=shutdown_idle (etape 4) ou shutdown (etape 6).

Performances et memoire

QuestionPage
quelle limite memoire poserMemoire
combien de verifications de mot de passe en paralleleVerification des mots de passe
que surveiller en productionMetriques

Memoire

La formule

pic ≈ 12 Mio                                  base (14,5 Mio en glibc)
    + 5,5 Mio                                 si un backend S3 sert
    + hash_workers × m + 2 Mio                verifications de mot de passe
    + sessions × 0,15 Mio
    + connexions en file de hachage × 70 Kio  au plus hash_queue
    + transferts locaux × 4 Mio
    + uploads S3 × 26 Mio + downloads S3 × 8 Mio
    + adresses suivies par les bans × 225 o

m est le plus grand m argon2 du fichier d’utilisateurs : 19 Mio au profil owasp-min (defaut de hash-password), 64 Mio au profil rfc9106-low-mem. La ligne de demarrage donne ce terme : peak_memory="1 thread × 19 MiB = 19 MiB". Voir Verification des mots de passe.

La formule est une borne haute : elle additionne des pics qui ne tombent pas tous au meme instant. Aucun cas mesure ne l’a depassee. La taille des fichiers n’y entre pas : les transferts sont en flux.

Les termes sont des mesures sur l’image publiee (binaire musl statique, x86_64), client OpenSSH ; une connexion en file de hachage pese ~42 Kio en Basic, un telechargement local 2,3 Mio. A grande echelle avec S3, les uploads dominent : 4 fils a m=65536 et 10 uploads S3 mesurent 415 Mio (formule : 537). Rien ne plafonne le nombre d’uploads S3 simultanes ; max_sessions_per_user borne par utilisateur.

Un petit deploiement sous 80 Mo

30 utilisateurs, ~900 fichiers par jour : quelques clients simultanes.

[auth]
hash_workers = 1      # une verification a la fois : 1 × 19 Mio
hash_queue = 32       # une file pleine : 32 × 70 Kio = 2,2 Mio

Avec des hashs hash-password (profil owasp-min), sans aucun hash rfc9106-low-mem dans le fichier :

VarianteFormuleTient sous 80 Mo
backend local45,5 Miooui
2 fils, backend local64,5 Miooui
S3, un upload a la fois64,7 Miooui
S3, 3 uploads simultanes116,5 Mionon
profil rfc9106-low-mem90,5 Mionon : une seule verification depasse le budget
file par defaut (1024)+70 Mio file pleinenon

Binaires glibc

Les images Docker (musl) rendent la memoire apres chaque pic ; un binaire glibc (releases, hote glibc) garde dans ses arenes la memoire des hashs : 95 Mio retenus apres une rafale (4 fils, m=19456), 19 Mio avec MALLOC_MMAP_THRESHOLD_=4194304. Posez cette variable (Environment= dans l’unite systemd) et ne reglez pas MALLOC_ARENA_MAX, qui monte le pic.

Kubernetes

ReglageCe qu’il doit couvrir
limits.memoryle pic de la formule, sur le nombre de transferts et de sessions simultanes attendus, plus une marge
requests.memoryla memoire au repos et en charge ordinaire : base et sessions

Le chart livre requests.memory: 64Mi et limits.memory: 256Mi. Avec son defaut (1 fil, tire de requests.cpu: 100m), 1000 connexions SSH en file au profil rfc9106-low-mem montent a 146 Mio. Sur un noeud charge, montez la requete vers le pic attendu.

Un pod sans limits.cpu voit les CPU du noeud : voir Verification des mots de passe pour fixer le pool.

Suivre la memoire

/metrics expose process_resident_memory_bytes et process_peak_resident_memory_bytes (Linux seulement), et la console les trace en direct. Voir Metriques.

Non mesure : transferts REST, proxy SFTP, telemetrie active, milliers d’utilisateurs, aarch64 et Windows.

Verification des mots de passe

[auth]
hash_workers = 2         # fils qui calculent les hashs
hash_queue = 1024        # tentatives qui attendent un fil
hash_per_address = 4     # places qu'une adresse peut tenir a la fois

Les options

Les trois cles sont lues au demarrage ; un rechargement ne change pas la taille du pool. Toutes les cles : Reference [auth].

Comment ca marche

Un hash argon2id, bcrypt ou sha512-crypt est du calcul pur : de 16 ms (owasp-min) a plus d’une seconde (argon2 lourd). Les verifications tournent sur des fils dedies (craft-file-gate-hash-0, -1… dans ps -L), hors du runtime qui sert les transferts, l’API et les sondes.

SituationCe qui se passe
un fil librela tentative est verifiee aussitot
fils occupes, place en filela tentative attend son tour
fils occupes, file pleinerefus immediat, sans recherche du compte, sans compter pour le ban
l’adresse tient deja hash_per_address placesmeme refus, quelle que soit la charge
client parti avant son tourla tentative n’est pas hachee ; sa place est rendue
adresse bannie pendant l’attentela tentative n’est pas hachee ; reponse d’une adresse bannie
arret du serveuraucune nouvelle tentative admise ; celles sur un fil finissent

Une tentative prend sa place avant la recherche du compte : un nom connu et un nom inconnu recoivent la meme reponse. Les limiteurs de debit et les bans (Bans) bornent ce qu’une rafale coute a la file.

Derriere un reverse proxy, declarez-le dans trusted_proxies : chaque client compte a sa propre adresse. Une adresse de NAT va dans whitelist_ips de la porte ([sftp.ban], [api.ban], [admin.ban]), qui n’est pas plafonnee. La connexion a la console (POST /admin/login) passe par le meme pool.

Choisir les tailles

QuestionReponse
pourquoi 2 fils au plus par defautchaque fil peut tenir un m argon2 entier ; 2 fils au profil owasp-min tiennent sous 80 Mo
combien de connexions par seconde2 fils : une trentaine au profil rfc9106-low-mem (68 ms), plus d’une centaine au profil owasp-min (16 ms)
pourquoi 1024 placesune place est une connexion qui attend (~70 Kio en SSH, ~42 Kio en Basic), pas une allocation argon2 ; 100 clients qui se reconnectent ensemble attendent au lieu d’etre refuses
attente de la derniere placehash_queue / hash_workers verifications : 6,5 s avec 2 fils au profil owasp-min ; restez sous sftp.login_grace_secs (120 s)
budget memoire serreune file plus courte, a la taille de la rafale attendue (hash_queue = 32) ; le pool tient hash_workers fois le plus grand m du fichier : Memoire

Ce que vous verrez

Au demarrage, une ligne dit chaque nombre et sa source :

INFO password hashing pool started: passwords are checked on these threads, off the async runtime, ...
     workers=2 workers_source="detected (8 CPUs ...), capped at 2 by default: ..."
     queue=1024 queue_source="default (1024, ...)"
     per_address=4 per_address_source="default (4 per address, ...)"
     peak_memory="2 threads × 19 MiB = 38 MiB"
     tokio_workers=8 tokio_workers_source="detected (std::thread::available_parallelism)"

tokio_workers : les fils du runtime async, sur la meme detection sans plafond, ou TOKIO_WORKER_THREADS.

Un rechargement de users.toml qui change le plus grand m le dit : INFO password hashing peak memory changed with the users file.

Kubernetes

Un pod sans limits.cpu voit les CPU du noeud (32, 64). Le plafond a 2 protege le pool par defaut, pas le runtime tokio. Pour suivre ce que le pod demande, passez requests.cpu par l’API Downward (arrondi a l’entier superieur) :

env:
  - name: CRAFT_FILE_GATE_HASH_WORKERS
    valueFrom:
      resourceFieldRef:
        containerName: craft-file-gate
        resource: requests.cpu
        divisor: "1"
  # de meme pour TOKIO_WORKER_THREADS

Une valeur explicite n’a pas de plafond : requests.cpu: 8 donne 8 fils et 8 x m de memoire. Le chart Helm le fait par passwordHashing.workers (vide : requests.cpu) et passwordHashing.queue.

Depannage

Un index des erreurs : le mot exact que vous voyez (message au demarrage, reason d’une ligne d’audit, statut SFTP, code REST) mene a la page du guide qui definit l’option en cause, puis, entre parentheses, a sa table dans la reference, et a l’identifiant de la regle de comportement, a citer au support. <...> marque une partie variable. Le vocabulaire complet des lignes d’audit est dans la reference.

Demarrage et configuration

Un reglage casse refuse le demarrage ; un reglage sans effet demarre avec un WARN qui nomme la cle.

MessageOu c’est regleRegle
--config is required when not using a subcommand, Configuration error: failed to read config file: <cause>Le fichier de configurationR-CONFIG-001
Configuration error: failed to parse config TOML: unknown field <cle>, expected one of ... (at line <l>, column <c>)la page du guide de la section nommee (reference) : la cle mal orthographieeR-CONFIG-005
... `sftp.<cle>` is now `server.<cle>`: move it to the [server] table : shutdown_grace_period_secs, max_sessions_per_user ou hidden_stores ecrite sous [sftp]Les cles de [sftp] ([server])R-CONFIG-015
[sftp]: this binary is built without the SFTP door (Cargo feature door-sftp) (ou [api], REST, door-rest), no door is configured, nothing would be served: ..., this binary is built without any door ...reference : [sftp], ou [api] avec [admin] ; Les features du binaire : un binaire construit sans cette porteR-CONFIG-016
server.probes_listen = <adresse> is also <cle> — the probes need a port of their ownUne porte ou deux ([server])R-ADMIN-020
server.backend_probe.<cle> = <n> is out of bounds: between <min> and <max>, server.backend_probe.timeout_secs = <n> is not below interval_secs = <m>: ...Disponibilite ([server.backend_probe])R-AVAIL-001
Configuration error: <section>: <cause>, <cle> = <n> is out of range: accepted values are <min> to <max>la page du guide de la section ou de la cle nommee (reference)R-CONFIG-010
Configuration error: failed to load local users: <cause>Les comptes locaux ([[users]])R-AUTH-004
duplicate username: <nom> — each [[users]] entry needs its own usernameLes comptes locaux ([[users]])R-CONFIG-011
user <nom> has a sha512-crypt password hash, which is not accepted — set [auth.methods] local.allow_sha512_crypt = true ... (ou bcrypt) ; WARN local user's password_hash is the example published in the README or the example files ..., local user's argon2 hash asks for more memory per verification than auth.methods.local.max_argon2_memory_kib allows: ..., local users whose password hash has another format or other parameters than most of the users file: ..., local users whose password hash cannot be verified (...): ..., accounts still depend on a legacy password hash flag: ..., legacy password hash flag enabled but no account uses this format ..., password hash is not argon2id — ...Les hashes ([auth.methods])R-AUTH-004, R-AUTH-005, R-AUTH-007, R-AUTH-008, R-AUTH-010, R-CONFIG-012
Configuration error: failed to load roles/backends: <cause> ; nom de role en double, role sans [[roles.mounts]], montage vers un backend inconnuLes cles d’un role et d’un montage, Les cles de tout backend ([[roles]], [[roles.mounts]], [[backends]])R-AUTH-028
unknown field backend (ou home_dir, acl) sous [[roles]] : une cle de montage ecrite au niveau du roleLes cles d’un role et d’un montage ([[roles.mounts]])R-CONFIG-005
role has no ACL entry: access is deny-by-default, ... (WARN)Ecrire une ACL ([[roles.mounts.acl]])R-ACL-003
role "<nom>" holds ACL paths that name one path on this backend, whose ACL compares paths folded (case and Unicode normalization), with different rights: [...]Casse des noms, Casse des chemins de l’ACL ([[roles.mounts.acl]], [[backends]])R-ACL-006
role missing required fields: role '<nom>': mount '<p>': home_dir = "<h>" climbs out of the backend's rootOu vont les fichiers ([[roles.mounts]])R-AUTH-032
user '<nom>': roles '<a>' and '<b>' both claim '<mount_path>' ... (au demarrage, ou failed to reload roles file) : montages en conflit d’un utilisateur localCumuler des roles, Recettes ([[roles.mounts]])R-AUTH-030
role '<nom>': user_key_algorithms: "<algorithme>" is not an algorithm this server supports; ...Les algorithmes de signature d’une cle ([[roles]])R-AUTH-019
no authentication methods enabledChoisir les methodes ([auth.methods])R-AUTH-001
aucune source de cle JWT, jwks_url avec une autre source, algorithm inconnu ; premier chargement JWKS en echec, the first fetch from auth.jwt.jwks_url failed, ... ; WARN auth.jwt.public_key_file is never read: ..., every JWT will be refused: auth.jwt.secret and auth.jwt.public_key_file are both set ..., [auth.jwt] is configured but auth.methods.jwt.enabled is false: ...JWT, Brancher un fournisseur d’identite ([auth.jwt])R-AUTH-020, R-AUTH-026, R-AUTH-025
auth: auth.hash_workers = 0 is refused: it must be between 1 and 1024 ...Verification des mots de passe ([auth])R-AUTH-014
unknown backend type "<type>": this binary knows <types>Les types, Les features du binaire ([[backends]])R-CONFIG-013
sftp.host_keys: <chemin> does not exist. Create it (ssh-keygen ...) or set sftp.generate_host_key ..., sftp.host_keys: cannot read <chemin>: <erreur> (la cle doit etre lisible par l’utilisateur de l’image)Cles d’hote, Docker, Secrets ([sftp])R-SFTP-003
une liste d’algorithmes refusee (nom inconnu, formes melangees, liste vide)Algorithmes ([sftp.algorithms])R-SFTP-006
[cluster] has no listen ..., ... has no peers ..., ... has no certificate source ..., ... sets both cert_file/key_file and secret_name ..., cluster.peers entry "<entree>" cannot be read: ..., cluster.secret_name is empty: ..., cluster.secret_name is set, but this binary was built without the k8s feature: ..., cannot listen on cluster.listen = <adresse>: <cause>, the cluster channel cannot start, refusing to boot: ..., cluster.<cle> is set without cluster.<autre> ..., cluster.peer_timeout_ms = <n> is out of bounds ..., cluster.listen = <adresse> is also <cle> ..., cluster certificate (cert_file=<c>, key_file=<k>): <raison> ; WARN [cluster] has nothing to add across instances: ...Plusieurs instances ([cluster])R-CLUSTER-001, R-CLUSTER-002
cluster.<cle> = <n> is out of bounds ..., cluster.unready_when names <detecteur>, but cluster.min_peers = 0 turns isolation off ..., ... but server.backend_probe.enabled = false ..., cluster.<cle> = <n> is under <min> s (...): one storage verdict could withdraw or restore the instance, CRAFT_FILE_GATE_CLUSTER_MIN_PEERS = ... cannot be read ..., CRAFT_FILE_GATE_CLUSTER_UNREADY_WHEN = "..." cannot be read: ... ; WARN a [cluster] detector key is set where it has no effectDetecteurs et retrait du service ([cluster])R-CLUSTER-016
WARN cluster detector active (detector, reason), cluster detector inactive ; withdrawn from service: ..., back in service: ... ; /readyz 503 withdrawn_by ; storage_alone actif sans retraitDetecteurs et retrait du service : storage_alone, le stockage de ce pod seul (montage, reseau du noeud) ; actif sans retrait : aucun pod en service et sain (sans backend en panne) ne voit tous ses backends en panne disponibles, ou un pod de plus petit identifiant passe d’abord ; isolated, le reseau entre pods ; isolated_and_storage_down, isole et chaque backend non local en panne : le reseau du noeud ; la raison est dans /admin/health (checks.cluster)R-CLUSTER-015, R-CLUSTER-016
<ancre>: [the directory ]<chemin> is <raison>: whoever can write there decides who gets in. ... ; a file this server trusts can be rewritten: ... (WARN)Les fichiers de confiance : chmod go-w, ou monter en lecture seuleR-TRUST-008
<ancre>: <chemin> is writable by the server's own group (<gid>): ...[security] ([security])R-TRUST-004
<ancre>: <chemin> cannot be resolved: <erreur>Les poserR-TRUST-007
cle privee lisible par les autres (WARN)Les fichiers de confianceR-TRUST-011
<X> and <X>_FILE are both set: one secret, two sources..., <X>_FILE is set but empty, ... is empty: an empty secret is no secret, ... is not UTF-8 textUn fichier plutot qu’une variable (Variables d’environnement)R-CONFIG-009
<VARIABLE>="<valeur>" is refused: it must be a whole number... ; env override ignored, the configured value stands, env override is empty (WARN)L’environnement (Variables d’environnement)R-CONFIG-008
server.shutdown_grace_period_secs must be > 0Arret ([server])R-SHUTDOWN-004
admin requires a bearer_token or at least one role under [[admin.roles]]S’authentifier ([admin], [[admin.roles]])R-ADMIN-001
admin.listen and sftp.listen are the same address — they cannot share a port, admin.control_listen = <adresse> is also admin.listen — the control door needs a port of its own, the admin door cannot start, refusing to boot: ... cannot listen on <adresse> (port deja pris)Une porte ou deux ([admin])R-ADMIN-001, R-ADMIN-002
tls: cert_file and key_file must both be set, tls: enabled but no cert source ..., tls: cert_file and auto_generate are mutually exclusive ; paire refusee au demarrage : <cause> (cert_file=<chemin>, key_file=<chemin>)Recommandations de deploiement ([admin.tls])R-ADMIN-001, R-ADMIN-003
[[admin.roles]] has an entry with a blank name, [[admin.roles]] names the role <nom> twice, [[admin.roles]] <nom> grants no permission: ..., unknown variant <permission> ; [[admin.roles]] is set but no credential can open the admin door: ...S’authentifier ([[admin.roles]], [auth.methods], [auth.jwt])R-ADMIN-006
role <nom> is also the name of an [[admin.roles]] entry: admin roles and file roles need different names (au demarrage, ou failed to reload roles file, keeping old config) ; an admin role and a file role have names that differ only by case or spaces: ... (WARN)S’authentifier ([[admin.roles]], [[roles]]) : renommer l’un des deuxR-ADMIN-006
this user's keys open no door: its authorities name no role with mounts, only admin roles, ... (WARN)S’authentifier ([[admin.roles]]) : retirer authorized_keys, ou donner un role de fichiersR-ADMIN-006
the admin door signs local accounts in with their passwords ... and has no [admin.ban]: ...Session de console ([admin.ban])R-ADMIN-022
admin.session.key_file <chemin> holds <n> bytes: a session key needs 32 at least ..., admin.session.key_file: ... writable by ..., admin.session.key_file <chemin>: <cause> ; admin.session.ttl_secs = <n> is out of range ..., admin.session.max_age_secs = <n> is under admin.session.ttl_secs ..., admin.session.key_file and admin.session.secret_name are both set ..., admin.session.secret_name is empty ..., admin.session.secret_name is set, but this binary was built without the k8s feature ...La cle de session ([admin.session]) : head -c 32 /dev/urandom, chmod 400 ; Les features du binaireR-ADMIN-022
auth.jwt.issuer = "craft-file-gate" is the issuer of the console's own session tokens ...[auth.jwt] : un autre issuerR-ADMIN-005
admin session key generated for this process: ... (WARN), jetons refuses par un autre pod ou apres un redemarrageLa cle de session ([admin.session]) : key_file ou secret_nameR-ADMIN-022
admin.allow_static_token = false, and a static admin token is set by <source>: ... ; the static admin token is enabled beside admin roles: ..., static admin token used: it is meant for break-glass only (WARN)S’authentifier ([admin]) : retirer le jeton de <source>, ou allow_static_token = falseR-ADMIN-024
api.enabled requires [admin] section to be configuredAPI REST de fichiers ([api], [admin])R-REST-001
[api.ui] enabled = true requires [api] enabled = true: ..., [api.ui] path "<path>" is not usable, ... collides with ...Explorateur ([api.ui], [api])R-EXPLORER-001, R-EXPLORER-002
[api] openapi = true requires [api] enabled = true: ...API REST de fichiers ([api])R-REST-012
admin.metrics_thresholds.jwks_age_secs = <age> is not above auth.jwt.jwks_refresh_interval_secs ... ; WARN a metrics threshold is set where it has no effect: ...Seuils de la console ([admin.metrics_thresholds], [auth.jwt])R-METRICS-017
invalid telemetry otlp_endpoint, invalid telemetry protocol, invalid telemetry metrics_interval_secs ; cannot build the OTLP <signal> exporter: ...; refusing to startLe point d’acces, [telemetry] ([telemetry]) : on_exporter_errorR-TELEMETRY-001, R-TELEMETRY-003
unknown ban backend ...: expected "file" or "configmap", sftp.ban: ..., api.ban: ..., admin.ban: ... ; the file API has no ban list: set [api.ban], ..., the admin routes have no ban list: set [admin.ban], ..., [api.ban] is set where it has no effect: ... (WARN)Les cles d’un ban, Une liste par porte ([sftp.ban], [api.ban], [admin.ban])R-BAN-018
[api.ban] trusted_proxies [...] and [admin.ban] trusted_proxies [...] differ, while the file API and the admin routes share [admin] listen: ..., [admin.ban] trusted_proxies [...] is set and the file API has no [api.ban]: ..., [api.ban] trusted_proxies [...] is set and there is no [admin.ban], while ... share [admin] listen: ...L’adresse d’un client HTTP ([api.ban], [admin.ban]) : les deux listes avec les memes trusted_proxies, ou [admin] control_listenR-BAN-021
hidden_stores : prefixe en forme de verrou, aucun affixe, grace_secs hors bornes (... is too short: ...)Ecritures atomiques ([server.hidden_stores], [[backends]], [[roles.mounts]])R-HIDDEN-004, R-HIDDEN-005, R-HIDDEN-014
uploads.takeover_idle_secs is not below uploads.idle_timeout_secs: ...Deux uploads vers la meme destination ([uploads])R-RESERVE-017, R-CONFIG-010
WARN create_home is set on a mount whose backend is not local: ..., home_dir holds a marker that is not {username}: ..., follow_symlinks = true on this local backend: ...Les cles communes sur un backend local, La racine et les liens symboliques, {username}R-LOCAL-002, R-LOCAL-005, R-AUTH-032
lock_prefix invalide ; cross_instance_reservation = true is not supported on a local backend on WindowsReservation entre instances ([[backends]])R-RESERVE-013, R-RESERVE-016
reload.poll_interval_secs = <n> is out of range: accepted values are 1 to 60 ; refusing to start avec reload.watch = "inotify" and the inotify instance cannot be created (ou a directory cannot be watched)Rechargement ([reload]) : watch = "auto" ou "poll"R-RELOAD-010
a [telemetry] metrics key is set where it has no effect, ... is set where it has no effect (WARN)la page du guide de la cle nommee (reference) : la retirerR-CONFIG-010
this secret is the example published in the README or the example files (WARN)Secrets : changer le secretR-CONFIG-012
outbound TLS is configured but there are no CA certificates to verify it with, ... (WARN), cause : SSL_CERT_FILE points at <chemin>, which does not exist, so it is ignoredRacines de confiance TLS (Variables d’environnement)R-TRUST-014

Rechargement

Un fichier refuse au rechargement garde ce qui est en vigueur. Voir Rechargement.

MessageOu c’est regleRegle
configuration edited but not applied until restart (keys=...)Ce qu’un rechargement applique, cle par cle (reference) : redemarrerR-RELOAD-003
failed to reload users file, keeping old config, failed to reload roles file, keeping old config, failed to build backends registry from reloaded roles, keeping old config, failed to re-read the config for its log level, keeping the current one, [log] level is not a log level (accepted values: ...); keeping the log filter currently in forceLes comptes locaux, Les cles d’un role et d’un montage, Les cles de tout backend, [log] ([[users]], [[roles]], [[backends]], [log]) : le champ e dit la causeR-RELOAD-005
password hashing pool size edit not applied: the pool is sized once, at startupVerification des mots de passe ([auth])R-AUTH-014
[log] format edit ignored: ...[log] ([log]) : redemarrerR-AUDIT-029
[log] level edit ignored: <variable> is in force ...Qui decide du niveau ([log], Variables d’environnement) : la variable l’emporteR-CONFIG-007
admin TLS certificate reload failed, still serving the previous certificate ; admin TLS certificate reload refused, still serving the previous certificate (ERROR)Ce qu’un rechargement applique, cle par cle, Les fichiers de confiance ([admin.tls]) : paire illisible, desaccordee ou refuseeR-RELOAD-008, R-TRUST-010
file hot reload cannot use inotify, falling back to re-reading the files every <n> s ; the file watcher lost events: its event queue overflowed, ..., the file watcher failed and may have lost events ...Rechargement ([reload])R-RELOAD-010, R-RELOAD-012
reload panicked, previous configuration kept; hot reload still armed (ERROR)a signalerR-RELOAD-013

Connexion et authentification

reason des lignes connection_rejected, et ce que voit le client. Les portes HTTP repondent une erreur en application/problem+json, champ detail.

reason, code ou detailOu c’est regleRegle
absent, 401 invalid credentials (l’explorateur : Invalid credentials ; 429 : Too many attempts) ; en DEBUG, local password authentication rejected (ou public key), reason no such user in the users file, password does not match the stored hash, the stored hash could not be parsed, no local user store configured, user has no authoritiesLes comptes locaux ([[users]]) : mot de passe faux ou nom inconnuR-AUDIT-022, R-AUTH-006, R-EXPLORER-006
no authorized key offeredLa cle publique ([[users]]) : authorized_keysR-AUTH-016
signature algorithm not allowedLes algorithmes de signature d’une cle ([[roles]], [sftp.algorithms])R-AUTH-018
missing credential, empty credential, malformed credential ; 401 missing or invalid authorization header, invalid Basic auth, JWT not configured, local auth not configuredAPI REST de fichiers, Choisir les methodes ([auth.methods]) : envoyer Basic ou BearerR-REST-003
invalid token, expired token, wrong issuer, wrong audience, 401 invalid JWTJWT ([auth.jwt]) : issuer, audience, cleR-AUTH-022
verifier unavailable ; ERROR JWKS background refresh failed ... (le cache garde ses cles)JWT, Brancher un fournisseur d’identite ([auth.jwt])R-AUTH-026, R-AUTH-027
no username claim, 401 token carries no username claimJWT ([auth.jwt]) : username_pathR-AUTH-024
method disabled, 401 authentication method disabledChoisir les methodes ([auth.methods])R-AUTH-003
no matching roles, 403 no roles resolved ; role resolution, 503 roles could not be resolved, retry later ; WARN authz service returned non-200, authz service call failedD’ou viennent les roles, Le contrat du service d’autorisation ([[users]], [auth.jwt], [auth]) : authorities, authorities_path, authz_base_urlR-AUTH-029
mount conflict, 503 ; backend initialization failed, 500 ; deconnexion SFTP server configuration error: <cause>; see the server logCumuler des roles, Les types ([[roles.mounts]], [[backends]]) : le journal applicatif dit la causeR-AUTH-030, R-AUTH-031
username not usable as home directory{username} ([[roles.mounts]])R-AUTH-032
session limit, session rejected: session limit exceeded for user <nom>Les cles de [sftp] ([server]) : max_sessions_per_userR-SFTP-011
banned, 403 IP temporarily banned ; SFTP : deconnexion address banned, WARN address banned while this login was in flight — ...La vie d’un ban, Lever un ban ([sftp.ban], [api.ban], [admin.ban])R-BAN-008, R-BAN-008
rate limit, 429 rate limit exceededLimites de debit ([sftp.rate_limit], [api.rate_limit])R-BAN-022
password checks saturated, 503 password checks saturated, retry laterVerification des mots de passe ([auth])R-AUTH-011
password checks saturated for addressVerification des mots de passe ([auth], [api.ban]) : NAT, trusted_proxiesR-AUTH-012
shutting downArretR-AUTH-015
invalid ticket, expired ticket, revoked ticket, 403 invalid download ticket, download ticket expired, download ticket revoked ; 400 a download ticket is only redeemed by GET, ... is asked for with POST ?ticket, ... and an Authorization header are exclusive, ... only downloads a file, ?ticket and ?rename are exclusiveTelecharger : le ticket, API REST de fichiers : redemander un ticketR-REST-009, R-REST-010
429 too many live download ticketsAPI REST de fichiers : 32 tickets vivants par utilisateur, attendre l’expiration du plus ancienR-REST-009
range not satisfiable, 416API REST de fichiers : le Range commence apres la fin du fichier, le client l’a deja entierR-REST-006
login grace time exceededSSH : [sftp] ([sftp]) : login_grace_secsR-TIMEOUT-001
WARN failed to accept SFTP connections; retrying with backoff (descripteurs epuises, ulimit -n)Porte SFTPR-SFTP-002
SSH session ended: the peer offered no algorithm in commonUn vieux client ([sftp.algorithms])R-SFTP-010
SSH request refused: ... (shell, exec, renvoi de port) ; SSH_MSG_CHANNEL_FAILURE sur un second sous-systeme sftpseul le sous-systeme sftp est servi, une fois par connexion : PortesR-SFTP-012, R-SFTP-013
SSH_FX_FAILURE internal server error: this SFTP session is closed ; ERROR a thread panicked: a defect in the server, ... (panic_payload, location, thread, backtrace avec RUST_BACKTRACE=1)un verbe a panique ; session_end reason=internal_error : a signalerR-SFTP-019

Operations sur fichiers

reason des lignes d’operation, statut SFTP et code REST.

reasonSFTPRESTOu c’est regleRegle
aclSSH_FX_PERMISSION_DENIED403Quelle entree decide ([[roles.mounts.acl]])R-ACL-003
acl subtreeSSH_FX_PERMISSION_DENIED403Les droits ([[roles.mounts.acl]]) : delete sur tout l’arbreR-DELETE-005
synthetic pathSSH_FX_PERMISSION_DENIED403Repertoires synthetiquesR-ACL-005
rename across mountsSSH_FX_OP_UNSUPPORTED422Un montage ou plusieursR-RENAME-010
invalid pathSSH_FX_PERMISSION_DENIED400caractere de controle, \, :, point final, nom court 8.3 : Les chemins des clientsR-UPLOAD-001
reserved nameSSH_FX_PERMISSION_DENIED403Deux uploads vers la meme destinationR-RESERVE-010
rename into restrictedSSH_FX_PERMISSION_DENIED403Les droits ([[roles.mounts.acl]]) : write a la destinationR-RENAME-008
existsSSH_FX_FAILURE409, 412destination existanteR-RENAME-002
is a directory, not a directorySSH_FX_FAILURE409rm d’un repertoire, rmdir d’un fichierR-DELETE-002
upload in progress ; recursive delete refused: an upload is in progress beneath (WARN)SSH_FX_PERMISSION_DENIED (curl : Permission denied (3))409Deux uploads vers la meme destinationR-RESERVE-001, R-RESERVE-012
quota exceededSSH_FX_FAILURE507Plafonner la taille d’un fichier ([[roles.mounts]]) : max_file_mbR-UPLOAD-007
session killedSSH_FX_CONNECTION_LOST-session coupee : SessionsR-SFTP-020
unsupportedSSH_FX_OP_UNSUPPORTED501Ecritures atomiques, Fichiers et repertoires sur S3 ([server.hidden_stores]) : reprise ou ajout, renvoyer le fichier entierR-UPLOAD-009
upload in progress au CLOSE, client : upload not published: another upload took this file: <chemin>SSH_FX_PERMISSION_DENIED409le verrou de cet upload a ete repris par une autre instance avant la publication (rafraichissement bloque) : rien n’est publie, renvoyer le fichier ; Reservation entre instancesR-RESERVE-008
taken overSSH_FX_FAILURE sur l’ancien handle409 a l’ancien uploadun upload bloque (client suspendu) repris par une relance du meme compte : Deux uploads vers la meme destination ([uploads]) : takeover_idle_secsR-RESERVE-017
upload idle timeout, upload below minimum rateSSH_FX_FAILURE408Uploads : [uploads] ([uploads])R-UPLOAD-013
session ended: admin_kick-503 the upload was cut by an administrator: nothing was writtenun transfert REST coupe depuis la console : SessionsR-REST-013
session ended: <cause>, session ended--le client est parti ; <cause> est le reason du session_endR-AUDIT-018
commit interrupted (result=unknown)--client parti pendant la publication : verifier le fichierR-AUDIT-017
no roles-403voir no matching roles plus hautR-AUDIT-020
-SSH_FX_OP_UNSUPPORTED-READLINK, SYMLINK, posix-rename@openssh.com : non servisR-LIST-012
--400 missing ?rename= query param ; 405 (verbe non servi)API REST de fichiersR-REST-002
-SSH_FX_FAILURE-handle inconnu ou d’un autre genre : bug du clientR-SFTP-017

Stockage

Une erreur du stockage porte son genre dans reason (result=error) ; son texte est dans le journal applicatif, a la meme heure.

reason, messageSFTPRESTOu c’est regleRegle
not foundSSH_FX_NO_SUCH_FILE404Les cles communes sur un backend local ([[roles.mounts]]) : chemin absent ; home_dir absent sans create_home ; sur S3, un repertoire ne se renomme pasR-UPLOAD-015, R-S3-004
permission deniedSSH_FX_PERMISSION_DENIED403droits du stockage ; S3 sans s3:ListBucket : Les droits du bucket ; WebHDFS (AuthorizationException, doAs non autorise) : PrerequisR-UPLOAD-015, R-WEBHDFS-005
symlink escapeSSH_FX_PERMISSION_DENIED403La racine et les liens symboliques ([[backends]] local) : metrique craftfilegate_local_symlink_refusals_totalR-LOCAL-004
already exists, directory not emptySSH_FX_FAILURE409etat du stockageR-UPLOAD-015
storage errorSSH_FX_FAILURE backend error500le journal applicatif dit la causeR-UPLOAD-015
not implementedSSH_FX_OP_UNSUPPORTED501operation que ce backend n’a pas : Les typesR-UPLOAD-015
root <chemin> cannot be canonicalized and opened as a directory: <erreur>--La racine et les liens symboliques ([[backends]] local)R-LOCAL-003
no host_key_fingerprint: the upstream's host key would not be checked, ... ; SFTP proxy: the upstream's host key does not match host_key_fingerprint (ERROR)--La cle d’hote de l’amont ([[backends]] sftp) : ssh-keyscan -p <port> <hote> | ssh-keygen -lf -R-PROXY-002, R-PROXY-004
SFTP proxy: authentication failed: the upstream accepts only ssh-rsa (SHA-1) ...--Les cles ([[backends]] sftp) : cle ed25519 ou ECDSAR-PROXY-005
WARN this store refused the conditional CopyObject with a 400 this code does not treat as "precondition not implemented": ... (chaque renommage echoue) ; could not list the multipart uploads under this backend's prefix, ..., listing the parts of a multipart upload was refused, ..., could not read the S3 service's clock (no usable Date header on its response) and ..., this S3 backend has no prefix, so its abandoned multipart uploads are never swept: ...--Les droits du bucket, Les uploads multipart, Les ecritures conditionnellesR-S3-010, R-S3-013
SFTP proxy connect: ... ; WARN accept_any_host_key = true on this SFTP proxy backend: ..., sftp proxy: the upstream cannot replace a file atomically (posix-rename@openssh.com), ..., sftp proxy: the second SFTP channel used for posix-rename@openssh.com could not be opened (...), ...--La cle d’hote de l’amont, Les envoisR-PROXY-001, R-PROXY-003, R-PROXY-007
ERROR sftp proxy: the destination was removed to publish an upload and the rename ... which is kept, sftp proxy: an upload was interrupted after the removal of its destination ... which is kept (champs temp, destination)--Les envois : le fichier en cours est la seule copie, le renommer a la mainR-PROXY-008
the conditional writes are not proven on this S3 store: <pourquoi> (WARN), refus avec cross_instance_reservation ; INFO S3 upload precondition self-test verdict = ignored, not implemented, if-match ignored, delete refused, inconclusive--Les ecritures conditionnelles ([[backends]]) : cross_instance_reservationR-S3-007, R-RESERVE-015
the SFTP upstream refuses the name of the upload lock files, ..., could not refresh the lock file of an upload in progress: ..., this upload's lock was taken over by another instance: it is not published (WARN)--Reservation entre instances ([[backends]]) : lock_prefixR-RESERVE-005, R-RESERVE-007, R-RESERVE-008
backend "<nom>" (webhdfs): Knox refused the service account on GETFILESTATUS of the root ... ; session refusee : nom d’utilisateur qui ne peut pas etre un doAs ; WARN this WebHDFS backend's url is plain http ..., this WebHDFS backend's gateway could not be checked at startup ..., this WebHDFS answers no LISTSTATUS_BATCH ...--Prerequis, Le montage sur WebHDFS ([[backends]] webhdfs) : auth, url, ca_bundleR-WEBHDFS-004, R-WEBHDFS-005, R-WEBHDFS-001, R-WEBHDFS-010
case_insensitive = true on a WebHDFS backend: ...--Le montage sur WebHDFS ([[backends]])R-WEBHDFS-014
the storage's clock differs from this server's by more than 30 s (WARN) ; stale in-flight files on this SFTP upstream are never collected (age_check = false): ..., stale in-flight files of this backend are aged on this server's clock (age_check = false): ..., abandoned multipart uploads of this S3 backend are aged on this server's clock (age_check = false): ..., could not read the storage's clock with a probe file in this directory (age_check is on), ..., this storage offers no lock to tell an in-flight upload from the leftovers of one killed with the server ...--Restes d’un transfert interrompu ([uploads.stale_partials]) : NTP du stockageR-HIDDEN-016, R-HIDDEN-011, R-HIDDEN-013
this filesystem does not support RENAME_NOREPLACE ... (WARN)--Performances et limites : NFS, FUSER-LOCAL-011
backend unavailable: its storage did not answer the availability probe (WARN, backend, backend_type, failures, reason) ; backend available again: ... (WARN, down_secs)--Disponibilite ([server.backend_probe]) : reason dit ce que la visite a trouve (racine absente, bucket refuse, amont injoignable, cle d’hote differente, delai)R-AVAIL-002
reason SFTP proxy: authentication failed — probe paused until reload--Proxy SFTP : le compte de service ou son mot de passe ; la sonde ne se reconnecte qu’au prochain rechargement des roles, pour ne pas faire bannir la passerelle par l’amontR-AVAIL-001

Console et API

Code et detailOu c’est regleRegle
401 missing or invalid authorization header, invalid tokenS’authentifier ([admin], [auth.jwt])R-ADMIN-005
401 invalid credentials sur POST /admin/login (compte pour [admin.ban] ; un compte sans role admin : admin sign-in refused: the password is right, ... en WARN), 503 password checks saturated, retry later, 400 expected a JSON body {"username": ..., "password": ...}, 415 expected Content-Type: application/json (un client qui n’envoie pas Content-Type: application/json), 403 sign-in from another site refused (Sec-Fetch-Site autre que same-origin ou none : une page d’un autre site ; ni l’un ni l’autre ne compte pour le ban), 400 username longer than 256 bytes ; 401 revoked token (compte retire, mot de passe change ; ou revoque, voir plus bas), 401 session too old: sign in again, 400 only a session token is renewed: ...Se connecter ([admin.session], users_file) : se reconnecterR-ADMIN-022
404 password sign-in is not available on this door sur POST /admin/login ; [admin.session] sets a session key or a revocation store, but no account signs in to the console: ... (WARN) ; console : seul le champ du jeton, pas de formulaire de mot de passe (GET /admin/login dit {"password": false})Session de console ([auth.methods], [[admin.roles]]) : mots de passe locaux et roles adminR-ADMIN-022, R-ADMIN-018
401 revoked token apres POST /admin/revocations ou POST /admin/logout : se reconnecter ; lift local_only et WARN the session tokens were revoked on this instance only: ... ; admin session revocations are kept in memory beside a shared session key: ..., [admin.session] sets revocation keys this store does not read, shared admin session revocation records refused ..., ... ahead of this clock ... (WARN) ; ERROR the shared admin session revocations cannot be read: ... ; refus de demarrer ... the revocations cannot be read ..., admin.session.backend = ..., admin.session.reread_interval_secs must be between 1 and 30 ..., admin.session.revocation_configmap_name is empty ..., admin.session.persist_file <chemin> is also the persist_file of a ban list ... ; 400 expected a JSON body {"username": ...}, username blank or longer than 256 bytes, 404 this door issues no session token: there is nothing to revokeRevoquer, se deconnecter, Partager les revocations ([admin.session]) : persist_file ou backend, NTPR-ADMIN-023
POST /admin/grants : 400 unknown role, until in the past, invalid period, duration over max ; 403 role not grantable ; 409 role already held, mount conflict, already granted, too many grants ; 403 the grant permission is required ; 404 this door grants no temporary access (ecouteur sans [admin]) ; DELETE : 404 grant not found, 409 grant ended, grant ended: it is already <etat> ; lift local_only ; refus de demarrer admin.grants.* ..., [cluster] is set and the temporary access grants are kept in memory: ..., ... the grants cannot be read ... ; WARN [admin.grants] is set, but no credential holds the grant permission: ... ; session SFTP coupee temporary access expired / temporary access revoked, upload REST 503 the upload was cut: the temporary access it used ended; nothing was written ; WARN a temporary access grant names a role this server does not define: ignored, ... names an admin role: ignored ; ERROR the shared temporary access grants cannot be read: ...Reference [admin.grants] : max_duration_secs, persist_file, backendR-GRANT-002, R-GRANT-006, R-GRANT-003, R-GRANT-005, R-GRANT-009, R-GRANT-010
console : Wrong username or password. (un mot de passe faux, un nom inconnu ou un compte sans role admin : un seul message) ; Your session was revoked: sign in again., Your session expired: sign in again., Your session reached its maximum duration: sign in again., Token rejected: sign in again. ; la page redemande la connexion apres un rechargement ou dans un nouvel ongletDans la console ([admin.session]) : se reconnecter ; le jeton ne vit que dans la pageR-ADMIN-018
console blanche ou sans style derriere un reverse proxy, Refused to ... / violates the following Content Security Policy directive dans la console du navigateurLa console : la CSP du proxy permet au moins celle de la pageR-ADMIN-018
403 no matching admin roles ; 403 the <permission> permission is requiredS’authentifier ([[admin.roles]])R-ADMIN-006, R-ADMIN-007
404 session not found: <id>, 400 invalid session ID formatSessionsR-ADMIN-010
404 no log files: [log] dir is not set, 404 unknown log source: expected app or audit, 500 the log file could not be read, 403 the log file is a symbolic link, which the log viewer does not follow ; 400 lines: not a number, level: unknown level, q: longer than 256 bytes, field: ... ; 429 too many log reads at once: retry in a momentFichiers de log, Journaux et flux ([log])R-ADMIN-013
429 too many admin streams open: close one or retry laterJournaux et fluxR-ADMIN-014
unban 409 lift=overruled (un ban posterieur a la demande s’applique), 200 lift=local_only (etat partage non ecrit), 404 not banned, no ban manager, 400 unknown protocol (sftp, api ou admin)Lever un ban, Partager les bans entre instances ([sftp.ban], [api.ban], [admin.ban])R-BAN-011
401 (404 avec l’explorateur, [api.ui]) sur /metrics, /admin, /health, /livez ou /readyz appeles sur admin.listen ; 404 sur <prefix> : [api] enabled absent, ou requete sur control_listenUne porte ou deux ([admin], [api]) : avec control_listen ou probes_listen, chaque route a son portR-METRICS-001, R-REST-001
404 ou 401 sur /api/docs, /api/openapi.jsonAPI REST de fichiers ([api]) : openapi = trueR-REST-012
port injoignable de l’exterieur d’un conteneur, connection refused ; conteneur unhealthy (craft-file-gate healthcheck en echec)Docker, Les outils du binaire ([admin]) : listen = "0.0.0.0:...", pas 127.0.0.1 ; l’adresse que healthcheck interrogeR-ADMIN-001, R-ADMIN-015
the admin door shares its listener with the file API ... (WARN)Une porte ou deux ([admin]) : poser control_listenR-ADMIN-002
admin TLS certificate expires soon, has expired, admin TLS certificate expiry could not be readCe que vous verrez ([admin.tls]) : renouveler ; un certificat expire est servi quand memeR-ADMIN-004

Kubernetes

Symptome ou messageOu c’est regleRegle
sondes en echec, connection refusedKubernetes ([sftp], [admin]) : listen = "0.0.0.0:..." ; sondes bloquees par la NetworkPolicy selon le CNI : le CIDR des noeuds dans networkPolicy.controlFromR-ADMIN-001
/readyz 503, data_runtime=unresponsive (runtime sature) ou sftp=not_accepting (port SFTP ferme, arret en cours)Les sondesR-K8S-002
rendu du chart en echec : networkPolicy.controlFrom is empty ..., config.existingConfigMap and config.inline are both set, no configuration: ..., adminRevocations.backend = ...: expected memory, file, configmap or empty, replicaCount > 1 with clusterSecret.enabled = false ..., replicaCount > 1 with a shared console session key and adminRevocations kept in memory ..., cluster is on (replicaCount > 1, or cluster.enabled) and clusterSecret.enabled is false ..., cluster.unreadyWhen names <valeur>: ...Le chart, Le Secret partage des podsR-K8S-005
backend = "configmap" refuse : ConfigMap illisible pendant 30 s (message nommant <namespace>/<nom> et le Role) ; WARN a chaque relecture des bans, propagation en 30 s (droit watch manquant)Le partage par ConfigMap ([sftp.ban], [api.ban], [admin.ban]) : Role et RoleBindingR-BAN-017
cannot read or fill the entry admin-session.key of Secret <ns>/<nom> ..., shared Secret not readable or writable yet; ... (WARN)Le Secret partage des pods ([admin.session]) : le Secret cree par le chart, le Role (get, update sur ce nom)R-ADMIN-022
cannot read the admin session revocations in ConfigMap <ns>/<nom> ..., admin session revocations not readable yet; ... (WARN)Partager les revocations : la ConfigMap et le Role du chart (adminRevocations.backend: configmap, get, update, patch sur ce nom)R-ADMIN-023
cannot read or fill the entry tls.key of Secret <ns>/<nom> ..., the entries tls.crt and tls.key of Secret <nom> do not go together ..., ... the key does not belong to the certificate ; WARN cluster peers name does not resolve: its last addresses are kept ; WARN cluster peer unreachable (seulement d’un pair qui a deja repondu ; avant, en DEBUG), cluster peer presents another certificate than this instance ..., cluster state cut to fit: ..., cluster peer state holds absurd records ..., cluster peer's failure series are all past their window ... (horloges a synchroniser, NTP)Le certificat partage : le Secret et le Role du chart, une rotation en cours ; le port cluster dans la NetworkPolicyR-CLUSTER-002, R-CLUSTER-006, R-CLUSTER-005, R-CLUSTER-003
onglet Instances, peers d’une liste scope=cluster, kick ou levee relayes : unauthorized on <instance>, forbidden on <instance> ; WARN audit: relayed credential refused ... sur le pair ; 400 unknown scope: ... (scope autre que local ou cluster)Plusieurs instances : le meme jeton statique, la meme cle de session, le meme [auth.jwt] et les memes [[admin.roles]] partoutR-CLUSTER-009, R-CLUSTER-010, R-CLUSTER-008
backend = "configmap" refuse par un binaire sans la feature k8sLes features du binaireR-BAN-018
failed to create the file watcher, falling back to re-reading ...inotify sur un noeud partage ([reload])R-RELOAD-010
pod OOMKilled pendant une rafale de connexionsMemoire, Verification des mots de passe ([auth])R-AUTH-014
« host key changed » d’un pod a l’autre ; WARN host key not found, generated a new one (sftp.generate_host_key)Cle d’hote SSH ([sftp])R-SFTP-004

Journaux, metriques et telemetrie

MessageOu c’est regleRegle
craft-file-gate: <n> log line(s) could not be written ... (stderr)Une piste incompleteR-AUDIT-031
audit target disabled by RUST_LOG, audit successes disabled by RUST_LOGRUST_LOG (Variables d’environnement) : RUST_LOG=warn,audit=infoR-AUDIT-002
process resource sampling failed; the process_* series are absent from /metricsMetriquesR-METRICS-010
OTLP collector unreachable — telemetry spans will be dropped until recoveryEnvois, relances et arret ([telemetry])R-TELEMETRY-010
failed to create the OTLP <signal> exporter, ... ([telemetry] on_exporter_error = "warn") (ERROR)[telemetry] ([telemetry])R-TELEMETRY-003
sessions were still ending when the grace period ran out: ...Regler l’orchestrateur ([server]) : shutdown_grace_period_secsR-SHUTDOWN-007
ban file was still being written when the shutdown stopped waiting ; the shutdown stopped waiting for the removal of upload lock files; ...ArretR-BAN-019, R-SHUTDOWN-008

Reference : configuration

Toute la configuration tient dans un fichier TOML, passe par craft-file-gate --config <fichier>. Les utilisateurs, les roles et les backends peuvent aussi vivre dans users_file et roles_file, a cote.

PageSections
[server], [cluster][server], [server.hidden_stores], [cluster]
[sftp] et la porte SSH[sftp], [sftp.algorithms], [sftp.ban], [sftp.rate_limit]
[auth], utilisateurs et roles[auth], [auth.methods], [auth.jwt], [[users]], [[roles]], [[roles.mounts]], [[roles.mounts.acl]]
[[backends]]cles communes, puis local, sftp, s3, webhdfs
[admin] et [api][admin], [[admin.roles]], [admin.tls], [admin.ban], [admin.metrics_thresholds], [api], [api.ban], [api.rate_limit], [api.ui]
[log], [telemetry] et le reste[log], [telemetry], [uploads], [uploads.stale_partials], [tcp_keepalive], [reload], [security]

Seule [auth] est obligatoire, avec au moins un role, et au moins une porte : [sftp], ou [api] avec [admin]. Sans porte configuree, le serveur refuse de demarrer. craft-file-gate config explain <cle> donne a chaque cle son type, son defaut et ses bornes (Verifier une configuration).

Lire ces tables

ColonneSens
Clele chemin complet ; [] marque une entree d’un tableau de tables : roles[].mounts[].home_dir est la cle home_dir d’un [[roles.mounts]] ; ⟳ : rechargee a chaud
Typechaine, entier, booleen, liste, table, tableau de tables, chemin, adresse (ip:port), URL
Defautla valeur quand la cle est absente ; (obligatoire) : son absence refuse le demarrage ; - : pas de valeur propre
Effetce que la cle regle, et ses bornes

Une cle sans marque attend un redemarrage. Une session ouverte garde ses roles et ses montages jusqu’a sa fin. Voir Rechargement.

Regles communes

RegleEffet
chemin relatifrelatif au repertoire du fichier de configuration, pas au repertoire courant
roles.toml, users.toml a cote de la configurationpris s’ils ne sont pas declares ; le journal de demarrage dit lesquels
variable d’environnementremplace la valeur du fichier : voir Variables d’environnement
cle inconnue, valeur hors bornes, cle sans effetDepannage

Reference : [server] et [cluster]

⟳ : rechargee a chaud ; sans marque : prise au redemarrage. Voir Rechargement.

Ce que toutes les portes partagent, et le canal entre instances. [server] peut manquer : chaque cle a son defaut.

[server]

Voir Arret, Kubernetes.

CleTypeDefautEffet
servertable-les reglages communs a toutes les portes
server.shutdown_grace_period_secsentier30secondes laissees au travail en cours de chaque porte a l’arret, une echeance pour toutes ; au moins 1
server.max_sessions_per_userentierillimitesessions simultanees par utilisateur, sur une porte a sessions (SFTP) ; avec [cluster], celles de chaque instance joignable s’ajoutent
server.probes_listenadresseabsent : sondes sur [admin]CRAFT_FILE_GATE_PROBES_LISTEN la remplace ; un port a part pour /livez, /readyz, /health et /metrics, servis la seulement, en HTTP, sans authentification ; sans [admin], le seul ecouteur HTTP

[server.hidden_stores]

Voir Uploads et ecritures atomiques.

CleTypeDefautEffet
server.hidden_storestable-ecritures atomiques, pour tout backend et tout montage qui ne dit rien
server.hidden_stores.enabledbooleenfalseecrire dans un fichier en cours de transfert, publie par renommage a la fin
server.hidden_stores.prefixchaine.in.le debut du nom de ce fichier
server.hidden_stores.extensionchaine.la fin du nom de ce fichier

[server.backend_probe]

Voir Les backends.

CleTypeDefautEffet
server.backend_probetable-la sonde de disponibilite de chaque backend, en arriere-plan, jamais sur le chemin d’une operation
server.backend_probe.enabledbooleentruefalse : aucune sonde, chaque backend reste unknown
server.backend_probe.interval_secsentier15secondes entre deux visites d’un backend, de 5 a 3600
server.backend_probe.timeout_secsentier5duree maximale d’une visite, de 1 a 60, sous interval_secs (sinon refus du demarrage)
server.backend_probe.failures_before_downentier2visites en echec d’affilee avant down, de 1 a 10 ; une visite reussie suffit a revenir up. Detection au pire : interval_secs × failures_before_down + timeout_secs, 35 s par defaut

[cluster]

Voir Plusieurs instances.

CleTypeDefautEffet
clustertableabsente : chaque instance est seulele canal entre les instances d’un deploiement ; CRAFT_FILE_GATE_CLUSTER_LISTEN la cree
cluster.listenadresse-le port du canal, a lui seul, en TLS 1.3 mutuel ; CRAFT_FILE_GATE_CLUSTER_LISTEN la remplace
cluster.peerschaine ou liste-dns:<nom>:<port> : chaque adresse du nom, relu toutes les 5 s (un Service headless rend chaque pod pret, Docker Compose chaque replique du service) ; ou des <hote>:<port>. Chacune est appelee chaque seconde ; celle qui repond l’identifiant de l’instance (<pod_name>/<boot_id>) est elle-meme ; CRAFT_FILE_GATE_CLUSTER_PEERS la remplace
cluster.cert_filechemin-le certificat partage ; quand lui et key_file manquent, generes (ECDSA P-256, 10 ans) par la premiere instance, lus par les autres sur un volume partage
cluster.key_filechemin-sa cle, creee en 0600
cluster.secret_namechaine-ou le Secret Kubernetes partage, entrees tls.crt et tls.key (feature k8s) ; CRAFT_FILE_GATE_CLUSTER_SECRET_NAME la remplace
cluster.peer_timeout_msentier2000delai d’un appel a un pair, de 100 a 10000
cluster.min_peersentier1sous ce nombre de pairs joignables pendant isolated_after_secs, l’instance est isolee ; 0 : jamais ; de 0 a 1000 ; CRAFT_FILE_GATE_CLUSTER_MIN_PEERS la remplace
cluster.unready_whenliste[] : alerter seulementles detecteurs qui retirent l’instance du service (/readyz non pret), en OU : storage_alone, isolated, isolated_and_storage_down ; CRAFT_FILE_GATE_CLUSTER_UNREADY_WHEN (separes par des virgules) la remplace
cluster.isolated_after_secsentier30duree sous min_peers avant d’etre isolee ; retour immediat ; de 5 a 3600
cluster.unready_after_secsentier60duree d’un detecteur choisi avant le retrait, de 10 a 3600, au moins 2 × server.backend_probe.interval_secs
cluster.ready_after_secsentier30duree sans detecteur choisi avant le retour, de 5 a 3600, au moins server.backend_probe.interval_secs

Reference : [sftp] et la porte SSH

⟳ : rechargee a chaud ; sans marque : prise au redemarrage. Voir Rechargement.

[sftp]

Voir Porte SFTP et algorithmes SSH, Delais.

CleTypeDefautEffet
sftptableabsent : porte SFTP eteintela porte SFTP (feature door-sftp)
sftp.listenadresse(obligatoire)adresse et port d’ecoute ; CRAFT_FILE_GATE_LISTEN la remplace
sftp.host_keysliste(obligatoire)les cles d’hote, fichiers presents : un chemin, ou une table { path, algorithms }
sftp.host_keys[].pathchemin(obligatoire)le fichier de la cle privee, forme table
sftp.host_keys[].algorithmslisteselon la cleles algorithmes de signature annonces pour cette cle, syntaxe de [sftp.algorithms]
sftp.generate_host_keychaineabsent"ed25519" ou "ecdsa-p256" : cree la cle d’un fichier de host_keys absent ; une instance unique seulement
sftp.login_grace_secsentier120secondes pour s’authentifier, connexion coupee au-dela ; 0 desactive
sftp.server_idchaineCraftFileGate_<version>la banniere SSH-2.0-<server_id>
sftp.inactivity_timeout_secsentier600connexion sans paquet coupee apres ce delai ; 0 jamais ; au plus 86400
sftp.keepalive_interval_secsentier0keepalive SSH apres ce silence du client ; 0 aucun ; au plus 86400
sftp.keepalive_maxentier3keepalives sans reponse avant la coupure ; 1 a 100

[sftp.algorithms]

CleTypeDefautEffet
sftp.algorithmstable-les algorithmes SSH : +nom ajoute au defaut, -nom retire, une liste sans prefixe remplace
sftp.algorithms.kexlistemlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group16-sha512, diffie-hellman-group14-sha256echange de cles
sftp.algorithms.cipherslistechacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com, aes256-ctr, aes192-ctr, aes128-ctrchiffrements
sftp.algorithms.macslistehmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512, hmac-sha2-256MAC
sftp.algorithms.host_keylistessh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, rsa-sha2-512, rsa-sha2-256signatures de la cle d’hote
sftp.algorithms.user_keylistessh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com, rsa-sha2-512, rsa-sha2-256signatures permises a une cle d’utilisateur ; ssh-rsa (SHA-1) hors defaut

[sftp.ban]

Voir Bans et limites de debit.

CleTypeDefautEffet
sftp.bantableabsent : aucun banban des adresses apres des echecs d’authentification sur la porte SFTP
sftp.ban.max_failuresentier5echecs dans la fenetre avant le ban ; au moins 1
sftp.ban.ban_duration_secsentier600duree d’un ban en secondes, a partir du verdict
sftp.ban.window_secsentier300fenetre de comptage des echecs en secondes, fixe, ouverte par le premier echec d’une serie
sftp.ban.whitelist_ipsliste[]adresses et reseaux CIDR, IPv4 ou IPv6, jamais bannis par cette instance
sftp.ban.trusted_proxiesliste[]sans effet sur SSH
sftp.ban.persist_filecheminabsent : en memoirefichier ou les bans sont gardes et partages entre instances
sftp.ban.backendchaine"file""file", ou "configmap" pour partager les bans entre pods Kubernetes
sftp.ban.ban_configmap_namechainecraft-file-gate-bansle ConfigMap des bans, avec backend = "configmap"
sftp.ban.reread_interval_secsentier5relecture du persist_file partage en secondes, en plus de la surveillance ; 1 a 30

[sftp.rate_limit]

CleTypeDefautEffet
sftp.rate_limittableabsent : aucune limiteseau a jetons par adresse, a l’acceptation d’une connexion
sftp.rate_limit.connections_per_minuteentier(obligatoire)debit soutenu de connexions par adresse ; au moins 1
sftp.rate_limit.burstentierconnections_per_minuteconnexions acceptees d’affilee ; au moins 1

Reference : [auth], utilisateurs et roles

⟳ : rechargee a chaud ; sans marque : prise au redemarrage. Voir Rechargement.

[auth]

Voir Authentification, Verification des mots de passe.

CleTypeDefautEffet
authtable(obligatoire)l’authentification et les sources de roles
auth.jwt_sentinel_usernamechainejwtle nom SSH qui demande une authentification par JWT (le jeton en mot de passe)
auth.timeout_secsentier5delai d’un appel au service d’autorisation
auth.authz_base_urlURLabsentservice qui traduit des authorities en noms de roles (POST /authz/resolve) ; sans lui, seules les authorities qui sont des noms de roles comptent
auth.roles_filecheminroles.toml a cote, s’il existefichier des [[roles]] et [[backends]] ; son contenu est relu a chaud
auth.users_filecheminusers.toml a cote, s’il existefichier des [[users]] ; son contenu est relu a chaud
auth.hash_workersentierles CPU vus (quota cgroup compris), au plus 2fils qui verifient les mots de passe, simultanement ; chacun tient le m argon2 de son hash ; 1 a 1024 ; CRAFT_FILE_GATE_HASH_WORKERS
auth.hash_queueentier1024verifications qui peuvent attendre un fil ; 1 a 65536 ; CRAFT_FILE_GATE_HASH_QUEUE
auth.hash_per_addressentier4verifications en vol ou en file pour une adresse ; 1 a 66560 ; sans plafond pour une adresse de whitelist_ips de la liste de bans de la porte
auth.users ⟳tableau de tables-[[auth.users]], comme [[users]] ; la racine l’emporte
auth.roles ⟳tableau de tables-[[auth.roles]], comme [[roles]] ; la racine l’emporte
auth.backends ⟳tableau de tables-[[auth.backends]], comme [[backends]] ; la racine l’emporte

[auth.methods]

CleTypeDefautEffet
auth.methodstable-les sources d’identite actives
auth.methods.jwttable-la methode JWT
auth.methods.jwt.enabledbooleentrueaccepter les JWT (SFTP sous le nom sentinel, REST en Bearer) ; exige [auth.jwt]
auth.methods.localtable-le magasin d’utilisateurs local
auth.methods.local.enabledbooleenfalseactiver le magasin local ([[users]] ou users_file)
auth.methods.local.passwordbooleentrueaccepter le mot de passe comme preuve
auth.methods.local.pubkeybooleentrueaccepter la cle publique SSH comme preuve
auth.methods.local.allow_sha512_cryptbooleenfalseaccepter aussi les hashes sha512-crypt, $6$ (migration)
auth.methods.local.allow_bcryptbooleenfalseaccepter aussi les hashes bcrypt, $2a$, $2b$, $2y$ (migration)
auth.methods.local.users_filecheminabsentfichier des utilisateurs, comme auth.users_file
auth.methods.local.max_argon2_memory_kibentierabsent : rien n’est verifiebudget memoire d’une verification, en Kio ; un hash qui le depasse est nomme au chargement ; au moins 1

[auth.jwt]

CleTypeDefautEffet
auth.jwttableabsente : aucun JWT verifiela verification des JWT : une source de cle, et les claims
auth.jwt.secretchaineabsentsecret HMAC (HS256/384/512) ; CRAFT_FILE_GATE_JWT_SECRET ou _FILE
auth.jwt.public_key_filecheminabsentcle publique PEM (RS*, ES*)
auth.jwt.jwks_urlURLabsentpoint JWKS du fournisseur d’identite ; exclusif de secret et public_key_file
auth.jwt.jwks_refresh_interval_secsentier3600periode du rafraichissement JWKS ; au moins 1
auth.jwt.algorithmchaineHS256HS256, HS384, HS512, RS256, RS384, RS512, ES256 ou ES384
auth.jwt.username_pathchaine/subJSON Pointer du nom de l’utilisateur dans le jeton
auth.jwt.authorities_pathchaine/groupsJSON Pointer de ses authorities (roles)
auth.jwt.issuerchaineabsentsi present, iss est exige et compare
auth.jwt.audiencechaineabsentsi present, aud est exige et compare

[[users]]

CleTypeDefautEffet
users ⟳tableau de tables-les utilisateurs locaux ; aussi dans users_file
users[].username ⟳chaine(obligatoire)le nom ; unique
users[].password_hash ⟳chaine(obligatoire)hash argon2 (craft-file-gate hash-password) ; jamais en clair
users[].authorized_keys ⟳liste[]cles publiques SSH, une ligne authorized_keys chacune
users[].authorities ⟳liste[]les noms des roles de l’utilisateur

[[roles]]

Voir Utilisateurs, roles et montages.

CleTypeDefautEffet
roles ⟳tableau de tables(obligatoire), ici ou dans roles_fileles roles ; aussi dans roles_file
roles[].name ⟳chaine(obligatoire)le nom du role, unique ; c’est lui que citent les authorities
roles[].user_key_algorithms ⟳liste[]algorithmes de signature de cle SSH permis en plus aux utilisateurs locaux de ce role, par leur nom (voir Porte SFTP)
roles[].mounts ⟳tableau de tables(obligatoire)les montages du role, [[roles.mounts]] ; au moins un

[[roles.mounts]]

CleTypeDefautEffet
roles[].mounts[].backend ⟳chaine(obligatoire)le backend monte, par le name d’un [[backends]]
roles[].mounts[].mount_path ⟳chemin/ou le montage apparait pour l’utilisateur : absolu, sans ., .., // ni / final
roles[].mounts[].home_dir ⟳chemin/ou le montage commence sur le stockage ; {username} y devient le nom de l’utilisateur
roles[].mounts[].create_home ⟳booleenfalsecreer home_dir a la connexion s’il manque ; backend local seulement, voir Local
roles[].mounts[].max_file_mb ⟳entierabsent : aucun plafondtaille maximale d’un fichier envoye, en Mo (1 048 576 octets) ; 0 : aucun envoi
roles[].mounts[].acl ⟳tableau de tables[] : tout refuseles droits du montage, [[roles.mounts.acl]], voir ACL
roles[].mounts[].hidden_stores ⟳tablecelle du backendecritures atomiques de ce montage, cle par cle (voir Uploads)
roles[].mounts[].hidden_stores.enabled ⟳booleencelui du backendvoir server.hidden_stores.enabled
roles[].mounts[].hidden_stores.prefix ⟳chainecelui du backendvoir server.hidden_stores.prefix
roles[].mounts[].hidden_stores.extension ⟳chainecelle du backendvoir server.hidden_stores.extension

[[roles.mounts.acl]]

CleTypeDefautEffet
roles[].mounts[].acl[].path ⟳chemin(obligatoire)chemin gouverne, relatif au montage
roles[].mounts[].acl[].rights ⟳liste(obligatoire)parmi read, write, list, delete, rename
roles[].mounts[].acl[].recursive ⟳booleenfalsetrue : l’entree gouverne aussi tout ce qui est sous path

Reference : [[backends]]

⟳ : rechargee a chaud ; sans marque : prise au redemarrage. Voir Rechargement.

[[backends]] : tous les types

Voir Les backends.

CleTypeDefautEffet
backends ⟳tableau de tables-les stockages ; aussi dans roles_file
backends[].name ⟳chaine(obligatoire)le nom que citent les montages ; unique
backends[].type ⟳chaine(obligatoire)local, sftp, s3 ou webhdfs, parmi ceux du binaire
backends[].stale_partials ⟳tablecelle de [uploads.stale_partials]balayage des restes d’uploads interrompus de ce backend
backends[].stale_partials.age_check ⟳booleencelui de [uploads.stale_partials]voir uploads.stale_partials.age_check
backends[].stale_partials.grace_secs ⟳entiercelui de [uploads.stale_partials]voir uploads.stale_partials.grace_secs
backends[].hidden_stores ⟳tablecelle de [server.hidden_stores]ecritures atomiques de ce backend, cle par cle ; sans objet sur S3 et WebHDFS
backends[].hidden_stores.enabled ⟳booleencelui de [server.hidden_stores]voir server.hidden_stores.enabled
backends[].hidden_stores.prefix ⟳chainecelui de [server.hidden_stores]voir server.hidden_stores.prefix
backends[].hidden_stores.extension ⟳chainecelle de [server.hidden_stores]voir server.hidden_stores.extension
backends[].refuse_upload_over_directory ⟳booleenfalseS3 : refuser un envoi sur une cle qui est aussi un repertoire ; sans objet ailleurs, ou un envoi ne remplace jamais un repertoire
backends[].cross_instance_reservation ⟳booleentrue (false en local sous Windows)poser un verrou sur le stockage pendant un envoi, pour les autres instances ; false convient a une instance seule
backends[].lock_prefix ⟳chaine.craftfilegate-upload.le debut du nom de ces verrous ; 8 a 64 octets, sans /
backends[].case_insensitive ⟳booleensonde (local), false (ailleurs)comparer les chemins de l’ACL sans la casse ni la normalisation Unicode ; absente sur WebHDFS

[[backends]] type = “local”

Voir Local.

CleTypeDefautEffet
backends[].root ⟳chemin(obligatoire)local : le repertoire racine
backends[].follow_symlinks ⟳booleenfalselocal : suivre les liens symboliques qui restent sous root + home_dir du montage

[[backends]] type = “sftp”

Voir Proxy SFTP.

CleTypeDefautEffet
backends[].host ⟳chaine(obligatoire)proxy SFTP : l’amont
backends[].port ⟳entier22proxy SFTP : son port ; 1 a 65535
backends[].host_key_fingerprint ⟳chaine(obligatoire, sauf accept_any_host_key)proxy SFTP : l’empreinte de la cle d’hote de l’amont, SHA256:<base64> (comme ssh-keygen -lf) ou SHA512:<base64>
backends[].accept_any_host_key ⟳booleenfalseproxy SFTP : true sans empreinte, toute cle d’hote est acceptee ; pour un amont jetable (tests, maquettes) seulement
backends[].auth ⟳table(obligatoire)proxy SFTP, WebHDFS : le compte de service
backends[].auth.type ⟳chaine(obligatoire)proxy SFTP : password ou private_key ; WebHDFS : basic
backends[].auth.username ⟳chaine(obligatoire)le compte de service ; WebHDFS : sans : ni caractere de controle
backends[].auth.password ⟳chaine-proxy SFTP, type = "password" : son mot de passe
backends[].auth.private_key_pem ⟳chaine-proxy SFTP, type = "private_key" : sa cle privee PEM

[[backends]] type = “s3”

Voir S3 et compatibles.

CleTypeDefautEffet
backends[].bucket ⟳chaine(obligatoire)S3 : le bucket
backends[].region ⟳chaine(obligatoire)S3 : la region
backends[].prefix ⟳chaine""S3 : le debut de toute cle
backends[].endpoint_url ⟳URLAWS S3S3 : le point d’acces d’un service compatible (MinIO, Garage…)
backends[].credentials ⟳table(obligatoire)S3 : { type = "iam_role" } ou { type = "static", ... }
backends[].credentials.type ⟳chaine(obligatoire)S3 : static (une paire de cles) ou iam_role (la chaine de l’environnement)
backends[].credentials.access_key_id ⟳chaine-S3, static : la cle d’acces
backends[].credentials.secret_access_key ⟳chaine-S3, static : son secret

[[backends]] type = “webhdfs”

Voir WebHDFS (Knox). auth, auth.type et auth.username : section du proxy SFTP ci-dessus.

CleTypeDefautEffet
backends[].url ⟳URL(obligatoire)WebHDFS : la base de la passerelle Knox, https://<knox>:<port>/gateway/<topologie> ; le backend y ajoute /webhdfs/v1 ; sans identifiants, requete ni fragment
backends[].auth.password_file ⟳chemin(obligatoire)WebHDFS : fichier du mot de passe du compte de service, relu a chaque rechargement du fichier qui definit le backend ; une ancre de confiance
backends[].ca_bundle ⟳cheminmagasin du systemeWebHDFS : les racines PEM qui authentifient url, seules ; une ancre de confiance
backends[].impersonate ⟳booleentrueWebHDFS : doAs=<utilisateur> sur chaque requete ; false : tout part sous le compte de service
backends[].read_ahead_bytes ⟳entier4194304WebHDFS : taille d’une plage lue ; 65536 a 67108864
backends[].timeout_secs ⟳entier30WebHDFS : delai d’une requete en secondes, corps compris ; au moins 1

Reference : [admin] et [api]

⟳ : rechargee a chaud ; sans marque : prise au redemarrage. Voir Rechargement.

[admin]

Voir Console et API d’administration.

CleTypeDefautEffet
admintableabsent : aucune porte HTTPl’ecouteur d’administration, de l’API et de l’explorateur
admin.listenadresse(obligatoire)adresse d’ecoute ; 0.0.0.0 dans un conteneur ; CRAFT_FILE_GATE_ADMIN_LISTEN
admin.control_listenadresseabsentune porte a part pour /admin, la console, /metrics et les sondes (sauf avec server.probes_listen) ; CRAFT_FILE_GATE_ADMIN_CONTROL_LISTEN
admin.bearer_tokenchaineabsentjeton qui a toutes les permissions ; lui ou un role de [[admin.roles]] est exige ; CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN, _FILE
admin.allow_static_tokenbooleentruefalse : aucun jeton statique, d’aucune source
admin.header_read_timeout_secsentier10secondes pour envoyer les en-tetes d’une requete ; 1 a 300 ; CRAFT_FILE_GATE_ADMIN_HEADER_READ_TIMEOUT_SECS
admin.long_request_threshold_secs ⟳entier30secondes apres lesquelles un transfert REST en cours apparait dans les sessions de la console ; 0 : tous

[[admin.roles]]

Voir Console et API d’administration.

CleTypeDefautEffet
admin.rolestableau de tables[]les roles admin nommes
admin.roles[].namechaine(obligatoire)le nom qu’une authority porte (compte local ou JWT) ; unique, distinct des noms de [[roles]]
admin.roles[].permissionsliste(obligatoire, non vide)parmi overview, sessions, kick, bans, unban, config, logs, audit, revoke

[admin.session]

Voir Console et API d’administration.

CleTypeDefautEffet
admin.sessiontablecle par processus, 1 h, 12 h, revocations en memoireles jetons de session de la console, signes par le serveur, et leurs revocations
admin.session.key_filecheminabsentla cle de signature, 32 octets au moins (head -c 32 /dev/urandom), lisible par le serveur seul, la meme sur toutes les instances ; sans elle ni secret_name, une cle tiree pour le processus
admin.session.secret_namechaineabsentle Secret Kubernetes dont l’entree admin-session.key tient la cle ; le chart le cree vide, le premier pod l’ecrit, les autres la lisent (feature k8s) ; CRAFT_FILE_GATE_ADMIN_SESSION_SECRET_NAME ; pas avec key_file
admin.session.ttl_secsentier3600vie d’un jeton ; 60 a 86400
admin.session.max_age_secsentier43200plus de renouvellement au-dela, depuis la connexion ; 60 a 604800, au moins ttl_secs
admin.session.persist_filecheminabsentle fichier ou les revocations sont partagees, a elles seules, distinct des fichiers de bans ; ni lui ni backend : en memoire, une instance, perdues au redemarrage
admin.session.backendchainefile avec persist_file, memoire sinonfile (avec persist_file) ou configmap (feature k8s), une ConfigMap comme les bans ; CRAFT_FILE_GATE_ADMIN_SESSION_BACKEND
admin.session.revocation_configmap_namechainecraft-file-gate-accessla ConfigMap des revocations, cle revocations, avec backend = "configmap" ; la meme que les acces temporaires ; CRAFT_FILE_GATE_ADMIN_SESSION_REVOCATION_CONFIGMAP_NAME
admin.session.reread_interval_secsentier5relecture des revocations partagees, en secondes ; 1 a 30

[admin.grants]

Les acces temporaires : un role de fichiers donne a un utilisateur jusqu’a une date, par POST /admin/grants (permission grant).

CleTypeDefautEffet
admin.grantstableen memoire, 7 jours au plusou les acces temporaires sont partages, et le plus long
admin.grants.persist_filecheminabsentle fichier ou les acces sont partages, a eux seuls (ni bans, ni revocations) ; ni lui ni backend : en memoire, une instance, refuse avec [cluster]
admin.grants.backendchainefile avec persist_file, memoire sinonfile ou configmap (feature k8s) ; CRAFT_FILE_GATE_ADMIN_GRANTS_BACKEND
admin.grants.grant_configmap_namechainecraft-file-gate-accessla ConfigMap, cle grants, a cote des revocations ; CRAFT_FILE_GATE_ADMIN_GRANTS_GRANT_CONFIGMAP_NAME
admin.grants.reread_interval_secsentier5relecture des acces partages, en secondes ; 1 a 30
admin.grants.max_duration_secsentier604800duree maximale d’un acces ; 60 a 2592000

[admin.tls]

CleTypeDefautEffet
admin.tlstableabsent : HTTPHTTPS sur tout l’ecouteur
admin.tls.cert_filecheminabsentcertificat PEM, relu quand le fichier change ; CRAFT_FILE_GATE_ADMIN_TLS_CERT
admin.tls.key_filecheminabsentcle privee PEM ; CRAFT_FILE_GATE_ADMIN_TLS_KEY
admin.tls.auto_generatebooleenfalsegenerer un certificat auto-signe (essais)
admin.tls.auto_generate_cnchainelocalhostson Common Name
admin.tls.auto_generate_dirchemin.ou l’ecrire
admin.tls.auto_generate_validity_daysentier31sa validite en jours

[admin.ban]

Memes cles que [sftp.ban]. Voir Bans et limites de debit.

CleTypeDefautEffet
admin.bantableabsent : aucun banban des adresses apres des echecs d’authentification sur l’API d’administration ; l’API de fichiers a sa liste, [api.ban]
admin.ban.max_failuresentier5echecs dans la fenetre avant le ban ; au moins 1
admin.ban.ban_duration_secsentier600duree d’un ban en secondes, a partir du verdict
admin.ban.window_secsentier300fenetre de comptage des echecs en secondes, fixe, ouverte par le premier echec d’une serie
admin.ban.whitelist_ipsliste[]adresses et reseaux CIDR, IPv4 ou IPv6, jamais bannis par cette instance
admin.ban.trusted_proxiesliste[]proxys dont X-Forwarded-For donne l’adresse du client ; sans control_listen, egale a api.ban.trusted_proxies
admin.ban.persist_filecheminabsent : en memoirefichier ou les bans sont gardes et partages entre instances
admin.ban.backendchaine"file""file", ou "configmap" pour partager les bans entre pods Kubernetes
admin.ban.ban_configmap_namechainecraft-file-gate-bansle ConfigMap des bans, avec backend = "configmap"
admin.ban.reread_interval_secsentier5relecture du persist_file partage en secondes, en plus de la surveillance ; 1 a 30

[admin.metrics_thresholds]

Voir Metriques.

CleTypeDefautEffet
admin.metrics_thresholdstable-seuils d’alerte de l’onglet Metrics de la console
admin.metrics_thresholds.hash_pool_percententier80places prises du pool de hachage, en %
admin.metrics_thresholds.tls_cert_daysentier14jours restants du certificat TLS admin, en dessous
admin.metrics_thresholds.rejections_per_minuteentier60refus SFTP par minute
admin.metrics_thresholds.jwt_refusals_per_minuteentier60JWT refuses par minute
admin.metrics_thresholds.jwks_age_secsentier2 x jwks_refresh_interval_secsage du cache JWKS
admin.metrics_thresholds.clock_skew_secsentier30ecart d’horloge d’un stockage
admin.metrics_thresholds.cpu_percententier90CPU du processus, 100 = un coeur

[api]

Voir Portes.

CleTypeDefautEffet
apitableabsent : pas d’APIl’API REST de fichiers, sur admin.listen
api.enabledbooleenfalseservir l’API, sur l’ecouteur de [admin] ; exige [admin]
api.prefixchaine/api/v1/fileschemin sous lequel l’API est servie
api.cors_originslisteabsent : pas de CORSorigines admises en CORS ; "*" : toutes (voir Bans)
api.openapibooleenfalseservir le Swagger UI (/api/docs) et le document OpenAPI (/api/openapi.json), sans authentification ; exige api.enabled

[api.ban]

Memes cles que [sftp.ban]. Voir Bans et limites de debit.

CleTypeDefautEffet
api.bantableabsent : aucun banban des adresses apres des echecs d’authentification sur l’API de fichiers, ses tickets et l’explorateur
api.ban.max_failuresentier5echecs dans la fenetre avant le ban ; au moins 1
api.ban.ban_duration_secsentier600duree d’un ban en secondes, a partir du verdict
api.ban.window_secsentier300fenetre de comptage des echecs en secondes, fixe, ouverte par le premier echec d’une serie
api.ban.whitelist_ipsliste[]adresses et reseaux CIDR, IPv4 ou IPv6, jamais bannis par cette instance, ni plafonnes dans la file de hachage
api.ban.trusted_proxiesliste[]proxys dont X-Forwarded-For donne l’adresse du client (ban, limiteur, audit) ; sans admin.control_listen, egale a admin.ban.trusted_proxies
api.ban.persist_filecheminabsent : en memoirefichier ou les bans sont gardes et partages entre instances
api.ban.backendchaine"file""file", ou "configmap" pour partager les bans entre pods Kubernetes
api.ban.ban_configmap_namechainecraft-file-gate-bansle ConfigMap des bans, avec backend = "configmap"
api.ban.reread_interval_secsentier5relecture du persist_file partage en secondes, en plus de la surveillance ; 1 a 30

[api.rate_limit]

Voir Bans et limites de debit.

CleTypeDefautEffet
api.rate_limittableabsent : aucune limiteseau a jetons par adresse pour l’API
api.rate_limit.requests_per_minuteentier(obligatoire)debit soutenu de requetes par adresse ; au moins 1
api.rate_limit.burstentierrequests_per_minuterequetes acceptees d’affilee ; au moins 1

[api.ui]

Voir Explorateur de fichiers.

CleTypeDefautEffet
api.uitable-l’explorateur de fichiers web
api.ui.enabledbooleenfalseservir l’explorateur ; exige [api]
api.ui.pathchaine/filesou la page est servie, sur l’ecouteur de l’API (admin.listen, meme avec control_listen) ; commence par /, sans / final ; ni sur api.prefix, ni sur une route du serveur (/admin, /ui, /metrics, /health, /livez, /readyz, /api/docs, /api/openapi.json)

Reference : [log], [telemetry] et le reste

⟳ : rechargee a chaud ; sans marque : prise au redemarrage. Voir Rechargement.

[log]

Voir Journaux, Audit.

CleTypeDefautEffet
logtable-les journaux et la piste d’audit
log.level ⟳chaineinfotrace, debug, info, warn, error ou off ; CRAFT_FILE_GATE_LOG_LEVEL (ou RUST_LOG), s’il est defini, l’emporte, au rechargement aussi ; voir Niveau de log
log.formatchainejsonjson (une ligne JSON par evenement) ou pretty (lisible)
log.audit ⟳chaineallvolume de la piste : all, changes ou failures ; refus et erreurs toujours ecrits
log.dircheminabsent : stdout seulrepertoire des fichiers de log et d’audit, tournes et compresses ; CRAFT_FILE_GATE_LOG_DIR
log.stdoutbooleentrueecrire aussi sur stdout ; false exige dir
log.max_file_size_mbentier100rotation avant cette taille, en Mio ; 1 a 1048576
log.retention_daysentier7jours de conservation des archives du journal applicatif ; 1 a 36500
log.audit_retention_daysentier90jours de conservation des archives de la piste d’audit ; 1 a 36500
log.refusal_summary_threshold ⟳entier10refus identiques ecrits par fenetre avant une ligne resumee ; 1 a 1000000
log.refusal_summary_window_secs ⟳entier60duree de cette fenetre ; 1 a 86400
log.refusal_summary_max_addresses ⟳entier10000refus distincts suivis a la fois ; 1 a 1000000

[telemetry]

Voir Telemetrie.

CleTypeDefautEffet
telemetrytableabsentl’export OpenTelemetry (OTLP)
telemetry.enabledbooleenfalseexporter les traces
telemetry.otlp_endpointURLhttp://localhost:4317le collecteur OTLP, URL http:// ou https://
telemetry.service_namechainecraft-file-gatel’attribut service.name ; service.version est la version du binaire
telemetry.protocolchainegrpcgrpc (OTLP/gRPC, port 4317) ou http (OTLP/HTTP protobuf, port 4318)
telemetry.metricsbooleenfalseexporter aussi les metriques en OTLP, vers le meme collecteur
telemetry.metrics_interval_secsentier60periode de cet export, en secondes ; 1 a 3600
telemetry.on_exporter_errorchainerefuseun exporteur impossible a construire : refuse (demarrage refuse) ou warn (demarrage sans ce signal)

[uploads]

Voir Delais.

CleTypeDefautEffet
uploadstable-les delais des envois, sur toutes les portes
uploads.idle_timeout_secsentier30envoi abandonne apres ce delai sans octet ; 1 a 86400 ; CRAFT_FILE_GATE_UPLOAD_IDLE_TIMEOUT_SECS
uploads.min_rate_bytes_per_secentier0 : desactivedebit moyen minimal d’un envoi depuis son debut, exige passe la grace ; au plus 1073741824
uploads.min_rate_grace_secsentier60attente avant d’exiger ce debit ; 1 a 86400
uploads.takeover_idle_secs ⟳entier10un upload sans octet depuis ce delai est repris par un nouvel upload du meme compte vers le meme fichier, sur cette instance ; n’agit qu’en dessous de idle_timeout_secs (sinon WARN) ; 0 : jamais ; 0 a 86400 ; au rechargement, une valeur hors bornes garde celle en vigueur

[uploads.stale_partials]

Voir Uploads et ecritures atomiques.

CleTypeDefautEffet
uploads.stale_partialstable-balayage des restes d’envois interrompus
uploads.stale_partials.age_checkbooleentruemesurer l’age sur l’horloge du stockage (un fichier sonde du meme repertoire), pas sur celle du serveur
uploads.stale_partials.grace_secsentier2 x idle_timeout_secs + max(300, idle_timeout_secs), soit 360 sage a partir duquel un reste est supprime ; plus de 2 x idle_timeout_secs + 60, au plus 2592000 (30 jours)

[tcp_keepalive]

CleTypeDefautEffet
tcp_keepalivetable-keepalive TCP de toute connexion acceptee
tcp_keepalive.enabledbooleentrueactiver SO_KEEPALIVE
tcp_keepalive.idle_secsentier30silence avant la premiere sonde ; 1 a 32767
tcp_keepalive.interval_secsentier10ecart entre deux sondes ; 1 a 32767
tcp_keepalive.countentier3sondes sans reponse avant la coupure ; 1 a 127
tcp_keepalive.user_timeout_secsentier0 : derive de idle_timeout_secsLinux : duree pendant laquelle des octets envoyes peuvent rester sans acquittement, puis coupure ; 5 a 86400

[reload]

CleTypeDefautEffet
reloadtable-comment un fichier modifie est remarque
reload.watchchaineautoauto : inotify, et la relecture periodique si inotify ne peut pas servir ; inotify : inotify obligatoire ; poll : relecture periodique seulement, aucune instance inotify
reload.poll_interval_secsentier5periode de la relecture periodique, en secondes ; 1 a 60

[security]

Voir Fichiers de confiance et TLS.

CleTypeDefautEffet
securitytable-le jugement des fichiers de confiance
security.allow_group_writable_trust_anchorsbooleenfalseun fichier de confiance inscriptible par le groupe du serveur : WARN au lieu d’un refus ; jamais le fichier de configuration

Reference : variables d’environnement

Les variables CRAFT_FILE_GATE_* remplacent une cle du fichier, apres sa lecture. Elles servent surtout en conteneur.

Celles qui remplacent une cle

VariableCle remplaceeValeur
CRAFT_FILE_GATE_LISTENsftp.listenip:port ; avec [sftp]
CRAFT_FILE_GATE_PROBES_LISTENserver.probes_listenip:port
CRAFT_FILE_GATE_ADMIN_LISTENadmin.listenip:port
CRAFT_FILE_GATE_ADMIN_CONTROL_LISTENadmin.control_listenip:port
CRAFT_FILE_GATE_ADMIN_HEADER_READ_TIMEOUT_SECSadmin.header_read_timeout_secsentier, 1 a 300
CRAFT_FILE_GATE_ADMIN_BEARER_TOKENadmin.bearer_tokenle jeton
CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN_FILEadmin.bearer_tokenun fichier qui contient le jeton
CRAFT_FILE_GATE_ADMIN_TLS_CERTadmin.tls.cert_filechemin ; cree [admin.tls] s’il manque
CRAFT_FILE_GATE_ADMIN_TLS_KEYadmin.tls.key_filechemin ; cree [admin.tls] s’il manque
CRAFT_FILE_GATE_ADMIN_SESSION_SECRET_NAMEadmin.session.secret_namele nom du Secret ; le chart la pose
CRAFT_FILE_GATE_ADMIN_SESSION_BACKENDadmin.session.backendfile ou configmap ; le chart la pose a cote du Secret partage
CRAFT_FILE_GATE_ADMIN_SESSION_REVOCATION_CONFIGMAP_NAMEadmin.session.revocation_configmap_namele nom de la ConfigMap des revocations ; le chart la pose
CRAFT_FILE_GATE_ADMIN_GRANTS_BACKENDadmin.grants.backendfile ou configmap ; le chart la pose
CRAFT_FILE_GATE_ADMIN_GRANTS_GRANT_CONFIGMAP_NAMEadmin.grants.grant_configmap_namele nom de la ConfigMap des acces temporaires ; le chart la pose
CRAFT_FILE_GATE_SFTP_BAN_CONFIGMAP_NAMEsftp.ban.ban_configmap_namele nom de la ConfigMap des bans ; le chart la pose
CRAFT_FILE_GATE_API_BAN_CONFIGMAP_NAMEapi.ban.ban_configmap_namede meme
CRAFT_FILE_GATE_ADMIN_BAN_CONFIGMAP_NAMEadmin.ban.ban_configmap_namede meme
CRAFT_FILE_GATE_CLUSTER_LISTENcluster.listenip:port ; cree [cluster] s’il manque ; le chart la pose
CRAFT_FILE_GATE_CLUSTER_PEERScluster.peersdns:<nom>:<port>, ou des <hote>:<port> separes par des virgules ; le chart la pose
CRAFT_FILE_GATE_CLUSTER_SECRET_NAMEcluster.secret_namele nom du Secret ; le chart la pose
CRAFT_FILE_GATE_CLUSTER_MIN_PEERScluster.min_peersentier ; le chart la pose (cluster.minPeers)
CRAFT_FILE_GATE_CLUSTER_UNREADY_WHENcluster.unready_whendetecteurs separes par des virgules ; le chart la pose avec cluster.unreadyWhen
CRAFT_FILE_GATE_JWT_SECRETauth.jwt.secretle secret HMAC
CRAFT_FILE_GATE_JWT_SECRET_FILEauth.jwt.secretun fichier qui contient le secret
CRAFT_FILE_GATE_HASH_WORKERSauth.hash_workersentier, 1 a 1024
CRAFT_FILE_GATE_HASH_QUEUEauth.hash_queueentier, 1 a 65536
CRAFT_FILE_GATE_LOG_LEVELlog.leveltrace … off
CRAFT_FILE_GATE_LOG_DIRlog.dirchemin
CRAFT_FILE_GATE_UPLOAD_IDLE_TIMEOUT_SECSuploads.idle_timeout_secsentier, 1 a 86400

Une variable vaut pour la cle qu’elle remplace, la ou cette cle a un effet (CRAFT_FILE_GATE_ADMIN_* avec [admin], CRAFT_FILE_GATE_JWT_SECRET avec un algorithm HMAC). CRAFT_FILE_GATE_LOG_LEVEL posee, le fichier ne regle plus le niveau. La ligne de demarrage config override from env nomme chaque variable appliquee, sans jamais afficher un secret.

Les secrets : la forme _FILE

CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN_FILE et CRAFT_FILE_GATE_JWT_SECRET_FILE nomment un fichier lu une fois, au demarrage. Preferez-les : une variable reste lisible dans /proc/<pid>/environ.

RegleEffet
le fichierun fichier de confiance, en UTF-8, non vide ; une seule des deux formes X et X_FILE
un saut de ligne finalretire
fichier modifiepris au prochain redemarrage

Un Secret Kubernetes monte en lecture seule convient tel quel : voir Secrets.

Les autres variables lues

VariableEffet
RUST_LOGremplace le filtre de log ; gardez audit=info dedans, sinon la piste d’audit s’eteint (WARN au demarrage)
OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOLlues pour dire en WARN qu’elles contredisent [telemetry] protocol ; le fichier decide
SSL_CERT_FILE, SSL_CERT_DIRles racines TLS, si l’image n’en porte pas (:scratch)
TOKIO_WORKER_THREADSfils du runtime principal ; cite sur la ligne de demarrage
HOSTNAMEle pod_name des sessions dans l’API d’administration

Reference : vocabulaire des lignes d’audit

Chaque ligne d’audit est un evenement du target audit, une ligne JSON par evenement. Lire et filtrer la piste : Audit. Ce que veut dire une erreur et ou la corriger : Depannage.

Les actions

actionsourceEcrite quand
listsftp, apilistage d’un repertoire
downloadsftp, apifin du transfert
uploadsftp, apifin du transfert
mkdirsftp, apiune ligne par repertoire cree, du plus exterieur au plus interieur
deletesftp, apiSFTP REMOVE d’un fichier ; REST DELETE d’un fichier ou d’un repertoire vide
rmdirsftpRMDIR d’un repertoire vide
delete_recursiveapiDELETE d’un repertoire non vide
rmdir_recursivesftpRMDIR d’un repertoire non vide
renamesftp, apirenommage ou deplacement
statsftp, apirefus seulement ; REST HEAD
setstatsftprefus seulement
requestapirequete refusee avant le handler sans route pour sa methode, ou GET ?rights
connection_acceptedsftp, api, adminauthentification reussie : une par session SFTP, une par requete REST, une par fenetre admin de 15 min
connection_rejectedsftp, api, adminauthentification refusee
connection_rejected_summarysftp, api, adminrefus anonymes repetes, resumes a la fin de la fenetre
ip_bannedsftp, api, adminune adresse est bannie, une fois par bascule
session_endsftpune session SFTP quitte le registre
session_kickadminDELETE /admin/sessions/{id}
unbanadminDELETE /admin/bans/{protocol}/{ip}
logoutadminPOST /admin/logout sous un JWT ou le jeton statique : rien n’est revoque
session_revokeadminPOST /admin/revocations, POST /admin/logout sous un jeton de session : username le compte revoque, admin l’auteur
grant_add, grant_revokeadminPOST /admin/grants, DELETE /admin/grants/{id} : username le beneficiaire, role, grant_id, until, grant_reason (le motif), lift
audit_readadminlecture de la piste par la console ou son flux
get_status, get_config, get_resources, list_sessions, session_roles, logs_status, get_logs, events, stream_logs, list_bans, list_grants, grant_candidatesadminrefus insufficient permission d’une route en lecture

Les resultats

resultSens
successl’operation a abouti
deniedle serveur l’a refusee
errorle stockage l’a fait echouer, ou rien n’a pu etre construit pour elle, ou elle a ete interrompue
unknownle serveur ne sait pas ce qu’elle est devenue (commit interrompu)

Les champs

ChampLignesSens
sourcetoutesla porte : sftp, api, admin ; pour ip_banned, la liste du ban
action, resulttoutesci-dessus
reasondenied, error, unknown ; absent sur successune chaine fixe, ci-dessous
usernametoutes sauf les resumesle nom presente ; vide pour un jeton non verifie
remote_addrtoutesl’IP du client ; avec le port sur connection_accepted et session_end SFTP
session_idoperations SFTP, session_end, session_kickl’identifiant de la session
pathoperationsle chemin vu par l’utilisateur ; la source d’un rename
new_pathrenamela destination
backendoperationsle nom du backend du montage ; "" sur un chemin synthetique
countupload, download, listoctets transferes, ou entrees listees ; 0 ailleurs
replaceduploadyes, no, unknown : l’upload a-t-il detruit un contenu present
removeddelete_recursive, rmdir_recursiveentrees supprimees ; unknown sur un backend local
took_overuploadl’upload bloque du meme compte que celui-ci a repris : son session_id SFTP, ou son transfer_id REST ; "" ailleurs
transfer_idupload RESTl’identifiant du transfert, celui de la console ; "" ailleurs
grant_idoperationsl’acces temporaire par lequel est venu le montage du chemin (plusieurs : separes par des virgules) ; "" ailleurs
auth_methodconnection_*SFTP password, pubkey, jwt ; API basic, bearer, ticket ; admin jwt, static_token, session, password (connexion a la console) ; none sans credential
signature_algorithmconnection_accepted SFTPalgorithme de la signature de la cle
suppressed, threshold, window_secs, overflowconnection_rejected_summaryrefus non ecrits, et les reglages de la fenetre
ban_duration_secs, expires_at_epochip_bannedduree et fin du ban
duration_secs, bytes_read, bytes_writtensession_endduree et volume de la session
admin, admin_addr, admin_auth_methodactions adminl’auteur
kicked_bysession_kickl’auteur du kick
protocol, liftunbanla liste (sftp, api, admin) ; held, local_only, overruled, not_banned
liftsession_revokeheld, local_only
permissionrefus insufficient permissionla permission manquante
lines, levels, since, until, q, fieldsaudit_readla lecture faite ; lines = 0 pour un flux

Un champ hors propos est vide ("", 0), pas absent. Aucune ligne ne porte de mot de passe, de jeton ni le texte d’une erreur du stockage : ce texte va dans le journal applicatif, a cote.

Les reason

Operations sur fichiers :

reasonresultSens
acldeniedl’ACL ne donne pas le droit
acl subtreedeniedsuppression d’un arbre que l’ACL ne couvre pas en entier
synthetic pathdeniedecriture sur un repertoire synthetique ou un point de montage
rename across mountsdeniedsource et destination sous deux montages
invalid pathdeniedcaractere de controle, \, forme Windows interdite
reserved namedeniednom que le serveur se reserve (verrou d’upload)
rename into restricteddeniedrenommage qui deposerait du contenu sans write
range not satisfiabledeniedtelechargement REST dont le Range commence apres la fin du fichier (416)
symlink escapedeniedlien symbolique hors de la racine locale
existsdenieddestination existante, ou creation exclusive sur un nom pris
is a directory, not a directorydeniedREMOVE d’un repertoire, RMDIR d’un fichier
upload in progressdenied, errorun autre upload tient la destination
taken overerrorupload bloque repris par un nouvel upload du meme compte (uploads.takeover_idle_secs)
quota exceeded (et , truncated file removed, , append not undone)deniedmax_file_mb depasse
session killeddeniedsession coupee pendant l’operation
no roles, username not usable as home directorydeniedREST : aucun role, nom inutilisable
role resolution, mount conflicterrorREST : roles non resolus, montages en conflit
not found, permission denied, already exists, not a directory, is a directory, directory not empty, storage error, not implemented, unsupported, upload in progresserrorgenre de l’erreur du stockage
session ended: <cause>errorsession SFTP finie pendant l’operation ; <cause> est le reason du session_end
session endederrorclient REST parti pendant la requete
upload idle timeout, upload below minimum rateerrorupload coupe par le serveur
commit interruptedunknownclient parti pendant la publication d’un upload

Authentification (connection_rejected, connection_rejected_summary) :

reasonresultSens
absentdeniedmot de passe faux, nom inconnu, cle ou JWT SFTP refuse : rien ne dit quels comptes existent
no authorized key offereddeniedaucune cle proposee n’etait autorisee
signature algorithm not alloweddeniedalgorithme de signature de la cle non permis
missing credential, empty credential, malformed credentialdenieden-tete Authorization absent, vide, illisible
invalid token, expired token, wrong issuer, wrong audiencedeniedjeton refuse par le verificateur
verifier unavailabledenied, erroraucune cle en face du jeton, ou rien pour verifier
no username claimdeniedjeton verifie sans nom
method disableddeniedmethode d’authentification coupee
no matching roles, no matching admin rolesdeniedcredential acceptee, aucun role
role resolution, mount conflict, backend initialization failederrorcredential acceptee, session impossible a construire
username not usable as home directorydeniedle nom ne peut pas etre un repertoire
session limitdeniedmax_sessions_per_user atteint
banned, rate limitdeniedadresse bannie, debit depasse
password checks saturated, password checks saturated for address, shutting downerrormot de passe non verifie : pool plein, part de l’adresse atteinte, arret
invalid ticket, expired ticket, revoked ticketdeniedticket de telechargement refuse
revoked tokendeniedjeton de session de la console dont le compte a ete retire ou a change de mot de passe, ou revoque

Fin de session (session_end) : closed, connection_ended, admin_kick, shutdown_idle, shutdown, banned, internal_error, grant_expired, grant_revoked (un acces temporaire que la connexion a utilise a fini : grant_id le nomme ; un transfert REST coupe ainsi dit session ended: grant_expired ou grant_revoked).

Actions admin : insufficient permission, session not found, no sessions, not banned, overruled, unknown protocol, invalid IP address, no ban manager.

Migration depuis ProFTPD

Bascule d’un ProFTPD a utilisateurs virtuels (AuthUserFile, ftpd.passwd) vers CraftFileGate.

Le modele

ProFTPD met identite, home et droits dans une entree de ftpd.passwd et des blocs <Directory>. CraftFileGate les separe : users.toml dit qui vous etes (username, password_hash, authorized_keys, authorities), roles.toml ce que vous voyez ([[backends]], [[roles]] et leurs montages). Le home appartient au montage d’un role, pas a l’utilisateur : Utilisateurs, roles et montages.

Traduire une entree ftpd.passwd

alice:$6$rounds=5000$xyz...:1001:1001:Alice:/srv/ftp/alice:/bin/false
# users.toml
[[users]]
username = "alice"
password_hash = "$6$rounds=5000$xyz..."     # repris tel quel, voir plus bas
authorized_keys = ["ssh-ed25519 AAAA... alice@poste"]
authorities = ["clients"]
# roles.toml : un role pour tous les clients
[[backends]]
name = "ftp"
type = "local"
root = "/srv/ftp"

[[roles]]
name = "clients"

[[roles.mounts]]
backend = "ftp"
home_dir = "/{username}"     # DefaultRoot ~ : alice voit /srv/ftp/alice comme /
create_home = true           # CreateHome on
acl = [{ path = "/", rights = ["read", "write", "list", "delete", "rename"], recursive = true }]
Champ ProFTPDDevient
mot de passepassword_hash
uid, gidrien : tout est ecrit par le compte du service, sans setuid
homehome_dir du montage, {username} pour %u
shell, gecosrien : seul le sous-systeme sftp est servi

{username} n’accepte que A-Z a-z 0-9 . _ -, 64 caracteres au plus, sans point en tete ; un autre nom ne peut pas se connecter a ce montage.

Les mots de passe

[auth.methods]
local = { enabled = true, allow_sha512_crypt = true }   # ou allow_bcrypt

$6$ et bcrypt sont repris tels quels (Les hashes) ; DES et MD5 ($1$) demandent un nouveau mot de passe ou une cle publique. Reprendre les hashes est une etape de migration : sha512-crypt n’a pas de durete memoire. Regenerez-les en argon2id a la premiere occasion :

echo -n "mot-de-passe" | craft-file-gate hash-password --user alice   # un bloc [[users]]

Un compte qui n’entre que par cle garde un password_hash (le champ est obligatoire) : mettez-y le hash d’un secret aleatoire, ou coupez les mots de passe pour tous (local.password = false). Un fournisseur d’identite peut aussi remplacer users.toml : voir Authentification.

Correspondance des directives

ProFTPDCraftFileGate
AuthUserFileauth.methods.local.users_file
AuthGroupFileauthorities et [[roles]]
DefaultRoot ~home_dir = "/{username}"
CreateHome oncreate_home = true (backend local)
<Directory> et <Limit>[[roles.mounts.acl]], voir plus bas
MaxLoginAttempts[sftp.ban] max_failures : bannit l’adresse
MaxClientsPerUserserver.max_sessions_per_user
MaxClientsrien ; [sftp.rate_limit] borne le debit de connexions
HiddenStores on[server.hidden_stores] enabled = true ; desactive par defaut, comme ProFTPD
mod_quotatabmax_file_mb du montage : un plafond par fichier, pas un volume cumule
TransferLog, ExtendedLogla piste d’audit, voir Audit
TLSEngine (FTPS), PassivePorts, MasqueradeAddressrien : SFTP sur un seul port TCP
SFTPHostKeysftp.host_keys
SFTPAuthorizedUserKeysauthorized_keys de l’utilisateur
mod_sql, mod_ldap[auth.jwt] ou auth.authz_base_url
Umaskl’umask herite au lancement, pour les fichiers des clients (Fichiers de confiance)
<Anonymous>un compte dedie, avec un role en lecture

Avec hidden_stores, le fichier en cours s’appelle comme chez ProFTPD, avec un jeton : .in.rapport.csv.<jeton>. (Ecritures atomiques).

Traduire les blocs <Limit>

Commandes FTPDroit
RETR, READread
STOR, APPE, MKD, WRITEwrite
LIST, NLST, CWD, DIRSlist
DELE, RMDdelete
RNFR, RNTOrename

Il n’y a pas d’heritage : l’entree la plus specifique decide seule, et une sous-arborescence en lecture (<Limit WRITE> DenyAll) re-enonce tout ce qui y reste permis, { path = "/archive", rights = ["read", "list"], recursive = true }. Un chemin qu’aucune entree ne couvre est refuse. Voir ACL.

La bascule

SujetCraftFileGate
reprise d’upload (AllowStoreRestart)servie, sauf avec hidden_stores et sur S3
propriete des fichiersle compte du service ; une autre chaine passe par les droits du repertoire (setgid, ACL POSIX)
FTP et FTPSnon servis : chaque client passe a SFTP ou a l’API REST
  1. Inventorier les clients ; extraire de ftpd.passwd noms, hashes et homes.
  2. Ecrire roles.toml et users.toml ; lancer sur un autre port.
  3. Valider avec un compte temoin (chaque droit, un refus), distribuer les secrets.
  4. Couper ProFTPD, basculer le port, suivre la piste d’audit.