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
| Besoin | Reponse |
|---|---|
| Deposer et prendre des fichiers | SFTP, API REST de fichiers, explorateur web |
| Ranger les fichiers | disque local, serveur SFTP amont, S3 et compatibles, HDFS via Knox (lecture seule) |
| Authentifier | mot de passe, cle publique SSH, jeton JWT (secret, cle publique ou JWKS) |
| Donner des droits | des roles, qui montent des stockages et y donnent des droits par chemin ; refus par defaut |
| Surveiller | piste d’audit, journaux JSON, metriques Prometheus, traces OpenTelemetry |
| Administrer | API d’administration et console web : sessions, bans, configuration en vigueur |
| Deployer | un binaire statique, des images Docker, un chart Helm |
Les mots du guide
| Mot | Sens |
|---|---|
| backend | un stockage nomme, decrit dans [[backends]] |
| role | un ensemble de montages ; un utilisateur en recoit un ou plusieurs |
| montage | un backend vu par l’utilisateur a un chemin (mount_path), a partir d’un sous-repertoire (home_dir), avec son ACL |
| ACL | les droits (read, write, list, delete, rename) par chemin, dans un montage |
| porte | une 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
| Pour | Lire |
|---|---|
| donner a chacun ce qu’il doit voir | Utilisateurs, roles et montages, Recettes |
| brancher S3 ou un serveur SFTP amont | Les backends |
| brancher un fournisseur d’identite | Authentification |
| deployer en conteneur | Docker, Kubernetes |
| comprendre un refus | Depannage, Audit |
| connaitre une cle | Reference 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
| Montages | Ce 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 roles | Dans 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_stores | un 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
| Source | Comment | Les roles |
|---|---|---|
| utilisateur local | authorities de son entree [[users]] | des noms de [[roles]] |
| jeton JWT | la claim authorities_path (defaut /groups) | des noms de [[roles]], ou des authorities a traduire |
| service d’autorisation | authz_base_url traduit les authorities inconnues | des 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
| Quand | Ligne |
|---|---|
demarrage, role a {username} | INFO role gives each user their own home directory |
| demarrage, role qui monte plusieurs backends | INFO role mounts several backends: its users hold the credentials of all of them ..., champs role, backends, writable |
create_home cree un repertoire | INFO 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 :
| Operation | Droit exige |
|---|---|
envoi, mkdir | write sur le chemin, et sur chaque parent qu’il faut creer |
| telechargement | read |
| listage | list sur le repertoire liste |
stat (SFTP), HEAD (REST) | read ou list |
| suppression d’un fichier, d’un repertoire vide | delete |
| suppression d’un repertoire non vide | delete sur tout l’arbre |
| renommage | rename 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
- Refus par defaut : un chemin qu’aucune entree ne gouverne est refuse.
- L’entree la plus specifique decide : celle du chemin exact, sinon
l’entree
recursivela plus proche au-dessus. - 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
recursiverend 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.
| Operation | Sur un chemin synthetique |
|---|---|
| listage | les montages et les repertoires synthetiques qu’il contient |
stat | un repertoire, date du debut de la session |
?rights (REST) | ["list"] |
| toute autre operation | refusee |
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).
| Porte | Pour qui | Ecoute | Credential | Section |
|---|---|---|---|---|
| SFTP | clients SFTP (OpenSSH sftp, WinSCP, FileZilla, rclone), sshfs | [sftp] listen | mot de passe, cle SSH, JWT (nom sentinel) | [sftp] |
| API REST de fichiers | scripts HTTP, applications | [admin] listen, sous [api] prefix | Basic, Bearer (JWT), ticket | [api] |
| Explorateur web | utilisateurs dans un navigateur | [admin] listen, a [api.ui] path | celle de l’API, gardee en memoire | [api.ui] |
| Console et API d’administration | exploitants | [admin] listen, ou [admin] control_listen | compte 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
sftpest servi : ni shell, niscp, niexec, ni renvoi de ports ou d’agent. - Par JWT : le nom sentinel (
jwtpar 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
| Requete | Effet |
|---|---|
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>?mkdir | creation 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>?ticket | 201 {"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
| Ligne | Sens |
|---|---|
INFO SFTP server listening | la porte SFTP est ouverte |
INFO starting admin API server (HTTP) (ou (HTTPS)) | l’ecouteur admin est ouvert |
INFO every configured door started | toutes les portes configurees servent (champ doors) |
audit connection_accepted | une 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
| Type | type = | Le stockage | Ce que home_dir y designe | Ecritures |
|---|---|---|---|---|
| Local | "local" | un repertoire du serveur | un sous-repertoire de root | oui |
| Proxy SFTP | "sftp" | un serveur SFTP amont, sous un compte de service | un repertoire de l’amont | oui |
| S3 et compatibles | "s3" | un bucket AWS S3, MinIO, Garage, Scaleway… | un prefixe de cle, apres prefix | oui |
| WebHDFS (Knox) | "webhdfs" | HDFS a travers Apache Knox | un repertoire HDFS | lecture 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 prefixeP) ; - 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.
| Reglage | Un lien dans le montage | Un lien qui en sort |
|---|---|---|
follow_symlinks = false (defaut) | jamais suivi | jamais suivi |
follow_symlinks = true | suivi s’il reste sous root + home_dir | jamais suivi |
- Un lien se liste comme un lien (type
l;"is_symlink": trueen 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.
| Reglage | Comparaison des chemins de l’ACL |
|---|---|
case_insensitive = true | pliee |
case_insensitive = false | octet a octet, sans normalisation |
| cle absente | une 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
| Cle | Sur 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_stores | un fichier en cours de transfert a cote de la destination, publie par renommage |
stale_partials | le balayage de ces fichiers, sur l’horloge du systeme de fichiers |
cross_instance_reservation | un fichier verrou a cote de la destination ; false par defaut sous Windows : une instance par racine |
lock_prefix | le 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 surrename(2): un ecrasement concurrent n’y est pas detecte, et l’audit d’un envoi qui ecrase ditreplaced=unknown. - Aucun
fsync: un succes ne promet pas la durabilite sur disque. mkdircree 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
| Quand | Ligne |
|---|---|
| demarrage, sonde de la casse | INFO avec backend, acl_paths = folded ou exact |
create_home cree un repertoire | INFO created missing home directory, champ home_dir |
| demarrage, balayage des restes | INFO 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 :
| Etape | Requete a l’amont | replaced |
|---|---|---|
| 1 | SSH_FXP_RENAME du fichier en cours sur la destination ; reussi si elle est libre | no |
| 2 | destination prise : posix-rename@openssh.com, atomique, sur un second canal SFTP ouvert au premier ecrasement | yes |
| 3 | sans cette extension ni second canal : suppression de la destination, puis renommage | yes |
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
| Cle | Sur un proxy SFTP |
|---|---|
home_dir (montage) | un chemin absolu sur l’amont |
create_home (montage) | sans effet : home_dir existe sur l’amont |
hidden_stores | un fichier en cours de transfert sur l’amont, publie par renommage |
stale_partials | le 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_reservation | un fichier verrou chez l’amont, a cote de la destination ; true par defaut, Windows compris |
lock_prefix | le nom de ces verrous ; un autre prefixe, sans point en tete, convient a un amont qui refuse le nom par defaut |
case_insensitive | false 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
READen 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 :
statsuit un lien, un listage ne marque aucune entree comme lien.
Ce que vous verrez
| Quand | Ligne |
|---|---|
| ouverture d’une session | INFO SFTP proxy: connecting, champs host, port |
| cle d’hote verifiee | INFO SFTP proxy: host key verified, champs upstream, fingerprint |
| session prete | INFO 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 client | Sur le stockage |
|---|---|
| un fichier | une cle |
| un repertoire | un marqueur <cle>/, ou toute cle sous <cle>/ |
un mkdir | ecrit le marqueur |
| parents manquants d’un envoi | un marqueur par niveau, chacun juge par l’ACL comme un mkdir |
| un repertoire sans marqueur | taille 0, sans date |
Une cle et un repertoire du meme nom peuvent coexister, sauf avec
refuse_upload_over_directory = true.
| Operation | Requetes S3 |
|---|---|
| lecture SFTP | un HeadObject a l’ouverture, puis un GetObject avec Range par lecture |
| telechargement REST | un GetObject lu en flux |
| envoi de moins de 8 Mio | un PutObject avec If-None-Match: *, a la fin |
| envoi de 8 Mio ou plus | un upload multipart, une part de 8 Mio a la fois |
| renommage d’un fichier | CopyObject avec If-None-Match: *, puis DeleteObject de la source |
| listage | ListObjectsV2 avec le delimiteur /, page apres page |
| suppression d’un arbre | ListObjectsV2 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>/* :
| Action | Pour |
|---|---|
s3:ListBucket | lister, distinguer un repertoire, trouver les parents manquants, mkdir, renommer, supprimer un arbre |
s3:GetObject | telecharger, stat, et l’examen qui precede une suppression ou un renommage |
s3:PutObject | envoyer, mkdir, renommer, l’auto-test des ecritures conditionnelles |
s3:DeleteObject | supprimer, renommer, nettoyer la cle de l’auto-test |
s3:AbortMultipartUpload | annuler un upload multipart qui ne sera pas complete |
s3:ListBucketMultipartUploads | balayer les uploads multipart abandonnes |
s3:ListMultipartUploadParts | dater 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 :
| Quand | Annulation |
|---|---|
| un envoi finit sans completer | aussitot, quelle qu’en soit la cause |
| arret propre | ceux encore en vol, 5 s au plus |
| un processus tue | le 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
prefixa 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
| Cle | Sur 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_directory | true : 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_reservation | un objet marqueur <lock_prefix><nom>, pose avec If-None-Match: * |
lock_prefix | le nom de ces marqueurs |
stale_partials | le balayage des uploads multipart abandonnes |
case_insensitive | false par defaut : les cles sont sensibles a la casse |
hidden_stores | sans 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
| Quand | Ligne |
|---|---|
| demarrage | INFO S3 backend initialized, champs bucket, prefix, endpoint |
| demarrage, auto-test | INFO S3 upload precondition self-test, champs backend, verdict = honoured, probe_key |
| balayage | INFO aborted an abandoned multipart upload: ..., champs bucket, key, upload_id, age_secs |
| arret | INFO 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
| Cle | Sur 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 url | Le 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 direct | svc-sftp declare mandataire dans core-site.xml (ci-dessous) |
| HttpFS | les 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
| Operation | Requete WebHDFS |
|---|---|
stat, existence d’un parent | GETFILESTATUS, une requete par chemin |
| listage | LISTSTATUS_BATCH, page par page (startAfter) ; LISTSTATUS en une reponse sur un cluster plus ancien que 2.8 |
| telechargement, reprise, lecture a un offset | OPEN?offset=&length=, par plages |
envoi, mkdir, renommage, suppression | aucune : lecture seule |
- Lecture anticipee : 10 Mio lus par paquets SFTP de 32 Kio coutent 3
OPENavec 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
307de Knox est suivi une fois, vers la meme origine (schema, hote, port) queurlseulement. - Une entree
SYMLINKest 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_secscouvre une plage entiere : sur un lien lent, montez-le plutot que de baisserread_ahead_bytes.- Les petits fichiers coutent chacun un
GETFILESTATUSet unOPEN: 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.
| Section | Ce qu’elle regle | Page |
|---|---|---|
[server] | ce que toutes les portes partagent : arret, sessions par utilisateur, ecritures atomiques, port des sondes | Arret, Uploads, Console et API |
[sftp] | la porte SSH, ses algorithmes, ses bans | Porte SFTP, Bans |
[auth] | qui entre, et sous quel nom | Authentification |
[[users]], [[roles]] | les comptes locaux, les roles et leurs montages | Utilisateurs, roles et montages |
[[backends]] | les stockages | Les backends |
[uploads], [tcp_keepalive] | les delais | Delais, Uploads |
[log], [telemetry] | journaux, piste d’audit, traces | Journaux, Telemetrie |
[admin], [api] | la console, l’API d’administration, l’API de fichiers, leurs bans | Console et API, Bans |
[reload] | le rechargement a chaud | Rechargement |
[security] | le jugement des fichiers de confiance | Fichiers 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 ecrire | Effet |
|---|---|
[[users]], [[roles]], [[backends]] a la racine de config.toml | lus au demarrage |
les memes sous [auth] ([[auth.users]], …) | idem ; la racine l’emporte si les deux existent |
auth.users_file, auth.roles_file | relus a chaque modification du fichier |
| rien de tout cela | users.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.
| Commande | Effet |
|---|---|
craft-file-gate hash-password | lit 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 schema | verifier une configuration sans demarrer, ecrire son schema JSON : Verifier une configuration |
craft-file-gate healthcheck | interroge /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
| Quand | Ligne |
|---|---|
| un fichier declare est lu | INFO auth source loaded, champs kind (roles, users) et path |
| un fichier non declare est trouve a cote | INFO found next to the config and used; declare it explicitly to pin it |
| une variable remplace une cle | INFO 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
| Option | Effet |
|---|---|
<fichier> | la configuration ; ses users_file, roles_file et fichiers de confiance sont lus et juges comme au demarrage |
--format text | par defaut : une ligne par constat, <level> [<cle>] <message> (see <lien>) |
--format json | {"valid": ..., "findings": [{"level", "key", "message", "rule", "doc"}]} |
| Champ | Sens |
|---|---|
level | error : le demarrage refuserait ; warn : un WARN du demarrage ; info : une ligne du demarrage, ou une etape laissee au demarrage, not checked offline: ... |
key | la cle, ecrite comme dans la reference (roles[].mounts[].backend) |
rule | la regle de spec citee par le message |
doc | la section de la cle dans ce guide |
| Sortie | Sens |
|---|---|
0 | aucune erreur, des warn possibles |
1 | une erreur, la premiere, comme au demarrage |
2 | fichier 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
| Porte | Credential | Source |
|---|---|---|
| SFTP | mot de passe | compte local, ou jeton JWT sous le nom sentinel |
| SFTP | cle publique | compte local |
| API de fichiers | Authorization: Basic | compte local, mot de passe |
| API de fichiers | Authorization: Bearer | jeton JWT |
| Console et API d’administration | Authorization: Bearer | jeton 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 argon2id | Memoire par verification |
|---|---|---|
owasp-min (defaut) | m=19456,t=2,p=1 | 19 Mio |
rfc9106-low-mem | m=65536,t=3,p=4 | 64 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
| Porte | Ou mettre le jeton |
|---|---|
| SFTP | nom de login jwt (auth.jwt_sentinel_username), le jeton en mot de passe |
| API de fichiers, API d’administration | Authorization: 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/"
| Moment | Ce que fait le serveur |
|---|---|
| demarrage | valide le fichier, ouvre le journal, puis charge le jeu de cles, avant d’ouvrir les portes |
| chaque requete | verifie le jeton contre la cle que son en-tete kid designe |
toutes les jwks_refresh_interval_secs | recharge le jeu ; une cle ajoutee par le fournisseur est apprise a ce moment |
| rafraichissement sans reponse | garde 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
| Quand | Ligne |
|---|---|
| chaque chargement des comptes | INFO password hashes in use, un champ par format |
| jeu JWKS charge | INFO JWKS cache refreshed, champ keys ; puis JWKS background refresh started |
| connexion acceptee | audit 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
| Type | Format |
|---|---|
| ed25519, ECDSA (P-256, P-384, P-521), RSA | OpenSSH (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 :
| Ecriture | Effet |
|---|---|
["+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,macsethost_keyse negocient avant que l’utilisateur soit connu : ils valent pour tout le serveur.user_keydit les algorithmes avec lesquels une cle d’utilisateur peut signer sa connexion (lePubkeyAcceptedAlgorithmsd’OpenSSH). Un role peut en permettre d’autres a ses seuls utilisateurs (user_key_algorithms, voir Authentification).- Les marqueurs
ext-info-*etkex-strict-*sont toujours ajoutes akex. - L’extension
server-sig-algsannonce au client la listehost_keyen vigueur, puis les algorithmes deuser_keyqu’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 pas | Ajouter |
|---|---|
| curve25519, ML-KEM | kex = ["+diffie-hellman-group14-sha1"] |
ssh-ed25519 en cle d’hote | une 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’utilisateur | user_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
| Quand | Ligne |
|---|---|
| demarrage, chaque cle d’hote | INFO loaded host key, champs path, key_type, fingerprint |
| demarrage | INFO SSH algorithms offered, champs kex, ciphers, macs, host_key, user_key |
| connexion acceptee | INFO new SSH connection |
| session ouverte | INFO session established and registered, champs session_id, auth_method, signature_algorithm, total_sessions |
| fin ordinaire d’une connexion | INFO 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.
enabled | Pendant le transfert | Upload qui echoue |
|---|---|---|
false | le fichier grossit sous son vrai nom | le fichier partiel reste sous son vrai nom |
true | le fichier en cours de transfert grossit a cote ; la destination est intacte | le 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
| Valeur | Effet |
|---|---|
| absente | aucun plafond |
N | chaque 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 |
0 | aucun envoi sous ce montage, lecture seule de fait |
Ce que vous verrez
| Quand | Ligne |
|---|---|
| demarrage et rechargement, par backend | INFO stale in-flight files: ..., champs backend, grace_secs, grace_from, age_check, idle_timeout_secs |
| un reste supprime | INFO removed a stale in-flight file: ..., champs backend, path, age_secs, clock, lock |
| verrou d’un processus disparu repris | INFO 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 stockage | metrique 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
| Liste | Section | Portes |
|---|---|---|
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.
| Porte | Compte |
|---|---|
| SFTP | mot 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 fichiers | Basic faux ou mal forme ; Bearer vide ou jeton juge faux |
| Console et API d’administration | jeton 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
| Moment | Effet |
|---|---|
max_failures echecs dans window_secs | l’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 verdict | les sessions SFTP ouvertes de l’adresse sont coupees, transferts compris |
| un echec de plus pendant le ban | l’echeance recule a ban_duration_secs apres lui |
| l’echeance | le 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
| Backend | Cles | Partage |
|---|---|---|
| memoire | aucune | chaque instance a ses bans, perdus au redemarrage ; avec [cluster], une levee est relayee a chaque instance, comme pour un fichier |
| fichier | persist_file | relu 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 |
| ConfigMap | backend = "configmap", ban_configmap_name | suivi 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
| Quand | Ligne |
|---|---|
| un ban decide | audit ip_banned ; INFO IP banned after auth failures |
| sessions coupees par un ban | INFO cut the sessions of a banned address ; audit session_end reason=banned |
| un pair publie des bans | INFO merged persisted bans from file |
Delais
Ce que chaque delai detecte
| Delai | Detecte | Defaut |
|---|---|---|
sftp.login_grace_secs | une connexion SSH qui ne s’authentifie pas | 120 s |
uploads.idle_timeout_secs | un client vivant qui n’envoie plus rien pendant un upload (suspendu, bloque) | 30 s |
uploads.min_rate_bytes_per_sec | un upload au goutte-a-goutte | desactive |
[tcp_keepalive] | un pair disparu sans rien en vol (machine eteinte, NAT qui a oublie la connexion) | ~60 s |
tcp_keepalive.user_timeout_secs | un pair disparu alors que le serveur lui envoyait quelque chose ; un client qui ne lit plus | 65 s (SFTP), 35 s (HTTP) |
sftp.inactivity_timeout_secs | une connexion SSH sans aucun paquet | 600 s |
sftp.keepalive_interval_secs | un client SSH fige tout entier | desactive |
admin.header_read_timeout_secs | une requete HTTP dont les en-tetes n’arrivent pas | 10 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.
| Porte | Le chronometre |
|---|---|
| REST | repart a chaque octet du corps |
| SFTP | un 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_secs | Port SFTP | Port HTTP |
|---|---|---|
0 ou absent | 2 x idle_timeout_secs + 5 s (65 s) | idle_timeout_secs + 5 s (35 s) |
N | N | N |
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
| Quand | Ligne |
|---|---|
| demarrage | INFO idle and liveness timeouts in force: ..., une valeur par champ, et from_config : les cles fixees par le fichier |
| pair disparu | INFO SSH session ended: the connection timed out: ... |
Journaux
Deux flux
| Flux | Target | Contenu |
|---|---|---|
| journal applicatif | tout sauf audit | demarrage, connexions, avertissements, erreurs |
| piste d’audit | audit | une 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.audit | Succes d’operation ecrits | connection_accepted, session_end ordinaires |
|---|---|---|
all | tous | oui |
changes | upload, delete, delete_recursive, rename, mkdir, rmdir, rmdir_recursive | non |
failures | aucun | non |
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
| Fichier | Contenu | Retention |
|---|---|---|
craft-file-gate.log | le journal applicatif | retention_days (7 jours) |
craft-file-gate-audit.log | la piste d’audit, et elle seule | audit_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.gztoujours 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 INFO | Cause |
|---|---|
connection ended without a channel close — unregistering session | le client est parti |
SFTP channel closed — unregistering session | le client a ferme le canal |
session cut by an administrator — unregistering session | DELETE /admin/sessions/{id} |
session cut by the ban of its address — unregistering session | l’adresse vient d’etre bannie |
idle session cut by the server shutting down — unregistering session | arret, session sans transfert |
session cut by the server shutting down — unregistering session | arret, 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
| Quand | Ligne |
|---|---|
| demarrage | INFO log filter in force, champs filter et source |
| demarrage | INFO audit trail filter in force (refusals and errors are always recorded), champ audit |
demarrage avec dir | INFO log files in force, les deux chemins, la taille et les retentions |
refusal_summary_* modifies | INFO 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.
level | Effet |
|---|---|
trace, debug | plus de detail, pour CraftFileGate seul ; les dependances (russh, hyper, le SDK AWS) restent a info |
info (defaut) | le fonctionnement normal |
warn, error, off | moins 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
| Source | Priorite | A chaud |
|---|---|---|
RUST_LOG | la plus forte : remplace tout le filtre | non : le niveau reste le sien pour la vie du processus |
CRAFT_FILE_GATE_LOG_LEVEL | passe avant le fichier | le fichier n’est plus suivi tant qu’elle est posee |
[log] level | le defaut | oui |
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
| Directive | Cible |
|---|---|
craft_file_gate=debug | le serveur (le nom du crate, avec des _) |
audit=info | la piste d’audit ; audit=warn n’en garde que les refus et les erreurs |
russh=trace | le 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
protocol | otlp_endpoint | Spans envoyes a |
|---|---|---|
http | http://collector:4318 | http://collector:4318/v1/traces (et /v1/metrics) |
http | http://gw/otel/v1/traces | tel quel |
grpc | http://collector:4317 | tel quel |
https://est chiffre dans les deux protocoles ; le certificat du collecteur est verifie contre les racines du systeme (voir Racines de confiance TLS).protocolfait autorite :OTEL_EXPORTER_OTLP_PROTOCOLetOTEL_EXPORTER_OTLP_TRACES_PROTOCOL, qu’injectent certains operateurs, ne le changent pas.- L’exporteur lit
OTEL_EXPORTER_OTLP_HEADERS(en-tetes d’authentification) etOTEL_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.
| Span | Attributs | Parent |
|---|---|---|
ssh_connection | peer, username | - |
auth_password, auth_publickey | username | ssh_connection |
sftp_session | session_id, username | la connexion |
sftp_operation | operation, session_id, username, path, bytes | sftp_session |
api_request | method, path (tel que recu, encode en pourcent), username | - |
api_operation | operation, path (decode), username | api_request |
authz_service_call | http.method, http.url, http.status_code | l’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
closea deux spanssftp_operationdu memeoperation(upload,download) : l’ouverture, puis la validation, qui seule portebytes. Comptez les spans qui portentbytes; - un
rmdird’un repertoire non vide a un spanrmdirpuis un spanrmdir_recursive, pour une seule ligne d’auditrmdir_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
| Moment | Comportement |
|---|---|
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 refus | une seule tentative |
| collecteur injoignable | le lot est perdu et compte, rien ne s’arrete ; une ligne a la panne, une au retour |
| arret | dernier envoi des metriques et des spans, 5 s au plus |
Les spans perdus se comptent sur /metrics :
| Metrique | Compte |
|---|---|
craftfilegate_otel_spans_ended_total | spans remis a l’export |
craftfilegate_otel_spans_exported_total | spans acceptes par le collecteur |
craftfilegate_otel_spans_export_failed_total | spans 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
| Quand | Ligne |
|---|---|
| demarrage, export coupe | INFO OpenTelemetry tracing disabled ([telemetry] absent or enabled = false); no spans are exported |
| demarrage, export actif | INFO OpenTelemetry tracing enabled, champs endpoint (complete en http), protocol, service_name |
| demarrage, metriques | INFO OpenTelemetry metrics export enabled, champs endpoint, interval_secs |
| collecteur revenu apres une panne | INFO OTLP collector recovered — telemetry export resumed |
| arret | INFO 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.
| Fichier | Juge |
|---|---|
le fichier passe a --config | demarrage, rechargement |
sftp.host_keys | demarrage |
auth.users_file, auth.methods.local.users_file, ou users.toml adopte | demarrage, rechargement |
auth.roles_file, ou roles.toml adopte | demarrage, rechargement |
auth.jwt.public_key_file | demarrage |
admin.tls.cert_file, admin.tls.key_file | demarrage, rechargement |
admin.session.key_file | demarrage |
CRAFT_FILE_GATE_JWT_SECRET_FILE, CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN_FILE | demarrage |
auth.password_file et ca_bundle d’un backend webhdfs | construction du backend |
SSL_CERT_FILE, SSL_CERT_DIR, s’ils sont poses | demarrage |
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
| Pose | Verdict |
|---|---|
| appartient a root, ecrit par son seul proprietaire, repertoires de meme | accepte sans un mot |
| appartient a l’uid du serveur, ecrit par lui seul | accepte, 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 chemin | accepte |
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
0440avec lefsGroupdu 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 serveur | Mode |
|---|---|
| cle d’hote generee, cle TLS admin generee | 0600 (certificat genere : 0644) |
| repertoire d’une cle generee, fichier de bans, fichiers temporaires | 0700 / 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’audit | 0640 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.
| Variable | Contenu |
|---|---|
SSL_CERT_FILE | un fichier PEM, un nombre quelconque de certificats |
SSL_CERT_DIR | des 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
| Sujet | Recommandation |
|---|---|
| fichiers de confiance | en lecture seule, a root ; un Secret ou un ConfigMap monte readOnly: true |
| secrets | les formes _FILE plutot que les variables, lisibles dans /proc/<pid>/environ |
| uid | un uid dedie par instance, NoNewPrivileges=yes, un filtre seccomp (SystemCallFilter=@system-service, seccompProfile: RuntimeDefault) |
| pod | le chart Helm applique le profil restricted : voir Kubernetes |
| port d’administration | control_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 brute | les trois listes de bans et les deux limiteurs (voir Bans) |
| piste d’audit | exportee en entier hors de l’hote ; les refus d’authentification sont en INFO, un filtre WARN les perd |
| JWT | rotation reguliere des cles de signature ; issuer et audience poses |
Exploiter
| Je veux | Page |
|---|---|
| installer : image, Compose, binaire | Docker |
| deployer dans un cluster, a plusieurs pods | Kubernetes, Secrets |
| voir les sessions, couper, lever un ban, lire les journaux | Console et API d’administration |
| donner un acces web aux fichiers | Explorateur de fichiers |
| savoir qui a fait quoi | Audit : lire la piste |
| surveiller et alerter | Metriques |
| changer la configuration sans redemarrer | Rechargement |
| arreter sans couper les transferts | Arret |
| dimensionner memoire et CPU | Performances et memoire |
| comprendre une erreur | Depannage |
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).
| Image | Contenu | Utilisateur | HEALTHCHECK |
|---|---|---|---|
:latest | distroless statique, sans shell | 65532 (nonroot) | oui |
:alpine | Alpine, avec un shell pour le diagnostic | 65534 (nobody) | oui |
:scratch | le binaire seul, sans magasin de certificats | aucun declare : posez --user 65532 | non |
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). /dataappartient 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(65534pour:alpine).:scratchn’a aucune racine de certificats : une connexion TLS sortante (S3, JWKS, OTLP, Knox) demande un bundle monte etSSL_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.
| Feature | Backend ou porte |
|---|---|
backend-local | local ; requise (etat, verrous et bans du serveur vivent sur le disque local) |
backend-sftp | sftp (proxy) |
backend-s3 | s3 |
backend-webhdfs | webhdfs |
door-sftp | la porte SFTP, [sftp] |
door-rest | l’API REST de fichiers et l’explorateur, [api] |
k8s | bans 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
| Valeur | Defaut | Effet |
|---|---|---|
replicaCount | 1 | nombre 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.allowStaticToken | false | allow_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.pullPolicy | craftogether/craft-file-gate, la version du chart, IfNotPresent | l’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.type | 2222, ClusterIP | le Service <release>, SFTP seul |
service.admin.port, service.admin.type | 8080, ClusterIP | le Service <release>-api : API de fichiers, explorateur ; absent avec un config.inline sans [admin] |
service.control.port | 8081 | control_listen : /admin, /metrics, sondes ; "" pour une seule porte ; ignore avec un config.inline sans [admin] |
service.control.type | ClusterIP | le 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.expose | false | ce port aussi sur le Service <release>-control : sans lui, sondes et collecte passent par l’IP du pod |
networkPolicy.enabled | true | une NetworkPolicy d’entree sur le pod |
networkPolicy.publicFrom | [] : toute source | sources admises sur SFTP et le port admin |
networkPolicy.controlFrom | - namespaceSelector: {} : tout pod du cluster | sources admises sur le port de controle et celui des sondes ; au moins une |
ban.backend | file | configmap pour partager les bans entre pods |
ban.configmapName | "" : <release>-bans | la ConfigMap des bans, creee par le chart et donnee au serveur par CRAFT_FILE_GATE_{SFTP,API,ADMIN}_BAN_CONFIGMAP_NAME |
stateDir.enabled, stateDir.path | true, /var/lib/craft-file-gate | un emptyDir pour le fichier de bans |
logFiles.enabled, logFiles.dir | false, /var/log/craft-file-gate | fichiers de log, dans un emptyDir ou logFiles.existingClaim |
passwordHashing.workers | "" : requests.cpu | fils de hachage ; voir Verification des mots de passe |
passwordHashing.queue | "" : 1024 | file de hachage |
resources | requests 100m, 64Mi ; limits 500m, 256Mi | voir Memoire |
tls.enabled | false | sondes 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 s | le 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 ; 8083 | le canal entre pods : Plusieurs instances |
cluster.minPeers, cluster.unreadyWhen | "" : floor(replicaCount / 2) ; "" : alerter seulement | les 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
ipBlockanetworkPolicy.controlFrom. Pour restreindre Prometheus et l’ingress, listez leurs namespaces dansnetworkPolicy.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.
| Reglage | Valeur |
|---|---|
| utilisateur | runAsNonRoot: true, runAsUser, runAsGroup, fsGroup : 65532 |
| privileges | allowPrivilegeEscalation: false, privileged: false, capabilities.drop: [ALL] |
| seccomp | RuntimeDefault |
| racine | readOnlyRootFilesystem: true : le serveur n’ecrit que dans un volume |
| jeton de service account | monte avec ban.backend: configmap, le Secret partage (clusterSecret) ou la ConfigMap d’acces (adminRevocations, adminGrants) |
| configuration, cles d’hote | montees 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 partage | Comment | Voir |
|---|---|---|
| les pods se trouvent | des 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 partage | Plusieurs instances |
| cle de session de la console | le Secret partage (clusterSecret), entree admin-session.key | Le Secret partage des pods |
| revocations de la console, acces temporaires | la ConfigMap <release>-access (access.configmapName) | Partager les revocations |
| bans | ConfigMap (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 eventuelle | Bans |
| sessions, metriques, limites de debit | rien : 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
| Secret | Par fichier | Par variable d’environnement |
|---|---|---|
| configuration entiere | config.toml monte dans /config | - |
| utilisateurs, roles | users_file, roles_file | - |
| cles d’hote SSH | host_keys | - |
| paire TLS admin | [admin.tls] cert_file, key_file | CRAFT_FILE_GATE_ADMIN_TLS_CERT, CRAFT_FILE_GATE_ADMIN_TLS_KEY |
| secret JWT | CRAFT_FILE_GATE_JWT_SECRET_FILE | CRAFT_FILE_GATE_JWT_SECRET |
jeton d’administration (secours ; interdit par allow_static_token = false, que le chart pose) | CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN_FILE | CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN |
| cles S3 | credentials = { type = "static", ... } | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY avec type = "iam_role" |
| cle des sessions de la console | [admin.session] key_file | CRAFT_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
subPathn’est jamais mis a jour : evitezsubPath. - Montez Secrets et ConfigMaps
readOnly: true, comme le chart, jamais sur unemptyDirou unhostPathinscriptible (Fichiers de confiance et TLS). - Un Secret passe par
envFrom(extraEnvFrom) n’est lu qu’au demarrage :kubectl rollout restartpour 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 chart | Effet |
|---|---|
clusterSecret.enabled | true 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.backend | vide 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.backend | vide 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.configmapName | la 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
| Configuration | Ecouteur | Ce qu’il sert | Runtime |
|---|---|---|---|
| sans API | listen | console, /admin/*, /metrics, sondes | admin (deux fils admin-rt) |
API, sans control_listen | listen | tout, et l’API | principal, partage avec les transferts |
control_listen | control_listen | /admin/*, /, /ui/*, /metrics, /health, /livez, /readyz | admin |
listen | API de fichiers, explorateur, /api/docs et /api/openapi.json | principal | |
[server] probes_listen | probes_listen | /livez, /readyz, /health et /metrics, en HTTP simple, et rien d’autre ; ils quittent listen et control_listen | admin |
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>.
| Jeton | Identite | Permissions |
|---|---|---|
un jeton de session, rendu par POST /admin/login (Session de console) | le compte local | celles des roles admin que nomment ses authorities, relues a chaque requete |
un JWT verifie par [auth.jwt] | son nom | celles 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).
| Permission | Routes |
|---|---|
| aucune (tout jeton admis) | GET /admin/me, POST /admin/logout ; POST /admin/session/renew (jeton de session) |
overview | GET /admin/resources, GET /admin/status, GET /admin/health, GET /admin/doors |
sessions | GET /admin/sessions ; GET /admin/events (evenements de session) |
kick | DELETE /admin/sessions/{id} |
config | GET /admin/config, GET /admin/sessions/{id}/roles |
bans | GET /admin/bans ; GET /admin/events (evenements de ban) |
unban | DELETE /admin/bans/{protocol}/{ip} |
logs | GET /admin/logs/app, GET /admin/logs/app/stream |
audit | GET /admin/logs/audit, GET /admin/logs/audit/stream (chaque lecture est auditee) |
revoke | GET /admin/revocations, POST /admin/revocations |
grant | GET, 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) |
| publiques | POST /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"}]}
| Champ | Sens |
|---|---|
mounts | un objet par montage : mount_path, backend (son nom), home_dir developpe, acl relative au montage |
last_sftp_op_at | la derniere requete SFTP, refus compris, keepalive non compris |
last_traffic_at | les derniers octets du client, keepalives compris ; la coupure par inactivity_timeout_secs part de la |
has_active_transfer | un fichier ouvert ; retient l’arret pendant le delai de grace |
user_key | la cle SSH : key_type, fingerprint, signature_algorithm |
weak_algorithms | les 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).
| Requete | Effet |
|---|---|
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}/roles | les 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/doors | les 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 :
| Flux | Evenements |
|---|---|
GET /admin/events | session.opened, session.closed, session.kicked, session.revoked, ban.added, ban.lifted, lagged |
GET /admin/logs/{app|audit}/stream | entry, 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
| Route | Repond | Usage |
|---|---|---|
/livez | 200 {"status": "alive"} tant que le processus vit | sonde de vie ; craft-file-gate healthcheck |
/readyz | 200 si le port SFTP accepte et que le runtime principal prend une tache en 500 ms, 503 sinon et des le signal d’arret | sonde de disponibilite |
/health | 200 toujours ; status ok ou degraded (service d’autorisation injoignable), checks.auth_service, checks.data_plane (verdict de /readyz), rien d’autre | supervision |
/admin/health | /health, avec la version, les sessions actives et les backends configures ; permission overview | la console |
/admin/status | ready, degraded ou not_ready, avec chaque verification (sftp, data_runtime, auth_service, jwks) et son detail | le 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.
| Onglet | Contenu | Permission |
|---|---|---|
| Overview | statut, version, sessions, service d’autorisation | overview |
| Metrics | bandeau de /admin/status, tuiles sur cinq minutes, backends, top users | overview (top users : sessions) |
| Instances | avec [cluster], chaque instance : statut, version, sessions, bans, fin du certificat, sante | overview |
| Sessions | une 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 |
| Configuration | backends (secrets masques, horloge du stockage), roles, cles d’hote | config |
| Bans | bans en cours, levee ; avec un pair, chaque instance (colonne Instance) | bans, unban |
| Logs | journal et piste d’audit, filtres, suivi en direct ; avec [log] dir | logs, audit |
| Revocations | revoquer les sessions d’un compte, celles en vigueur ; quand la porte connecte des comptes | revoke |
| Temporary access | accorder 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 session | grant |
| Hash tools | hash-password et verify-password en WebAssembly dans la page : le mot de passe ne quitte pas le navigateur | aucune |
- 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
| Quand | Ligne |
|---|---|
| demarrage | INFO 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
| Requete | Reponse |
|---|---|
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
| Requete | Effet |
|---|---|
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/logout | de 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/revocations | les 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_secsapres la connexion ; un onglet ferme la laisse finir attl_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 ouPOST /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 lesreread_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.
| Geste | Requete | Droit |
|---|---|---|
| se connecter | GET <prefix>?rights | - |
| ouvrir un dossier | GET <dossier>?list&offset=&limit=200, puis ?rights | list |
| telecharger | POST <fichier>?ticket, puis l’URL rendue confiee au navigateur | read |
| deposer | HEAD <fichier> (« Replace file? » s’il existe), puis PUT en flux | write |
| nouveau dossier | PUT <dossier>?mkdir | write |
| renommer | POST <chemin>?rename=<destination> | rename |
| supprimer | DELETE <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_addressenBasic. - 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.
| Entree | Affichage |
|---|---|
| point de montage | icone de stockage, etiquette mount ; s’ouvre comme un dossier |
| repertoire synthetique | dossier 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
| Limite | Effet |
|---|---|
| pas de reprise de depot | un depot interrompu est a refaire ; un telechargement relance apres dix minutes demande un nouveau clic |
| pas d’apercu ni d’edition | ni 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
| Reglage | Ou va la piste |
|---|---|
| defaut | stdout, melee au journal applicatif, une ligne JSON par evenement, target = audit |
[log] dir | en plus, craft-file-gate-audit.log (la piste seule) a cote de craft-file-gate.log |
[log] stdout = false | les 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
| Niveau | Lignes |
|---|---|
WARN | ce 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 |
INFO | ce 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
| Question | Filtre |
|---|---|
| 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
| Sortie | Forme | Acces |
|---|---|---|
GET /metrics | texte Prometheus 0.0.4 | public ; sur [server] probes_listen ou control_listen seulement s’ils sont poses |
GET /admin/resources | le meme instantane en JSON, plus sessions, bans actifs, backends, seuils | permission overview |
| export OTLP | les 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 (
nullen JSON, aucun point en OTLP), jamais0: la colonne « Absente quand » dit quand. - Le nom OTLP est le nom Prometheus sans
_total.
Catalogue
| Serie Prometheus | Instrument OTLP | Type | Labels | Absente quand | Sens |
|---|---|---|---|---|---|
craftfilegate_connections_total | craftfilegate_connections | counter | sessions SSH ouvertes apres une authentification reussie | ||
craftfilegate_connections_rejected_total | craftfilegate_connections_rejected | counter | refus de la porte SFTP, un par tentative (lignes connection_rejected de source=sftp, resumes compris) | ||
craftfilegate_sftp_operations_total | craftfilegate_sftp_operations | counter | op (21 valeurs) | paquets SFTP recus, refus compris | |
craftfilegate_sftp_bytes_read_total | craftfilegate_sftp_bytes_read | counter | octets servis par SFTP | ||
craftfilegate_sftp_bytes_written_total | craftfilegate_sftp_bytes_written | counter | octets acceptes par SFTP | ||
craftfilegate_api_requests_total | craftfilegate_api_requests | counter | requetes recues par l’API de fichiers, 429 et refus compris | ||
craftfilegate_banned_ips_total | craftfilegate_banned_ips | counter | verdicts de ban de ce pod, toutes portes | ||
craftfilegate_sftp_accept_errors_total | craftfilegate_sftp_accept_errors | counter | accept() en echec sur le port SFTP | ||
craftfilegate_log_write_errors_total | craftfilegate_log_write_errors | counter | lignes de journal ou d’audit non ecrites | ||
craftfilegate_audit_refusals_suppressed_total | craftfilegate_audit_refusals_suppressed | counter | source (sftp, api, admin) | refus anonymes resumes au lieu d’etre ecrits | |
craftfilegate_audit_refusal_summary_overflow_total | craftfilegate_audit_refusal_summary_overflow | counter | refus arrives table des resumes pleine | ||
craftfilegate_otel_spans_ended_total | craftfilegate_otel_spans_ended | counter | telemetrie coupee | spans termines | |
craftfilegate_otel_spans_exported_total | craftfilegate_otel_spans_exported | counter | telemetrie coupee | spans acceptes par le collecteur | |
craftfilegate_otel_spans_export_failed_total | craftfilegate_otel_spans_export_failed | counter | telemetrie coupee | spans perdus | |
craftfilegate_admin_tls_cert_not_after_seconds | craftfilegate_admin_tls_cert_not_after_seconds | gauge | sans TLS, date illisible | expiration du certificat servi, secondes Unix | |
craftfilegate_admin_tls_cert_expiry_unreadable | craftfilegate_admin_tls_cert_expiry_unreadable | gauge | sans TLS | 1 date illisible, 0 lue | |
craftfilegate_sftp_rate_limit_tracked_addresses | craftfilegate_sftp_rate_limit_tracked_addresses | gauge | sans [sftp.rate_limit], ou avant son premier balayage | adresses suivies par le limiteur | |
craftfilegate_api_rate_limit_tracked_addresses | craftfilegate_api_rate_limit_tracked_addresses | gauge | sans [api.rate_limit], ou avant son premier balayage | idem pour l’API | |
craftfilegate_local_users | craftfilegate_local_users | gauge | hash_format | sans magasin local | comptes locaux par format de hash |
craftfilegate_reload_watch_mode | craftfilegate_reload_watch_mode | gauge | mode (inotify, poll, sighup) | avant l’armement | 1 pour le mode en vigueur |
craftfilegate_jwks_cache_age_seconds | craftfilegate_jwks_cache_age_seconds | gauge | sans JWKS, aucun chargement reussi | age du cache JWKS | |
craftfilegate_jwks_refresh_failures_total | craftfilegate_jwks_refresh_failures | counter | sans JWKS | rafraichissements en echec | |
craftfilegate_jwks_cached_keys | craftfilegate_jwks_cached_keys | gauge | sans JWKS | cles en cache | |
craftfilegate_grants_active | craftfilegate_grants_active | gauge | sans [admin] | acces temporaires qui donnent leur role maintenant | |
craftfilegate_grant_sessions_cut_total | craftfilegate_grant_sessions_cut | counter | reason (expired, revoked) | sans [admin] | sessions SFTP et transferts REST coupes a la fin d’un acces temporaire qu’ils utilisaient |
craftfilegate_grants_store_local_only | craftfilegate_grants_store_local_only | gauge | sans [admin] | 1 : acces temporaires en memoire alors qu’un pair du cluster repond | |
craftfilegate_cluster_peers | craftfilegate_cluster_peers | gauge | status | sans [cluster] | pairs du canal entre instances par statut : reachable, unreachable, certificate_mismatch |
craftfilegate_cluster_relay_total | craftfilegate_cluster_relay | counter | route (health, sessions, bans, kick, unban), result (answered, refused, failed) | sans [cluster] | requetes d’operateur relayees a un pair |
craftfilegate_cluster_cert_not_after_seconds | craftfilegate_cluster_cert_not_after_seconds | gauge | sans [cluster], date illisible | expiration du certificat partage, secondes Unix ; jamais verifiee | |
craftfilegate_cluster_reachable_peers | craftfilegate_cluster_reachable_peers | gauge | sans [cluster] | pairs joignables au dernier tour | |
craftfilegate_cluster_isolated | craftfilegate_cluster_isolated | gauge | sans [cluster] | 1 : moins de min_peers pairs joignables depuis isolated_after_secs (detecteurs) | |
craftfilegate_unready_detector | craftfilegate_unready_detector | gauge | detector (storage_alone, isolated, isolated_and_storage_down) | sans [cluster] | 1 tant que le detecteur est actif |
craftfilegate_withdrawn | craftfilegate_withdrawn | gauge | sans [cluster] | 1 : /readyz non pret a cause d’un detecteur de unready_when | |
craftfilegate_stale_partials_clock_skew_seconds | craftfilegate_stale_partials_clock_skew_seconds | gauge | backend | rien mesure | horloge du stockage moins celle du serveur |
craftfilegate_password_hash_pool_places | craftfilegate_password_hash_pool_places | gauge | sans magasin local | fils plus places de file du pool de hachage | |
craftfilegate_password_hash_pool_in_use | craftfilegate_password_hash_pool_in_use | gauge | sans magasin local | places prises | |
craftfilegate_password_hash_pool_full_total | craftfilegate_password_hash_pool_full | counter | sans magasin local | verifications refusees pool plein | |
craftfilegate_jwt_refused_total | craftfilegate_jwt_refused | counter | sans JWT | jetons refuses par le verificateur | |
craftfilegate_backend_operations_total | craftfilegate_backend_operations | counter | backend | backend inutilise | appels au stockage |
craftfilegate_backend_errors_total | craftfilegate_backend_errors | counter | backend | backend inutilise | pannes du stockage (entree-sortie, injoignable, identifiants refuses) ; pas les refus du client |
craftfilegate_backend_bytes_read_total | craftfilegate_backend_bytes_read | counter | backend | backend inutilise | octets lus du stockage |
craftfilegate_backend_bytes_written_total | craftfilegate_backend_bytes_written | counter | backend | backend inutilise | octets ecrits vers le stockage |
craftfilegate_backend_up | craftfilegate_backend_up | gauge | backend | avant la premiere visite, sonde coupee | 1 le stockage repond a la sonde, 0 il ne repond plus |
craftfilegate_backend_probe_seconds | craftfilegate_backend_probe_seconds | gauge | backend | idem | duree de la derniere visite |
craftfilegate_local_symlink_refusals_total | craftfilegate_local_symlink_refusals | counter | backend | backend non local ou inutilise | chemins refuses symlink escape |
process_resident_memory_bytes | process_resident_memory_bytes | gauge | hors Linux, /proc muet | memoire residente | |
process_peak_resident_memory_bytes | process_peak_resident_memory_bytes | gauge | idem | pic de memoire residente | |
process_cpu_seconds_total | process_cpu_seconds | counter | idem | temps CPU user + system | |
process_threads | process_threads | gauge | idem | threads |
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 applique | Il garde |
|---|---|
| les cles marquees ⟳ dans la reference | les sessions SFTP etablies, avec ce qu’elles ont resolu a leur authentification |
users_file : utilisateurs, hashs, cles, authorities | un fichier refuse : rien n’en est applique |
roles_file : roles, montages, ACL, backends, client auth.authz_base_url | les 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 ;
pollconvient 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 INFO | Sens |
|---|---|
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 reloaded | ce qui est applique |
inline config — roles not hot-reloadable | au 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.
| Etape | Ce qui se passe | Borne |
|---|---|---|
| 1 | la porte SFTP n’accepte plus ; /readyz repond 503 ; plus aucune verification de mot de passe admise | - |
| 2 | les listes de bans ([sftp.ban], [api.ban], [admin.ban]) sont ecrites dans leur persist_file | 5 s par liste |
| 3 | server shutting down envoye a toutes les sessions SFTP ; la porte admin/API n’accepte plus et ferme ses flux | - |
| 4 | les sessions sans transfert en cours sont coupees | - |
| 5 | attente des transferts en cours, s’il y en a ; finit avec le dernier | shutdown_grace_period_secs |
| 6 | toute session encore la est coupee | - |
| 7 | attente de la fin des sessions coupees (spans, dernieres lignes d’audit) | reste du delai |
| 8 | attente des requetes REST et admin en cours | reste du delai |
| 9 | uploads multipart S3 encore ouverts annules | 5 s |
| 10 | nettoyages de quota et retraits des verrous d’upload | 5 s |
| 11 | dernier envoi OTLP (metriques, spans) | 5 s |
| 12 | graceful 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 :
| Composante | Defaut |
|---|---|
shutdown_grace_period_secs | 30 s |
| ecriture des bans | 5 s par liste ([sftp.ban], [api.ban], [admin.ban]) |
| travaux des backends | 5 s |
| nettoyages et verrous | 5 s |
| envoi OTLP | 5 s |
| total | 50 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
| Question | Page |
|---|---|
| quelle limite memoire poser | Memoire |
| combien de verifications de mot de passe en parallele | Verification des mots de passe |
| que surveiller en production | Metriques |
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 :
| Variante | Formule | Tient sous 80 Mo |
|---|---|---|
| backend local | 45,5 Mio | oui |
| 2 fils, backend local | 64,5 Mio | oui |
| S3, un upload a la fois | 64,7 Mio | oui |
| S3, 3 uploads simultanes | 116,5 Mio | non |
profil rfc9106-low-mem | 90,5 Mio | non : une seule verification depasse le budget |
| file par defaut (1024) | +70 Mio file pleine | non |
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
| Reglage | Ce qu’il doit couvrir |
|---|---|
limits.memory | le pic de la formule, sur le nombre de transferts et de sessions simultanes attendus, plus une marge |
requests.memory | la 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.
| Situation | Ce qui se passe |
|---|---|
| un fil libre | la tentative est verifiee aussitot |
| fils occupes, place en file | la tentative attend son tour |
| fils occupes, file pleine | refus immediat, sans recherche du compte, sans compter pour le ban |
l’adresse tient deja hash_per_address places | meme refus, quelle que soit la charge |
| client parti avant son tour | la tentative n’est pas hachee ; sa place est rendue |
| adresse bannie pendant l’attente | la tentative n’est pas hachee ; reponse d’une adresse bannie |
| arret du serveur | aucune 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
| Question | Reponse |
|---|---|
| pourquoi 2 fils au plus par defaut | chaque fil peut tenir un m argon2 entier ; 2 fils au profil owasp-min tiennent sous 80 Mo |
| combien de connexions par seconde | 2 fils : une trentaine au profil rfc9106-low-mem (68 ms), plus d’une centaine au profil owasp-min (16 ms) |
| pourquoi 1024 places | une 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 place | hash_queue / hash_workers verifications : 6,5 s avec 2 fils au profil owasp-min ; restez sous sftp.login_grace_secs (120 s) |
| budget memoire serre | une 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.
| Message | Ou c’est regle | Regle |
|---|---|---|
--config is required when not using a subcommand, Configuration error: failed to read config file: <cause> | Le fichier de configuration | R-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 orthographiee | R-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 porte | R-CONFIG-016 |
server.probes_listen = <adresse> is also <cle> — the probes need a port of their own | Une 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 username | Les 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 inconnu | Les 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 role | Les 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 root | Ou 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 local | Cumuler 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 enabled | Choisir 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 effect | Detecteurs 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 retrait | Detecteurs 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 seule | R-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 poser | R-TRUST-007 |
cle privee lisible par les autres (WARN) | Les fichiers de confiance | R-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 text | Un 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 > 0 | Arret ([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 deux | R-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 fichiers | R-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 binaire | R-ADMIN-022 |
auth.jwt.issuer = "craft-file-gate" is the issuer of the console's own session tokens ... | [auth.jwt] : un autre issuer | R-ADMIN-005 |
admin session key generated for this process: ... (WARN), jetons refuses par un autre pod ou apres un redemarrage | La cle de session ([admin.session]) : key_file ou secret_name | R-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 = false | R-ADMIN-024 |
api.enabled requires [admin] section to be configured | API 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 start | Le point d’acces, [telemetry] ([telemetry]) : on_exporter_error | R-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_listen | R-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 Windows | Reservation 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 retirer | R-CONFIG-010 |
this secret is the example published in the README or the example files (WARN) | Secrets : changer le secret | R-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 ignored | Racines de confiance TLS (Variables d’environnement) | R-TRUST-014 |
Rechargement
Un fichier refuse au rechargement garde ce qui est en vigueur. Voir Rechargement.
| Message | Ou c’est regle | Regle |
|---|---|---|
configuration edited but not applied until restart (keys=...) | Ce qu’un rechargement applique, cle par cle (reference) : redemarrer | R-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 force | Les 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 cause | R-RELOAD-005 |
password hashing pool size edit not applied: the pool is sized once, at startup | Verification des mots de passe ([auth]) | R-AUTH-014 |
[log] format edit ignored: ... | [log] ([log]) : redemarrer | R-AUDIT-029 |
[log] level edit ignored: <variable> is in force ... | Qui decide du niveau ([log], Variables d’environnement) : la variable l’emporte | R-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 refusee | R-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 signaler | R-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 detail | Ou c’est regle | Regle |
|---|---|---|
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 authorities | Les comptes locaux ([[users]]) : mot de passe faux ou nom inconnu | R-AUDIT-022, R-AUTH-006, R-EXPLORER-006 |
no authorized key offered | La cle publique ([[users]]) : authorized_keys | R-AUTH-016 |
signature algorithm not allowed | Les 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 configured | API REST de fichiers, Choisir les methodes ([auth.methods]) : envoyer Basic ou Bearer | R-REST-003 |
invalid token, expired token, wrong issuer, wrong audience, 401 invalid JWT | JWT ([auth.jwt]) : issuer, audience, cle | R-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 claim | JWT ([auth.jwt]) : username_path | R-AUTH-024 |
method disabled, 401 authentication method disabled | Choisir 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 failed | D’ou viennent les roles, Le contrat du service d’autorisation ([[users]], [auth.jwt], [auth]) : authorities, authorities_path, authz_base_url | R-AUTH-029 |
mount conflict, 503 ; backend initialization failed, 500 ; deconnexion SFTP server configuration error: <cause>; see the server log | Cumuler des roles, Les types ([[roles.mounts]], [[backends]]) : le journal applicatif dit la cause | R-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_user | R-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 exceeded | Limites de debit ([sftp.rate_limit], [api.rate_limit]) | R-BAN-022 |
password checks saturated, 503 password checks saturated, retry later | Verification des mots de passe ([auth]) | R-AUTH-011 |
password checks saturated for address | Verification des mots de passe ([auth], [api.ban]) : NAT, trusted_proxies | R-AUTH-012 |
shutting down | Arret | R-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 exclusive | Telecharger : le ticket, API REST de fichiers : redemander un ticket | R-REST-009, R-REST-010 |
429 too many live download tickets | API REST de fichiers : 32 tickets vivants par utilisateur, attendre l’expiration du plus ancien | R-REST-009 |
range not satisfiable, 416 | API REST de fichiers : le Range commence apres la fin du fichier, le client l’a deja entier | R-REST-006 |
login grace time exceeded | SSH : [sftp] ([sftp]) : login_grace_secs | R-TIMEOUT-001 |
WARN failed to accept SFTP connections; retrying with backoff (descripteurs epuises, ulimit -n) | Porte SFTP | R-SFTP-002 |
SSH session ended: the peer offered no algorithm in common | Un vieux client ([sftp.algorithms]) | R-SFTP-010 |
SSH request refused: ... (shell, exec, renvoi de port) ; SSH_MSG_CHANNEL_FAILURE sur un second sous-systeme sftp | seul le sous-systeme sftp est servi, une fois par connexion : Portes | R-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 signaler | R-SFTP-019 |
Operations sur fichiers
reason des lignes d’operation, statut SFTP et code REST.
reason | SFTP | REST | Ou c’est regle | Regle |
|---|---|---|---|---|
acl | SSH_FX_PERMISSION_DENIED | 403 | Quelle entree decide ([[roles.mounts.acl]]) | R-ACL-003 |
acl subtree | SSH_FX_PERMISSION_DENIED | 403 | Les droits ([[roles.mounts.acl]]) : delete sur tout l’arbre | R-DELETE-005 |
synthetic path | SSH_FX_PERMISSION_DENIED | 403 | Repertoires synthetiques | R-ACL-005 |
rename across mounts | SSH_FX_OP_UNSUPPORTED | 422 | Un montage ou plusieurs | R-RENAME-010 |
invalid path | SSH_FX_PERMISSION_DENIED | 400 | caractere de controle, \, :, point final, nom court 8.3 : Les chemins des clients | R-UPLOAD-001 |
reserved name | SSH_FX_PERMISSION_DENIED | 403 | Deux uploads vers la meme destination | R-RESERVE-010 |
rename into restricted | SSH_FX_PERMISSION_DENIED | 403 | Les droits ([[roles.mounts.acl]]) : write a la destination | R-RENAME-008 |
exists | SSH_FX_FAILURE | 409, 412 | destination existante | R-RENAME-002 |
is a directory, not a directory | SSH_FX_FAILURE | 409 | rm d’un repertoire, rmdir d’un fichier | R-DELETE-002 |
upload in progress ; recursive delete refused: an upload is in progress beneath (WARN) | SSH_FX_PERMISSION_DENIED (curl : Permission denied (3)) | 409 | Deux uploads vers la meme destination | R-RESERVE-001, R-RESERVE-012 |
quota exceeded | SSH_FX_FAILURE | 507 | Plafonner la taille d’un fichier ([[roles.mounts]]) : max_file_mb | R-UPLOAD-007 |
session killed | SSH_FX_CONNECTION_LOST | - | session coupee : Sessions | R-SFTP-020 |
unsupported | SSH_FX_OP_UNSUPPORTED | 501 | Ecritures atomiques, Fichiers et repertoires sur S3 ([server.hidden_stores]) : reprise ou ajout, renvoyer le fichier entier | R-UPLOAD-009 |
upload in progress au CLOSE, client : upload not published: another upload took this file: <chemin> | SSH_FX_PERMISSION_DENIED | 409 | le 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 instances | R-RESERVE-008 |
taken over | SSH_FX_FAILURE sur l’ancien handle | 409 a l’ancien upload | un upload bloque (client suspendu) repris par une relance du meme compte : Deux uploads vers la meme destination ([uploads]) : takeover_idle_secs | R-RESERVE-017 |
upload idle timeout, upload below minimum rate | SSH_FX_FAILURE | 408 | Uploads : [uploads] ([uploads]) | R-UPLOAD-013 |
session ended: admin_kick | - | 503 the upload was cut by an administrator: nothing was written | un transfert REST coupe depuis la console : Sessions | R-REST-013 |
session ended: <cause>, session ended | - | - | le client est parti ; <cause> est le reason du session_end | R-AUDIT-018 |
commit interrupted (result=unknown) | - | - | client parti pendant la publication : verifier le fichier | R-AUDIT-017 |
no roles | - | 403 | voir no matching roles plus haut | R-AUDIT-020 |
| - | SSH_FX_OP_UNSUPPORTED | - | READLINK, SYMLINK, posix-rename@openssh.com : non servis | R-LIST-012 |
| - | - | 400 missing ?rename= query param ; 405 (verbe non servi) | API REST de fichiers | R-REST-002 |
| - | SSH_FX_FAILURE | - | handle inconnu ou d’un autre genre : bug du client | R-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, message | SFTP | REST | Ou c’est regle | Regle |
|---|---|---|---|---|
not found | SSH_FX_NO_SUCH_FILE | 404 | Les cles communes sur un backend local ([[roles.mounts]]) : chemin absent ; home_dir absent sans create_home ; sur S3, un repertoire ne se renomme pas | R-UPLOAD-015, R-S3-004 |
permission denied | SSH_FX_PERMISSION_DENIED | 403 | droits du stockage ; S3 sans s3:ListBucket : Les droits du bucket ; WebHDFS (AuthorizationException, doAs non autorise) : Prerequis | R-UPLOAD-015, R-WEBHDFS-005 |
symlink escape | SSH_FX_PERMISSION_DENIED | 403 | La racine et les liens symboliques ([[backends]] local) : metrique craftfilegate_local_symlink_refusals_total | R-LOCAL-004 |
already exists, directory not empty | SSH_FX_FAILURE | 409 | etat du stockage | R-UPLOAD-015 |
storage error | SSH_FX_FAILURE backend error | 500 | le journal applicatif dit la cause | R-UPLOAD-015 |
not implemented | SSH_FX_OP_UNSUPPORTED | 501 | operation que ce backend n’a pas : Les types | R-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 ECDSA | R-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 conditionnelles | R-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 envois | R-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 main | R-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_reservation | R-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_prefix | R-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_bundle | R-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 stockage | R-HIDDEN-016, R-HIDDEN-011, R-HIDDEN-013 |
this filesystem does not support RENAME_NOREPLACE ... (WARN) | - | - | Performances et limites : NFS, FUSE | R-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’amont | R-AVAIL-001 |
Console et API
Code et detail | Ou c’est regle | Regle |
|---|---|---|
401 missing or invalid authorization header, invalid token | S’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 reconnecter | R-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 admin | R-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 revoke | Revoquer, se deconnecter, Partager les revocations ([admin.session]) : persist_file ou backend, NTP | R-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, backend | R-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 onglet | Dans la console ([admin.session]) : se reconnecter ; le jeton ne vit que dans la page | R-ADMIN-018 |
console blanche ou sans style derriere un reverse proxy, Refused to ... / violates the following Content Security Policy directive dans la console du navigateur | La console : la CSP du proxy permet au moins celle de la page | R-ADMIN-018 |
403 no matching admin roles ; 403 the <permission> permission is required | S’authentifier ([[admin.roles]]) | R-ADMIN-006, R-ADMIN-007 |
404 session not found: <id>, 400 invalid session ID format | Sessions | R-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 moment | Fichiers de log, Journaux et flux ([log]) | R-ADMIN-013 |
429 too many admin streams open: close one or retry later | Journaux et flux | R-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_listen | Une porte ou deux ([admin], [api]) : avec control_listen ou probes_listen, chaque route a son port | R-METRICS-001, R-REST-001 |
404 ou 401 sur /api/docs, /api/openapi.json | API REST de fichiers ([api]) : openapi = true | R-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 interroge | R-ADMIN-001, R-ADMIN-015 |
the admin door shares its listener with the file API ... (WARN) | Une porte ou deux ([admin]) : poser control_listen | R-ADMIN-002 |
admin TLS certificate expires soon, has expired, admin TLS certificate expiry could not be read | Ce que vous verrez ([admin.tls]) : renouveler ; un certificat expire est servi quand meme | R-ADMIN-004 |
Kubernetes
| Symptome ou message | Ou c’est regle | Regle |
|---|---|---|
sondes en echec, connection refused | Kubernetes ([sftp], [admin]) : listen = "0.0.0.0:..." ; sondes bloquees par la NetworkPolicy selon le CNI : le CIDR des noeuds dans networkPolicy.controlFrom | R-ADMIN-001 |
/readyz 503, data_runtime=unresponsive (runtime sature) ou sftp=not_accepting (port SFTP ferme, arret en cours) | Les sondes | R-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 pods | R-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 RoleBinding | R-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 NetworkPolicy | R-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]] partout | R-CLUSTER-009, R-CLUSTER-010, R-CLUSTER-008 |
backend = "configmap" refuse par un binaire sans la feature k8s | Les features du binaire | R-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 connexions | Memoire, 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
| Message | Ou c’est regle | Regle |
|---|---|---|
craft-file-gate: <n> log line(s) could not be written ... (stderr) | Une piste incomplete | R-AUDIT-031 |
audit target disabled by RUST_LOG, audit successes disabled by RUST_LOG | RUST_LOG (Variables d’environnement) : RUST_LOG=warn,audit=info | R-AUDIT-002 |
process resource sampling failed; the process_* series are absent from /metrics | Metriques | R-METRICS-010 |
OTLP collector unreachable — telemetry spans will be dropped until recovery | Envois, 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_secs | R-SHUTDOWN-007 |
ban file was still being written when the shutdown stopped waiting ; the shutdown stopped waiting for the removal of upload lock files; ... | Arret | R-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.
| Page | Sections |
|---|---|
[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
| Colonne | Sens |
|---|---|
| Cle | le 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 |
| Type | chaine, entier, booleen, liste, table, tableau de tables, chemin, adresse (ip:port), URL |
| Defaut | la valeur quand la cle est absente ; (obligatoire) : son absence refuse le demarrage ; - : pas de valeur propre |
| Effet | ce 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
| Regle | Effet |
|---|---|
| chemin relatif | relatif au repertoire du fichier de configuration, pas au repertoire courant |
roles.toml, users.toml a cote de la configuration | pris s’ils ne sont pas declares ; le journal de demarrage dit lesquels |
| variable d’environnement | remplace la valeur du fichier : voir Variables d’environnement |
| cle inconnue, valeur hors bornes, cle sans effet | Depannage |
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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
server | table | - | les reglages communs a toutes les portes |
server.shutdown_grace_period_secs | entier | 30 | secondes laissees au travail en cours de chaque porte a l’arret, une echeance pour toutes ; au moins 1 |
server.max_sessions_per_user | entier | illimite | sessions simultanees par utilisateur, sur une porte a sessions (SFTP) ; avec [cluster], celles de chaque instance joignable s’ajoutent |
server.probes_listen | adresse | absent : 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
server.hidden_stores | table | - | ecritures atomiques, pour tout backend et tout montage qui ne dit rien |
server.hidden_stores.enabled | booleen | false | ecrire dans un fichier en cours de transfert, publie par renommage a la fin |
server.hidden_stores.prefix | chaine | .in. | le debut du nom de ce fichier |
server.hidden_stores.extension | chaine | . | la fin du nom de ce fichier |
[server.backend_probe]
Voir Les backends.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
server.backend_probe | table | - | la sonde de disponibilite de chaque backend, en arriere-plan, jamais sur le chemin d’une operation |
server.backend_probe.enabled | booleen | true | false : aucune sonde, chaque backend reste unknown |
server.backend_probe.interval_secs | entier | 15 | secondes entre deux visites d’un backend, de 5 a 3600 |
server.backend_probe.timeout_secs | entier | 5 | duree maximale d’une visite, de 1 a 60, sous interval_secs (sinon refus du demarrage) |
server.backend_probe.failures_before_down | entier | 2 | visites 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
cluster | table | absente : chaque instance est seule | le canal entre les instances d’un deploiement ; CRAFT_FILE_GATE_CLUSTER_LISTEN la cree |
cluster.listen | adresse | - | le port du canal, a lui seul, en TLS 1.3 mutuel ; CRAFT_FILE_GATE_CLUSTER_LISTEN la remplace |
cluster.peers | chaine 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_file | chemin | - | 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_file | chemin | - | sa cle, creee en 0600 |
cluster.secret_name | chaine | - | ou le Secret Kubernetes partage, entrees tls.crt et tls.key (feature k8s) ; CRAFT_FILE_GATE_CLUSTER_SECRET_NAME la remplace |
cluster.peer_timeout_ms | entier | 2000 | delai d’un appel a un pair, de 100 a 10000 |
cluster.min_peers | entier | 1 | sous 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_when | liste | [] : alerter seulement | les 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_secs | entier | 30 | duree sous min_peers avant d’etre isolee ; retour immediat ; de 5 a 3600 |
cluster.unready_after_secs | entier | 60 | duree d’un detecteur choisi avant le retrait, de 10 a 3600, au moins 2 × server.backend_probe.interval_secs |
cluster.ready_after_secs | entier | 30 | duree 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
sftp | table | absent : porte SFTP eteinte | la porte SFTP (feature door-sftp) |
sftp.listen | adresse | (obligatoire) | adresse et port d’ecoute ; CRAFT_FILE_GATE_LISTEN la remplace |
sftp.host_keys | liste | (obligatoire) | les cles d’hote, fichiers presents : un chemin, ou une table { path, algorithms } |
sftp.host_keys[].path | chemin | (obligatoire) | le fichier de la cle privee, forme table |
sftp.host_keys[].algorithms | liste | selon la cle | les algorithmes de signature annonces pour cette cle, syntaxe de [sftp.algorithms] |
sftp.generate_host_key | chaine | absent | "ed25519" ou "ecdsa-p256" : cree la cle d’un fichier de host_keys absent ; une instance unique seulement |
sftp.login_grace_secs | entier | 120 | secondes pour s’authentifier, connexion coupee au-dela ; 0 desactive |
sftp.server_id | chaine | CraftFileGate_<version> | la banniere SSH-2.0-<server_id> |
sftp.inactivity_timeout_secs | entier | 600 | connexion sans paquet coupee apres ce delai ; 0 jamais ; au plus 86400 |
sftp.keepalive_interval_secs | entier | 0 | keepalive SSH apres ce silence du client ; 0 aucun ; au plus 86400 |
sftp.keepalive_max | entier | 3 | keepalives sans reponse avant la coupure ; 1 a 100 |
[sftp.algorithms]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
sftp.algorithms | table | - | les algorithmes SSH : +nom ajoute au defaut, -nom retire, une liste sans prefixe remplace |
sftp.algorithms.kex | liste | mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group16-sha512, diffie-hellman-group14-sha256 | echange de cles |
sftp.algorithms.ciphers | liste | chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com, aes256-ctr, aes192-ctr, aes128-ctr | chiffrements |
sftp.algorithms.macs | liste | hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512, hmac-sha2-256 | MAC |
sftp.algorithms.host_key | liste | ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, rsa-sha2-512, rsa-sha2-256 | signatures de la cle d’hote |
sftp.algorithms.user_key | liste | ssh-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-256 | signatures permises a une cle d’utilisateur ; ssh-rsa (SHA-1) hors defaut |
[sftp.ban]
Voir Bans et limites de debit.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
sftp.ban | table | absent : aucun ban | ban des adresses apres des echecs d’authentification sur la porte SFTP |
sftp.ban.max_failures | entier | 5 | echecs dans la fenetre avant le ban ; au moins 1 |
sftp.ban.ban_duration_secs | entier | 600 | duree d’un ban en secondes, a partir du verdict |
sftp.ban.window_secs | entier | 300 | fenetre de comptage des echecs en secondes, fixe, ouverte par le premier echec d’une serie |
sftp.ban.whitelist_ips | liste | [] | adresses et reseaux CIDR, IPv4 ou IPv6, jamais bannis par cette instance |
sftp.ban.trusted_proxies | liste | [] | sans effet sur SSH |
sftp.ban.persist_file | chemin | absent : en memoire | fichier ou les bans sont gardes et partages entre instances |
sftp.ban.backend | chaine | "file" | "file", ou "configmap" pour partager les bans entre pods Kubernetes |
sftp.ban.ban_configmap_name | chaine | craft-file-gate-bans | le ConfigMap des bans, avec backend = "configmap" |
sftp.ban.reread_interval_secs | entier | 5 | relecture du persist_file partage en secondes, en plus de la surveillance ; 1 a 30 |
[sftp.rate_limit]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
sftp.rate_limit | table | absent : aucune limite | seau a jetons par adresse, a l’acceptation d’une connexion |
sftp.rate_limit.connections_per_minute | entier | (obligatoire) | debit soutenu de connexions par adresse ; au moins 1 |
sftp.rate_limit.burst | entier | connections_per_minute | connexions 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
auth | table | (obligatoire) | l’authentification et les sources de roles |
auth.jwt_sentinel_username | chaine | jwt | le nom SSH qui demande une authentification par JWT (le jeton en mot de passe) |
auth.timeout_secs | entier | 5 | delai d’un appel au service d’autorisation |
auth.authz_base_url | URL | absent | service 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_file | chemin | roles.toml a cote, s’il existe | fichier des [[roles]] et [[backends]] ; son contenu est relu a chaud |
auth.users_file | chemin | users.toml a cote, s’il existe | fichier des [[users]] ; son contenu est relu a chaud |
auth.hash_workers | entier | les CPU vus (quota cgroup compris), au plus 2 | fils 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_queue | entier | 1024 | verifications qui peuvent attendre un fil ; 1 a 65536 ; CRAFT_FILE_GATE_HASH_QUEUE |
auth.hash_per_address | entier | 4 | verifications 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]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
auth.methods | table | - | les sources d’identite actives |
auth.methods.jwt | table | - | la methode JWT |
auth.methods.jwt.enabled | booleen | true | accepter les JWT (SFTP sous le nom sentinel, REST en Bearer) ; exige [auth.jwt] |
auth.methods.local | table | - | le magasin d’utilisateurs local |
auth.methods.local.enabled | booleen | false | activer le magasin local ([[users]] ou users_file) |
auth.methods.local.password | booleen | true | accepter le mot de passe comme preuve |
auth.methods.local.pubkey | booleen | true | accepter la cle publique SSH comme preuve |
auth.methods.local.allow_sha512_crypt | booleen | false | accepter aussi les hashes sha512-crypt, $6$ (migration) |
auth.methods.local.allow_bcrypt | booleen | false | accepter aussi les hashes bcrypt, $2a$, $2b$, $2y$ (migration) |
auth.methods.local.users_file | chemin | absent | fichier des utilisateurs, comme auth.users_file |
auth.methods.local.max_argon2_memory_kib | entier | absent : rien n’est verifie | budget memoire d’une verification, en Kio ; un hash qui le depasse est nomme au chargement ; au moins 1 |
[auth.jwt]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
auth.jwt | table | absente : aucun JWT verifie | la verification des JWT : une source de cle, et les claims |
auth.jwt.secret | chaine | absent | secret HMAC (HS256/384/512) ; CRAFT_FILE_GATE_JWT_SECRET ou _FILE |
auth.jwt.public_key_file | chemin | absent | cle publique PEM (RS*, ES*) |
auth.jwt.jwks_url | URL | absent | point JWKS du fournisseur d’identite ; exclusif de secret et public_key_file |
auth.jwt.jwks_refresh_interval_secs | entier | 3600 | periode du rafraichissement JWKS ; au moins 1 |
auth.jwt.algorithm | chaine | HS256 | HS256, HS384, HS512, RS256, RS384, RS512, ES256 ou ES384 |
auth.jwt.username_path | chaine | /sub | JSON Pointer du nom de l’utilisateur dans le jeton |
auth.jwt.authorities_path | chaine | /groups | JSON Pointer de ses authorities (roles) |
auth.jwt.issuer | chaine | absent | si present, iss est exige et compare |
auth.jwt.audience | chaine | absent | si present, aud est exige et compare |
[[users]]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
roles ⟳ | tableau de tables | (obligatoire), ici ou dans roles_file | les 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]]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
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 ⟳ | booleen | false | creer home_dir a la connexion s’il manque ; backend local seulement, voir Local |
roles[].mounts[].max_file_mb ⟳ | entier | absent : aucun plafond | taille maximale d’un fichier envoye, en Mo (1 048 576 octets) ; 0 : aucun envoi |
roles[].mounts[].acl ⟳ | tableau de tables | [] : tout refuse | les droits du montage, [[roles.mounts.acl]], voir ACL |
roles[].mounts[].hidden_stores ⟳ | table | celle du backend | ecritures atomiques de ce montage, cle par cle (voir Uploads) |
roles[].mounts[].hidden_stores.enabled ⟳ | booleen | celui du backend | voir server.hidden_stores.enabled |
roles[].mounts[].hidden_stores.prefix ⟳ | chaine | celui du backend | voir server.hidden_stores.prefix |
roles[].mounts[].hidden_stores.extension ⟳ | chaine | celle du backend | voir server.hidden_stores.extension |
[[roles.mounts.acl]]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
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 ⟳ | booleen | false | true : 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
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 ⟳ | table | celle de [uploads.stale_partials] | balayage des restes d’uploads interrompus de ce backend |
backends[].stale_partials.age_check ⟳ | booleen | celui de [uploads.stale_partials] | voir uploads.stale_partials.age_check |
backends[].stale_partials.grace_secs ⟳ | entier | celui de [uploads.stale_partials] | voir uploads.stale_partials.grace_secs |
backends[].hidden_stores ⟳ | table | celle de [server.hidden_stores] | ecritures atomiques de ce backend, cle par cle ; sans objet sur S3 et WebHDFS |
backends[].hidden_stores.enabled ⟳ | booleen | celui de [server.hidden_stores] | voir server.hidden_stores.enabled |
backends[].hidden_stores.prefix ⟳ | chaine | celui de [server.hidden_stores] | voir server.hidden_stores.prefix |
backends[].hidden_stores.extension ⟳ | chaine | celle de [server.hidden_stores] | voir server.hidden_stores.extension |
backends[].refuse_upload_over_directory ⟳ | booleen | false | S3 : 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 ⟳ | booleen | true (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 ⟳ | booleen | sonde (local), false (ailleurs) | comparer les chemins de l’ACL sans la casse ni la normalisation Unicode ; absente sur WebHDFS |
[[backends]] type = “local”
Voir Local.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
backends[].root ⟳ | chemin | (obligatoire) | local : le repertoire racine |
backends[].follow_symlinks ⟳ | booleen | false | local : suivre les liens symboliques qui restent sous root + home_dir du montage |
[[backends]] type = “sftp”
Voir Proxy SFTP.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
backends[].host ⟳ | chaine | (obligatoire) | proxy SFTP : l’amont |
backends[].port ⟳ | entier | 22 | proxy 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 ⟳ | booleen | false | proxy 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
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 ⟳ | URL | AWS S3 | S3 : 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
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 ⟳ | chemin | magasin du systeme | WebHDFS : les racines PEM qui authentifient url, seules ; une ancre de confiance |
backends[].impersonate ⟳ | booleen | true | WebHDFS : doAs=<utilisateur> sur chaque requete ; false : tout part sous le compte de service |
backends[].read_ahead_bytes ⟳ | entier | 4194304 | WebHDFS : taille d’une plage lue ; 65536 a 67108864 |
backends[].timeout_secs ⟳ | entier | 30 | WebHDFS : 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
admin | table | absent : aucune porte HTTP | l’ecouteur d’administration, de l’API et de l’explorateur |
admin.listen | adresse | (obligatoire) | adresse d’ecoute ; 0.0.0.0 dans un conteneur ; CRAFT_FILE_GATE_ADMIN_LISTEN |
admin.control_listen | adresse | absent | une porte a part pour /admin, la console, /metrics et les sondes (sauf avec server.probes_listen) ; CRAFT_FILE_GATE_ADMIN_CONTROL_LISTEN |
admin.bearer_token | chaine | absent | jeton qui a toutes les permissions ; lui ou un role de [[admin.roles]] est exige ; CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN, _FILE |
admin.allow_static_token | booleen | true | false : aucun jeton statique, d’aucune source |
admin.header_read_timeout_secs | entier | 10 | secondes pour envoyer les en-tetes d’une requete ; 1 a 300 ; CRAFT_FILE_GATE_ADMIN_HEADER_READ_TIMEOUT_SECS |
admin.long_request_threshold_secs ⟳ | entier | 30 | secondes apres lesquelles un transfert REST en cours apparait dans les sessions de la console ; 0 : tous |
[[admin.roles]]
Voir Console et API d’administration.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
admin.roles | tableau de tables | [] | les roles admin nommes |
admin.roles[].name | chaine | (obligatoire) | le nom qu’une authority porte (compte local ou JWT) ; unique, distinct des noms de [[roles]] |
admin.roles[].permissions | liste | (obligatoire, non vide) | parmi overview, sessions, kick, bans, unban, config, logs, audit, revoke |
[admin.session]
Voir Console et API d’administration.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
admin.session | table | cle par processus, 1 h, 12 h, revocations en memoire | les jetons de session de la console, signes par le serveur, et leurs revocations |
admin.session.key_file | chemin | absent | la 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_name | chaine | absent | le 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_secs | entier | 3600 | vie d’un jeton ; 60 a 86400 |
admin.session.max_age_secs | entier | 43200 | plus de renouvellement au-dela, depuis la connexion ; 60 a 604800, au moins ttl_secs |
admin.session.persist_file | chemin | absent | le 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.backend | chaine | file avec persist_file, memoire sinon | file (avec persist_file) ou configmap (feature k8s), une ConfigMap comme les bans ; CRAFT_FILE_GATE_ADMIN_SESSION_BACKEND |
admin.session.revocation_configmap_name | chaine | craft-file-gate-access | la 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_secs | entier | 5 | relecture 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).
| Cle | Type | Defaut | Effet |
|---|---|---|---|
admin.grants | table | en memoire, 7 jours au plus | ou les acces temporaires sont partages, et le plus long |
admin.grants.persist_file | chemin | absent | le 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.backend | chaine | file avec persist_file, memoire sinon | file ou configmap (feature k8s) ; CRAFT_FILE_GATE_ADMIN_GRANTS_BACKEND |
admin.grants.grant_configmap_name | chaine | craft-file-gate-access | la ConfigMap, cle grants, a cote des revocations ; CRAFT_FILE_GATE_ADMIN_GRANTS_GRANT_CONFIGMAP_NAME |
admin.grants.reread_interval_secs | entier | 5 | relecture des acces partages, en secondes ; 1 a 30 |
admin.grants.max_duration_secs | entier | 604800 | duree maximale d’un acces ; 60 a 2592000 |
[admin.tls]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
admin.tls | table | absent : HTTP | HTTPS sur tout l’ecouteur |
admin.tls.cert_file | chemin | absent | certificat PEM, relu quand le fichier change ; CRAFT_FILE_GATE_ADMIN_TLS_CERT |
admin.tls.key_file | chemin | absent | cle privee PEM ; CRAFT_FILE_GATE_ADMIN_TLS_KEY |
admin.tls.auto_generate | booleen | false | generer un certificat auto-signe (essais) |
admin.tls.auto_generate_cn | chaine | localhost | son Common Name |
admin.tls.auto_generate_dir | chemin | . | ou l’ecrire |
admin.tls.auto_generate_validity_days | entier | 31 | sa validite en jours |
[admin.ban]
Memes cles que [sftp.ban]. Voir Bans et limites de debit.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
admin.ban | table | absent : aucun ban | ban des adresses apres des echecs d’authentification sur l’API d’administration ; l’API de fichiers a sa liste, [api.ban] |
admin.ban.max_failures | entier | 5 | echecs dans la fenetre avant le ban ; au moins 1 |
admin.ban.ban_duration_secs | entier | 600 | duree d’un ban en secondes, a partir du verdict |
admin.ban.window_secs | entier | 300 | fenetre de comptage des echecs en secondes, fixe, ouverte par le premier echec d’une serie |
admin.ban.whitelist_ips | liste | [] | adresses et reseaux CIDR, IPv4 ou IPv6, jamais bannis par cette instance |
admin.ban.trusted_proxies | liste | [] | proxys dont X-Forwarded-For donne l’adresse du client ; sans control_listen, egale a api.ban.trusted_proxies |
admin.ban.persist_file | chemin | absent : en memoire | fichier ou les bans sont gardes et partages entre instances |
admin.ban.backend | chaine | "file" | "file", ou "configmap" pour partager les bans entre pods Kubernetes |
admin.ban.ban_configmap_name | chaine | craft-file-gate-bans | le ConfigMap des bans, avec backend = "configmap" |
admin.ban.reread_interval_secs | entier | 5 | relecture du persist_file partage en secondes, en plus de la surveillance ; 1 a 30 |
[admin.metrics_thresholds]
Voir Metriques.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
admin.metrics_thresholds | table | - | seuils d’alerte de l’onglet Metrics de la console |
admin.metrics_thresholds.hash_pool_percent | entier | 80 | places prises du pool de hachage, en % |
admin.metrics_thresholds.tls_cert_days | entier | 14 | jours restants du certificat TLS admin, en dessous |
admin.metrics_thresholds.rejections_per_minute | entier | 60 | refus SFTP par minute |
admin.metrics_thresholds.jwt_refusals_per_minute | entier | 60 | JWT refuses par minute |
admin.metrics_thresholds.jwks_age_secs | entier | 2 x jwks_refresh_interval_secs | age du cache JWKS |
admin.metrics_thresholds.clock_skew_secs | entier | 30 | ecart d’horloge d’un stockage |
admin.metrics_thresholds.cpu_percent | entier | 90 | CPU du processus, 100 = un coeur |
[api]
Voir Portes.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
api | table | absent : pas d’API | l’API REST de fichiers, sur admin.listen |
api.enabled | booleen | false | servir l’API, sur l’ecouteur de [admin] ; exige [admin] |
api.prefix | chaine | /api/v1/files | chemin sous lequel l’API est servie |
api.cors_origins | liste | absent : pas de CORS | origines admises en CORS ; "*" : toutes (voir Bans) |
api.openapi | booleen | false | servir 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
api.ban | table | absent : aucun ban | ban des adresses apres des echecs d’authentification sur l’API de fichiers, ses tickets et l’explorateur |
api.ban.max_failures | entier | 5 | echecs dans la fenetre avant le ban ; au moins 1 |
api.ban.ban_duration_secs | entier | 600 | duree d’un ban en secondes, a partir du verdict |
api.ban.window_secs | entier | 300 | fenetre de comptage des echecs en secondes, fixe, ouverte par le premier echec d’une serie |
api.ban.whitelist_ips | liste | [] | adresses et reseaux CIDR, IPv4 ou IPv6, jamais bannis par cette instance, ni plafonnes dans la file de hachage |
api.ban.trusted_proxies | liste | [] | 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_file | chemin | absent : en memoire | fichier ou les bans sont gardes et partages entre instances |
api.ban.backend | chaine | "file" | "file", ou "configmap" pour partager les bans entre pods Kubernetes |
api.ban.ban_configmap_name | chaine | craft-file-gate-bans | le ConfigMap des bans, avec backend = "configmap" |
api.ban.reread_interval_secs | entier | 5 | relecture du persist_file partage en secondes, en plus de la surveillance ; 1 a 30 |
[api.rate_limit]
Voir Bans et limites de debit.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
api.rate_limit | table | absent : aucune limite | seau a jetons par adresse pour l’API |
api.rate_limit.requests_per_minute | entier | (obligatoire) | debit soutenu de requetes par adresse ; au moins 1 |
api.rate_limit.burst | entier | requests_per_minute | requetes acceptees d’affilee ; au moins 1 |
[api.ui]
Voir Explorateur de fichiers.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
api.ui | table | - | l’explorateur de fichiers web |
api.ui.enabled | booleen | false | servir l’explorateur ; exige [api] |
api.ui.path | chaine | /files | ou 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]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
log | table | - | les journaux et la piste d’audit |
log.level ⟳ | chaine | info | trace, 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.format | chaine | json | json (une ligne JSON par evenement) ou pretty (lisible) |
log.audit ⟳ | chaine | all | volume de la piste : all, changes ou failures ; refus et erreurs toujours ecrits |
log.dir | chemin | absent : stdout seul | repertoire des fichiers de log et d’audit, tournes et compresses ; CRAFT_FILE_GATE_LOG_DIR |
log.stdout | booleen | true | ecrire aussi sur stdout ; false exige dir |
log.max_file_size_mb | entier | 100 | rotation avant cette taille, en Mio ; 1 a 1048576 |
log.retention_days | entier | 7 | jours de conservation des archives du journal applicatif ; 1 a 36500 |
log.audit_retention_days | entier | 90 | jours de conservation des archives de la piste d’audit ; 1 a 36500 |
log.refusal_summary_threshold ⟳ | entier | 10 | refus identiques ecrits par fenetre avant une ligne resumee ; 1 a 1000000 |
log.refusal_summary_window_secs ⟳ | entier | 60 | duree de cette fenetre ; 1 a 86400 |
log.refusal_summary_max_addresses ⟳ | entier | 10000 | refus distincts suivis a la fois ; 1 a 1000000 |
[telemetry]
Voir Telemetrie.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
telemetry | table | absent | l’export OpenTelemetry (OTLP) |
telemetry.enabled | booleen | false | exporter les traces |
telemetry.otlp_endpoint | URL | http://localhost:4317 | le collecteur OTLP, URL http:// ou https:// |
telemetry.service_name | chaine | craft-file-gate | l’attribut service.name ; service.version est la version du binaire |
telemetry.protocol | chaine | grpc | grpc (OTLP/gRPC, port 4317) ou http (OTLP/HTTP protobuf, port 4318) |
telemetry.metrics | booleen | false | exporter aussi les metriques en OTLP, vers le meme collecteur |
telemetry.metrics_interval_secs | entier | 60 | periode de cet export, en secondes ; 1 a 3600 |
telemetry.on_exporter_error | chaine | refuse | un exporteur impossible a construire : refuse (demarrage refuse) ou warn (demarrage sans ce signal) |
[uploads]
Voir Delais.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
uploads | table | - | les delais des envois, sur toutes les portes |
uploads.idle_timeout_secs | entier | 30 | envoi abandonne apres ce delai sans octet ; 1 a 86400 ; CRAFT_FILE_GATE_UPLOAD_IDLE_TIMEOUT_SECS |
uploads.min_rate_bytes_per_sec | entier | 0 : desactive | debit moyen minimal d’un envoi depuis son debut, exige passe la grace ; au plus 1073741824 |
uploads.min_rate_grace_secs | entier | 60 | attente avant d’exiger ce debit ; 1 a 86400 |
uploads.takeover_idle_secs ⟳ | entier | 10 | un 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.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
uploads.stale_partials | table | - | balayage des restes d’envois interrompus |
uploads.stale_partials.age_check | booleen | true | mesurer l’age sur l’horloge du stockage (un fichier sonde du meme repertoire), pas sur celle du serveur |
uploads.stale_partials.grace_secs | entier | 2 x idle_timeout_secs + max(300, idle_timeout_secs), soit 360 s | age a partir duquel un reste est supprime ; plus de 2 x idle_timeout_secs + 60, au plus 2592000 (30 jours) |
[tcp_keepalive]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
tcp_keepalive | table | - | keepalive TCP de toute connexion acceptee |
tcp_keepalive.enabled | booleen | true | activer SO_KEEPALIVE |
tcp_keepalive.idle_secs | entier | 30 | silence avant la premiere sonde ; 1 a 32767 |
tcp_keepalive.interval_secs | entier | 10 | ecart entre deux sondes ; 1 a 32767 |
tcp_keepalive.count | entier | 3 | sondes sans reponse avant la coupure ; 1 a 127 |
tcp_keepalive.user_timeout_secs | entier | 0 : derive de idle_timeout_secs | Linux : duree pendant laquelle des octets envoyes peuvent rester sans acquittement, puis coupure ; 5 a 86400 |
[reload]
| Cle | Type | Defaut | Effet |
|---|---|---|---|
reload | table | - | comment un fichier modifie est remarque |
reload.watch | chaine | auto | auto : inotify, et la relecture periodique si inotify ne peut pas servir ; inotify : inotify obligatoire ; poll : relecture periodique seulement, aucune instance inotify |
reload.poll_interval_secs | entier | 5 | periode de la relecture periodique, en secondes ; 1 a 60 |
[security]
Voir Fichiers de confiance et TLS.
| Cle | Type | Defaut | Effet |
|---|---|---|---|
security | table | - | le jugement des fichiers de confiance |
security.allow_group_writable_trust_anchors | booleen | false | un 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
| Variable | Cle remplacee | Valeur |
|---|---|---|
CRAFT_FILE_GATE_LISTEN | sftp.listen | ip:port ; avec [sftp] |
CRAFT_FILE_GATE_PROBES_LISTEN | server.probes_listen | ip:port |
CRAFT_FILE_GATE_ADMIN_LISTEN | admin.listen | ip:port |
CRAFT_FILE_GATE_ADMIN_CONTROL_LISTEN | admin.control_listen | ip:port |
CRAFT_FILE_GATE_ADMIN_HEADER_READ_TIMEOUT_SECS | admin.header_read_timeout_secs | entier, 1 a 300 |
CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN | admin.bearer_token | le jeton |
CRAFT_FILE_GATE_ADMIN_BEARER_TOKEN_FILE | admin.bearer_token | un fichier qui contient le jeton |
CRAFT_FILE_GATE_ADMIN_TLS_CERT | admin.tls.cert_file | chemin ; cree [admin.tls] s’il manque |
CRAFT_FILE_GATE_ADMIN_TLS_KEY | admin.tls.key_file | chemin ; cree [admin.tls] s’il manque |
CRAFT_FILE_GATE_ADMIN_SESSION_SECRET_NAME | admin.session.secret_name | le nom du Secret ; le chart la pose |
CRAFT_FILE_GATE_ADMIN_SESSION_BACKEND | admin.session.backend | file ou configmap ; le chart la pose a cote du Secret partage |
CRAFT_FILE_GATE_ADMIN_SESSION_REVOCATION_CONFIGMAP_NAME | admin.session.revocation_configmap_name | le nom de la ConfigMap des revocations ; le chart la pose |
CRAFT_FILE_GATE_ADMIN_GRANTS_BACKEND | admin.grants.backend | file ou configmap ; le chart la pose |
CRAFT_FILE_GATE_ADMIN_GRANTS_GRANT_CONFIGMAP_NAME | admin.grants.grant_configmap_name | le nom de la ConfigMap des acces temporaires ; le chart la pose |
CRAFT_FILE_GATE_SFTP_BAN_CONFIGMAP_NAME | sftp.ban.ban_configmap_name | le nom de la ConfigMap des bans ; le chart la pose |
CRAFT_FILE_GATE_API_BAN_CONFIGMAP_NAME | api.ban.ban_configmap_name | de meme |
CRAFT_FILE_GATE_ADMIN_BAN_CONFIGMAP_NAME | admin.ban.ban_configmap_name | de meme |
CRAFT_FILE_GATE_CLUSTER_LISTEN | cluster.listen | ip:port ; cree [cluster] s’il manque ; le chart la pose |
CRAFT_FILE_GATE_CLUSTER_PEERS | cluster.peers | dns:<nom>:<port>, ou des <hote>:<port> separes par des virgules ; le chart la pose |
CRAFT_FILE_GATE_CLUSTER_SECRET_NAME | cluster.secret_name | le nom du Secret ; le chart la pose |
CRAFT_FILE_GATE_CLUSTER_MIN_PEERS | cluster.min_peers | entier ; le chart la pose (cluster.minPeers) |
CRAFT_FILE_GATE_CLUSTER_UNREADY_WHEN | cluster.unready_when | detecteurs separes par des virgules ; le chart la pose avec cluster.unreadyWhen |
CRAFT_FILE_GATE_JWT_SECRET | auth.jwt.secret | le secret HMAC |
CRAFT_FILE_GATE_JWT_SECRET_FILE | auth.jwt.secret | un fichier qui contient le secret |
CRAFT_FILE_GATE_HASH_WORKERS | auth.hash_workers | entier, 1 a 1024 |
CRAFT_FILE_GATE_HASH_QUEUE | auth.hash_queue | entier, 1 a 65536 |
CRAFT_FILE_GATE_LOG_LEVEL | log.level | trace … off |
CRAFT_FILE_GATE_LOG_DIR | log.dir | chemin |
CRAFT_FILE_GATE_UPLOAD_IDLE_TIMEOUT_SECS | uploads.idle_timeout_secs | entier, 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.
| Regle | Effet |
|---|---|
| le fichier | un fichier de confiance, en UTF-8, non vide ; une seule des deux formes X et X_FILE |
| un saut de ligne final | retire |
| fichier modifie | pris au prochain redemarrage |
Un Secret Kubernetes monte en lecture seule convient tel quel : voir Secrets.
Les autres variables lues
| Variable | Effet |
|---|---|
RUST_LOG | remplace 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_PROTOCOL | lues pour dire en WARN qu’elles contredisent [telemetry] protocol ; le fichier decide |
SSL_CERT_FILE, SSL_CERT_DIR | les racines TLS, si l’image n’en porte pas (:scratch) |
TOKIO_WORKER_THREADS | fils du runtime principal ; cite sur la ligne de demarrage |
HOSTNAME | le 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
action | source | Ecrite quand |
|---|---|---|
list | sftp, api | listage d’un repertoire |
download | sftp, api | fin du transfert |
upload | sftp, api | fin du transfert |
mkdir | sftp, api | une ligne par repertoire cree, du plus exterieur au plus interieur |
delete | sftp, api | SFTP REMOVE d’un fichier ; REST DELETE d’un fichier ou d’un repertoire vide |
rmdir | sftp | RMDIR d’un repertoire vide |
delete_recursive | api | DELETE d’un repertoire non vide |
rmdir_recursive | sftp | RMDIR d’un repertoire non vide |
rename | sftp, api | renommage ou deplacement |
stat | sftp, api | refus seulement ; REST HEAD |
setstat | sftp | refus seulement |
request | api | requete refusee avant le handler sans route pour sa methode, ou GET ?rights |
connection_accepted | sftp, api, admin | authentification reussie : une par session SFTP, une par requete REST, une par fenetre admin de 15 min |
connection_rejected | sftp, api, admin | authentification refusee |
connection_rejected_summary | sftp, api, admin | refus anonymes repetes, resumes a la fin de la fenetre |
ip_banned | sftp, api, admin | une adresse est bannie, une fois par bascule |
session_end | sftp | une session SFTP quitte le registre |
session_kick | admin | DELETE /admin/sessions/{id} |
unban | admin | DELETE /admin/bans/{protocol}/{ip} |
logout | admin | POST /admin/logout sous un JWT ou le jeton statique : rien n’est revoque |
session_revoke | admin | POST /admin/revocations, POST /admin/logout sous un jeton de session : username le compte revoque, admin l’auteur |
grant_add, grant_revoke | admin | POST /admin/grants, DELETE /admin/grants/{id} : username le beneficiaire, role, grant_id, until, grant_reason (le motif), lift |
audit_read | admin | lecture 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_candidates | admin | refus insufficient permission d’une route en lecture |
Les resultats
result | Sens |
|---|---|
success | l’operation a abouti |
denied | le serveur l’a refusee |
error | le stockage l’a fait echouer, ou rien n’a pu etre construit pour elle, ou elle a ete interrompue |
unknown | le serveur ne sait pas ce qu’elle est devenue (commit interrompu) |
Les champs
| Champ | Lignes | Sens |
|---|---|---|
source | toutes | la porte : sftp, api, admin ; pour ip_banned, la liste du ban |
action, result | toutes | ci-dessus |
reason | denied, error, unknown ; absent sur success | une chaine fixe, ci-dessous |
username | toutes sauf les resumes | le nom presente ; vide pour un jeton non verifie |
remote_addr | toutes | l’IP du client ; avec le port sur connection_accepted et session_end SFTP |
session_id | operations SFTP, session_end, session_kick | l’identifiant de la session |
path | operations | le chemin vu par l’utilisateur ; la source d’un rename |
new_path | rename | la destination |
backend | operations | le nom du backend du montage ; "" sur un chemin synthetique |
count | upload, download, list | octets transferes, ou entrees listees ; 0 ailleurs |
replaced | upload | yes, no, unknown : l’upload a-t-il detruit un contenu present |
removed | delete_recursive, rmdir_recursive | entrees supprimees ; unknown sur un backend local |
took_over | upload | l’upload bloque du meme compte que celui-ci a repris : son session_id SFTP, ou son transfer_id REST ; "" ailleurs |
transfer_id | upload REST | l’identifiant du transfert, celui de la console ; "" ailleurs |
grant_id | operations | l’acces temporaire par lequel est venu le montage du chemin (plusieurs : separes par des virgules) ; "" ailleurs |
auth_method | connection_* | SFTP password, pubkey, jwt ; API basic, bearer, ticket ; admin jwt, static_token, session, password (connexion a la console) ; none sans credential |
signature_algorithm | connection_accepted SFTP | algorithme de la signature de la cle |
suppressed, threshold, window_secs, overflow | connection_rejected_summary | refus non ecrits, et les reglages de la fenetre |
ban_duration_secs, expires_at_epoch | ip_banned | duree et fin du ban |
duration_secs, bytes_read, bytes_written | session_end | duree et volume de la session |
admin, admin_addr, admin_auth_method | actions admin | l’auteur |
kicked_by | session_kick | l’auteur du kick |
protocol, lift | unban | la liste (sftp, api, admin) ; held, local_only, overruled, not_banned |
lift | session_revoke | held, local_only |
permission | refus insufficient permission | la permission manquante |
lines, levels, since, until, q, fields | audit_read | la 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 :
reason | result | Sens |
|---|---|---|
acl | denied | l’ACL ne donne pas le droit |
acl subtree | denied | suppression d’un arbre que l’ACL ne couvre pas en entier |
synthetic path | denied | ecriture sur un repertoire synthetique ou un point de montage |
rename across mounts | denied | source et destination sous deux montages |
invalid path | denied | caractere de controle, \, forme Windows interdite |
reserved name | denied | nom que le serveur se reserve (verrou d’upload) |
rename into restricted | denied | renommage qui deposerait du contenu sans write |
range not satisfiable | denied | telechargement REST dont le Range commence apres la fin du fichier (416) |
symlink escape | denied | lien symbolique hors de la racine locale |
exists | denied | destination existante, ou creation exclusive sur un nom pris |
is a directory, not a directory | denied | REMOVE d’un repertoire, RMDIR d’un fichier |
upload in progress | denied, error | un autre upload tient la destination |
taken over | error | upload bloque repris par un nouvel upload du meme compte (uploads.takeover_idle_secs) |
quota exceeded (et , truncated file removed, , append not undone) | denied | max_file_mb depasse |
session killed | denied | session coupee pendant l’operation |
no roles, username not usable as home directory | denied | REST : aucun role, nom inutilisable |
role resolution, mount conflict | error | REST : 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 progress | error | genre de l’erreur du stockage |
session ended: <cause> | error | session SFTP finie pendant l’operation ; <cause> est le reason du session_end |
session ended | error | client REST parti pendant la requete |
upload idle timeout, upload below minimum rate | error | upload coupe par le serveur |
commit interrupted | unknown | client parti pendant la publication d’un upload |
Authentification (connection_rejected, connection_rejected_summary) :
reason | result | Sens |
|---|---|---|
| absent | denied | mot de passe faux, nom inconnu, cle ou JWT SFTP refuse : rien ne dit quels comptes existent |
no authorized key offered | denied | aucune cle proposee n’etait autorisee |
signature algorithm not allowed | denied | algorithme de signature de la cle non permis |
missing credential, empty credential, malformed credential | denied | en-tete Authorization absent, vide, illisible |
invalid token, expired token, wrong issuer, wrong audience | denied | jeton refuse par le verificateur |
verifier unavailable | denied, error | aucune cle en face du jeton, ou rien pour verifier |
no username claim | denied | jeton verifie sans nom |
method disabled | denied | methode d’authentification coupee |
no matching roles, no matching admin roles | denied | credential acceptee, aucun role |
role resolution, mount conflict, backend initialization failed | error | credential acceptee, session impossible a construire |
username not usable as home directory | denied | le nom ne peut pas etre un repertoire |
session limit | denied | max_sessions_per_user atteint |
banned, rate limit | denied | adresse bannie, debit depasse |
password checks saturated, password checks saturated for address, shutting down | error | mot de passe non verifie : pool plein, part de l’adresse atteinte, arret |
invalid ticket, expired ticket, revoked ticket | denied | ticket de telechargement refuse |
revoked token | denied | jeton 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 ProFTPD | Devient |
|---|---|
| mot de passe | password_hash |
| uid, gid | rien : tout est ecrit par le compte du service, sans setuid |
| home | home_dir du montage, {username} pour %u |
| shell, gecos | rien : 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
| ProFTPD | CraftFileGate |
|---|---|
AuthUserFile | auth.methods.local.users_file |
AuthGroupFile | authorities et [[roles]] |
DefaultRoot ~ | home_dir = "/{username}" |
CreateHome on | create_home = true (backend local) |
<Directory> et <Limit> | [[roles.mounts.acl]], voir plus bas |
MaxLoginAttempts | [sftp.ban] max_failures : bannit l’adresse |
MaxClientsPerUser | server.max_sessions_per_user |
MaxClients | rien ; [sftp.rate_limit] borne le debit de connexions |
HiddenStores on | [server.hidden_stores] enabled = true ; desactive par defaut, comme ProFTPD |
mod_quotatab | max_file_mb du montage : un plafond par fichier, pas un volume cumule |
TransferLog, ExtendedLog | la piste d’audit, voir Audit |
TLSEngine (FTPS), PassivePorts, MasqueradeAddress | rien : SFTP sur un seul port TCP |
SFTPHostKey | sftp.host_keys |
SFTPAuthorizedUserKeys | authorized_keys de l’utilisateur |
mod_sql, mod_ldap | [auth.jwt] ou auth.authz_base_url |
Umask | l’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 FTP | Droit |
|---|---|
RETR, READ | read |
STOR, APPE, MKD, WRITE | write |
LIST, NLST, CWD, DIRS | list |
DELE, RMD | delete |
RNFR, RNTO | rename |
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
| Sujet | CraftFileGate |
|---|---|
reprise d’upload (AllowStoreRestart) | servie, sauf avec hidden_stores et sur S3 |
| propriete des fichiers | le compte du service ; une autre chaine passe par les droits du repertoire (setgid, ACL POSIX) |
| FTP et FTPS | non servis : chaque client passe a SFTP ou a l’API REST |
- Inventorier les clients ; extraire de
ftpd.passwdnoms, hashes et homes. - Ecrire
roles.tomletusers.toml; lancer sur un autre port. - Valider avec un compte temoin (chaque droit, un refus), distribuer les secrets.
- Couper ProFTPD, basculer le port, suivre la piste d’audit.