# Use a License Key (/licensing/license-key)



A license key licenses an **online** server: the server exchanges the key for a lease on
first boot and refreshes it hourly. This page covers everything about running with a
key. If your server cannot reach the internet at all, see
[Air-gapped installations](/licensing/air-gap) instead.

## Pass the key to the server [#pass-the-key-to-the-server]

The key is read from one environment variable, `KUBEMQ_LICENSE_KEY`, in every
deployment mode. Replace `YOUR_LICENSE_KEY` with your key.

<Tabs items="[&#x22;Docker&#x22;, &#x22;Docker Compose&#x22;, &#x22;Helm (Secret)&#x22;, &#x22;Raw manifests&#x22;]">
  <Tab value="Docker">
    Pass the key with `-e` and **mount a volume** for the store — the lease cache lives
    there (see [The lease cache](#the-lease-cache) below):

    <RunKubeMQ />
  </Tab>

  <Tab value="Docker Compose">
    Keep the key out of the file by reading it from the shell environment:

    ```yaml title="docker-compose.yml"
    services:
      kubemq:
        image: europe-docker.pkg.dev/kubemq/images/kubemq-next:latest
        container_name: kubemq
        hostname: kubemq
        ports:
          - "50000:50000"
          - "9090:9090"
          - "8080:8080"
        environment:
          - KUBEMQ_LICENSE_KEY=${KUBEMQ_LICENSE_KEY:?set KUBEMQ_LICENSE_KEY in your shell}
        volumes:
          - kubemq-data:/kubemq/store
        restart: unless-stopped

    volumes:
      kubemq-data:
    ```

    ```bash title="Terminal"
    export KUBEMQ_LICENSE_KEY=YOUR_LICENSE_KEY
    docker compose up -d
    ```
  </Tab>

  <Tab value="Helm (Secret)">
    Create a Secret and point the chart at it. The key never appears in the Helm release
    values or on the `KubemqCluster` object:

    ```bash title="Terminal"
    kubectl create namespace kubemq
    kubectl create secret generic kubemq-license -n kubemq \
      --from-literal=key=YOUR_LICENSE_KEY

    helm repo add kubemq-next https://kubemq-io.github.io/charts-next
    helm install kubemq-next kubemq-next/kubemq-next -n kubemq \
      --set licenseKeySecretRef.name=kubemq-license \
      --set licenseKeySecretRef.key=key \
      --set replicas=1 --set standalone=true \
      --set volume.size=5Gi
    ```

    This installs a **single standalone node**, which is what a **trial** license allows
    (one installation, one node — `max_instances=1`); the chart's 3-node default only
    starts on a paid license whose cap covers it, and every pod otherwise exits with
    [`#cluster-over-cap`](/licensing/troubleshooting#cluster-over-cap). Choose `replicas`
    at install time — it is immutable once the cluster exists. `volume.size` gives the
    node a PersistentVolumeClaim; without one the cached lease is lost on every pod
    replacement ([`#store-not-persistent`](/licensing/troubleshooting#store-not-persistent)).

    For a quick, non-GitOps install you can pass the key directly with
    `--set licenseKey=YOUR_LICENSE_KEY`. The full chart reference is in
    [Install with Helm](/deploy/kubernetes-helm); the operator's behaviour is in
    [Kubernetes](/licensing/kubernetes).
  </Tab>

  <Tab value="Raw manifests">
    Without the operator, inject the key from a Secret into the server container and give
    the server the RBAC it needs to read its fingerprint:

    ```yaml title="kubemq-license.yaml"
    apiVersion: v1
    kind: Secret
    metadata:
      name: kubemq-license
      namespace: kubemq
    stringData:
      key: YOUR_LICENSE_KEY
    ```

    ```yaml title="statefulset-excerpt.yaml"
    spec:
      template:
        spec:
          serviceAccountName: kubemq-cluster-next
          containers:
            - name: kubemq
              image: europe-docker.pkg.dev/kubemq/images/kubemq-next:latest
              env:
                - name: KUBEMQ_LICENSE_KEY
                  valueFrom:
                    secretKeyRef:
                      name: kubemq-license
                      key: key
              volumeMounts:
                - name: store
                  mountPath: /kubemq/store
      volumeClaimTemplates:
        - metadata:
            name: store
          spec:
            accessModes: ["ReadWriteOnce"]
            resources:
              requests:
                storage: 20Gi
    ```

    The `kubemq-cluster-next` ServiceAccount and its ClusterRole are described in
    [Kubernetes without the operator](/licensing/kubernetes#without-the-operator).
  </Tab>
</Tabs>

<Callout type="info">
  The key is **only** read from the environment. It is not a `config.yaml` field, not a
  command-line flag, and a license block inside the base64 `CONFIG` variable is rejected
  by the loader. The value is redacted from `GET /config` and from the dashboard's
  configuration page.
</Callout>

## Activation [#activation]

On the first boot of an installation the server has no cached lease, so it activates:
it sends the key, its installation fingerprint and a description of where it runs
(`standalone`, `docker` or `kubernetes`, plus the server version and, when
`KUBEMQ_IMAGE_DIGEST` is injected, the image digest — operator 1.1.1 and later inject
`NODE_NAME` on every pod and `KUBEMQ_IMAGE_DIGEST` when the cluster's image reference is
pinned by digest, `…@sha256:…`; a tag-only image reference leaves the digest empty) to
`https://license.kubemq.io`. The service answers with a signed **lease** — a 7-day token
bound to this fingerprint (24 hours for a trial) — which the server caches and verifies
from then on. The key itself is not stored by the server.

If activation fails:

* **Bad key** (`401`, three refusals 30 seconds apart) — the server refuses to start.
  There is one error for an unknown key, a disabled license, a revoked one and an
  expired one; the log line ends in `#first-boot-bad-key`.
* **Bad request** (`400`, three times) — the service could not parse the activation
  request itself; the server refuses to start with `#first-boot-bad-request`. This is a
  version mismatch between the server build and the service, not a key problem.
* **Untrusted signing key** (`422 untrusted_kid`) — the service signs with a key this
  server build does not trust; the server refuses to start immediately with
  `#untrusted-kid`. Upgrade the server.
* **Service unreachable** (DNS, timeout, 5xx) or **any other answer** (`429`, an
  unexpected status) — the server retries with backoff (1, 2, 5, 15, then 30 minutes,
  ±10%), honouring a `Retry-After` header up to a cap of 30 minutes, for
  `KUBEMQ_LICENSE_FIRST_BOOT_RETRY_MINUTES` (default 30 minutes), then refuses to
  start with `#first-boot-unreachable`, which points at the
  [pre-activated file](#pre-activated-file-for-egress-restricted-networks) path.
  A `429 rate_limited` here is **expected on a multi-node first boot**: the pods of
  one installation activate in parallel and share a per-license activate rate limit,
  so some of them log `first boot: activate answered HTTP 429 rate_limited; retrying
  in …s #first-boot-unreachable` and wait 1–2 minutes before succeeding. That is
  not a fault and needs no action — the server is not unreachable, only rate-limited,
  and the same `#first-boot-unreachable` anchor covers both cases.

After activation, the server refreshes hourly and activates again only when the cached
lease can no longer be used: it is past its expiry plus grace, or it is bound to
another installation's fingerprint (a store volume copied from another host). In both
cases the server logs a warning and activates with the key instead of refusing.

## The lease cache [#the-lease-cache]

The server keeps four small files under its store directory:

| File                              | Purpose                                                                                                                                                                                                                                                                                                                        |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `<store>/license/lease.jws`       | The last good lease, replaced atomically on every successful refresh                                                                                                                                                                                                                                                           |
| `<store>/license/instance.id`     | The installation fingerprint outside Kubernetes (a random id generated on first boot)                                                                                                                                                                                                                                          |
| `<store>/license/clock.hwm`       | The clock high-water mark used to detect a clock rolled backwards                                                                                                                                                                                                                                                              |
| `<store>/license/revocation.json` | Counted revocation confirmations for the current license, so a restart cannot reset the count. Three on record → the server refuses to boot; the record is cleared only by a lease carrying a newer `token_version` (a reinstated license), which you obtain by deleting both `lease.jws` and `revocation.json` and restarting |

The default store path inside the container is `/kubemq/store`.

<Callout type="warn" title="The store must be on a persistent volume">
  The cache is what lets the server survive an outage of the licensing service — the
  fail-open window described in the [overview](/licensing#hard-stop-and-the-fail-open-window)
  exists only if the cached lease exists. Without a persistent store:

  * every container restart is a **first boot**, which needs the service to be reachable
    at that moment or the server does not start;
  * outside Kubernetes every restart generates a **new fingerprint**, so each restart
    counts as a new installation against your cap.

  The server logs a warning at boot (`#store-not-persistent`) when the store path is not a
  mount. Anything but a throwaway run should mount a volume at `/kubemq/store`.
</Callout>

## Regenerate a key [#regenerate-a-key]

A key can be regenerated — after a leak, a staff change, or simply because it was lost.
Regeneration issues a new key and invalidates the old one for **new activations**.
Running servers are unaffected: they hold a lease and refresh with it, never with the
key. Update the value wherever you store it (Secret, `.env`, CI variable) so that the
*next* new installation activates with the new key.

Trial keys are regenerated through the resend option on
[kubemq.io/trial](https://kubemq.io/trial); paid keys through your KubeMQ contact.

## Check the license: `GET /api/v1/license` [#check-the-license-get-apiv1license]

The management API reports the full licensing state. It is authenticated like every
other management route.

```bash title="Terminal"
curl -s http://localhost:8080/api/v1/license | jq
```

```json title="Response (abridged)"
{
  "license_id": "6f1c2a3e-9b4d-4c5e-8a7f-1d2e3f4a5b6c",
  "issued_to": "Example Corp",
  "plan": "pro",
  "mode": "online",
  "source": "key",
  "license_expires_at": "2027-09-19T00:00:00Z",
  "lease_expires_at": "2026-09-26T12:15:00Z",
  "grace_days": 7,
  "run_until": "2026-10-03T12:15:00Z",
  "max_instances": 3,
  "fingerprint": "3f2a9c1e-7b6d-4e5f-9a8b-0c1d2e3f4a5b",
  "fingerprint_source": "persisted",
  "token_version": 3,
  "kid": "license-signing-v2",
  "last_refresh": { "at": "2026-09-19T12:15:00Z", "outcome": "ok", "status": 200, "next_at": "2026-09-19T13:12:00Z" },
  "clock": { "skew_seconds": 1, "skew_warning": false, "high_water_mark": "2026-09-19T12:15:00Z" },
  "revocation": { "seen_count": 0, "first_seen": null, "last_seen": null, "reason": null, "draining": false },
  "over_cap": false,
  "silent": false,
  "usage": { "enabled": true, "last_report_at": "2026-09-19T12:10:00Z", "last_outcome": "accepted", "last_status": 200, "cursor_to": "2026-09-19T12:10:00Z", "boot_id": "0b3d5f7a-9c1e-4a2b-8d6f-1e3c5a7b9d0f" },
  "warnings": []
}
```

The fields to watch:

| Field                   | Meaning                                                                                                                                                                                                                           |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lease_expires_at`      | When the current lease ends. Healthy online servers show a value about 7 days ahead that moves forward every hour                                                                                                                 |
| `license_expires_at`    | The commercial expiry — the end of your contract or trial                                                                                                                                                                         |
| `run_until`             | Lease expiry + grace: the moment the server drains if it never reaches the service again                                                                                                                                          |
| `last_refresh.outcome`  | `ok`, or why the last refresh failed (`unreachable`, `unsigned_4xx`, `unsigned_5xx`, `malformed_200`, `rate_limited`, `revocation`); `never` on an offline server, or online before the first activation or refresh has completed |
| `fingerprint`           | This installation's identity — what you send when requesting an offline file                                                                                                                                                      |
| `token_version`         | The entitlement version of the current token; it increases whenever KubeMQ changes the license (plan, expiry, cap, reinstatement)                                                                                                 |
| `kid`                   | The id of the signing key that signed the current token (`license-signing-v1` or `license-signing-v2`)                                                                                                                            |
| `revocation.seen_count` | Non-zero means a signed revocation has been seen; at 3 the server drains                                                                                                                                                          |
| `over_cap`, `silent`    | The license has more installations than allowed, or has not reported usage for 72 hours                                                                                                                                           |
| `warnings`              | The exact warning lines currently logged every minute                                                                                                                                                                             |

### `?include=lease` [#includelease]

`GET /api/v1/license?include=lease` adds a `lease` field carrying the bare lease token —
the server's bearer credential. It is guarded accordingly: the call is refused with `403`
when management-API authentication is disabled, it requires the `admin` role, and every
call (granted or denied) is written to the audit log as `license.lease_read`. It exists
for the operator's lease cache write-back
([Kubernetes](/licensing/kubernetes#the-lease-cache-secret)); `kmq license` never
requests it.

### `kmq license` [#kmq-license]

The CLI renders the same object:

```bash title="Terminal"
kmq license
kmq license -o yaml
kmq license --fields plan,mode,lease_expires_at,run_until
```

`-o` selects the output format (`json`, `ndjson`, `yaml`, `table`); `--fields` projects
the output to the named fields. The command needs a service key with at least the
`read_only` role — see [kmq CLI](/operate/kmq-cli).

## Change the licensing endpoint [#change-the-licensing-endpoint]

`KUBEMQ_LICENSE_ENDPOINT` (default `https://license.kubemq.io`) is the base URL the
server activates, refreshes and reports usage against. Change it only when told to by
KubeMQ — for example to point at a forwarder inside your network that reaches the same
host. The value is an `http://` or `https://` base URL (plain `http://` is accepted for
a development endpoint); a trailing slash is trimmed and the `/v1/...` paths are
appended. Anything else is a configuration error and the server exits with code 1.

## Pre-activated file for egress-restricted networks [#pre-activated-file-for-egress-restricted-networks]

Some networks can reach the internet in general but cannot open egress to
`license.kubemq.io` — a strict allow-list, a proxy that strips request bodies, a
regulated environment. The answer is **not** to open the firewall: ask KubeMQ for a
**pre-activated file**.

A pre-activated file is an offline license file bound to your installation's
fingerprint list, valid for up to one year. The server then runs exactly like an
air-gapped installation — no activation, no refresh, no usage reports — and is renewed
by reissuing the file before it expires. To request one, read each installation's
`fingerprint` from `GET /api/v1/license` and follow
[Air-gapped installations](/licensing/air-gap).

## Related settings [#related-settings]

| Variable                                  | Default                     | Meaning                                                                           |
| ----------------------------------------- | --------------------------- | --------------------------------------------------------------------------------- |
| `KUBEMQ_LICENSE_KEY`                      | unset                       | The 43-character license key                                                      |
| `KUBEMQ_LICENSE_ENDPOINT`                 | `https://license.kubemq.io` | Base URL of the licensing service                                                 |
| `KUBEMQ_LICENSE_DRAIN_SECONDS`            | `300`                       | Drain window before a licensing exit (0–3600)                                     |
| `KUBEMQ_LICENSE_FIRST_BOOT_RETRY_MINUTES` | `30`                        | How long a first boot retries activation when the service is unreachable (1–1440) |
| `KUBEMQ_LICENSE_CACHED_LEASE`             | unset                       | A lease used only when no cache file exists — written by the operator, not by you |

A drain or retry value outside its range (or one that is not an integer) is a
configuration error: the server exits with code 1, not 3, and the log line names the
variable.

The complete table, including the file variables, is in
[Core & Licensing](/configure/reference/core).
