Skip to content
← Alle Beiträge
· Delta1 Labs LicensingKeyright

Eine containerisierte .NET-App lizenzieren, wenn die Maschinenidentität flüchtig ist

Die knotengebundene Aktivierung bindet eine Lizenz an einen stabilen Maschinen-Fingerabdruck, doch Container erhalten bei jedem Neustart eine frische Identität, sodass naive Bindung Aktivierungen aufbraucht. So lizenzierst du eine containerisierte .NET-App mit Keyright: durch Einschleusen einer stabilen Deployment-Identität oder durch das Leasen gleichzeitiger Plätze für skalierte Replikas — und durch sauberes Fehlschlagen im geschlossenen Zustand beim Start.

Die knotengebundene Lizenzierung beruht auf einer bequemen Annahme: dass eine Maschine eine stabile Identität besitzt. Bilde den Hash einiger dauerhafter Hardware-Attribute, binde die Lizenz an diesen Fingerabdruck, und eine Lizenz entspricht einer Maschine für die gesamte Lebensdauer der Aktivierung. Diese Annahme trifft auf eine Workstation oder eine langlebige VM zu. Sie zerbricht in dem Moment, in dem du die App in einen Container steckst.

Ein Container hat keine dauerhafte Identität. Er hat eine frische, bei jedem Start geprägt. /etc/machine-id wird neu erzeugt, die MAC-Adresse ist synthetisch und wird neu zugewiesen, der Hostname ist ein zufälliger kurzer Hash, und das Dateisystem wird beim Beenden verworfen. Für einen fingerabdruckbasierten Lizenzclient sieht jeder Neustart, jeder Redeploy und jedes Skalierungsereignis wie eine völlig neue Maschine aus. Binde naiv, und du brauchst deine Aktivierungen in einer Woche gewöhnlicher Deployments auf — oder schlimmer: Ein rollierendes Update rollt eine Flotte von Containern aus, die allesamt gleichzeitig die Knotenbindung verfehlen.

Kernaussage: Höre in Containern auf, die Maschinenidentität aus dem Container abzuleiten. Binde die Lizenz entweder an eine stabile Identität, die der Betreiber einschleust (eine Umgebungsvariable oder ein eingehängtes Secret, das Neustarts und Deployments überlebt), oder verwende — für elastische, horizontal skalierte Replikas — eine gleichzeitige Lease-Lizenzierung, bei der jede Replika einen kurzlebigen Platz abruft und ihn beim Sterben automatisch zurückgibt. Beides erlaubt, beim Start zu validieren und im geschlossenen Zustand fehlzuschlagen, ohne bei jedem Deployment Aktivierungen zu verbrennen.

Warum Container-Fingerabdrücke nutzlos sind

Es lohnt sich, genau zu sein, warum die üblichen Fingerabdruck-Eingaben dich im Stich lassen, denn der Fehler bleibt stumm, bis deine Plätze aufgebraucht sind.

  • /etc/machine-id wird vom Init-System beim ersten Boot befüllt. In einem Container gibt es keinen persistenten ersten Boot — die Datei ist leer oder wird in der Image-Schicht neu erzeugt —, sodass entweder jeder Container einen einzigen eingebackenen Wert teilt (nutzlos zum Unterscheiden von Knoten) oder jeder einen neuen erhält (nutzlos für die Stabilität).
  • MAC-Adressen gehören zu virtuellen Schnittstellen, die die Laufzeit mit dem Container erzeugt und zerstört. Sie sind nicht die Host-NIC und bleiben nicht erhalten.
  • Der Hostname ist standardmäßig die gekürzte Container-ID — bei jedem docker run und bei jedem vom Orchestrator geplanten Pod anders.
  • Festplatten- und Volume-Seriennummern verweisen auf ein Overlay oder ein flüchtiges Volume, das beim Beenden verworfen wird.

Der Orchestrator macht dies absichtlich schlimmer: Seine ganze Aufgabe besteht darin, Instanzen wie Vieh zu behandeln und sie frei umzuplanen und zu ersetzen. Ein Fingerabdruck, der sich beim Umplanen ändert, identifiziert keine Maschine; er identifiziert eine Container-Lebensdauer, was genau die falsche Granularität für eine Lizenz ist.

Die beiden Diagramme unten stellen gegenüber, was während eines einzigen rollierenden Deployments geschieht.

Rollierendes Deployment: v1-Pods leeren sich, v2-Pods startenContainer-Fingerabdruck (flüchtig)pod v1-aid=7f3a → Platz 1pod v1-bid=b1c9 → Platz 2pod v2-aid=2e8d → Platz 3pod v2-bid=9a04 → Platz 44 Aktivierungen durch ein Deployment verbranntKEYRIGHT_DEPLOYMENT_ID eingeschleust (stabil)pod v1-aid=checkout-svcpod v1-bid=checkout-svcpod v2-aid=checkout-svcpod v2-bid=checkout-svc1 Identität, Reaktivierung ist idempotentLinks sieht die Flotte für den ausstellenden Dienst wie vier verschiedene Maschinen aus; rechtssieht sie wie ein sich reaktivierender Knoten aus, sodass beim Rollout kein zusätzlicher Platz verbraucht wird.

Strategie 1 — an eine eingeschleuste Deployment-Identität binden

Die saubere Lösung für ein einzelnes logisches Deployment (ein lizenzierter Dienst, egal wie oft er neu startet) ist, das SDK die Maschinenidentität nicht mehr erraten zu lassen und ihm stattdessen eine zu übergeben, die der Betreiber kontrolliert. Der Betreiber legt einen Wert fest — nennen wir ihn KEYRIGHT_DEPLOYMENT_ID —, der über Neustarts und Deployments desselben Dienstes hinweg konstant bleibt. Der Container liest ihn beim Start und übergibt ihn dem SDK als Bindungsidentität.

KeyrightOptions stellt genau dafür eine MachineId bereit. Ist sie gesetzt, bindet das SDK die Aktivierung an deinen Wert und rührt den Container-Fingerabdruck nie an:

using Delta1Labs.Keyright;

static string RequireDeploymentId()
{
    // Prefer a mounted secret file; fall back to an env var. Never derive
    // this from the container — that is the whole point.
    var path = Environment.GetEnvironmentVariable("KEYRIGHT_DEPLOYMENT_ID_FILE");
    if (!string.IsNullOrEmpty(path) && File.Exists(path))
        return File.ReadAllText(path).Trim();

    var id = Environment.GetEnvironmentVariable("KEYRIGHT_DEPLOYMENT_ID");
    if (string.IsNullOrWhiteSpace(id))
        throw new InvalidOperationException(
            "KEYRIGHT_DEPLOYMENT_ID is not set. A containerized deployment must " +
            "inject a stable license identity; refusing to start unbound.");
    return id.Trim();
}

static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "checkout-service",
    PublicKeyBase64 = "MIIBIjANBgkq...",                    // verify-only public key
    ServiceUrl      = "https://keyright.example.com",       // your self-hosted instance
    MachineId       = RequireDeploymentId()                 // the injected stable identity
});

Mit festgenageltem MachineId ist der Aktivierungsablauf der gewohnte — aber nun stabil:

// Run once at startup, before serving traffic.
LicenseInfo info = await License.ActivateAsync(
    Environment.GetEnvironmentVariable("KEYRIGHT_LICENSE_KEY")!, ct);

if (!info.IsLicensed)
{
    // Fail closed. See "Failing closed cleanly" below.
    logger.LogError("License not active: {Reason}", info.Reason);
    return StartupResult.Unlicensed(info);
}

logger.LogInformation("Licensed: {Tier}, bound to {Id}", info.Tier, info.MachineId);

Da sich jeder Container dieses Deployments gegen dieselbe MachineId aktiviert, erkennt Keyright einen Neustart oder einen ausgerollten Ersatz als Reaktivierung eines bereits bekannten Knotens. Die Reaktivierung einer bereits gebundenen Identität ist idempotent: Sie erneuert das Lease und verbraucht keinen zusätzlichen Platz. Hundert Neustarts kosten einen Platz. Das signierte Lease, das das SDK zwischenspeichert, wird in das Dateisystem des Containers geschrieben und beim Beenden auf natürliche Weise verworfen — was in Ordnung ist, denn der nächste Container reaktiviert sich einfach gegen die stabile ID und erhält ein frisches Lease zurück.

Woher die Identität kommen sollte

Die eingeschleuste ID muss stabil und vom Betreiber kontrolliert sein, nicht aus etwas abgeleitet, das die Laufzeit neu erzeugt. Gute Quellen: ein Wert, den der Betreiber pro Dienst in seiner Deployment-Konfiguration festlegt, ein eingehängtes Secret oder die stabile Workload-Identität deines Orchestrators (nicht der Pod-Name — der Name der Workload). Behandle den Lizenzschlüssel genauso: Hänge ihn als Secret ein, backe ihn nie in die Image-Schicht ein.

Strategie 2 — gleichzeitige Leases für skalierte Replikas

Strategie 1 ist richtig, wenn „das Deployment“ ein lizenzierter Knoten ist. Sie ist falsch für einen elastischen Replikasatz, denn eine einzige eingeschleuste ID würde zwanzig Replikas wie einen Knoten aussehen lassen — du würdest unterzählen statt ehrlich zu lizenzieren. Wenn du wirklich N austauschbare Replikas betreibst und Parallelität lizenzieren willst, verwende stattdessen floating Leases.

Jede Replika erwirbt beim Start ein kurzlebiges Platz-Lease, erneuert es per Heartbeat während des Laufs, und das Lease läuft von selbst ab, wenn die Replika entfernt oder herunterskaliert wird. Du kaufst eine Parallelitätsobergrenze; der Orchestrator kann frei zwischen null und dieser Obergrenze skalieren, und eine mit SIGKILL beendete Replika lässt nie einen festhängenden Platz zurück.

static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "checkout-service",
    PublicKeyBase64 = "MIIBIjANBgkq...",
    ServiceUrl      = "https://keyright.example.com"
    // No MachineId here: each replica is a distinct lease holder, not a bound node.
});

SeatLease lease;
try
{
    // A fresh instance id per replica is fine and expected — the seat is tied
    // to a live lease, not to a durable fingerprint.
    lease = await License.AcquireSeatAsync(
        Environment.GetEnvironmentVariable("KEYRIGHT_LICENSE_KEY")!, ct);
}
catch (SeatsExhaustedException)
{
    // Concurrency ceiling hit. Fail closed: do not report healthy.
    logger.LogError("All concurrent seats are in use; refusing to start.");
    return StartupResult.Unlicensed(reason: "seats-exhausted");
}

// Keep the lease alive for the life of the replica.
var keepAlive = new SeatKeepAlive(License, lease);
hostLifetime.ApplicationStopping.Register(() =>
    License.ReleaseSeatAsync(lease).GetAwaiter().GetResult());   // prompt check-in

Der Absturzfall ist der, den Container hier gratis bekommen: Wird der Pod per OOMKilled beendet oder verdrängt, läuft ReleaseSeatAsync nie, der Heartbeat hört auf, und das Lease erreicht seine TTL und wird serverseitig zurückgeholt. Kein festhängender Platz, keine verbrannte Aktivierung. (Der Lease-Lebenszyklus, der Heartbeat-Timer und der elegante Wiederholungsversuch bei belegten Plätzen werden ausführlich in floating gleichzeitige Plätze behandelt.)

Welche Strategie passt:

Eine stabile ID einschleusen (Strategie 1)Gleichzeitige Leases (Strategie 2)
ModellEin lizenzierter Knoten, viele NeustartsN austauschbare Replikas
Gebunden anEine vom Betreiber eingeschleuste IdentitätEin lebendes Lease pro Replika
SkalierungsereignisGleiche ID → idempotente ReaktivierungJede Replika nimmt/gibt einen Platz
Braucht den DienstBei Aktivierung / Lease-ErneuerungDurchgehend, um Parallelität zu zählen
Gut fürEinen Singleton- oder Aktiv-Passiv-DienstElastische, horizontal skalierte Sätze

Sauber im geschlossenen Zustand fehlschlagen

Ein Container darf nie beginnen, Verkehr auf einer ungeklärten Lizenz zu bedienen, und er darf auch nicht wegen eines Lizenz-Schluckaufs in eine Neustartschleife geraten. Das Keyright-SDK überlässt dir die Richtlinie: Validate() und ActivateAsync() werfen bei gewöhnlichen Fehlern keine Ausnahme — ein fehlender Schlüssel, ein nicht erreichbarer Dienst, ein aufgebrauchter Platz, eine widerrufene Lizenz —, sie liefern ein LicenseInfo (oder lösen auf dem Lease-Pfad die typisierte SeatsExhaustedException aus), das du prüfst und nach dem du handelst. Lege die Richtlinie einmal fest, beim Start, bevor die erste Anfrage bedient wird.

Es gibt zwei vernünftige geschlossene Zustände, und du solltest bewusst wählen:

  1. Auf Community Edition herunterstufen. Hat dein Produkt eine kostenlose Stufe, kann ein unlizenzierter Container den Funktionsumfang der Community Edition ausführen. Validate() liefert ein LicenseInfo in der Edition Community, das den Grund trägt; schränke die kostenpflichtigen Berechtigungen anhand von info.Entitlements ein und laufe.
  2. Verweigern, gesund zu sein. Verlasse bei einem Produkt ohne kostenlosen Pfad den Prozess nicht (das gerät nur in eine Neustartschleife und überflutet den ausstellenden Dienst). Starte stattdessen den Prozess, melde aber an deiner Readiness-Sonde nicht gesund, damit der Orchestrator den Verkehr zurückhält, und protokolliere den Grund laut und deutlich. Das gibt einem Betreiber ein Zeitfenster, um den eingeschleusten Schlüssel zu korrigieren oder die Konnektivität wiederherzustellen, ohne einen Neustartsturm.
app.MapHealthChecks("/ready", new HealthCheckOptions
{
    Predicate = c => c.Tags.Contains("ready")
});

// A readiness check that reflects the license decision made at startup.
services.AddHealthChecks().AddCheck("license", () =>
    LicenseState.Current.IsLicensed
        ? HealthCheckResult.Healthy($"Licensed: {LicenseState.Current.Tier}")
        : HealthCheckResult.Unhealthy($"Unlicensed: {LicenseState.Current.Reason}"),
    tags: new[] { "ready" });

Schränke die eigentliche lizenzierte Funktionalität überall auf dieselbe Weise ein — prüfe eine Berechtigung, nie einen zwischengespeicherten Bool-Wert aus einem früheren Lauf:

LicenseInfo info = License.Validate();              // offline, no network, never throws
bool canUsePremiumRouting = info.Entitlements.IsEnabled("premium-routing");
long maxThroughput        = info.Entitlements.GetLimit("max-rps", 50);

Es im Image und in der Compose-Datei verdrahten

Das Container-Image bleibt sauber: kein Lizenzschlüssel, keine eingebackene Identität. Alles Lizenzbezogene trifft zur Laufzeit als Umgebung und Secrets ein, die der Betreiber kontrolliert.

# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["CheckoutService.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
# No KEYRIGHT_LICENSE_KEY and no KEYRIGHT_DEPLOYMENT_ID in the image.
# They are injected at run time. Fail closed if the id is missing (see startup code).
ENTRYPOINT ["dotnet", "CheckoutService.dll"]

Der Compose-Dienst ist der Ort, von dem die stabile Identität und das Schlüsselmaterial tatsächlich stammen. Der Schlüssel ist ein eingehängtes Secret; die Deployment-ID ist eine schlichte, vom Betreiber gesetzte Umgebungsvariable, die über up, Neustart und Redeploy hinweg konstant bleibt:

services:
  checkout:
    image: registry.example.com/checkout-service:2.3.0
    environment:
      # Stable across restarts/redeploys — operator-owned, NOT container-derived.
      KEYRIGHT_DEPLOYMENT_ID: "checkout-svc-prod"
      KEYRIGHT_LICENSE_KEY_FILE: "/run/secrets/keyright_key"
      ServiceUrl: "https://keyright.example.com"
    secrets:
      - keyright_key
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/ready"]
      interval: 15s
      timeout: 3s
      retries: 5          # readiness reflects the license state; unlicensed = not ready
    deploy:
      replicas: 1         # Strategy 1: one bound node. For Strategy 2, raise replicas
                          # and drop KEYRIGHT_DEPLOYMENT_ID (use leases instead).
secrets:
  keyright_key:
    file: ./secrets/keyright.key

Zwei Regeln machen das robust. Erstens: Identität und Schlüssel sind nie im Image — ein Image wird kopiert, umgetaggt und geteilt, und alles, was darin eingebacken ist, sickert durch. Zweitens: Für Strategie 1 wird die KEYRIGHT_DEPLOYMENT_ID einmal vom Betreiber gesetzt und als Teil der Deployment-Konfiguration behandelt, sodass sie jeden Neustart und jedes rollierende Update unverändert übersteht. Für Strategie 2 lässt du sie ganz weg und erhöhst die Replika-Anzahl — das Lease-Modell zählt stattdessen die Parallelität.

Zusammenfassung

Container haben die eine Annahme zerbrochen, auf der die Knotenbindung errichtet wurde — eine stabile Maschinenidentität —, und die Lösung ist, nicht länger so zu tun, als hätte der Container eine. Schleuse für einen einzelnen lizenzierten Dienst eine stabile KEYRIGHT_DEPLOYMENT_ID ein, die dem Betreiber gehört, und binde mit KeyrightOptions.MachineId daran, sodass Neustarts und Deployments denselben Knoten idempotent reaktivieren und nie einen Platz verbrennen. Lizenziere bei elastischen Replikasätzen die Parallelität mit floating Leases, sodass jede Replika einen kurzlebigen Platz abruft und eine beendete Replika ihn von selbst zurückgibt. Kläre in beiden Fällen die Lizenz beim Start, halte Schlüssel und Identität aus dem Image heraus, und schlage an deiner Readiness-Sonde im geschlossenen Zustand fehl, statt in eine Neustartschleife zu geraten. Das Keyright.NET-SDK gibt dir beide Pfade — MachineId-genagelte Aktivierung und lease-basierte Parallelität — mit einem geschlossenen LicenseInfo an jeder Stelle dazwischen, sodass ein Orchestrator deine App den ganzen Tag umplanen kann, ohne deinen Aktivierungszähler anzurühren.

Nebula.NET testen

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