Skip to content
← Alle Beiträge
· Delta1 Labs Lizenzierung.NETLeitfaden

Container und automatisch skalierte .NET-Dienste mit Keyright lizenzieren

Maschinengebundene Lizenzierung setzt eine Maschine voraus: einen stabilen Rechner mit einem Fingerabdruck, dem ein Platz zugewiesen werden kann. Ein Container hat nichts davon — das Image ist über alle Replikate identisch, der Host ist flüchtig, und ein Autoscaler startet und stoppt Instanzen schneller, als ein Aktivierungsablauf folgen kann. So lizenzierst du einen horizontal skalierten .NET-Dienst mit Keyright: einmal an der Deployment-Grenze aktivieren, eine signierte Lizenz in jedes Replikat ausliefern, offline ohne Aufruf pro Instanz validieren und vorab entscheiden, ob ein Platz ein Deployment oder eine laufende Instanz bedeutet.

Jedes Lizenzierungs-SDK, das sich an eine Maschine bindet, setzt voraus, dass es eine Maschine gibt: einen dauerhaften Host mit einem Fingerabdruck — eine CPU-ID, eine Registry-GUID, eine MAC-Adresse —, dem du einen Platz zuweisen und den du morgen wiedererkennen kannst. Diese Annahme ist das Fundament des Node-Locking, und sie ist innerhalb eines Containers völlig falsch.

Ein Replikat ist keine Maschine. Zehn Replikate deines Dienstes laufen mit demselben Image, also kollidiert ein naiver, aus der Umgebung berechneter Fingerabdruck und sie sehen alle wie ein Host aus — oder schlimmer, der Fingerabdruck stützt sich auf etwas Container-Eigenes und jedes Replikat sieht wie eine brandneue Maschine aus, die ihren eigenen Platz braucht. Der Host darunter ist flüchtig: Der Fingerabdruck, gegen den du heute Morgen aktiviert hast, ist nach dem Deployment am Nachmittag weg. Und ein Autoscaler erzeugt und zerstört Instanzen schneller, als eine Aktivierungs-Aushandlung folgen kann, sodass ein Aktivierungsablauf pro Replikat entweder dein Platzkontingent in Minuten erschöpft oder tote Leases auf Hosts hinterlässt, die es nicht mehr gibt.

Dieser Beitrag zeigt, wie man einen horizontal skalierten .NET-Dienst mit Keyright lizenziert, ohne dass irgendetwas davon passiert. Der zentrale Zug ist, nicht mehr Replikate zu aktivieren, sondern das Deployment — einmal, an einer tatsächlich stabilen Grenze — und dann eine signierte Lizenz in jedes Replikat auszuliefern, die jedes offline überprüft. Wir bauen das, fügen eine Heartbeat-Variante hinzu für den Fall, dass du wirklich Instanzen messen willst, und verdrahten einen eigenen Maschinen-Identitätsanbieter, damit du genau steuerst, was „dieselbe Maschine” bedeutet.

Die tatsächlich stabile Grenze

Das ganze Problem löst sich auf, sobald du die richtige Einheit wählst. Es gibt nur zwei ehrliche Antworten auf „was ist hier ein Platz”, und sie führen zu unterschiedlicher Verdrahtung:

  • Ein Platz ist ein Deployment. Du verkaufst eine Lizenz pro Produktionsumgebung. Ob es auf drei Replikate skaliert oder auf dreißig, ist ein Implementierungsdetail der Infrastruktur des Kunden, kein abrechenbares Ereignis. Für ein Serverprodukt willst du fast immer das.
  • Ein Platz ist eine laufende Instanz. Du misst wirklich die Nebenläufigkeit — N Instanzen dürfen gleichzeitig laufen — und du willst, dass Autoscaling über N hinaus verweigert wird oder aus einem Pool zieht.

Wähle bewusst. Der Fehlermodus ist, die Messung pro Instanz versehentlich zu wählen, weil du einen Aktivierungsablauf aus einer Desktop-App übernommen hast, wo eine Installation gleich einer Maschine gleich einem Platz ist. Auf einem Desktop hält diese Identität jahrelang. In einem Cluster hält sie die Lebensdauer eines Pods, die neunzig Sekunden betragen kann.

Ausstellungsdienst(nur Aktivierung)einmalBootstrap-SchrittActivateAsync()→ signierte Lizenzlease.bin (schreibgesch.)Replikat 1Validate() offline ✓Replikat 2Validate() offline ✓Replikat N (autoskaliert)Validate() offline ✓eine Aktivierung · eine signierte Lizenz · N Offline-Prüfungen · kein Netzwerk pro Replikat

Einmal aktivieren, an der Grenze

Die Aktivierung ist der einzige Schritt, der mit deinem Ausstellungsdienst spricht, und der einzige Schritt, der nicht in einem Anfragen bedienenden Replikat laufen darf. Führe ihn einmal aus — in einer Release-Pipeline, einem Init-Job oder einem einmaligen Bootstrap-Container — und erfasse die signierte Lizenz, die er zurückgibt.

// bootstrap: runs ONCE per deployment, not per replica.
// Output is a signed lease blob that every replica will verify offline.
var client = new KeyrightClient(new KeyrightOptions
{
    LicenseKey = Environment.GetEnvironmentVariable("KEYRIGHT_KEY"),
    // Pin identity to the DEPLOYMENT, not the ephemeral host (see below).
    MachineIdentity = new DeploymentIdentity("prod-eu-west"),
});

ActivationResult result = await client.ActivateAsync();
if (!result.Succeeded)
{
    Console.Error.WriteLine($"activation failed: {result.Status}");
    return 1;
}

// Persist the signed lease where replicas can read it read-only.
await File.WriteAllBytesAsync("/license/lease.bin", result.Lease.ToBytes());
return 0;

result.Lease ist das signierte Artefakt: die Lizenz-Nutzlast plus der Lease pro Maschine, signiert mit dem privaten Schlüssel deines Ausstellungsdienstes und überprüfbar gegen den in dein Binary kompilierten öffentlichen Schlüssel. Er trägt sein eigenes Gültigkeitsfenster (standardmäßig 14 Tage) und die Deployment-Identität, für die er geprägt wurde. Nichts Geheimes lebt darin, das ein Angreifer nicht ohnehin aus einer laufenden Instanz lesen könnte, also ist seine Verteilung als gewöhnliche schreibgeschützte Konfiguration in Ordnung: Er ist signiert, nicht vertraulich.

In jedem Replikat überprüfen, offline

Jedes Replikat lädt diese Lizenz und validiert sie lokal. Kein ActivateAsync, kein Aufruf des Ausstellungsdienstes, kein geteilter veränderlicher Zustand — nur eine Signaturprüfung gegen den eingebetteten öffentlichen Schlüssel.

public sealed class LicenseGate : IHostedService
{
    private volatile LicenseSnapshot _snapshot = LicenseSnapshot.Denied;
    public LicenseSnapshot Current => _snapshot;

    public Task StartAsync(CancellationToken ct)
    {
        var leaseBytes = File.ReadAllBytes("/license/lease.bin");

        var client = new KeyrightClient(new KeyrightOptions
        {
            // Same deployment identity the lease was minted for; the
            // offline check confirms the lease matches this identity.
            MachineIdentity = new DeploymentIdentity("prod-eu-west"),
            LeaseSource = LeaseSource.FromBytes(leaseBytes),
        });

        LicenseInfo info = client.Validate();   // offline signature verification
        _snapshot = info.IsValid
            ? new LicenseSnapshot(true, info.Edition, client.Entitlements(), info.Status)
            : LicenseSnapshot.Denied;

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

Validate() ist eine lokale kryptografische Operation. Sie läuft in einem netzwerkisolierten Pod, in einem netzgetrennten Cluster und bei einem Kaltstart aus dem Nichts, wo der Ausstellungsdienst vielleicht gar nicht erreichbar ist — was der ganze Sinn ist, die Aktivierung an der Grenze zu machen. Ein Replikat, das nichts erreichen kann, kann dennoch beweisen, dass es eine gültige Lizenz hält, denn der Beweis lebt in der Signatur, nicht in einem Rückruf.

Registriere es einmal und lies den Snapshot überall, genau wie du es mit einem einzigen Gate in einer Desktop-App tätest:

builder.Services.AddSingleton<LicenseGate>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<LicenseGate>());

// In a request path — read the cached snapshot, never re-validate inline.
app.MapPost("/export", (LicenseGate gate) =>
    gate.Current.Entitlements.IsEnabled("bulk-export")
        ? Results.Ok(RunExport())
        : Results.StatusCode(StatusCodes.Status402PaymentRequired));

Steuern, was „dieselbe Maschine” bedeutet

Das Teil, das all dies zum Funktionieren bringt, ist der Maschinen-Identitätsanbieter. Keyrights Standard leitet eine Identität aus Host-Merkmalen ab — richtig für einen Desktop, falsch für ein Replikat. Überschreibe ihn, damit du die Granularität bestimmst. Für deployment-bezogene Lizenzierung muss jedes Replikat zur gleichen Identität auflösen, damit die für das Deployment geprägte Lizenz in allen validiert:

public sealed class DeploymentIdentity : IMachineIdentityProvider
{
    private readonly string _deployment;
    public DeploymentIdentity(string deployment) => _deployment = deployment;

    // Stable across replicas and restarts; independent of the ephemeral host.
    // Source it from a value that is constant for the deployment, e.g. an
    // injected environment name, NOT from hostname/CPU/MAC.
    public string GetMachineId() =>
        Convert.ToHexString(
            SHA256.HashData(Encoding.UTF8.GetBytes($"deployment:{_deployment}")));
}

Die Regel steht im Kommentar: Leite die ID aus etwas ab, das für das Deployment konstant ist (ein Umgebungsname, eine Mandanten-ID, die du injizierst), und niemals aus Dns.GetHostName(), der Container-ID oder irgendeinem Wert pro Replikat. Variiert die ID pro Replikat, bist du wieder bei der Messung pro Instanz, ob du es wolltest oder nicht.

Wenn du wirklich Instanzen messen willst

Manchmal ist die Nebenläufigkeit das Produkt und du willst, dass genau N Instanzen laufen. Dann lehnst du dich an die kurze Lebensdauer des Lease an, statt sie zu bekämpfen. Präge Leases pro Instanz mit kurzer Gültigkeit und einem Heartbeat; ein herunterskaliertes Replikat hört einfach auf zu erneuern, und sein Platz wird bei Ablauf des Lease zurückgewonnen — ohne einen expliziten Deaktivierungsaufruf, den ein getöteter Container nie machen würde.

var client = new KeyrightClient(new KeyrightOptions
{
    LicenseKey = key,
    MachineIdentity = new InstanceIdentity(),     // distinct per replica, on purpose
    LeaseDuration = TimeSpan.FromMinutes(10),      // short, so scale-in self-heals
});

// A background heartbeat renews while the instance lives.
// Miss two renewals (e.g. the pod was killed) and the seat frees itself.
_ = Task.Run(async () =>
{
    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
    while (await timer.WaitForNextTickAsync(ct))
        await client.RefreshAsync();   // the only runtime call to the issuing service
});

Das ist die eine Topologie, in der Replikate zur Laufzeit mit dem Ausstellungsdienst sprechen, und es ist ein bewusster Handel: Du akzeptierst eine Erneuerungsabhängigkeit im Austausch für eine genaue Nebenläufigkeitsabrechnung. Alles, was das Gate bedient, liest weiterhin den zwischengespeicherten Snapshot; der Heartbeat tauscht nur die Referenz dieses Snapshots bei einer erfolgreichen Erneuerung aus, und ein vorübergehender Erneuerungsfehler reitet auf der verbleibenden Gültigkeit des Lease, statt beim ersten Aussetzer den Dienst zu verweigern — dieselbe Kulanz-Logik, die du auf jedes offline-first Gate anwenden würdest.

Was man mitnehmen sollte

Container-Lizenzierung geht schief, wenn ein Replikat für eine Maschine gehalten wird. Korrigiere zuerst die Einheit — Deployment oder Instanz — und die Verdrahtung folgt. Für den häufigen Fall ist ein Deployment die stabile Grenze: dort einmal aktivieren, eine signierte Lizenz prägen und ein Offline-Validate() in jedem Replikat den Rest erledigen lassen, mit einem Maschinen-Identitätsanbieter, der alle Replikate auf eine Identität fixiert. Reserviere Leases pro Instanz und Heartbeats für den Fall, dass Nebenläufigkeit wirklich das ist, was du verkaufst. In beiden Fällen bleiben die Anfragen bedienenden Replikate offline-first und der Ausstellungsdienst bleibt vom kritischen Pfad fern — das ist es, was einem Dienst erlaubt, von null auf dreißig und zurück zu skalieren, ohne dein Platzkontingent oder deine Latenz anzutasten.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.