Skip to main content
A build server rack beside a firmware engineer's CI dashboard in an Indian embedded lab at dusk, the Arm User-Based Licensing pipeline setup GSAS Micro Systems works through with teams in India

Arm User-Based Licensing in CI/CD: armlm, Proxy Activation and What Breaks

GSAS Engineering · · 24 min read

A firmware pipeline that has been green all week goes red on a Tuesday. Nothing in the repository changed. The compiler step fails with a licensing error, and it fails on some of the matrix jobs but not all of them. The developer who opened the pull request has no useful way to act on that, and the DevOps engineer who owns the runners has no obvious place to start looking.

That failure is almost never a bug in the build. It is a consequence of how Arm User-Based Licensing works, and specifically of the fact that it does not work the way FlexNet floating licensing worked. This article is about what actually has to happen for armclang to be licensed on a runner, what has to survive between jobs, and what breaks.

It deliberately does not cover two adjacent questions that already have their own answers. If you are asking how many seats to buy, that is a different exercise, and it is the subject of our UBL seat sizing guide for Indian CI/CD teams. If you are asking how to invoke the build itself headlessly, see Cortex-M CI/CD with csolution.yml, cbuild and GitHub Actions. This one starts at the moment a job spins up on a runner.

Every claim below is sourced to Arm’s own published documentation, listed at the end.

A UBL licence is a cached certificate, not a checkout

Start with vocabulary, because the wrong mental model is what produces most of the wrong pipeline designs.

Arm’s User-based Licensing User Guide defines a licence as the “Output of an activation, representing an entitlement for a user. A license is also referred to as a certificate. A license is typically stored on the local file system of the device used when activating your Arm development tool.”

The same terminology section defines floating separately, and the contrast is the point: a floating licence “is checked out for the duration that a license-managed feature is in use, and ends with the license being checked back in”, and “Unless the license is borrowed or cached, the client must maintain a network connection with the license server for the entire duration that the license-managed feature is in use.”

UBL is neither of the two models most build engineers have in their heads. It is not node-locked, and it is not a checkout. A licence is a file on disk with a validity window.

The lifecycle section spells the window out. In the activation phase, “Activating a product creates a license in the local license cache for the device. This cached license is valid for 7 days.” In the usage phase, “Each day your development tool runs, the development tool automatically makes one attempt to renew the cached license for another 7 days by connecting to the local license server or the Arm license server. If the cached license cannot be renewed for 7 days, it becomes invalid.”

Then comes the sentence that explains the Tuesday morning:

Unmet network requirements are only reported if the cached license becomes invalid.

A pipeline whose egress rules were changed on a Friday afternoon does not fail on the Friday. It fails up to a week later, in a build that has nothing to do with the change, on whichever runner happens to be holding the oldest cache. The first red build is not the first broken build. When you are bisecting a licensing failure, the interesting window is the previous seven days of infrastructure changes, not the last commit.

The cache itself is a directory. Arm gives the default locations as C:\Users\<your username>\.armlm on Windows, /home/<your username>/.armlm on Linux and /Users/<your username>/.armlm on macOS. Nothing else on the runner holds the entitlement. If .armlm is not there and is not valid, the build is unlicensed, regardless of what the tool installation looks like.

Three ways to activate, and the one Arm tells CI not to use

Arm documents three ways a device obtains a licence, and they are not equivalent for a pipeline.

RouteWhat you run or setSuitability for CI, per Arm
Licence serverarmlm activate --server <server_URL> --product <product_code>Arm’s Administration Guide says a small number of cloud instances with intranet access can activate using a Local License Server
Activation codearmlm activate --code <activation_code>Arm’s Administration Guide says a small number of cloud instances with internet access can activate using the Cloud License Server
Ambient environment variableARMLM_ONDEMAND_ACTIVATION=<product_code>@<server_URL>Arm attaches an explicit caution about CI workflows

That third row is the load-bearing one, and Arm’s caution is worth quoting in full. It appears both as a note under “Activate your product using a license server” and again in the environment variable table in Appendix A:

Using this environment variable for licensing is unsuitable when many devices request license activation in a short period. For example, this could happen in a Continuous Integration (CI) workflow. This type of workflow floods the license server with requests, which can cause the server to reject license requests. For this type of workflow, use Proxy activation to activate the license once and then import the resultant license file on all devices.

The naive CI pattern is exactly this: bake the licence source into the runner image or the job environment, let every job activate on its own, scale the matrix. Arm is telling you that pattern degrades as you scale it, and pointing at a different one.

The Administration Guide backs the caution with a number. Section 5.23 states: “By default, the license server rate limit is 10 license requests per second.” Requests beyond the limit “are rejected with an error code that instructs the client to wait a defined time and then retry”, and “If the rate limit is constantly exceeded, the license server response time degrades and license requests fail as requests exceed the client timeout.” Arm’s first suggested remedy is not to raise the limit but to change the licensing method: “if you have many devices trying to access the license server in a short time-period, use a transfer file that requires only one license server request.” Arm adds that it “recommends that you do not change this default value. A sustained rate of more than 10 requests per second indicates unexpected behavior.”

Two things about that figure matter. It is a Local License Server figure, documented for a server you run and configurable through the rate-limit parameter in <installation_directory>/server/local-configuration.yaml. And Arm publishes no equivalent figure for the Cloud License Server, so do not plan capacity against it as though it applied there.

What has to survive between jobs

If the licence is a directory, the pipeline design question becomes: how does that directory get onto the runner, and how does it stay valid.

Arm documents exactly four ways to give one user’s licence to multiple devices, in section 2.5 of the User Guide:

  1. Activate the licence on each device individually.
  2. Use proxy activation to create a licence file, and import it on the target devices.
  3. Change the local licence cache directory so the cache files sit on a network drive several devices can reach, via ARMLM_CACHED_LICENSES_LOCATION.
  4. Copy the .armlm directory to the home directory on each device.

The Administration Guide restates the same choice in cloud terms, which is closer to how CI estates are actually shaped. For a small number of cloud instances with intranet access, use the Local License Server. For a small number with internet access, use the Cloud License Server. And “For large number of cloud instances using Arm development tools, or for cloud instances that do not have access to the internet or your company intranet”, either create a transfer file by proxy activation and import it, or activate on one device and then replace the licence cache in each instance or make the cache available on a network drive with ARMLM_CACHED_LICENSES_LOCATION pointing at it. Arm’s warning applies to both: “if the process is not repeated before the license expires, the license is deactivated.”

ARMLM_CACHED_LICENSES_LOCATION “Specifies the full path to the directory containing the license files”. Arm lists the reasons to use it as a device with no local drive, a user with no home directory on the device, or not enough disk space, and every one of those is a normal condition inside a container. The caveat is also Arm’s: “If you change the local cache directory to a location on another device or network, license activation and renewal will be slower.”

One thing not on that list is worth saying out loud, because it is the first idea most pipeline authors have: Arm does not document storing licence material in a CI cache artifact. There is no published guidance for actions/cache or its equivalents holding .armlm, and there is no published statement that a cache survives a runner image refresh. If you go that way you are designing without a net, and you are putting licence material into a store that is usually readable by anything with repository access. The documented routes are the four above.

Proxy activation, in the exact commands Arm publishes

Proxy activation is the mechanism Arm points CI at, and it is described plainly: it “allows you to create a license activation file on one device and then use that file to provide licenses on one or many other devices. These ‘target devices’ can include local devices or devices running in the cloud.”

Arm names the CI case in the definition itself. Proxy activation is useful when “A device does not meet the network requirements” and when “Using multiple devices in an automated and parallelised workflow, such as Continuous Integration (CI)”. The task page adds a third reason that reads like it was written by someone who has watched a build farm: “You want to protect your workflow from failures to activate a product that can, for example, be caused by intermittent network or license server access.”

The command surface is two commands. On a device that meets the network requirements:

armlm activate \
  [--server <server_URL> --product <product_code> | --code <activation_code>] \
  --as-user <user_name> \
  --to-file <transfer_filename> \
  [--borrow-period <days>]

<user_name> is, in Arm’s words, “the name of the Arm product user on the target device”. That is not a detail. It is the entire identity contract, and it is covered in the next section.

Then, on each target device:

armlm import --file <transfer_filename> [--enable-renewal]

Four documented behaviours make this the right shape for ephemeral runners:

It is one licence server request, not one per runner. That is precisely the remedy Arm recommends in the rate-limit section.

The validity window is adjustable. --borrow-period <days> “extends the time a license cache is valid, from the default of 7 days. <days> can be any integer from 7 to 365.” It is available when the transfer file is created with Arm licensing utilities version 1.3.2 or later.

By default a proxy-activated licence makes no network calls. Arm: “By default, licenses created by proxy activation do not attempt to renew the license, stopping all attempts to access a network.” Passing --enable-renewal on armlm import “allows attempts to renew the license every 24 hours”, but with a consequence worth knowing before you use it: “When the license is renewed, the time that the license cache is valid is set to 7 days. This overrides the time specified using the armlm activate --borrow-period <days> command.” Enabling renewal on a licence you deliberately borrowed for 90 days silently collapses it back to 7.

It is not fire and forget. Arm’s next-steps note: “If your target device needs to use the license for longer than the license renewal period, you must repeat the proxy activation process before the renewal period expires.” A transfer file is an operational artefact with an expiry date, and something in your infrastructure has to own regenerating it.

The trade-off is stated by Arm too, and it should be read alongside the seat-count conversation: “Activating a product locks the license to the user and the license is unavailable to other users. Extending the time a license is valid also extends the time that the license is locked and cannot be used by another user.” A 365-day borrow on a bot identity is a 365-day commitment of that identity’s seat.

Two limits are worth stating because Arm does not state otherwise. The guide does not put a number on how many devices one transfer file may be imported onto, and it does not describe what happens if the same file is imported concurrently across a large fleet. Treat both as open questions to validate against your own estate rather than assumptions to design around.

And treat the transfer file itself as licence material. It is not a config file. It belongs wherever your activation codes belong.

Identity: whoami decides whether the build is licensed

Under UBL the entitlement binds to a user. Arm’s overview: user-based licensing “binds the entitlement to use an Arm product to the user. A user is entitled to use an Arm product license with no limits on concurrent usage, including using the same product on multiple devices. For example, you could use a single license with a service account to automatically build and test your products with Arm development tools on any number of devices.”

Arm’s own definitions make the CI mapping explicit. A service account is “The operating system account that coordinates the automated use of development tools”. User is “A generic term used to identify the entity interacting with the Arm development tool; can be an end-user or service account”. And Seat: “Under the user-based licensing model, there is exactly one seat for each user, regardless of the number of devices on which your Arm development tool is installed or run.”

Read the concurrency sentence carefully. “No limits on concurrent usage” is a licensing statement about what your entitlement permits. Arm publishes no maximum number of parallel jobs, devices or concurrent tool instances per identity, and no supported concurrency ceiling. It is a permission, not a benchmark. How many identities your estate actually needs is a sizing question, and it is the one our seat sizing guide is about.

What breaks first in practice is not the count. It is the name.

Arm defines error 400 - Unauthorized user as “The user identifier associated with the runtime process of your Arm development tool does not match the information in the license”, and lists three causes: a username that is different on different devices, a licence cache copied from another user, and a relocated cache directory populated with another user’s licence information. Arm’s first diagnostic step is blunt and correct: “Check the username by running whoami in a console window.”

Elsewhere Arm states the rule directly: “The username activating the product must be identical on all devices”, and for licence server version 1.2024010.0 or earlier “the username is also case-sensitive.”

This is why --as-user exists on the proxy-activation command, and why it is the field most likely to be wrong. Whatever whoami returns on the runner is the identity the licence has to be bound to. Not the GitHub account, not the service principal in your identity provider, not the name on the Arm portal. The operating system username of the process that executes armclang. Container base images, USER directives, sudo steps and job-level container: blocks all move that value around, and none of them announce that they have.

The practical check is one line at the top of a job, before anything expensive runs:

whoami && armlm inspect

armlm inspect lists the active and deactivated licences with their product or activation codes, which tells you whether the cache the process can see is the cache you think it is. armlm inspect --server <license server URL> goes further and checks that the server address is reachable from the device, which product licences are available on it, and which you already hold locally. That is a cheap step that turns a mid-build failure into an immediate, readable one.

One more identity note, because it is the correction most teams need. Deactivation does not do what its name suggests in a pipeline context. Arm: “Deactivating a license is an activity local to a single device. Deactivation does not release the license so it can be used by another user.” For activation codes, an administrator releases a licence by revoking the code, and even then “the administrator cannot reallocate the revoked license to another user until at least 7 days after the current user renewed the license cache on any device.” For a licence server, “a license is automatically released for use by another user when it has not been renewed for a minimum of 7 days.”

That is consistent with what Arm ships. The ARM-software/cmsis-actions/armlm GitHub Action has three inputs (code, server, product), a single composite step that runs armlm activate, no deactivate input and no post-job step. Arm’s own CMSIS_6 CoreValidation workflow activates and never deactivates. A per-job armlm deactivate step does not free a seat for another user and does not protect a concurrency budget.

Containers: what must never be baked into the image

Arm publishes a Docker learning path for Arm Compiler for Embedded, and the most instructive part of it is the part that is commented out. The Dockerfile ships with a licensing block the reader is told to edit, offering three options:

# License configuration
# Uncomment and modify below as appropriate
#
# ENV ARMLM_ONDEMAND_ACTIVATION=product_code@https://internal.ubl.server
#   or
# RUN armlm activate --code <activation-code>
#   or
# ENV ARMLMD_LICENSE_FILE=port@server

Arm’s accompanying instruction is “You will need to edit the licensing portion of the file to match your internal license setup.”

Read those three against the rest of the documentation and the shape of the decision becomes clear. The first is ARMLM_ONDEMAND_ACTIVATION, which is the setting Arm’s User Guide explicitly flags as unsuitable for CI workflows. The second writes an activation code into an image layer, where it stays, readable by anyone who can pull the image, and survives every docker history and every registry copy. The third, ARMLMD_LICENSE_FILE, is the legacy FlexNet variable and is not UBL at all.

The rule that falls out is simple and worth writing into your container standards: licence material is runtime state, not image content. An image should contain the tools and armlm. It should not contain an activation code, a licence server credential, or a populated .armlm.

The shapes that satisfy that, using only mechanisms Arm documents:

Pattern A, mount the cache. Set ARMLM_CACHED_LICENSES_LOCATION in the container to a path you mount at run time from a location the container can reach, holding a cache activated for the identity the container runs as. This is Arm’s third method in section 2.5, and the Administration Guide’s cloud variant. Expect activation and renewal to be slower on a network location, per Arm’s own caveat.

Pattern B, import at start. Ship no licence in the image. At container start, fetch the transfer file from your secret store and run armlm import --file <transfer_filename>. Regenerate the transfer file on a schedule shorter than its borrow period.

Both are patterns assembled from Arm’s documented mechanisms, not workflows Arm publishes verbatim. Arm publishes the mechanisms; the orchestration is yours. If your armlm needs to exist somewhere without a full tool install, Arm supports that too: the standalone Arm licensing utilities archive ships for Windows x86 64-bit, Linux x86 64-bit, Linux Arm 64-bit and macOS Universal, unpacks with a plain tar -xf, and puts the CLI in <installation_directory>/bin. Arm names creating a transfer file and activating a cache for another device as exactly the use case.

Credentials, and what Arm’s own pipeline does

Arm’s GitHub Action takes the activation code as an input:

- name: Activate Arm tool license
  uses: ARM-software/cmsis-actions/armlm@v1
  with:
    code: "${{ env.ARM_UBL_ACTIVATION_CODE }}"

Arm’s own CMSIS_6 CoreValidation workflow declares ARM_UBL_ACTIVATION_CODE: ${{ secrets.ARM_UBL_ACTIVATION_CODE }} at job level and passes it in exactly that form, in the step immediately after the one that sets up the tool environment. Arm’s README describes that workflow as activating a commercial product using a licence activation code provided via a GitHub secret, and links to GitHub’s own secrets documentation. That is the reference implementation, published by the vendor, and there is no reason to invent a different one.

Three things to get right around it.

The action’s product: input defaults to KEMDK-COM0, which is a non-commercial edition. A commercial pipeline must supply its own activation code, or its own licence server URL together with the product code for the entitlement it holds. Do not let the default stand in a production workflow.

An activation code is, in Arm’s words, “a sequence of letters and digits” in an 8-4-4-4-12 pattern, and per Arm it “is not associated with any user or service account until its first use in the product activation process.” Which means the first job that runs with it decides who owns it. Arm’s documented remedy is administrative: “If you accidentally activated the product with an incorrect username, contact your license administrator to revoke the activation code and generate a new one.” A revoked code then sits at status “Inactive” in the Arm portal until cached licences expire, and per the Administration Guide “Usually, cached licenses can take up to 7 days to expire, but this can be longer for licenses created using proxy activation.” A misdirected first activation is a week-long problem, not a five-minute one. Restrict which workflows and which branches can read that secret accordingly.

And watch what reaches the log. ARMLM_ECHO_LOG_CONTENT (License Management Utility version 1.3.2 or later) set to true echoes log errors to standard output and standard error, which is genuinely the thing that makes a licensing failure visible in a CI log instead of silent. It is also output you may be publishing. Turn it on deliberately, and know where your logs are readable.

The firewall ask, in the form a network team can action

An Indian corporate build network behind an egress policy needs a specific ask, and “allow arm.com” is not one a network team can action. Here is the version that is actually in Arm’s documentation.

For activation-code licensing, a device requires internet access to:

  • https://api.arm.com/p-software-licensing
  • https://arm.compliance.flexnetoperations.eu/instances

Arm’s own firewall note: “Protocol: HTTP with SSL. TCP port: 443. Hostnames: api.arm.com and arm.compliance.flexnetoperations.eu. The use of IP addresses is not recommended because the IP addresses of the URLs are not static.”

That last clause is the part to hand to whoever asks for a CIDR range. Arm publishes no IP allow-list and advises against pinning to addresses.

Arm also publishes two reachability checks you can run from the runner itself, which is far more useful than running them from a laptop on a different network:

curl https://api.arm.com/p-software-licensing/v1/health
# expect: { "health": "up" }

curl https://arm.compliance.flexnetoperations.eu/api/1.0/instances/ping
# expect output including: "connectionCheck" : "success"

For a Local License Server the ask is smaller. Users “require access to the IP address and port of the user-based licensing license server for license activation and renewal”, and Arm notes “This is the only license server port that users require access to.” The default is TCP 7070, settable at install.

It is also worth knowing which operations need the network at all, because that shapes what you can defer. Activation needs it, since activation is what delivers a licence into the local cache. Renewal needs it, once every 24 hours. Deactivation does not, because Arm describes it as “an activity local to a single device”. And for the security review that will inevitably ask: “During product activation or license renewal, the only personal data sent to the local license server or to the Arm license server is the username.”

Cloud or local licence server for a CI estate

Arm gives its own selection criteria rather than leaving this to preference.

Features of the Cloud License Server: “No internal infrastructure requirements / The administrator must manage each activation code / Requires Internet access to activate and to refresh licenses.”

Features of the Local License Server: “Requires internal infrastructure to manage and maintain a local license server / Easy to manage many licenses on a single server / Allows deployment of licenses to a large developer population / Generates local usage diagnostics.”

And the recommendation: “Arm recommends LLS for licensing a large number of developers and CLS for licensing a few developers. If your users are spread across multiple sites or networks, you can use both types of deployment.”

For a CI estate specifically, three server-side details matter beyond that.

The rate limit is a design input. 10 requests per second by default, Arm recommends leaving it alone, and Arm’s first remedy for pressure against it is a transfer file rather than a larger number.

Failures are observable in the log. Activation requests appear in <data_directory>/flexnetls.log as lines containing request preview and count=. Per Arm, “You can see failed license activations by searching for lines that include request preview and count=0”, successful ones carry a count of 1 or greater, and “The value of count= is the number of available licenses before the activation request.” That is a monitoring hook, and on an LLS-backed CI estate it is a useful early signal that your identity budget is tight.

A CI identity can be scoped. From licence server version 1.2024050.0, an administrator can load an access control list with armlm_set_acl --acl-file <ACLfile>. Arm’s published allow-list example includes a CI identity by name:

model "userAccessList" {
  on hostid("jasmine/USER", "ciuser/USER", "anyuser/USER") {
    use "default"
    accept
  }
  on any() {
    deny
  }
}

Template files ship in <installation_directory>/templates.

Air-gapped build networks

For Indian defence, aerospace and secure-medical programmes where the build network cannot reach the public internet, this is a documented configuration rather than a workaround.

Arm lists the proxy-activation network requirements as “A device with license server or activation code network access to create a license file” plus “A way of copying the license file to the devices that require Arm development tool access”, and states that proxy activation is used when “The target device does not meet the network requirements to activate a product or renew a license.” Combined with a borrow period of up to 365 days and the default of no renewal attempts at all, that is a licensing model an isolated build network can actually run.

If the answer is a Local License Server on the secure side, the host requirements are published: minimum dual-core 64-bit x86 at 2GHz, 4GB memory, 500MB storage; Red Hat Enterprise Linux or CentOS 7 and 8, Ubuntu 20.04 LTS, and from server version 1.2024010.0 Ubuntu 22.04 LTS; systemd, Python 3.6 or later, and Oracle Java SE 8, OpenJDK 8 or OpenJDK 11. One detail catches virtualised deployments: licences are locked to the server’s hostid, which “is the MAC address of one of the available network adapters on the host machine”, so a virtual machine needs a fixed number of ethernet adapters with static MAC addresses that survive a power cycle. Arm also advises against running Arm development tools on the licence server machine itself.

Whether to run an LLS on the secure network or to feed it transfer files from a connected device is a genuine architecture decision with an operational cost attached either way, and it is one worth making with your IT security team in the room rather than after the first failed build.

Triage: mapping the symptom to the cause

Arm assigns a code to each licensing error. Here is the set a pipeline actually meets, with Arm’s own remedies.

CodeSymptomWhat Arm says to do
100No licence found in the local licence cacheActivate the product with the information from your licence administrator. In CI, this usually means the cache did not reach the runner, or the process is running as a different user than you think
101Product activation conflict during renewalRun armlm inspect, deactivate products you do not use, then deactivate and reactivate the remaining ones one at a time. From utilities version 1.3.2, activating a product automatically deactivates conflicting products
200Expired cached licenceThree documented causes: no network connection to the licence server; the cache cannot be modified, either out of disk space or wrong permissions on the cache directory; or the licence server URL changed. For a device that cannot meet the network requirements, Arm’s fallback is proxy activation
201Support and maintenance contract expiredRenew through your usual channel, or continue on an earlier tool version
202Expired licenceRenew the entitlement. If the renewal uses the same product code, restart the tool to refresh the cache; if it uses a different product code, activate the product
300Corrupted licenceDeactivate and reactivate the product
301Corrupted local licence cacheRecord your codes with armlm inspect, back up and delete the cache directory, then deactivate and reactivate all Arm products
400Unauthorized userCheck whoami. The username must be identical on all devices, and is case-sensitive on licence server version 1.2024010.0 or earlier
501Library communication errorCheck disk space and that a home directory exists, reinstall or update the tool, delete the cache directory and activate again
Product not supportede.g. armclang: error: FuSa versions of the Product is not supported with this licenseOccurs with a licence server, or after upgrading the tool to a version the cached licence does not support. If the licence was activated by proxy, you must proxy-activate again; otherwise the cache picks the change up on renewal

That last row deserves a note of its own, because it is the one a pinned-toolchain pipeline hits. Upgrading a tool version can invalidate a licence the cache is perfectly happy with, and for proxy-activated caches the fix is not automatic. Tool upgrades and licence refreshes are the same change window on a proxy-activated estate.

When you need to escalate, armlm report [--to-file <filepath>] produces a diagnostic zip containing the local licence cache, licence details, logs, transient caches and environment information. Set ARMLM_LOG_ENABLED=true and reproduce the failure first; Arm advises enabling it only while diagnosing, because of the volume. ARMLM_MAX_LOG_FILES (default 10) bounds what is kept.

The complete environment-variable surface

This list is short, and knowing that it is complete is as useful as knowing what is on it. Arm’s Appendix A publishes exactly five variables:

VariablePurpose
ARMLM_CACHED_LICENSES_LOCATIONFull path to the directory containing the licence files. Default: .armlm in the user home directory
ARMLM_ECHO_LOG_CONTENTUtilities 1.3.2 or later. true echoes log errors to stdout and stderr
ARMLM_LOG_ENABLEDtrue logs all licensing operations to <license_directory>/logs. Default: errors only
ARMLM_MAX_LOG_FILESNumber of log files kept. Default 10
ARMLM_ONDEMAND_ACTIVATION<product_code>@<server_URL>, always used to license Arm tools. Carries Arm’s CI caution

There is no documented proxy variable. If your runners cannot reach api.arm.com because a corporate proxy sits in the way, the sourced answers are proxy activation with a transfer file, or a Local License Server on the reachable side. Setting HTTPS_PROXY on the runner is not something Arm’s licensing documentation describes, and it should not be the first thing you try.

Two more one-line references, both owned in more depth by other posts. UBL does not apply to Arm development tools released before 2022, and the first versions that support it include Arm Compiler for Embedded 6.18, Arm Compiler for Embedded FuSa 6.16.2, Arm Development Studio 2022.0 (Gold) and 2022.a (Platinum), Keil MDK 5.37 and Arm Fast Models 11.17.1; the strategic version of that conversation is in our FlexNet node-locked migration guide. And on a mixed estate, “Arm development tools that implement user-based licensing automatically choose user-based licensing over other technologies, as long as a valid user-based license is found. Otherwise, Arm development tools use pre-existing licensing implementations.”

A short checklist

If you are building or repairing a pipeline this quarter:

  1. Run whoami and armlm inspect as the first step of a job and read the output. Confirm the identity the licence is bound to is the identity the compiler runs as.
  2. Decide whether your matrix is small enough for per-job activation or large enough for proxy activation, using Arm’s own framing of small versus large numbers of cloud instances.
  3. If you go proxy, choose a --borrow-period deliberately, know that --enable-renewal resets the window to 7 days, and put the regeneration of the transfer file on a schedule.
  4. Get the activation code and the transfer file out of your images and into your secret store. Audit image layers and workflow history for anything already baked in.
  5. Send the network team the hostnames, the protocol and the port, with the two curl checks to run from a runner. Not an IP range.
  6. If you run a Local License Server, put request preview ... count=0 into whatever watches your logs.
  7. Remember that a licensing failure surfaces up to seven days after the change that caused it. Widen the bisect window accordingly.

Where GSAS fits

GSAS Micro Systems has been an authorized Arm engineering partner in India since 2018, for Keil MDK, Arm Compiler for Embedded, Arm Compiler for Embedded FuSa and Arm Development Studio. The part of that work relevant here is not commercial. It is that our field application engineers sit in the same time zone as your build farm, and work through the failure modes above with Indian firmware teams starting from the shape of the log.

The work GSAS takes on here:

  • A pipeline that has started failing on licensing and nobody can reproduce it locally, because locally the cache is valid and on the runner it is not.
  • A move from per-job activation to proxy activation, including who regenerates the transfer file and how often.
  • Local License Server deployment for air-gapped defence, aerospace and secure-medical programmes, run alongside the customer’s IT security team, including the access control list for build identities.
  • Container and runner standards, so licence material stops arriving in image layers.
  • Toolchain upgrade windows on proxy-activated estates, where the licence refresh has to move with the tool version.

Our engineers work with Indian teams across Bengaluru, Hyderabad, Chennai, Pune, Mumbai, Delhi NCR and Visakhapatnam.

Talk to GSAS about your Arm UBL CI setup

For seat counts rather than mechanics, start with the UBL seat sizing guide. For the build steps themselves, see the csolution and cbuild pipeline guide.

Sources

Every claim in this article traces to one of the following Arm publications, read in the versions noted.

  • Arm, User-based Licensing User Guide, document 102516. Read as PDF 102516_1.4_01_en, issued 2025-10-31. Sections used: 1 (overview), 1.1 (backwards compatibility), 1.2 (interoperability), 1.3 (licence lifecycle), 1.4 (requirements and network requirements), 1.6 (utilities release history), 1.7 (terminology), 2 (activation preamble), 2.1 (activate using a licence server), 2.2 (activate using an activation code), 2.3 (proxy activation), 2.4 (change the local licence cache directory), 2.5 (activate on multiple devices), 2.6 (activate without installing tools), 2.8 (deactivate and reactivate), 3.1 to 3.12 (error codes), 3.14 (diagnostic report), Appendix A (environment variables). Current version: https://developer.arm.com/documentation/102516/latest/
  • Arm, User-based Licensing Administration Guide, document 107573. Read as PDF 107573_1.2024500_04_en, issued 2025-04-30. Sections used: 1 (getting started, deployment types, cloud activation, user data transfer), 3.2 and 3.3 (activation code status and revocation), 4.1 and 4.2 (licence server requirements and install), 5.10 (tracking licence activations and failures), 5.11 (access control lists), 5.23 (maximum number of licence server requests). Current version: https://developer.arm.com/documentation/107573/latest/
  • Arm, Arm Software Licensing install guide, which restates the cache window in plainer language (“A user-based license is cached locally for up to 7 days”): https://learn.arm.com/install-guides/license/
  • Arm, Prepare Docker image for Arm embedded development, Arm Learning Paths: https://learn.arm.com/learning-paths/embedded-and-microcontrollers/docker/dockerfile/
  • Arm, ARM-software/cmsis-actions (README and armlm/action.yml): https://github.com/ARM-software/cmsis-actions
  • Arm, ARM-software/CMSIS_6 CoreValidation workflow: https://github.com/ARM-software/CMSIS_6/blob/main/.github/workflows/corevalidation.yml

Arm revises this documentation regularly. Where a figure matters to a decision you are making, check it against the current version at the two latest links above before you commit to it.

Interested in Arm tools?

Talk to our application engineers for personalized tool recommendations.

Frequently asked questions

How long is an Arm UBL licence valid on a CI runner without network access?
Arm's User-based Licensing User Guide states that activating a product creates a licence in the device's local licence cache and that 'This cached license is valid for 7 days.' Each day the development tool runs it makes one attempt to renew the cached licence for another 7 days. If the licence was created by proxy activation with Arm licensing utilities version 1.3.2 or later, the validity period can be set with --borrow-period to any integer from 7 to 365 days.
Should every CI job run armlm activate against our licence server?
Arm's documentation cautions against that shape. On the ARMLM_ONDEMAND_ACTIVATION environment variable the User Guide says it 'is unsuitable when many devices request license activation in a short period. For example, this could happen in a Continuous Integration (CI) workflow. This type of workflow floods the license server with requests, which can cause the server to reject license requests. For this type of workflow, use Proxy activation to activate the license once and then import the resultant license file on all devices.' Arm's Administration Guide adds that the default Local License Server rate limit is 10 licence requests per second.
Does running armlm deactivate at the end of a CI job release a seat?
No. Arm's User Guide states that 'Deactivating a license is an activity local to a single device' and that 'Deactivation does not release the license so it can be used by another user.' For a licence server, a licence is automatically released for another user only when it has not been renewed for a minimum of 7 days. Arm's own GitHub Action ARM-software/cmsis-actions/armlm has no deactivate input and no post-job step, and Arm's own CMSIS_6 CoreValidation workflow does not deactivate.
What is proxy activation and how do I use it in a pipeline?
Proxy activation creates a licence transfer file on one device that has network access, and imports it on the target devices. Arm documents it as armlm activate with --as-user and --to-file on the connected device, optionally with --borrow-period, then armlm import --file on each target, optionally with --enable-renewal. Arm names Continuous Integration as a use case in the definition itself. Arm also notes that if a target device needs the licence for longer than the renewal period, the proxy activation process must be repeated before that period expires.
Where does the Arm licence actually live on a build runner?
In the local licence cache, by default a .armlm directory in the user's home directory: C:\Users\<your username>\.armlm on Windows, /home/<your username>/.armlm on Linux and /Users/<your username>/.armlm on macOS. The ARMLM_CACHED_LICENSES_LOCATION environment variable specifies the full path to the directory containing the licence files, which Arm suggests for devices with no local drive, no home directory for the user, or insufficient disk space.
Why does my build fail with 400 - Unauthorized user only on some runners?
Arm defines error 400 as the user identifier associated with the runtime process of the Arm development tool not matching the information in the licence. The causes Arm lists are a username that differs across devices, a licence cache copied from another user, or a relocated cache directory populated with another user's licence information. Arm's first diagnostic step is to check the username by running whoami in a console window, and Arm states the username activating the product must be identical on all devices.
Which hostnames does an Arm licensing client need through the firewall?
For activation-code licensing, Arm's User Guide lists https://api.arm.com/p-software-licensing and https://arm.compliance.flexnetoperations.eu/instances, over HTTP with SSL on TCP port 443, with hostnames api.arm.com and arm.compliance.flexnetoperations.eu. Arm states that using IP addresses is not recommended because the IP addresses of the URLs are not static. For a local licence server, users need access to the IP address and port of the licence server, which Arm notes is the only licence server port users require.
Can an air-gapped CI farm use Arm User-Based Licensing?
Yes. Arm documents proxy activation for target devices that do not meet the network requirements to activate a product or renew a licence. It requires a device with licence server or activation code network access to create the licence file, and a way of copying that file to the devices needing Arm development tool access. Arm's cloud guidance also covers replacing the licence cache in each instance or making the cache available on a shared location via ARMLM_CACHED_LICENSES_LOCATION. GSAS engineers support Local License Server deployment and network policy review for Indian teams whose build networks cannot reach the public internet.

Stay in the Loop

Get monthly compliance updates, product insights, and engineering best practices delivered to your inbox.