# Air-Gapped Installations (/licensing/air-gap)



An air-gapped server is licensed with a **file**: a token signed by KubeMQ that the
server verifies locally with the public key embedded in its binary. The server never
opens an outbound connection — no activation, no refresh, no usage report.

The same file is also the answer for networks that are online but cannot allow egress to
the licensing endpoint (a *pre-activated* file — see
[Use a license key](/licensing/license-key#pre-activated-file-for-egress-restricted-networks)).

## Request a file [#request-a-file]

A license file is bound to a **list of installation fingerprints** — the installations
allowed to run it. Collect one fingerprint per installation and send them with your
request.

<Steps>
  <Step>
    ### Read each installation's fingerprint [#read-each-installations-fingerprint]

    The fingerprint is stable per installation: on Kubernetes it is the UID of the
    `kube-system` namespace (one per cluster); elsewhere it is an id generated on first boot
    and persisted at `<store>/license/instance.id`.

    The easiest way to read it is from a running server:

    ```bash title="Terminal"
    curl -s http://localhost:8080/api/v1/license | jq -r '.fingerprint, .fingerprint_source'
    ```

    If you do not have a licensed server yet: on **Kubernetes** the fingerprint is simply
    the cluster's `kube-system` namespace UID, which you can read directly —

    ```bash title="Terminal"
    kubectl get namespace kube-system -o jsonpath='{.metadata.uid}'
    ```

    — provided the server's ServiceAccount has the permission described in
    [Kubernetes](/licensing/kubernetes#without-the-operator), so that the running server
    will derive the same value (`fingerprint_source: "kube-system"`, not `persisted`).
    Elsewhere the fingerprint is generated on the server's first boot and persisted at
    `<store>/license/instance.id`; the reliable way to obtain it is to run the server once
    with an online key (a [trial key](/licensing/trial) works) on the same store volume you
    will keep, and read `GET /api/v1/license`.
  </Step>

  <Step>
    ### Send the list to KubeMQ [#send-the-list-to-kubemq]

    Send your fingerprints, the number of installations, and the plan to your KubeMQ
    contact. Ask for an **unbound** file (empty fingerprint list) only if your installations
    are genuinely ephemeral; an unbound file is accepted by any installation.
  </Step>

  <Step>
    ### Receive the armored file [#receive-the-armored-file]

    You receive a text file that looks like this:

    ```text title="kubemq.license"
    -----BEGIN KUBEMQ LICENSE-----
    eyJhbGciOiJFUzI1NiIsImtpZCI6ImxpY2Vuc2Utc2lnbmluZy12MiJ9.<PAYLOAD>.<SIGNATURE>
    -----END KUBEMQ LICENSE-----
    ```

    The body is one signed token. Its header names the signing key id (`kid`) — the server
    binary trusts `license-signing-v1` and `license-signing-v2`; the payload and signature
    above are placeholders, not a real license.

    Store it like any other secret. It carries your license id, plan, commercial expiry,
    installation cap and fingerprint list, all signed.
  </Step>
</Steps>

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

Two variables accept a file. `KUBEMQ_LICENSE_FILE` takes a **path**; `KUBEMQ_LICENSE_DATA`
takes the **contents** (the armored text, or the bare token inside it).

<Tabs items="[&#x22;Docker&#x22;, &#x22;Docker Compose&#x22;, &#x22;Kubernetes Secret (operator)&#x22;, &#x22;Kubernetes Secret (no operator)&#x22;]">
  <Tab value="Docker">
    Mount the file read-only and point `KUBEMQ_LICENSE_FILE` at it:

    ```bash title="Terminal"
    docker run -d \
      --name kubemq \
      --hostname kubemq \
      -p 50000:50000 \
      -p 9090:9090 \
      -p 8080:8080 \
      -e KUBEMQ_LICENSE_FILE=/kubemq/license/kubemq.license \
      -v "$(pwd)/kubemq.license:/kubemq/license/kubemq.license:ro" \
      -v "$(pwd)/kubemq-store:/kubemq/store" \
      europe-docker.pkg.dev/kubemq/images/kubemq-next:latest
    ```
  </Tab>

  <Tab value="Docker Compose">
    ```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_FILE=/kubemq/license/kubemq.license
        volumes:
          - ./kubemq.license:/kubemq/license/kubemq.license:ro
          - kubemq-data:/kubemq/store
        restart: unless-stopped

    volumes:
      kubemq-data:
    ```
  </Tab>

  <Tab value="Kubernetes Secret (operator)">
    Store the file in a Secret and reference it from the `KubemqCluster`; the operator
    injects its contents as `KUBEMQ_LICENSE_DATA`:

    ```bash title="Terminal"
    kubectl create secret generic kubemq-license -n kubemq \
      --from-file=license=./kubemq.license
    ```

    ```yaml title="kubemq-cluster.yaml"
    apiVersion: next.kubemq.io/v1
    kind: KubemqCluster
    metadata:
      name: kubemq-next
      namespace: kubemq
    spec:
      replicas: 3
      licenseFileSecretRef:
        name: kubemq-license
        key: license
    ```

    With Helm: `--set licenseFileSecretRef.name=kubemq-license --set licenseFileSecretRef.key=license`.
  </Tab>

  <Tab value="Kubernetes Secret (no operator)">
    Inject the Secret value directly as `KUBEMQ_LICENSE_DATA`:

    ```yaml title="statefulset-excerpt.yaml"
    env:
      - name: KUBEMQ_LICENSE_DATA
        valueFrom:
          secretKeyRef:
            name: kubemq-license
            key: license
    ```

    Or mount the Secret as a file and set `KUBEMQ_LICENSE_FILE` to the mount path. Either
    way the pods still need the `kube-system` read permission described in
    [Kubernetes](/licensing/kubernetes#without-the-operator), because the fingerprint in the
    file is the cluster's namespace UID.
  </Tab>
</Tabs>

## Precedence [#precedence]

| Set                                 | Result                                                                                                             |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `KUBEMQ_LICENSE_FILE` only          | The file is used                                                                                                   |
| `KUBEMQ_LICENSE_DATA` only          | The data is used                                                                                                   |
| Both `FILE` and `DATA`              | `FILE` wins; a warning is logged                                                                                   |
| A file **and** `KUBEMQ_LICENSE_KEY` | The file wins; a warning is logged (`#both-key-and-file`). The key is ignored entirely — no activation, no refresh |
| None                                | The server refuses to start (`#no-license-input`)                                                                  |

## What the server does — and does not do — with a file [#what-the-server-does--and-does-not-do--with-a-file]

* **Verifies locally, at boot.** Signature, signing key id, expiry (with a 5-minute
  leeway), and that its own fingerprint is in the file's list (or the list is empty).
  Every minute after that it re-checks only the expiry and its own clock against the
  persisted high-water mark — the signature and fingerprint are not re-verified while
  the server runs, so a file swapped or edited on disk takes effect at the next boot.
* **Makes no network calls.** There is no activation, no hourly refresh and no usage
  report. `GET /api/v1/license` shows `mode: "offline"`, `usage.enabled: false`, and
  `last_refresh.outcome: "never"`.
* **Has no grace period.** An offline file's `exp` is a hard date: the server refuses to
  start after it, and a running server drains and exits with code 3 when it passes
  (`#offline-expired`). Renew before that.
* **Keeps no lease cache.** The file is the source of truth. The server still writes
  two things under `<store>/license/`: the persisted instance id used as a fingerprint
  outside Kubernetes, and the clock high-water mark (`clock.hwm`). A clock set back more
  than 10 minutes behind that mark is a refusal at boot and a stop on a running server
  (`#clock-rollback`) — an offline server has no service to confirm the time with.

## Renewal [#renewal]

A file is valid for at most **one year** from issuance and never beyond the commercial
expiry of the license. Renewal is a reissue: request a new file before the current one
expires, replace it (update the Secret, or swap the mounted file) and restart the server
— the file is read at boot.

Check `lease_expires_at` in `GET /api/v1/license` (or `kmq license`) to see the current
file's expiry, and plan the reissue ahead of it.

## What revocation means for a file [#what-revocation-means-for-a-file]

A file that has been issued **cannot be revoked**. There is no channel through which a
revocation could reach an air-gapped server, and the server does not look for one. This
is a deliberate property of the design, not a gap: the file's expiry (at most one year),
its fingerprint list and your contract are what bound its use.

Consequently, if a file leaks, the remedy is contractual and the exposure ends at the
file's expiry. Keep files as tightly controlled as any other credential, and prefer
bound files (with a fingerprint list) over unbound ones.
