DEV Community

Cover image for How to Prevent License Key Sharing (Three Layers That Work Together)
PermitCore
PermitCore

Posted on Originally published at permitcore.dev

How to Prevent License Key Sharing (Three Layers That Work Together)

Originally published on the PermitCore blog.

A license key on its own is just a string. Nothing stops someone from pasting a valid one into a forum thread or a Discord server unless the system that checks the key actually does something about it. Here are three complementary techniques we use in PermitCore. Each one covers a different sharing pattern, and together they make casual key-sharing genuinely inconvenient.

1. Node-locking: tie a key to a device

Every license has a Max Activations limit: how many distinct devices it can be active on at once. With node-locking on, the first activate() call binds the key to a hardware fingerprint (a hash derived from stable machine identifiers, nothing personally identifying). Reactivating on the same device doesn't use a new seat, but a genuinely different machine does, and once the limit is reached the next machine is rejected.

That's the first line of defense: sharing a key with a friend costs one of your own seats the moment they use it, and once the limit is hit, both of you find out immediately. The device activation guide covers how the fingerprinting works.

2. Floating licenses: shared seats, not shared copies

For team products where node-locking doesn't fit, for example a license that should float to whoever on the team is using the app right now, floating (concurrent) licenses work better. A fixed number of sessions are checked out and released as people start and stop the app. When every seat is taken, the next activation waits until someone checks theirs back in.

Simultaneous use stays bounded by what was paid for; it's just counted per concurrent session instead of per permanent device.

3. Abuse detection: the pattern seat limits can't see

A key with a generous seat count (say a 50-seat team license) can still be posted publicly and used by far more than 50 people over time, as long as no more than 50 are active at once. Every individual request looks legitimate.

So we watch for that pattern directly: per license key, how many distinct caller IPs hit validate() / activate() within a rolling one-hour window.

private const int RequestSuspiciousThreshold   = 200;  // requests/hour
private const int RequestBlockThreshold        = 2000; // requests/hour
private const int IpFanoutSuspiciousMultiplier = 5;    // × MaxActivations
private const int IpFanoutSuspiciousFloor      = 20;   // distinct IPs/hour, minimum
private const int IpFanoutBlockMultiplier      = 20;   // × MaxActivations
private const int IpFanoutBlockFloor           = 100;  // distinct IPs/hour, minimum
Enter fullscreen mode Exit fullscreen mode

The threshold scales with the license's own seat count. A 1-seat license reaching 20 distinct IPs in an hour is already abnormal, while a 500-seat license needs proportionally more before it's flagged. There are two tiers:

  • Suspicious: the request is still served, so a real customer is never blocked over a false positive, but an audit entry is written and a license.abuse_suspected webhook fires (at most once per key per hour) so you can decide what to do.
  • Block: well past the suspicious threshold, the request is rejected outright before any database work, which protects your infrastructure from a key that's clearly being hammered from everywhere.

Why layering matters

Node-locking and floating limits bound how much simultaneous use one key can support. Abuse detection catches the slower leak, where a key spreads more widely than its seat count alone would reveal. Combined with revocation, you can shut a leaked key down the moment the webhook tells you about it, without touching paying customers.

How do you handle leaked keys in your own products today: hard limits, monitoring, or just revoking when someone reports it? 🙂

Top comments (0)