Fail Closed, nie destruktiv: Der Zaris-Lizenzlebenszyklus von innen
Lizenzierung ist der Teil eines Produkts, über den die meisten Ingenieure nie nachdenken wollen, und man sieht es. Das übliche Muster ist eine Zeitbombe: Ein Datum wird geprüft, und wenn es verstreicht, bestraft die Software Sie — sie wirft beim Start eine Exception, sie beschädigt oder löscht lokalen Zustand, um Sie am „Stehlen" zu hindern, oder sie stürzt schlicht ab und nimmt Ihre Daten mit. Dieses Design behandelt die eigenen Operatoren als Gegner, und es versagt genau in dem Moment, in dem Sie es sich am wenigsten leisten können: in der Produktion, nachdem die Uhr unbemerkt umgesprungen ist.
Zaris nimmt die entgegengesetzte Haltung ein. Die Lizenzprüfung ist der sicherheitskritischste Schalter eines verteilten Datenspeichers und wird entsprechend gebaut: eine einzige reine, deterministische Policy-Funktion, ein sanfter und vollständig umkehrbarer Durchsetzungspfad, der kein einziges Byte zerstört, und eine Uhr pro Maschine, die in dem Moment weiterläuft, in dem Sie einen Schlüssel aktivieren. Dieser Beitrag geht durch, wie dieser Lebenszyklus im ausgelieferten Code tatsächlich funktioniert — denn die interessante Ingenieursarbeit steckt darin, was er Ihnen verweigert anzutun.
Der Lebenszyklus als Zustandsmaschine
Eine nie lizenzierte Maschine durchläuft eine kleine, explizite Menge von Stufen. Eine automatische 30-Tage-Testphase startet beim ersten Lauf ohne Schlüssel und ohne Anmeldung. Läuft sie ab, hört das Produkt nicht auf — es tritt in ein Grace-Fenster ein, in dem die Web-Konsole gesperrt ist, die Datenebene aber weiter bedient. Erst nach Ablauf auch des Grace-Fensters hört das Produkt sanft auf zu bedienen, wobei Ihre Daten erhalten bleiben. Die Aktivierung einer gültigen Lizenz springt zu jedem Zeitpunkt direkt auf Licensed.
Beide Defaults betragen 30 Tage — eine Basis-Testphase von 30 Tagen, danach 30 Tage Produkt-Grace — und beide sind einfache Konstanten, die pro Deployment überschrieben werden können:
public const int DefaultBaseTrialDays = 30;
public const int DefaultProductGraceDays = 30;
Es gibt zwei weitere nahezu terminale Zustände, in die ein Schlüssel Sie direkt versetzen kann: Licensed (eine gültige Enterprise-Lizenz, alles funktioniert, die lokale Uhr wird ignoriert) und Non-Production (eine gültige Dev-Tier-Lizenz, alles funktioniert, ist aber als nicht für die Produktion markiert).
Die Policy ist eine reine Funktion
Die Versuchung im Lizenzcode besteht darin, if (DateTime.Now > expiry)-Prüfungen über Konsole, Management-Dienst und Node-Host zu verstreuen — und dann zuzusehen, wie sie sich widersprechen, weil eine die Uhr eine Sekunde später las, eine eine veraltete Datei zwischengespeichert hatte, eine das Grace-Fenster vergaß. Zaris hat genau eine Stelle, die entscheidet, und sie ist eine reine Funktion ohne I/O, ohne Uhrzugriffe und ohne SDK-Aufrufe:
public static LicenseVerdict Evaluate(
DateTime nowUtc,
PaidLicenseStatus paid, // resolved Keyright license (or None)
TrialStatus trial, // the local machine-bound trial marker
int baseTrialDays = DefaultBaseTrialDays,
int productGraceDays = DefaultProductGraceDays)
Alles, wovon das Urteil abhängt, ist ein Argument. Die Web-Konsole, der Management-Dienst und jeder Node-Host ermitteln ihre eigenen Eingaben — die aktuelle Zeit, den Status der bezahlten Lizenz, den lokalen Trial-Marker — und speisen sie durch dieselbe Funktion, was überall ein identisches Urteil garantiert. Und weil die Übergangslogik nichts Externes berührt, ist jede Kante der Zustandsmaschine erschöpfend unit-testbar, ohne ein Dateisystem oder eine Uhr zu mocken: Sie übergeben ein nowUtc und prüfen die Stufe.
Die Ausgabe ist ein einzelner Record, der sowohl die für Menschen bestimmte Meldung als auch die beiden Booleans trägt, die die Durchsetzung steuern:
public sealed record LicenseVerdict(
LicenseStage Stage,
bool IsConsoleGated, // -> the console returns HTTP 402
bool IsProductStopped, // -> the data plane refuses to serve
bool IsPaid, bool IsTrial,
int DaysRemaining, DateTime? ExpiryUtc,
string StatusBadge, string Message);
Fail Closed — aber „closed" heißt die Testphase, nicht ein Absturz
Kann der Resolver für bezahlte Lizenzen den ausstellenden Dienst nicht erreichen, ist eine Lease-Datei unlesbar oder geht beim Auflösen der Keyright-Lizenz irgendetwas schief, wirft der Dienst keine Exception und nimmt auch nicht optimistisch an, dass Sie lizenziert sind. Er fällt auf die lokale Trial-Uhr zurück:
try { paid = _paid.Resolve(); }
catch { paid = PaidLicenseStatus.None; } // fail-closed to the trial clock
„Fail Closed" heißt hier bewusst nicht „Fail Stopped". Ein vorübergehender Netzwerkaussetzer bei der Lizenzauflösung setzt Sie auf den eigenen Offline-Trial-Marker der Maschine, der maßgeblich ist und kein Netzwerk braucht. Befinden Sie sich noch innerhalb Ihres Trial- oder Grace-Fensters, ändert sich für Ihren laufenden Cluster nichts. Nur das tatsächliche Verstreichen von Zeit — nicht ein Ausfall — bringt Sie einem Stopp näher.
Zwei Durchsetzungspunkte, keiner davon destruktiv
Das Urteil stellt zwei unabhängige Schalter bereit, weil die Konsole und die Datenebene absichtlich zu unterschiedlichen Zeitpunkten gesperrt werden.
Das Konsolen-Gate (IsConsoleGated). In dem Moment, in dem die Testphase abläuft, geben die Admin- und /v1-API-Routen der Web-Konsole 402 Payment Required zurück, mit einer Meldung, die darauf hinweist, wie man sie wieder aktiviert. Das ist Middleware vor der Konsole — sie berührt die Datenebene überhaupt nicht:
if (sw.IsGated) {
context.Response.StatusCode = StatusCodes.Status402PaymentRequired;
// ... reason + support URL ...
}
Entscheidend: In dieser Stufe läuft das Produkt weiter. Ihre Clients lesen und schreiben weiter; Replikation, Koordination und die Datenebene bleiben unberührt. Was Sie verlieren, ist die Verwaltungsoberfläche, mit lauten Warnungen, die Ihnen sagen, dass das Fenster geschlossen ist — ein produktionssicherer Anstoß, kein Ausfall.
Das Datenebenen-Gate (IsProductStopped). Erst nach Ablauf des Grace-Fensters beginnt der Server, Datenoperationen abzulehnen. Er tut das, indem er einen Fehlerstatus zurückgibt — niemals durch Löschen von Daten, niemals durch einen Absturz:
if (_license?.IsProductStopped == true && !IsLicenseExempt(request.Command))
return new ZarisResponse {
Success = false,
Status = KvStatus.Unavailable,
Error = _license.ProductStoppedMessage // "...activate a license to resume"
};
Zwei Details machen das menschlich statt feindselig. Erstens hat die Kern-Engine keinerlei Abhängigkeit von der Lizenz-Implementierung — sie konsultiert eine winzige ILicenseGate-Schnittstelle mit zwei Membern (IsProductStopped, ProductStoppedMessage), die in der Abstractions-Assembly liegt. Der Node-Host hängt den echten, Keyright-gestützten Dienst dahinter. Der Datenspeicher weiß nicht, was Keyright ist.
Zweitens ist ein gestoppter Server kein toter Server. Eine bestimmte Menge von Befehlen bleibt ausgenommen, damit ein Operator sich immer verbinden, inspizieren und wiederherstellen kann:
private static bool IsLicenseExempt(ZarisCommandType cmd) =>
cmd == ZarisCommandType.Hello
|| cmd == ZarisCommandType.ClusterInfoRequest
|| /* the read-only diagnostic snapshots */ ;
Der Handshake, die Cluster-Info-Abfrage und die schreibgeschützten Diagnose-Snapshots funktionieren weiterhin. Sie können sich verbinden, den Cluster-Zustand einsehen, bestätigen, dass die Daten intakt sind, und eine Lizenz aktivieren — alles während „gestoppt". Der Zustand bleibt auf Datenträger bzw. im Speicher genau so erhalten, wie er war; nur das Bedienen von Lese- und Schreibzugriffen wird vorenthalten, und zwar umkehrbar.
Eine Uhr pro Maschine
Eine Testphase, die bei jeder Neuinstallation zurückgesetzt wird oder pro Node in einem Cluster frische 30 Tage gewährt, ist keine Testphase. Zaris setzt eine Maschine = ein Seat durch: eine einzige Trial-Uhr, die von jedem Zaris-Prozess auf einem Rechner geteilt wird — Manager, kolokierte Nodes, Konsole —, unabhängig vom Dienstkonto, unter dem sie laufen. Erreicht wird das, indem der Trial-Marker an einem maschinenweiten Ort (CommonApplicationData / ProgramData) abgelegt und in eine zweite maschinenweite Kopie gespiegelt wird.
Der Marker selbst ist so gebaut, dass er beiläufiger Manipulation widersteht, und zugleich ehrlich darin, dass er kein DRM ist:
- Er ist an einen Maschinen-Fingerprint gebunden — einen Hash der Betriebssystem-Maschinen-GUID (
MachineGuidunter Windows,/etc/machine-idunter Linux) plus des Maschinennamens. Eine auf eine andere Maschine kopierte Marker-Datei wird dort schlicht ignoriert; sie gewährt dieser Maschine nichts. - Jede Kopie ist HMAC-signiert. Eine bearbeitete oder gefälschte Kopie wird erkannt und als manipuliert behandelt.
- Der früheste gültige Start gewinnt über die gespiegelten Kopien hinweg, und Verlängerungen bewegen sich immer nur vorwärts, sodass eine Testphase nicht neu gestartet oder durch das Löschen einer von zwei Dateien unbemerkt zurückgespult werden kann.
- Ein monotones
lastSeenwird verfolgt; wird die Systemuhr über eine Toleranz von 24 Stunden hinaus zurückgedreht, wird das ebenfalls als Manipulation markiert — Sie können keine Zeit gewinnen, indem Sie die Uhr zurückstellen.
Und ein manipulierter Marker wird nie belohnt. Statt stillschweigend zu gewähren oder stillschweigend zurückzusetzen, behandelt die Policy eine erkannte Manipulation als schlimmsten Fall und springt direkt auf gestoppt:
if (trial.Tampered)
return Stopped(tampered: true); // never reward an altered/rolled-back marker
Die ehrliche Einschränkung, direkt im Quellcode dokumentiert: Das Löschen jeder Kopie des Markers setzt die Testphase tatsächlich zurück. Das ist eine akzeptierte Grenze des Bedrohungsmodells — das Ziel ist, die Hürde gegen beiläufige Umgehung bei einer quelloffen einsehbaren Codebasis zu erhöhen, nicht so zu tun, als sei dies ein unknackbarer Kopierschutz.
Verlängerungen verlängern; sie starten nie neu
Brauchen Sie mehr Evaluierungszeit? Ein vom Hersteller ausgestellter Trial-Tier-Schlüssel verlängert das Fenster — zum Beispiel auf insgesamt 90 Tage —, statt eine zweite Testphase zu starten. Wird ein solcher Schlüssel beobachtet, wird sein Ablaufdatum in den Marker geschrieben und verschiebt das Ende der Testphase nach vorn:
// only ever forward — never shortens, never resets the base start
var extended = current is { } c && c >= untilUtc ? c : untilUtc;
Die Persistierung der Verlängerung ist wichtig: Sie sorgt dafür, dass das längere Fenster (und sein Grace) überlebt, wenn der Schlüssel später ausläuft oder die Maschine offline geht. Sie müssen nicht mit dem Lizenzdienst verbunden bleiben, um die gewährte Verlängerung zu behalten.
Die Aktivierung setzt ohne Neustart fort
Weil ein gestopptes Produkt umkehrbar ist, muss die Aktivierung in einem laufenden Prozess wirksam werden. Das tut sie. Der Lizenzdienst speichert sein Urteil zwischen und bewertet es bei einem periodischen Refresh neu (standardmäßig alle fünf Minuten); eine explizite Aktivierung erzwingt eine sofortige Neubewertung:
public async Task<LicenseVerdict> ActivateAsync(string licenseKey, ...)
{
await _paid.ActivateAsync(licenseKey.Trim(), ct);
return Refresh(); // new verdict is live at once; the data plane resumes on its own
}
Ein gestoppter Cluster, den Sie mitten im Ausfall lizenzieren, beginnt bei der nächsten Bewertung schlicht wieder zu bedienen — kein Rolling Restart, kein erneutes Laden der Daten, kein Fenster, in dem der Zustand gefährdet ist.
Es gibt drei Wege, einen Schlüssel einzubringen, passend dazu, wie Menschen tatsächlich arbeiten:
Set-ZrLicense -Key "LIC-…"aus der Admin-Shell wendet den Schlüssel auf jeder Manager-Maschine im Workspace an (jede bindet ihren eigenen Seat pro Maschine).-Deactivategibt einen Seat frei.Get-ZrLicenseStatusgibt Stufe und Badge jedes Managers aus.ZARIS_SUPPORT_KEYals Umgebungsvariable wird beim Start einmalig aktiviert — setzen, neu starten, fertig. Das ist idempotent: Die erneute Aktivierung einer bereits gebundenen Maschine verbraucht keinen zusätzlichen Seat.- Ein Offline-Token für Air-Gapped-Deployments: ein vorab ausgestellter, signierter Blob, den Keyright vollständig offline verifiziert, ohne Rückruf.
Eine Handvoll Umgebungsvariablen stimmt den Rest für ein Deployment ab: ZARIS_SUPPORT_WINDOW_DAYS und ZARIS_PRODUCT_GRACE_DAYS passen die beiden Fenster an, ZARIS_DATA_ROOT verlegt den Marker, und ZARIS_LICENSE_SERVICE_URL zeigt auf einen selbst gehosteten Ausstellungsdienst.
Warum man es so baut
Es wäre weniger Code gewesen, bei einem abgelaufenen Datum eine Exception zu werfen. Die zusätzliche Mechanik — eine reine Policy-Funktion, ein zweistufiges Gate, ausgenommene Wiederherstellungsbefehle, ein gespiegelter, manipulationssichtbarer Marker, Fortsetzen ohne Neustart — existiert, um ein Prinzip zu wahren: Eine Lizenzentscheidung darf Sie nie Ihre Daten kosten und muss stets dadurch umkehrbar sein, dass man das Naheliegende tut.
Die Testphase läuft ab, und Sie laufen weiter, mit Warnungen. Das Grace läuft ab, und der Store hört auf zu bedienen, bewahrt dabei aber alles und bleibt für die Wiederherstellung erreichbar. Sie aktivieren einen Schlüssel, und er setzt live fort. Und die gesamte Entscheidung stammt aus einer deterministischen Funktion, mit der jede Komponente übereinstimmt, sodass keine Ecke des Systems unbemerkt etwas anderes durchsetzt. Lizenzierung ist der Ort, an dem sich viel gute Software gegen die eigenen Nutzer wendet; das Ziel hier war, daraus den einen Ort zu machen, an dem ein verteilter Store „Nein" sagen darf, ohne je „Weg" sagen zu dürfen.