Aller au contenu principal

Sécurité et comptes

SekoiaCraft est fait pour être mis en ligne devant une communauté restreinte. Deux frontières s'y répondent, et elles ne protègent pas la même chose :

  • une session ferme l'interface : il faut un compte pour ouvrir un écran ;
  • un secret partagé ferme les cinq API : il faut le connaître pour les interroger.

La seconde est la moins visible et la plus importante. En dev, preprod et prod, les cinq backends sont publiés sur Internet par Traefik, chacun sur son propre nom d'hôte : sans ce secret, n'importe qui pourrait lire vos communautés, vos jeux de données et vos réglages sans jamais voir l'écran de connexion.

Les comptes​

Deux rôles, et pas un de plus.

RôleCe qu'il peut faire
AdministrateurTout, y compris créer, modifier et supprimer les comptes.
LecteurOuvrir tous les écrans, n'en modifier aucun.

Un compte se ferme (Fermer le compte) plutôt qu'il ne se supprime : la suppression efface aussi la trace qu'il a existé, et sa date de dernière connexion avec. Un compte fermé reste dans la liste et ne peut plus se connecter.

Le dernier administrateur actif est protégé. Le rétrograder, le fermer ou le supprimer est refusé tant qu'aucun autre ne peut prendre le relais — sinon plus personne ne pourrait rouvrir l'écran des comptes.

La première connexion​

Le compte administrateur est créé au premier chargement des données de référence (pnpm db:seed, ou le conteneur migrate en environnement déployé) :

  • adresse : admin@sekoiacraft.local ;
  • mot de passe : tiré au hasard et imprimé une seule fois, dans la sortie de cette étape.

Ce mot de passe n'existe nulle part ailleurs : la base ne porte que son empreinte bcrypt, et aucun fichier ne le reçoit. Notez-le, connectez-vous, et changez-le immédiatement depuis Configuration → Utilisateurs.

Sur un poste, il se lit dans les journaux du conteneur qui a semé :

docker compose -f infrastructure/docker/docker-compose.dev.yml logs migrate

Sur un environnement déployé, il faut aller le lire sur le serveur, dans le répertoire de déploiement (/home/<SSH_USER>/sekoiacraft/<env> par défaut — un répertoire par environnement, pour que dev et preprod partagent un hôte sans partager un .env) :

ssh <SSH_USER>@<SSH_HOST>
cd ~/sekoiacraft/prod
docker compose -f docker-compose.prod.yml --env-file .env logs migrate
Ces journaux ne survivent pas au déploiement suivant

Le pipeline ne les rapatrie pas — le mot de passe n'apparaît jamais dans la sortie d'un job GitLab, et c'est délibéré : les journaux de job sont conservés et lisibles par tous les mainteneurs du projet.

Et deploy.sh commence chaque déploiement par un docker compose down, qui supprime le conteneur migrate et ses journaux avec. La fenêtre de lecture se referme donc au déploiement d'après. Ajoutez à cela que la bannière n'est imprimée qu'une fois dans la vie de l'environnement — le semis est create-only, et les déploiements suivants se contentent de constater que le compte existe.

En pratique : relevez le mot de passe juste après la première mise en ligne, ou choisissez-le vous-même par la section suivante.

En dev, chaque déploiement est une première installation

Un commit sur develop redéploie dev à partir de zéro : deploy.sh supprime les conteneurs, les volumes et les images applicatives du déploiement précédent avant de lancer le nouveau. La base repart vide, le compte administrateur est recréé, et son mot de passe est re-tiré à chaque commit — celui relevé la veille n'ouvre plus rien. Posez DEV_ADMIN_INITIAL_PASSWORD (section suivante) et le compte garde le même mot de passe d'un déploiement à l'autre.

La preprod et la prod gardent leurs volumes : le compte, comme tout le reste, survit à leurs déploiements, et prisma migrate deploy y fait évoluer le schéma sans rien perdre.

Choisir le mot de passe initial depuis un coffre​

Posez ADMIN_INITIAL_PASSWORD dans l'environnement du semis : il est alors utilisé tel quel, et n'est jamais imprimé — il vient déjà d'un coffre, l'inscrire dans des journaux serait l'en ressortir.

  • Sur un poste : une ligne dans le .env de la racine, ou en tête de commande (ADMIN_INITIAL_PASSWORD=… pnpm db:seed).
  • En déploiement : une variable GitLab <ENV>_ADMIN_INITIAL_PASSWORD, marquée « Masquée », dans Settings → CI/CD → Variables. C'est la seule variable d'environnement que deploy.sh n'exige pas : sans elle, le déploiement se fait normalement et le mot de passe est tiré.

Elle n'est lue qu'à la première installation. La poser sur un environnement déjà semé ne change rien tant que le compte existe — voyez la procédure ci-dessous.

Le mot de passe initial est perdu​

Il n'y a pas de « mot de passe oublié » : rien n'est envoyé par courriel, et le semis rejoué ne réinitialise jamais un compte en service (Comptes : administrateur déjà présent — inchangé). Ce serait couper l'accès dans le dos de son propriétaire, et le nouveau mot de passe partirait dans les journaux.

Si un autre administrateur peut encore se connecter, la réponse est immédiate : qu'il ouvre Configuration → Utilisateurs et pose un nouveau mot de passe sur le compte concerné.

Sinon, la voie de secours est de retirer le compte semé et de le laisser se recréer. Sur le serveur, dans le répertoire de déploiement :

cd ~/sekoiacraft/prod
# Le .env écrit par la CI porte POSTGRES_USER et POSTGRES_DB ; le shell, non.
set -a; . ./.env; set +a
C="docker compose -f docker-compose.prod.yml --env-file .env"

# 1. Retirer le compte semé.
$C exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-c "delete from app_user where email = 'admin@sekoiacraft.local';"

# 2. Rejouer le seul semis des comptes.
$C run --rm migrate sh -c 'node prisma/seed/seed-app-users.ts'

Deux choses méritent d'être sues sur ces deux lignes :

  • La seconde imprime la bannière dans votre propre terminal, et non dans les journaux d'un conteneur que le prochain déploiement effacera. C'est la façon robuste de récupérer le mot de passe, et elle vaut aussi bien pour une première installation. Si <ENV>_ADMIN_INITIAL_PASSWORD est posée, la bannière ne montre rien : le mot de passe est celui du coffre, comme prévu.
  • La première contourne le garde-fou du dernier administrateur, qui vit dans api-config et non en SQL. C'est précisément pour cela que c'est une procédure de secours et non un bouton de l'écran : entre les deux commandes, l'installation peut n'avoir aucun administrateur. Enchaînez-les.

La même paire fonctionne sur un poste, avec pnpm db:seed à la place de l'étape 2 — elle ne recrée que ce qui manque.

Aucun mot de passe n'est écrit dans le dépôt, et c'est délibéré : un admin/admin versionné est connu de tout lecteur du code, y compris en production, et il y survit d'autant plus longtemps que personne ne se souvient qu'il existe.

Le secret partagé entre le frontend et les API​

Une seule variable, la même partout : INTERNAL_API_KEY. Le frontend la pose en en-tête x-internal-key sur chacun de ses appels ; les cinq backends la vérifient sur toutes leurs routes.

# Une valeur au hasard, à poser dans le .env de l'installation
openssl rand -base64 32

Ce qu'elle authentifie est l'appelant, jamais un utilisateur. Les backends n'ont aucune notion d'acteur : les rôles vivent dans la session du frontend, et c'est lui qui refuse une écriture à un lecteur. Un jeton porteur d'identité n'aurait aucun usage ici, et il faudrait le révoquer, le faire tourner et accorder les horloges de cinq services qui ne se parlent pas.

Trois comportements méritent d'être connus :

  • Sans clé, les API sont ouvertes, et elles le disent au démarrage (INTERNAL_API_KEY n'est pas définie : cette API accepte toutes les requêtes). C'est délibéré : une installation qui se met à jour ne doit pas tomber en erreur avant que son opérateur ait posé la clé. En dev, preprod et prod, c'est pourtant la seule chose qui ferme les API — posez-la.
  • Les routes de santé restent libres (/health, /health/ready, /health/stream). Les vérifications de Docker les interrogent sans en-tête ; un refus rendrait chaque conteneur unhealthy, donc le déploiement en échec.
  • Swagger reste affiché sur /api, mais ses appels « Try it out » exigent l'en-tête. Le bouton « Authorize » de la page permet de le saisir.

Ce que cette sécurité ne couvre pas​

Le dire vaut mieux que le laisser croire.

  • Rien n'est chiffré en base — ni les clés d'intake, ni la clé du feed, ni le secret partagé. La clé de déchiffrement vivrait sur le même hôte que PostgreSQL, ce qui n'ajouterait qu'une étape à qui a déjà accès à la base. Les mots de passe, eux, ne sont pas stockés : seule leur empreinte bcrypt l'est, et elle ne sort jamais de PostgreSQL.
  • Pas de second facteur, pas de rotation automatique de la clé partagée, pas de journal des connexions au-delà de la date de la dernière.
  • Un changement de rôle prend effet à la reconnexion, ou au bout de douze heures — la durée d'une session. Le rôle est porté par le cookie signé, ce qui évite d'interroger la base à chaque navigation. L'écran des comptes, lui, revalide toujours : un administrateur rétrogradé ne peut pas s'y redonner un rôle avec un cookie périmé.
  • Une communauté a une seule clé d'API Sekoia, et elle configure autant qu'elle envoie. Elle envoie les inventaires, et les boutons Créer dans Sekoia s'en servent pour créer des intakes — entité, configuration de module, intake — et des connecteurs d'assets — configuration de module, connecteur, puis son arrêt. Elle porte donc les droits des assets et ceux de configuration des intakes et des connecteurs (Symphony) de son espace ; ne lui donnez rien de plus que ce que la communauté utilise. Clé des inventaires quotidiens, elle ne s'efface pas une fois les intakes d'une session créées sans les arrêter : SekoiaCraft la conserve, en clair, comme les autres clés.
  • Le secret partagé est le même pour les cinq API. C'est ce qui le rend générique ; c'est aussi ce qui fait qu'en changer demande de redémarrer les six services ensemble.

Récapitulatif des variables​

VariableQui la litRôle
SESSION_SECRETle frontendsigne le cookie de session
INTERNAL_API_KEYle frontend et les cinq backendsferme les API
ADMIN_INITIAL_PASSWORDle semis, si elle est poséechoisit le mot de passe initial

Les trois sont injectées par infrastructure/scripts/deploy.sh depuis les variables GitLab <ENV>_SESSION_SECRET, <ENV>_INTERNAL_API_KEY et <ENV>_ADMIN_INITIAL_PASSWORD. Les deux premières sont exigées — un déploiement s'arrête en les nommant si elles manquent — et figurent dans .env.example.

La troisième est facultative des deux côtés, et cela se tient : un environnement qui laisse tirer son mot de passe initial doit pouvoir se déployer, et l'exiger couperait toute installation qui n'a rien à choisir. Elle ne figure d'ailleurs dans aucun .env versionné — le contrat d'environnement du dépôt y impose une valeur non vide, ce qui reviendrait exactement à publier l'identifiant qu'on refuse de publier.