Aller au contenu principal

Secrets et placeholders de configuration

Les fichiers appsettings.json et appsettings.Development.json sont versionnés : ils ne doivent contenir ni mot de passe, ni clé d'API, ni chaîne de connexion à une base partagée. En développement, ces valeurs vont dans le magasin de secrets utilisateur de .NET (dotnet user-secrets) ; en production, dans des variables d'environnement ou un coffre de secrets. Les placeholders ${Clé} permettent de garder une valeur versionnée (une chaîne de connexion, par exemple) et de n'en sortir que la partie secrète.

Le magasin de secrets utilisateur est un fichier JSON stocké dans le profil du développeur, en dehors du dossier du projet :

SystèmeEmplacement
Windows%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json
Linux / macOS~/.microsoft/usersecrets/<UserSecretsId>/secrets.json

Une application Lagoon lit ces secrets à trois endroits. Les trois doivent donc utiliser le même UserSecretsId :

ProjetQuandChargement des secrets
Serverà l'exécutionASPNETCORE_ENVIRONMENT=Development uniquement
Modelau design time (dotnet ef migrations add, dotnet ef database update, …)toujours
Batchà l'exécutionDOTNET_ENVIRONMENT=Development uniquement
Environnement du batch

Le batch lit son environnement dans DOTNET_ENVIRONMENT, pas dans ASPNETCORE_ENVIRONMENT. Réglez DOTNET_ENVIRONMENT=Development dans les profils de Batch/Properties/launchSettings.json, sinon le batch ne charge pas les secrets.

Déclarer le UserSecretsId dans Directory.Build.props​

dotnet user-secrets init écrit un <UserSecretsId> dans un seul .csproj. Les autres projets de l'application n'en ont pas, ou en reçoivent un autre s'ils sont initialisés séparément. Déclarez plutôt le GUID une seule fois, dans un fichier Directory.Build.props placé dans le dossier de l'application, au-dessus des dossiers Server, Model, Batch… MSBuild l'applique automatiquement à tous les projets situés en dessous.

APP/
├── Directory.Build.props ← UserSecretsId commun
├── Server/APP.Server.csproj
├── Model/APP.Model.csproj
├── Batch/APP.Batch.csproj
└── ...
Directory.Build.props
<Project>

<!-- A single user secrets store for the whole application: the "Server" project reads it at runtime
(Development environment) and the "Model" project at design time ("dotnet ef"). -->
<PropertyGroup>
<UserSecretsId>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</UserSecretsId>
</PropertyGroup>

</Project>

Remplacez xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx par un GUID propre à votre application. Deux applications avec le même identifiant partageraient leurs secrets.

Générer un GUID (PowerShell)
[guid]::NewGuid()
Générer un GUID (Linux / macOS)
uuidgen
attention
Pas de dotnet user-secrets init

N'exécutez pas dotnet user-secrets init et ne laissez pas Visual Studio (Gérer les secrets utilisateur) ajouter un <UserSecretsId> dans un .csproj. Le .csproj est lu après Directory.Build.props : sa valeur remplacerait le GUID commun pour ce projet seulement, et le serveur et dotnet ef liraient deux magasins différents. Si un <UserSecretsId> est déjà présent dans un .csproj, supprimez-le.

Gérer les secrets​

Lancez les commandes depuis le dossier de n'importe quel projet de l'application, ou passez --project. Elles agissent toutes sur le même magasin, puisque le UserSecretsId est commun. Les sections JSON s'écrivent avec :.

Depuis le dossier Server
dotnet user-secrets set "ConnectionStrings:<Nom>" "Server=localhost;Database=App;User Id=app;Password=..."
dotnet user-secrets list
dotnet user-secrets remove "ConnectionStrings:<Nom>"
dotnet user-secrets clear

Une valeur des secrets remplace celle de même clé dans appsettings*.json. Laissez donc dans appsettings.json une valeur sans secret, ou ne mettez pas la clé du tout, et placez la vraie valeur dans les secrets. Pour ne sortir que le mot de passe d'une chaîne de connexion, utilisez un placeholder (section suivante).

Placeholders ${Clé} dans la configuration​

Une valeur de configuration peut contenir des placeholders ${Clé}. Chacun est remplacé, à la lecture, par la valeur de la clé de configuration Clé, quelle que soit sa source : appsettings*.json, secrets utilisateur, variable d'environnement, coffre de secrets branché comme source de configuration… La chaîne de connexion reste ainsi versionnée, et seul le mot de passe est stocké ailleurs :

Server/appsettings.json
{
"ConnectionStrings": {
"MainDb": "Server=db01;Database=App;User Id=app;Password=${DB_PASSWORD}"
}
}
En développement : secret utilisateur
dotnet user-secrets set "DB_PASSWORD" "..."
En production : variable d'environnement
export DB_PASSWORD="..."

L'application lit la chaîne complète avec Configuration.GetConnectionString("MainDb"), sans appel spécifique : le remplacement est automatique, y compris pour les options liées (GetSection(...).Bind(...), IOptions<T>) et pour les bibliothèques qui lisent la configuration elles-mêmes.

SyntaxeRésultat
${DB_PASSWORD}valeur de la clé DB_PASSWORD (secret utilisateur ou variable d'environnement DB_PASSWORD)
${Database:Password}valeur de la clé Database:Password (variable d'environnement Database__Password)
${Database:Port ?? 1433}valeur de la clé, ou 1433 si elle n'existe pas (${Clé ??} donne une chaîne vide)
$${le texte littéral ${

Un nom de clé ne peut pas contenir ?. Une valeur par défaut est du texte littéral : elle ne peut contenir ni } ni placeholder. Un ${ sans } fermant est laissé tel quel.

Règles :

  • Clé absente : sans ??, la lecture de la valeur lève une exception qui nomme la clé lue et la clé manquante, sans jamais afficher de valeur. Une clé présente mais vide est remplacée par une chaîne vide.
  • Pas de remplacement en cascade : si la valeur insérée contient elle-même ${…}, elle est insérée telle quelle.
  • Échappement dans ConnectionStrings : une valeur insérée qui contient ;, ', " ou des espaces de bord est entourée de guillemets (Password="a;b"), les " internes étant doublés. Si la valeur est déjà délimitée dans la chaîne (Password="${DB_PASSWORD}" ou Password='${DB_PASSWORD}'), seul le caractère qui la délimite (" ou ') est doublé dans la valeur insérée. Un mot de passe contenant un ; ne casse donc pas la chaîne de connexion. La syntaxe ODBC {…} n'est pas gérée.
  • Périmètre : serveur, batch et design time (dotnet ef). Le client Blazor ne remplace pas les placeholders : sa configuration (wwwroot/appsettings*.json) est publique et ne doit contenir aucun secret.

Pour désactiver le remplacement, par exemple si des valeurs contiennent ${ pour une autre raison :

Server/appsettings.json
{
"Lagoon": {
"ExpandPlaceholders": false
}
}

Ordre de chargement​

OrdreSourceServeur et batch (Development)Design time (dotnet ef)
1appsettings.json✔✔ (celui du projet Server)
2appsettings.{Environment}.json✔✔ (appsettings.Development.json du projet Server)
3Secrets utilisateur✔✔
4Variables d'environnement✔✔
5Arguments de la ligne de commande✔—
6Remplacement des placeholders ${…}✔✔

Chaque source remplace les valeurs des sources précédentes. Une variable d'environnement ConnectionStrings__<Nom> (avec __ à la place de :) a donc priorité sur les secrets, à l'exécution comme au design time, ce qui est pratique pour un pipeline de CI. Les placeholders sont remplacés en dernier, avec les valeurs finales de toutes les sources.

Design time : DesignTimeApplicationDbContextFactory​

Les commandes dotnet ef créent le contexte avec la fabrique DesignTimeDbContextFactory du projet Model, qui dérive de DesignTimeApplicationDbContextFactory<T> (voir Migrations et seeders EF Core). La classe de base choisit le magasin de secrets d'après le UserSecretsId compilé dans l'assembly qui déclare le contexte, c'est-à-dire l'assembly du projet Model, et non d'après celui du projet Server. Le Directory.Build.props de l'application donne à Model le même UserSecretsId qu'à Server : dotnet ef lit donc les mêmes secrets que le serveur. Si l'identifiant n'était déclaré que dans le .csproj du serveur, Model n'en aurait aucun, et les secrets seraient ignorés au design time sans message d'erreur.

Pour ajouter ou retirer des sources de configuration au design time, redéfinissez OnConfigureConfiguration. Elle est appelée après le chargement des fichiers appsettings*.json.

Model/Context/DesignTimeDbContextFactory.cs
protected override void OnConfigureConfiguration(IConfigurationBuilder builder)
{
// Keep the user secrets and the environment variables.
base.OnConfigureConfiguration(builder);
// Local file, not versioned, read last so that it overrides the other sources.
// Relative paths are resolved from the "Server" project folder, where the appsettings files live.
builder.AddJsonFile("appsettings.Local.json", true);
}

Les placeholders ${Clé} sont remplacés après toutes les sources, y compris celles ajoutées dans OnConfigureConfiguration.

Production​

Les secrets utilisateur sont un outil de développement : ils ne sont pas chiffrés, et le serveur comme le batch ne les lisent qu'en environnement Development. En production, fournissez les valeurs sensibles par des variables d'environnement, un coffre de secrets ou le fichier de configuration du serveur de déploiement.