How licensing works
vCluster Platform holds the license for a deployment. The license determines which features are enabled and sets limits on how many of certain resources you can create.
In the normal connected model, clusters hold no license of their own. They read a list of enabled features from the platform. Most licensing questions therefore come down to how the platform obtained its license and whether clusters can reach the platform to read features from it. A cluster can instead be given its own offline license key, which the sections below cover separately.
This page explains how that works, what each component caches, and how licensing degrades when it becomes unavailable. For the setup procedures themselves, see the air-gapped guides for an offline license key or an offline license server.
How the platform obtains its license​
The platform selects one of three licensing modes at startup, based on environment variables set on the platform deployment.
| Mode | Enabled by | Source of the license |
|---|---|---|
| Online | Default, no configuration needed | https://admin.loft.sh/license/v2 |
| Offline license key | LICENSE_KEY environment variable | The key itself, a signed token verified locally against a built-in public key |
| Offline license server | LICENSE_SERVER environment variable | An in-cluster license server that serves the same API as the hosted one |
Offline license key mode never contacts a license server. The platform decodes and verifies the key in-process, so use this mode when no egress is possible at all.
A fourth variable, LICENSE_TOKEN, doesn't change the mode. It supplies a static token that the platform sends when it requests a license, so the instance doesn't need interactive activation. The platform logs LICENSE_TOKEN env var is set, platform activation is not required at startup when it's present.
To find out which mode a running platform is in, see Inspect and debug licensing.
Instance identity​
In online and offline license server modes, the platform identifies itself to the license server with:
- The UID of the
kube-systemnamespace in the control plane cluster. - A short-lived token signed with the private key from the
loft-certSecret, sent along with that Secret's certificate.
This pairing ties a license to one platform instance. If the loft-cert Secret is lost, the platform generates a new identity and the existing license no longer matches it. Preserve that Secret across reinstalls and migrations. See Restore license for the backup and restore procedure.
The loft-license-cache Secret isn't part of the platform's identity. It holds a cached copy of the license response and the platform rebuilds it automatically, so it doesn't need to be migrated.
How clusters receive features​
In the normal connected model, a cluster reads its enabled features from the platform. The control plane detects which licensing mechanism is available at startup and selects it.
| Type | Detected when | Features come from |
|---|---|---|
| Online | A platform host and access key are both available, and FIPS mode is off | The platform's management API Features endpoint |
| Offline | A key is set but no platform host | A signed offline license key, verified locally |
| Standalone | controlPlane.standalone.enabled is set, and no platform host and access key are available (see below) | All Enterprise features stay enabled. Built-in allowances and Feature resources determine license warnings |
| In-cluster API | Feature resources exist in the control plane cluster and the cluster's control plane has RBAC to list them | Feature resources in the control plane cluster |
| None | Nothing above is detected | No Enterprise features are enabled |
In production builds, the control plane evaluates these types in the order shown and selects the first match. It logs the selected type at startup, for example detected license type {"licenseType": "Online"}.
Online is the common case. By default, the control plane reads the platform connection from the vcluster-platform-api-key Secret in the cluster's host namespace. To point it at a different Secret, set platform.apiKey.secretName and platform.apiKey.namespace in vcluster.yaml.
Within whichever Secret applies, the control plane resolves the access key in this order.
- The
LICENSEenvironment variable. - The
LOFT_PLATFORM_ACCESS_KEYenvironment variable. - The only key in the Secret, if the Secret holds exactly one key.
- The
licensekey in the Secret, then theaccessKeykey.
It resolves the host from LOFT_PLATFORM_HOST first, then from the Secret's host key.
The host recorded in the Secret must be reachable from inside the control plane cluster. Logging in through a port-forward records a localhost address that the cluster's control plane pod can't reach, so feature retrieval fails. See Resolve Pro feature license errors.
Two cases trip people up when the detected type isn't what they expected.
FIPS mode is the first. With FIPS enabled, a host and access key don't select the online type, and detection falls through to the remaining rows. A cluster that looks correctly configured can land on standalone, in-cluster API, or none. Check the logged type before assuming the credentials are at fault.
The second is that the standalone deployment mode and the standalone license type aren't synonymous. With FIPS off, a standalone deployment configured with a platform host and access key selects the online type first. It uses the non-expiring file cache described below.
The standalone type also doesn't disable Enterprise features. It allows the standalone and Private Nodes features by default. For other Enterprise features, Feature resources in the standalone cluster determine whether kubectl requests receive an unlicensed-use warning.
Required network access​
Only the platform contacts the license server. Neither connected cluster agents nor clusters do.
| Source | Destination | Protocol and port | Purpose |
|---|---|---|---|
| Platform pods | admin.loft.sh | HTTPS, TCP 443 | License retrieval every 6 hours and usage snapshots every 15 minutes |
| Platform pods | Analytics endpoint from the license response | As specified by the endpoint URL | Analytics every 30 minutes when there has been activity |
| Cluster control plane pods | Platform endpoint | HTTPS, TCP 443 | Feature retrieval and registration |
| Connected cluster agents | None for licensing | Not applicable | Agents don't perform license validation |
In offline license key mode, no egress to admin.loft.sh is required at all. For license retrieval in offline license server mode, the platform reaches the in-cluster server at the address given in LICENSE_SERVER.
Analytics calls go to the exact endpoint URL named in the license response, which isn't necessarily admin.loft.sh. They only happen when there has been activity since the last call. A response with no analytics endpoint stops them entirely.
The platform Service listens on port 443 and forwards to port 10443 on the platform pods. Kubernetes NetworkPolicy matches the pod port rather than the Service port, so an egress rule targeting platform pods directly has to allow 10443.
The cluster path is the same connection used for the rest of platform communication, so it needs no licensing-specific firewall rules. For the full connectivity model, see Platform communication and connectivity.
Cache and refresh behavior​
The online platform and cluster paths cache their last successful results. These caches keep a temporary licensing outage from immediately taking those components down.
| Component | Refresh trigger | Cache location | Cache lifetime |
|---|---|---|---|
| Platform in online or license server mode | Every 6 hours | loft-license-cache Secret in the platform namespace | Reloaded at startup, with a 7-day grace period after retrieval starts failing |
| Platform in offline license key mode | Re-evaluated locally every minute | None needed, the key is in the environment | Until the key expires, plus a 30-day grace period |
| Cluster, online | At startup, and whenever the credentials Secret changes | <vcluster-name>-features-list Secret in the host namespace, encrypted | 7 days |
| Standalone deployment, online type | At startup, and whenever the credentials Secret changes | platform/feature-cache.json in the data directory | Doesn't expire |
| Standalone type | Polled with backoff starting at 30 seconds, capped at 5 minutes | None | Not applicable |
| Cluster, in-cluster API | Polled with backoff starting at 30 seconds, capped at 5 minutes | None | Not applicable |
| Cluster, offline | Re-checked locally every hour | None needed, the key is in the environment | Until the key expires |
The online cluster path isn't polled. The control plane retrieves features at startup and then only when the credentials Secret changes. A license change made in the platform reaches a running cluster on the next control plane restart, not on a timer.
The platform's one-minute cycle in offline license key mode doesn't contact anything. It re-reads current resource usage, so limits are enforced against live counts.
The cluster feature cache is encrypted with a key derived from the vCluster Service UID. A tampered Secret therefore can't grant features the platform never returned.
What happens when licensing is unavailable​
Behavior depends on which component lost access and for how long.
| Component / scenario | What happens |
|---|---|
| The platform can't reach the license server | Keeps running on the cached license and retries every 6 hours. Exits if no retrieval succeeds within 7 days of the last success (a restart mid-outage gets up to 6 hours less, since the measurement starts from a trailing timestamp). A platform that has never retrieved a license has nothing to fall back on, so it exits immediately rather than starting unlicensed. |
| The platform's offline license key is near or past expiry | Raises an expiration warning starting 90 days before expiry. After it expires, a 30-day grace period follows during which the platform keeps working, then the instance blocks all create requests once that period ends. |
| The platform is over a resource limit | Blocks creating more of that resource until usage drops below the limit; deleting enough of the resource clears the block automatically. In offline license key mode, the platform evaluates limits itself and warns once usage passes 90%. In online mode, the license server decides which limits apply and which announcements to return. |
| A cluster on the online type can't reach the platform | A control plane that's already running is unaffected, since the online path holds its feature set in memory and doesn't poll. At startup or a credentials-triggered reload, it tries the platform first and falls back to the cache. A cache older than 7 days, or a control plane that has never retrieved features at all, both fail initialization rather than starting unlicensed. |
| A cluster on the standalone or in-cluster API type can't reach the platform | Unaffected, since these types read Feature resources from the control plane cluster rather than from the platform. What changes their feature set is the platform updating those resources, which a disconnected platform stops doing. On the standalone type, Enterprise features stay enabled either way, so only the unlicensed-use warning changes. |
| A cluster's own offline license key expires | The control plane warns for the last 30 days, then exits once the key expires. This path has no grace period, unlike the platform's offline key mode. |
| The licensed feature set changes | The platform restarts itself to apply the new set. |
Expiration, limit, and feature warnings surface in the platform UI. See Inbox.
Inspect and debug licensing​
In the platform​
View the current license in the platform UI under Admin > Platform Config > License and Billing.
Retrieve the same data through the API. This reports the license the platform currently holds, without contacting the license server.
kubectl get licenses.management.loft.sh license -o yaml
A license the platform holds shows a non-empty status.license.entity (your organization's name) and a status.license.plans list. An empty or missing status.license means the platform has never successfully retrieved one.
To make the platform retrieve a fresh license first, add the extended=true query parameter. It's a URL parameter rather than a field, so kubectl get can't set it and the request has to go through --raw.
kubectl get --raw '/apis/management.loft.sh/v1/licenses/license?extended=true'
The response is the same License object as above, so check it against the same entity and plans fields. If the platform can't reach the license server, this request fails with an error instead of returning the cached license. Drop extended=true and re-run the first command to read the cached license without forcing a refresh.
Confirm which license server the platform is using. The platform logs this once at startup, so this also tells you which mode it's in.
kubectl logs -n vcluster-platform -l app=loft | grep "Using license server"
This logs once per process, on every mode, with the server it's using as the endpoint field, for example admin.loft.sh in online mode or the in-cluster address in offline license server mode. No match usually means the platform pod restarted since and rotated the line out of its log buffer, not that the platform failed to start.
Check the cached license and the instance identity Secrets.
kubectl get secret loft-license-cache -n vcluster-platform -o jsonpath='{.data.updatedAt}' | base64 -d
kubectl get secret loft-cert -n vcluster-platform
Use updatedAt to confirm the cache exists, not to work out how much grace period is left. The platform writes the timestamp it held before the current retrieval, so the stored value trails the real one by a refresh cycle. After the very first write it reads 0001-01-01T00:00:00Z, because there was no previous retrieval to record. A missing loft-cert Secret means the platform generated a new identity, as described in Instance identity above, and any existing license no longer matches it.
On restart, the platform reloads updatedAt and measures the 7-day grace period from it. Because the value trails, a restart during a license server outage gets up to 6 hours less grace than the constant implies. A 0001-01-01T00:00:00Z value is handled safely. The platform treats it as unset and falls back to the Secret's creation timestamp, so the grace period still runs from roughly the first successful retrieval. Tracked as ENGPLAT-1069.
Look for retrieval failures in the platform logs. Each of these lines carries a full stack trace in an errorVerbose field, so truncate them to keep the output readable.
kubectl logs -n vcluster-platform -l app=loft | grep -i "error retrieving license" | cut -c1-260
An occasional failure here normally needs no action. The platform keeps serving from its cached license and retries on the next cycle. Outside the restart and timestamp edge cases above, only failures spanning more than 7 days make the platform exit.
In a cluster​
If you've just fixed credentials or the platform now grants a different feature set, restart the control plane before checking anything below. The online path only re-reads features at startup or when the credentials Secret changes, not on a timer, so a fix that's correct won't show up until then.
Confirm which license type the control plane selected, and which features it enabled.
kubectl logs -n my-namespace my-vcluster-0 | grep -A5 "detected license type\|Enabled features"
A healthy online cluster logs the host it connected to, followed by the features it enabled.
loader/loader.go Using online license with host loft.vcluster-platform
online/license.go Enabled features:
online/license.go dra-sync
Confirm the credentials Secret and the feature cache both exist.
kubectl get secret vcluster-platform-api-key -n my-namespace
kubectl get secret my-vcluster-features-list -n my-namespace
A NotFound error on the credentials Secret means the cluster was never connected to the Platform, or points at a different Secret name or namespace than configured. The feature cache is encrypted and can't be read directly, so its presence and age are what matter rather than its contents: a missing cache means the control plane has never successfully retrieved features, and the cache expires 7 days after it was last written.
A control plane can crashloop with an error naming a feature the license doesn't allow. The cause is usually a missing credentials Secret rather than a plan limitation. See Resolve Pro feature license errors.