DEV Community

Zero Heartbeat
Zero Heartbeat

Posted on Originally published at delta1labs.com

Node-locking and seat management for software licenses

Node-locking binds a license to a specific machine so a single key can't be copied everywhere, and seat management lets you say "this key runs on N machines at once" and actually enforce it — but the second one only works with an online activation server, because a machine on its own can't count how many others share its key. This post walks through both mechanics honestly: how a machine fingerprint with swap tolerance keeps honest users from getting locked out, how a server enforces a seat cap, how customers move their own seats, and why online activation is the piece that turns "3 seats" from a promise into a rule.

What node-locking actually is

A node-lock ties a license to a machine fingerprint — a value the SDK derives from stable hardware and OS attributes on the device. When you activate a key, that fingerprint is recorded against the license. Every later validation recomputes the fingerprint locally and checks it matches. Copy the same key to a second machine and the fingerprint won't match, so the license simply doesn't apply there.

That's the whole idea, and it's genuinely useful: it means a key handed to one customer doesn't unlock a thousand installs. But the naive version has a well-known failure mode, and getting past it is the difference between a lock customers tolerate and a lock that fills your support queue.

The swap-tolerance problem

If your fingerprint hashes too many hardware attributes, the lock is brittle. A customer swaps a network card, upgrades an SSD, or updates a driver, the fingerprint shifts, and a paying user is suddenly locked out of software they own — followed immediately by an angry ticket. Hash too few attributes and the lock is trivial to spoof.

The fix is tolerance: allow a small amount of drift before the machine is considered a different one. Keyright's .NET SDK derives a stable fingerprint and applies a configurable NodeLockTolerance (default 1), so an ordinary single-component change — a new NIC, a replaced disk — doesn't trip the lock, while wholesale changes still read as a different machine. For unusual environments like VMs, containers, or CI runners, you can control exactly which components identify a machine by implementing IMachineComponents and setting MachineComponents on the SDK options. See the .NET SDK integration guide for the exact surface.

Seats: N machines, one key

A seat count is a promise — "this license runs on up to 3 machines" — and the moment you make it, you've walked into territory a client-side check can't handle. A single machine validating a license offline has no idea how many other machines are running the same key right now. Seat counting is a shared, stateful decision, and shared state lives on a server.

Here's the flow when it's done properly:

  • Activation records a seat. When a machine activates a key, the server records the activation (the machine's fingerprint and metadata) against the license and returns a short-lived signed lease bound to that machine.
  • The cap is enforced server-side. Activate more machines than the license allows and the server refuses — it returns a seat-limit result and no lease, so the extra machine stays in the free/unlicensed state. There's no client-side counter to patch around.
  • Re-activation is idempotent. A machine that's already bound can re-activate without burning a second seat. Restarting the app or refreshing the lease doesn't slowly leak your seat allowance away.

In Keyright, activation is a POST to the runtime /v1/activate endpoint carrying the key and the machine id; the server consumes a seat and hands back the lease. From the client you never touch that endpoint directly — you call ActivateAsync(key) and inspect the returned LicenseInfo. If seats are exhausted it comes back fail-closed with a message like "All seats for this license are in use," not an exception.

Letting customers move a seat themselves

Seats create a support problem the day after you ship them: a customer retires a laptop, buys a new one, and can't activate because all their seats are taken by machines they no longer use. If moving a seat means emailing you, every hardware refresh is a ticket.

The answer is self-service deactivation — let the customer free a seat without involving you:

  • Your app can release the seat the current machine holds by calling the runtime /v1/free-seat endpoint, so a "deactivate this device" button in your own UI just works.
  • Keyright's hosted customer portal lets a licensee view their activations and free a seat on a device they no longer use, then activate the new one — no ticket to you.
  • On your side, an admin can deactivate a specific machine from the dashboard's per-license activations view, which also shows device metadata so you can see where seats are actually in use.

Revocation: the other thing only a server can do

Seats aren't the only enforcement that needs a server. A refunded, charged-back, or leaked key has to die, and a signed license file issued last year knows nothing about a refund last week — its payload is fixed at issue time. Online activation closes that gap: revoke a key (POST /admin/licenses/{id}/revoke, or one click in the dashboard) and the client drops to the free edition on its next lease refresh. Because the lease is short-lived, "next refresh" comes around on its own.

For purely offline installs, Keyright can also build and ship a signed revocation list your app honors without a network call — slower, moving at the speed of your releases, but it means even a disconnected client isn't defenseless against a known-bad key.

Offline-first, online where it counts

None of this means abandoning offline validation — it means pairing the two. Keyright is offline-first: your app verifies a signed license or a cached lease locally, against an embedded public key, with zero network. Activation happens once; after that the cached lease keeps the app working offline for the life of the lease, and only near expiry does it reach the server again to reconcile seats and revocations. If the server is briefly unreachable, a still-valid cached lease covers the gap — a grace window — so an outage doesn't lock out a paying user.

Be honest about the boundary, though. Node-locking and offline checks run on the user's machine, so a determined attacker with a decompiler can patch them out — the same truth that applies to any client-side protection. That's why the enforcement that matters — seat caps and revocation — lives on a server the attacker doesn't control, and why you pair the client check with obfuscation to make patching costly. The client check is a fast, honest gate; the server is where the rules are real.

Wiring it up with Keyright

Keyright gives you node-locking with swap tolerance, server-enforced seats, self-service deactivation, and instant revocation as one system, with a .NET SDK (plus Node, Python, and Java) that fails closed by design. The client half is a few lines — initialize once, call ActivateAsync when a customer enters a key, and read the result. See the .NET SDK integration guide for the exact code, or the Keyright product page for the whole picture. The free plan is enough to wire real seat enforcement end to end before you pay anything.

Top comments (0)