Licencier une application .NET sous Linux et macOS : ni registre, ni AppData — où réside le bail Keyright, sur quoi s'ancre l'empreinte, et le service systemd qui ne se relance jamais
La plupart des conseils sur les licences sont écrits pour Windows, et le SDK Keyright le masque discrètement : les mêmes appels Validate() et ActivateAsync() fonctionnent sous Linux et macOS. Mais les trois choses dont dépend un SDK de licences — un dossier de données par utilisateur, une identité de machine stable, et une application relancée assez souvent pour rafraîchir son bail — sont toutes différentes hors de Windows. Le bail atterrit dans ~/.local/share, où une application Mac en bac à sable ou un service systemd avec ProtectHome= ne peut pas écrire. L'empreinte de la machine s'ancre sur /etc/machine-id ou sur l'IOPlatformUUID au lieu d'une valeur de registre, et une image de référence dont on a oublié d'effacer le machine-id transforme toute une flotte en une seule machine. Et un démon qui démarre une fois par trimestre ne reçoit jamais le rafraîchissement au démarrage sur lequel compte une application de bureau, si bien que son bail de 14 jours expire alors que le processus tourne toujours. Ce billet passe en revue où le SDK écrit sur chaque plateforme, de quoi l'empreinte est construite et les deux pièges du machine-id, une unité systemd avec StateDirectory= et LoadCredential= qui donne un chez-soi au SDK, la boucle de rafraîchissement du bail dont un service de longue durée a besoin, le mode de fichier que le bail mérite sur un hôte multi-utilisateur, et la boucle de vérification qui le prouve.
Le SDK Keyright se moque du système d’exploitation sur lequel il tourne. Keyright.NET cible netstandard2.0 et net8.0, Validate() est un pur calcul sur des octets, et ActivateAsync est un appel HTTPS. Déployez un worker sous licence sur Ubuntu ou livrez un build Mac de votre application de bureau, et le même code s’exécute. C’est la bonne nouvelle, et c’est aussi pourquoi les trois différences de plateforme qui comptent vraiment ont tendance à faire surface sous forme de ticket de support plutôt que d’erreur de compilation.
Un SDK de licences dépend de trois choses que fournit la plateforme : un endroit par utilisateur où conserver un petit fichier signé, une identité stable pour la machine, et — moins évident — une application relancée assez souvent pour rafraîchir ce qu’elle a mis en cache. Sous Windows, ce sont %LocalAppData%, le MachineGuid du registre, et le fait que les applications de bureau démarrent chaque matin. Hors de Windows, les trois sont différents, et une intégration Keyright testée sur le portable Windows d’un développeur peut discrètement se comporter de travers sur la machine Linux où elle est déployée. Ce billet est la version Linux et macOS de l’histoire : où vont les fichiers, de quoi est faite l’empreinte, comment donner un chez-soi à un service systemd, et la boucle de rafraîchissement dont a besoin un processus qui ne redémarre jamais.
Où vont les fichiers quand il n’y a pas d’AppData
Un client Keyright conserve jusqu’à trois fichiers par produit, tous enracinés sur Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) :
{LocalApplicationData}/Keyright/{product}/lease.json signed activation lease (cached after ActivateAsync)
{LocalApplicationData}/Keyright/{product}/state.json first-seen / last-seen timestamps (trial clock, clock-tamper)
{LocalApplicationData}/Keyright/{product}/license.json default location for an offline license file
Sous Unix, .NET résout LocalApplicationData à la manière XDG : $XDG_DATA_HOME s’il est défini, sinon $HOME/.local/share. Et il le fait aussi sous macOS — pas ~/Library/Application Support, là où un utilisateur Mac irait chercher. Pour une utilisatrice dana, le bail se trouve donc à :
Linux /home/dana/.local/share/Keyright/acme-agent/lease.json
macOS /Users/dana/.local/share/Keyright/acme-studio/lease.json
Trois choses découlent de « c’est sous $HOME », et elles constituent toute la première moitié de ce billet.
C’est par utilisateur. Deux utilisateurs sur la même machine ont chacun leur propre cache et activent chacun une fois. Ce ne sont pas deux postes : les deux activations prennent l’empreinte de la même machine et envoient le même machineId, donc le service lie un seul poste et le rafraîchit deux fois — exactement le comportement du PC partagé décrit dans le billet MSIX. Le cache est par utilisateur ; le poste a toujours été par machine.
Cela suppose que HOME soit réel et accessible en écriture. Une unité systemd avec ProtectHome=yes voit un /home vide et en lecture seule. Une unité DynamicUser=yes s’exécute sous un UID transitoire dont le home est /. Un compte de service créé à la main l’est souvent avec --home /nonexistent ou --home /var/empty. Dans tous ces cas, ~/.local/share/Keyright ne peut pas être créé. Le SDK ne lève pas d’exception : le cache du bail est délibérément en mode meilleur effort, donc une écriture échouée est avalée, ActivateAsync renvoie un LicenseInfo parfaitement valide, et le service tourne sous licence — jusqu’au prochain redémarrage, où il n’y a pas de bail en cache et où il active à nouveau. En ligne, c’est invisible ; cela coûte juste un aller-retour. Hors ligne, c’est NoLicense et un service en échec fermé (fail closed) qui fonctionnait hier.
Une application Mac en bac à sable a un autre HOME. Sous App Sandbox, HOME est redirigé vers le conteneur de l’application, donc ~/.local/share/Keyright/acme-studio/lease.json atterrit à ~/Library/Containers/com.acme.studio/Data/.local/share/Keyright/acme-studio/lease.json. Tout fonctionne — le bac à sable est accessible en écriture, il survit aux mises à jour, et il est supprimé quand l’utilisateur retire l’application et son conteneur. C’est le pendant macOS de la virtualisation des écritures de MSIX : le chemin que le SDK journalise n’est pas celui où se trouvent les octets. Rendez-le explicite avec LeaseCachePath et consorts si un ingénieur support aura un jour besoin du fichier.
Sur quoi s’ancre l’empreinte
L’empreinte de machine que le SDK envoie à l’activation est un ensemble de hachages salés, KRM1:anchor:soft1:soft2, construit à partir d’une liste ordonnée de composants où l’index 0 est l’ancre qui doit toujours correspondre et le reste sont des signaux faibles que NodeLockTolerance (1 par défaut) laisse dériver un à la fois. Sous Windows, l’ancre est HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid. Hors de Windows, il n’y a pas de registre, et le SDK lit l’équivalent de la plateforme :
- Linux :
/etc/machine-id, l’identifiant de 32 caractères hexadécimaux que systemd génère au premier démarrage, avec/var/lib/dbus/machine-iden repli pour les systèmes qui n’ont que celui de D-Bus. Il est lisible par tous (mode0444), donc un service exécuté sous un utilisateur non privilégié peut le lire sans aucune capacité — c’est pourquoi les unitésDynamicUser=prennent correctement l’empreinte. - macOS : l’
IOPlatformUUIDde l’entréeIOPlatformExpertDevicedu registre I/O, lu viaioreg. C’est l’identifiant par machine que les services d’Apple utilisent eux-mêmes, et il survit aux mises à niveau de l’OS, aux changements de nom d’hôte et à un utilisateur qui efface~/. - Rien de lisible : le SDK génère un identifiant aléatoire, le persiste dans
~/.local/share/Keyright/machine.id, et s’ancre dessus. Sous Windows et sur toute installation Linux ou Mac normale, ce fichier n’existe jamais ; c’est le dernier recours pour un conteneur sansmachine-id, ou un bac à sable verrouillé qui ne peut pas lancerioreg. Notez ce que cela coûte : le repli est par utilisateur, donc deux utilisateurs sur une telle machine seraient deux machines et deux postes.
Les deux composants faibles sont le nom d’hôte et Environment.OSVersion.Platform plus la version majeure — Unix:6 sur un noyau Linux 6.x, la version majeure de macOS sur un Mac. La tolérance est ce qui les garde honnêtes : renommez l’hôte et l’ancre correspond toujours, un composant faible diffère, le bail est validé. Passez une distribution d’un noyau 5.x à un 6.x, même chose. Faites les deux la même semaine et vous avez deux écarts faibles, ce qui dépasse la tolérance par défaut — le bail signale MachineMismatch et le prochain ActivateAsync relie la machine, de façon idempotente, parce que le service fait la correspondance sur l’ancre. La conception est expliquée dans le billet sur l’empreinte ; le point propre à Linux est simplement que votre ancre « matérielle » est un fichier.
Les deux pièges du machine-id
Parce que l’ancre est un fichier, elle peut être copiée, et elle peut manquer. Chaque cas est une défaillance différente.
Copiée : l’image de référence. Construisez une image de VM, installez votre agent, prenez un instantané, provisionnez quarante hôtes à partir de l’instantané. À moins que quelque chose n’ait effacé /etc/machine-id avant que l’instantané ne soit scellé, les quarante démarrent avec le même identifiant, se hachent vers la même ancre et présentent le même machineId. Keyright fait ce qu’il fait pour une machine qu’il a déjà vue : le premier hôte active et lie un poste, les trente-neuf autres le rafraîchissent. Le tableau de bord affiche un seul appareil. Vous avez vendu un poste et en faites tourner quarante — c’est le piège qui échoue ouvert, celui pour lequel on ne reçoit pas de ticket. Le correctif est dans l’image, pas dans le SDK : truncate -s0 /etc/machine-id avant de sceller (un fichier vide dit à systemd de générer un nouvel identifiant au premier démarrage), ou exécutez systemd-machine-id-setup depuis le provisionnement au premier démarrage. cloud-init le fait ; les modèles construits à la main, souvent pas. Le billet sur les VM clonées couvre la détection des doublons côté service quand vous ne contrôlez pas les images. Si vous voulez que plusieurs hôtes comptent comme un seul déploiement, dites-le avec un IMachineComponents personnalisé, comme dans le billet sur les conteneurs — une décision dans le code, pas un accident dans un modèle.
Manquante : le conteneur. Les images de conteneur minimales livrent un /etc/machine-id vide, ou pas du tout. Le SDK lit une chaîne vide, la traite comme illisible, et retombe sur le repli aléatoire persisté — qui vit dans la couche accessible en écriture du conteneur et disparaît avec lui. Chaque redémarrage est une nouvelle machine et un nouveau poste. C’est le même problème que le billet sur les conteneurs résout en remplaçant la source d’identité, et rien sur cette page ne le change ; ce que cette page ajoute, c’est pourquoi le comportement par défaut est celui-là : le fichier est vide.
Un service systemd : donner un chez-soi au SDK
Place maintenant à la moitié du billet qui compte pour la plupart des déploiements Linux : un worker .NET sous licence exécuté sous systemd. Une unité écrite avec le durcissement moderne ressemble à ceci, et chaque ligne qui compte pour les licences est annotée :
# /etc/systemd/system/acme-agent.service
[Unit]
Description=Acme Agent
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/opt/acme/acme-agent
User=acme
Group=acme
Restart=on-failure
# Hardening: the filesystem is read-only and /home is empty. $HOME/.local/share is not writable.
ProtectSystem=strict
ProtectHome=yes
# The one directory this service may write. systemd creates /var/lib/acme-agent, chowns it to acme,
# and exports it as $STATE_DIRECTORY. This is where the lease, local state and offline license go.
StateDirectory=acme-agent
StateDirectoryMode=0700
# Site/OEM deployments: hand the service a signed license file nobody else on the host can read.
# Exposed at $CREDENTIALS_DIRECTORY/license.json, readable by this service only, not in the environment.
LoadCredential=license.json:/etc/acme/license.json
# Key-activated deployments instead: the license key, same mechanism. One or the other.
# LoadCredential=license-key:/etc/acme/license.key
[Install]
WantedBy=multi-user.target
StateDirectory= est la réponse au problème de HOME. Il donne au service un répertoire qui existe, appartient à l’utilisateur du service, survit aux redémarrages et aux mises à niveau, et est exempté de ProtectSystem=strict. StateDirectoryMode=0700 en tient les autres utilisateurs de l’hôte à l’écart. LoadCredential= est la réponse à « d’où vient la licence sur une machine sans humain » : systemd copie le fichier dans un tmpfs propre au service et indique au processus où il se trouve via CREDENTIALS_DIRECTORY. Contrairement à une ligne Environment=, le contenu n’apparaît jamais dans systemctl show, /proc/<pid>/environ, ni dans un processus enfant.
Le client lit les deux :
using Keyright.Client;
static KeyrightClient CreateLicenseClient()
{
var options = new KeyrightOptions
{
Product = "acme-agent",
PublicKeyBase64 = AcmeKeys.LicensePublicKey, // public key only; safe to ship in the binary
ServiceUrl = "https://licensing.acme.example",
};
// systemd StateDirectory= — may list several directories, colon-separated; the first is ours.
string? state = Environment.GetEnvironmentVariable("STATE_DIRECTORY")?.Split(':')[0];
if (!string.IsNullOrEmpty(state))
{
string root = Path.Combine(state, "keyright");
options.LeaseCachePath = Path.Combine(root, "lease.json");
options.LocalStatePath = Path.Combine(root, "state.json");
options.DefaultLicensePath = Path.Combine(root, "license.json");
}
// systemd LoadCredential= — a site/OEM license file. Explicit path = authoritative, no fall-through.
string? creds = Environment.GetEnvironmentVariable("CREDENTIALS_DIRECTORY");
if (creds is not null && File.Exists(Path.Combine(creds, "license.json")))
options.LicenseFilePath = Path.Combine(creds, "license.json");
return KeyrightClient.Initialize(options);
}
Hors de systemd — un développeur qui lance le binaire depuis un shell — aucune des deux variables n’est définie, le SDK retombe sur ~/.local/share par défaut, et le même code est correct. Il n’y a aucun drapeau « suis-je un service ? » sur lequel se tromper.
La branche LicenseFilePath est la même règle de source explicite que partout ailleurs dans le SDK : quand elle est définie, les sources ambiantes (KEYRIGHT_LICENSE, le chemin de configuration, le chemin par défaut, le bail en cache) ne sont pas consultées, et un fichier manquant ou invalide donne NoLicense ou SignatureInvalid plutôt qu’un repli. Pour un déploiement où la licence est une décision de l’opérateur, c’est ce que vous voulez. Notez que le fichier de licence n’est pas un secret au sens où un mot de passe l’est — c’est un document signé lié à l’empreinte de cette machine, inutilisable ailleurs — mais il porte bel et bien la clé de licence et le nom du titulaire, et LoadCredential= est simplement la façon la plus propre de placer un fichier devant exactement un processus sur un hôte Linux.
L’histoire du démon macOS est la même avec d’autres noms. Un LaunchDaemon exécuté sous un utilisateur dédié _acme a pour home /var/empty, qui est en lecture seule ; HOME pour un démon root est /var/root. Placez les trois chemins sous /Library/Application Support/Acme/keyright/ avec le mode 0700, appartenant à l’utilisateur du démon, et le SDK est satisfait. launchd n’a pas de LoadCredential= ; un fichier en 0600 sous /Library/Application Support/Acme/ référencé via LicenseFilePath en est l’équivalent.
Le démon qui ne se relance jamais
Voici la différence qui n’a rien à voir avec les chemins de fichiers et qui mord le plus fort.
Un bail d’activation est un jeton signé de courte durée — 14 jours par défaut — et le schéma de rafraîchissement recommandé par le SDK est appeler ActivateAsync au démarrage. Pour une application de bureau, c’est une solution complète : l’utilisateur la lance presque tous les jours, chaque lancement rafraîchit le bail quand la machine est en ligne, et un portable qui reste hors ligne deux semaines est le cas limite pour lequel la fenêtre a été dimensionnée. Un service inverse les fréquences. Il se lance une fois, après un déploiement, puis tourne pendant des mois. Rien n’appelle ActivateAsync à nouveau, le bail atteint leaseExpiresUtc, et le prochain Validate() — dans un gestionnaire de requête, le jour 15 — renvoie Expired avec le message « reconnect to re-validate ». Le processus est actif, le réseau fonctionne, la licence est payée, et le service a basculé en mode fermé vers Free parce que personne ne l’a relancé.
Le correctif est une boucle de rafraîchissement dans le service. ActivateAsync peut être appelé de façon répétée sans risque : réactiver une machine que le service a déjà vue est idempotent — même poste, bail neuf — et si le service de licences est injoignable, le SDK retombe sur le bail en cache encore valide au lieu de le jeter, donc la boucle ne peut jamais qu’allonger la fenêtre, jamais la raccourcir.
using Keyright.Client;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
/// <summary>Keeps the activation lease fresh in a process that may run for months without restarting.</summary>
public sealed class LeaseRefresher(KeyrightClient license, ILogger<LeaseRefresher> log) : BackgroundService
{
private static readonly TimeSpan Interval = TimeSpan.FromHours(6);
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Key-activated deployment: the key arrives as a systemd credential, never in the environment.
string? creds = Environment.GetEnvironmentVariable("CREDENTIALS_DIRECTORY");
string? keyPath = creds is null ? null : Path.Combine(creds, "license-key");
if (keyPath is null || !File.Exists(keyPath))
{
log.LogInformation("No license key credential; running from a license file, nothing to refresh.");
return; // site/OEM file deployments: the file IS the license
}
string key = (await File.ReadAllTextAsync(keyPath, ct)).Trim();
using var timer = new PeriodicTimer(Interval);
do
{
LicenseInfo info = await license.ActivateAsync(key, ct); // idempotent: same machine, same seat, new lease
if (info.IsValid)
log.LogInformation("License lease refreshed; {Status}, {DaysRemaining} days remaining.", info.Status, info.DaysRemaining);
else
log.LogWarning("License refresh did not yield a valid lease: {Status} — {Message}", info.Status, info.Message);
}
while (await timer.WaitForNextTickAsync(ct));
}
}
Enregistrez-la avec services.AddHostedService<LeaseRefresher>() à côté du client singleton. Six heures est un intervalle raisonnable : le bail n’a alors jamais plus de six heures, donc la fenêtre de 14 jours devient quatorze jours de panne tolérée de votre service de licences plutôt que quatorze jours de disponibilité. La première itération s’exécute immédiatement, ce qui tient lieu d’activation au démarrage. Journalisez le statut, jamais la clé — le billet sur la journalisation structurée explique pourquoi info.Status et info.DaysRemaining sont les bons champs.
Deux choses que la boucle ne fait pas, délibérément. Elle ne redémarre pas le processus quand le statut se dégrade : Validate() échoue déjà en mode fermé de lui-même, et le billet sur le contrôle de santé montre comment remonter une licence dégradée à l’orchestrateur sans boucle de plantage. Et elle ne s’exécute pas pour les déploiements par fichier : un fichier de licence de site signé a sa propre expiration et rien à rafraîchir, donc la boucle vérifie la présence du credential contenant la clé et se retire.
Le mode de fichier que le bail mérite
Sous Windows, le bail vit sous un profil que seul son utilisateur peut lire. Sous Linux, File.WriteAllText crée les fichiers en 0666 & ~umask, ce qui, avec l’habituel 022, donne 0644 — lisible par chaque compte de l’hôte. Le bail n’est pas dangereux à lire : il est signé, donc le modifier donne SignatureInvalid, et il est lié à l’empreinte de cette machine, donc le copier ailleurs donne MachineMismatch. Mais il porte bel et bien la clé de licence, et une clé permet à son détenteur de tenter une activation contre votre limite de postes depuis n’importe quelle machine. Sur un portable mono-utilisateur, ce n’est rien ; sur un hôte de build partagé ou un bastion, c’est une fuite de clé via ls.
StateDirectoryMode=0700 ferme déjà cette brèche pour un service systemd. Pour tout le reste, resserrez le répertoire avant que le SDK n’y écrive — KeyrightClient.Initialize ne crée pas les répertoires, c’est la première écriture qui le fait, donc les créer vous-même d’abord l’emporte :
using System.IO;
static void SecureLicenseDirectory(string leaseCachePath)
{
if (OperatingSystem.IsWindows()) return; // ACLs on the profile already do this
string dir = Path.GetDirectoryName(leaseCachePath)!;
Directory.CreateDirectory(dir, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); // 0700
if (File.Exists(leaseCachePath))
File.SetUnixFileMode(leaseCachePath, UnixFileMode.UserRead | UnixFileMode.UserWrite); // 0600
}
Directory.CreateDirectory(path, UnixFileMode) et File.SetUnixFileMode sont des API .NET 7+ ; sur des cibles plus anciennes, un P/Invoke de chmod fait la même chose. Un répertoire en 0700 suffit à lui seul — un fichier en 0644 dans un répertoire que personne d’autre ne peut traverser est illisible — donc l’appel sur le mode du fichier est une ceinture et des bretelles pour un bail qui existait déjà avant que vous ne resserriez le répertoire.
Affichez ce que la porte verra
Chaque billet de cette série finit par recommander le même diagnostic, parce que c’est ce qui transforme un ticket en une seule ligne : un drapeau --license-info qui affiche les chemins et l’empreinte depuis le chemin de code que la porte utilise, pour que ce qu’il montre soit ce que le SDK fait.
using Keyright.Client;
using Keyright.NodeLock;
// acme-agent --license-info
static void PrintLicenseInfo(KeyrightClient license)
{
var fp = MachineFingerprint.Current();
LicenseInfo info = license.Validate(out LicenseSourceKind source);
Console.WriteLine($"Machine id : {fp.ToBoundString()}"); // what the dashboard's Devices panel lists
Console.WriteLine($"Display id : {fp.DisplayId}"); // short form for a support conversation
Console.WriteLine($"Anchor source : {AnchorSource()}");
Console.WriteLine($"Lease cache : {license.LeaseCachePath} (exists: {File.Exists(license.LeaseCachePath)})");
Console.WriteLine($"Local state : {license.LocalStatePath}");
Console.WriteLine($"License file : {license.DefaultPath}");
Console.WriteLine($"Status : {info.Status} via {source} — {info.Message}");
}
static string AnchorSource()
{
if (OperatingSystem.IsWindows()) return "registry MachineGuid";
if (OperatingSystem.IsMacOS()) return "IOPlatformUUID";
foreach (var p in new[] { "/etc/machine-id", "/var/lib/dbus/machine-id" })
if (File.Exists(p) && File.ReadAllText(p).Trim().Length > 0) return p;
return "persisted fallback (~/.local/share/Keyright/machine.id) — per user, not per machine";
}
La ligne Anchor source est celle qui attrape les pièges de l’image de référence et du conteneur avant qu’ils n’atteignent la production : /etc/machine-id sur un vrai hôte, le repli dans un conteneur nu. Le suffixe via {source} — Lease, ExplicitFile, EnvironmentVariable, DefaultPath — vous dit laquelle des sources l’a emporté, ce qui est la première question quand un service censé lire un fichier de credential active mystérieusement en ligne à la place.
Vérifiez-le
Tout l’argument est testable sur un seul hôte en quelques minutes. L’astuce importante est à l’étape 2 : exécutez le diagnostic dans l’environnement de l’unité, pas depuis votre propre shell en tant qu’utilisateur du service — sudo -u acme vous donne l’utilisateur mais ni STATE_DIRECTORY ni les credentials, et les chemins qu’il affiche sont les mauvais.
# 1. The anchor. Non-empty, and different on every host built from the same image.
cat /etc/machine-id
# → e.g. 8c3f1a2b... (empty? the image was not cleared: truncate -s0 /etc/machine-id and reboot)
# 2. Activate once, as the unit would. systemd-run builds a transient unit with the same properties.
sudo systemd-run --pty --wait -p User=acme -p StateDirectory=acme-agent \
-p LoadCredential=license-key:/etc/acme/license.key \
/opt/acme/acme-agent license activate
sudo systemd-run --pty --wait -p User=acme -p StateDirectory=acme-agent \
/opt/acme/acme-agent --license-info
# → Lease cache : /var/lib/acme-agent/keyright/lease.json (exists: True), Status: Valid via Lease
# Dashboard: the license shows 1 device, the KRM1:… machine id from the line above.
# 3. Owner and mode: the service user, nobody else.
sudo ls -la /var/lib/acme-agent/keyright/
# drwx------ acme acme . -rw------- acme acme lease.json
# 4. Restart: the lease survives, no second seat. Dashboard: still 1 device.
sudo systemctl restart acme-agent && sudo journalctl -u acme-agent -n 3
# → "License lease refreshed; Valid, 364 days remaining."
# 5. The loop. Block the licensing service for an hour and watch the warning line; unblock and watch it recover.
# Then confirm with the dashboard's last-seen time that the lease is refreshed every 6 hours without a restart.
Ajoutez celle qui compte pour une flotte : provisionnez un second hôte à partir de la même image et exécutez l’étape 1 sur les deux. Si les deux identifiants correspondent, arrêtez-vous et corrigez l’image ; rien de ce qui suit n’a de sens tant qu’ils ne diffèrent pas.
Ce qu’il faut retenir
Keyright traite Linux et macOS comme des plateformes de premier rang, et les appels du SDK sont identiques, mais la plateforme sous un SDK de licences ne l’est pas. Le dossier de données est ~/.local/share sur les deux et il est par utilisateur, donc un service durci a besoin d’un chez-soi explicite — StateDirectory= lui en donne un, et LoadCredential= lui remet un fichier ou une clé de licence sans le moindre secret dans l’environnement. L’empreinte s’ancre sur /etc/machine-id ou l’IOPlatformUUID au lieu d’une valeur de registre, ce qui est tout aussi stable — tant que vos images ne livrent pas un seul et même identifiant à quarante hôtes, et que vos conteneurs n’en livrent pas aucun du tout. Et le bail se rafraîchit en relançant, ce qu’un démon ne fait jamais, alors donnez-lui une boucle. Rendez les chemins explicites, affichez-les avec la source de l’ancre dans un diagnostic, et le jour où un ingénieur support demandera pourquoi un service sous licence est passé en Free le jour 15, la réponse sera déjà dans le journal.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.