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 · · 11 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 cost is immediate. The release branch is blocked. A firmware engineer who was writing driver code spends the morning reading armlm output. The DevOps engineer who owns the runners has no obvious place to start, because the change that caused the failure may be up to seven days old and may have nothing to do with the build.

This is almost never a bug in the pipeline. It is how Arm User-Based Licensing (UBL) works, and it does not work the way FlexNet floating licensing worked. Here is what has to happen for armclang to be licensed on a runner, and what breaks. Seat counts are covered separately in our UBL seat sizing guide, and headless build invocation in the csolution and cbuild guide. Every claim below is sourced to Arm’s published documentation, listed at the end.

The seven-day fuse

Under UBL a licence is not checked out and checked back in. It is a certificate cached on disk with a validity window.

Arm’s User Guide: activating a product creates a licence in the local licence cache for the device, and “This cached license is valid for 7 days.” Each day the development tool runs, it automatically makes one attempt to renew that cache for another 7 days by connecting to the local licence server or the Arm licence server. If it cannot be renewed for 7 days, it becomes invalid.

Then the sentence that explains Tuesday morning:

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

An egress rule changed on a Friday afternoon does not fail on the Friday. It fails up to a week later, in a build unrelated to the change, on whichever runner holds the oldest cache. The first red build is not the first broken build, and the window to search is the previous seven days of infrastructure changes, not the last commit.

The cache is a directory: 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.

Three ways to activate, and the one Arm rules out for CI

RouteWhat you run or setArm’s positioning
Licence serverarmlm activate --server <server_URL> --product <product_code>For a small number of cloud instances with intranet access
Activation codearmlm activate --code <activation_code>For a small number of cloud instances with internet access
Ambient environment variableARMLM_ONDEMAND_ACTIVATION=<product_code>@<server_URL>Carries an explicit caution about CI workflows

Arm’s caution on that third row:

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 that: bake the licence source into the runner image, let every job activate on its own, scale the matrix. Arm is telling you it degrades as you scale it.

The Administration Guide puts a number on it. By default the licence server rate limit is 10 licence requests per second, and Arm’s first remedy for pressure against that limit is a transfer file needing only one licence server request, not a higher limit. That is a Local License Server figure, set by the rate-limit parameter in <installation_directory>/server/local-configuration.yaml. Arm publishes no equivalent figure for the Cloud License Server, so do not plan capacity as though it applied there.

Proxy activation: the pattern Arm points CI at

Proxy activation creates a licence activation file on one device and uses it to provide licences on one or many others, local or in the cloud. Arm names CI in the definition itself, alongside devices that do not meet the network requirements.

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>]

Then on each target device:

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

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

One licence server request, not one per runner. Exactly the remedy Arm recommends against the rate limit.

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

No network calls by default. Arm: “By default, licenses created by proxy activation do not attempt to renew the license, stopping all attempts to access a network.” --enable-renewal on armlm import allows renewal attempts every 24 hours, and on renewal the cache validity is set to 7 days, overriding --borrow-period. A licence you deliberately borrowed for 90 days silently collapses to 7.

It is not fire and forget. If a target device needs the licence for longer than the renewal period, the proxy activation process must be repeated before that period expires. A long borrow also locks the seat: activating a product makes the licence unavailable to other users, and extending validity extends that lock.

A CI cache artifact is not one of Arm’s four documented ways to share one user’s licence across devices. Treat the transfer file as licence material, not configuration.

Identity: whoami decides whether the build is licensed

UBL binds the entitlement to a user, with no limits on concurrent usage, including the same product on multiple devices. Arm’s own example is a service account building and testing on any number of devices, and a seat is exactly one per user regardless of device count. Read that as a licensing permission, not a benchmark: Arm publishes no supported concurrency ceiling.

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

Arm defines error 400 - Unauthorized user as the user identifier associated with the runtime process of the tool not matching the information in the licence. The listed causes: a username that differs across 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 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. On licence server version 1.2024010.0 or earlier it is also case-sensitive.

That is what --as-user has to match: the operating system username of the process that executes armclang. Not the GitHub account, not the service principal in your identity provider. Container base images, USER directives, sudo steps and job-level container: blocks all move that value without announcing it.

whoami && armlm inspect

armlm inspect lists active and deactivated licences with their product or activation codes. Two lines at the top of a job, before anything expensive, turn a mid-build failure into an immediate and readable one.

One correction most teams need: deactivation does not free a seat. Arm states it is an activity local to a single device and does not release the licence for 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 ARM-software/cmsis-actions/armlm Action has inputs code, server and product, no deactivate input and no post-job step, and Arm’s CMSIS_6 CoreValidation workflow activates and never deactivates.

Containers and credentials

Arm’s Docker learning path ships a Dockerfile with the licensing block commented out and three options offered:

# 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

The first is the variable Arm flags as unsuitable for CI. The second writes an activation code into an image layer, where it survives every docker history and registry copy. The third is the legacy FlexNet variable, not UBL at all.

The rule for your container standards: licence material is runtime state, not image content. An image should contain the tools and armlm, never an activation code, a licence server credential or a populated .armlm.

Two shapes satisfy that using only documented mechanisms:

  • Mount the cache. Set ARMLM_CACHED_LICENSES_LOCATION to a path mounted at run time, holding a cache activated for the identity the container runs as. Arm’s caveat applies: activation and renewal are slower on a network location.
  • 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 on a schedule shorter than the borrow period.

For credentials, Arm’s own workflow is the reference implementation: CMSIS_6 CoreValidation passes ${{ secrets.ARM_UBL_ACTIVATION_CODE }} to the ARM-software/cmsis-actions/armlm Action. Two things to get right around it. The Action’s product: input defaults to KEMDK-COM0, a non-commercial edition, so a commercial pipeline must supply its own activation code, or its own server URL plus the product code for its entitlement. And an activation code is not associated with any user or service account until its first use, so the first job that runs with it decides who owns it. Fixing that is administrative: the administrator revokes the code and issues a new one, after which cached licences usually take up to 7 days to expire, longer for proxy-activated ones.

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

“Allow arm.com” is not actionable. This is.

For activation-code licensing, a device requires internet access to https://api.arm.com/p-software-licensing and https://arm.compliance.flexnetoperations.eu/instances. Arm’s firewall note: protocol HTTP with SSL, TCP port 443, hostnames api.arm.com and arm.compliance.flexnetoperations.eu, and the use of IP addresses is not recommended because the IP addresses of the URLs are not static. Hand that last clause to anyone asking for a CIDR range.

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"

Run those from the runner, not from a laptop on a different network.

For a Local License Server the ask is smaller: users require access to the IP address and port of the licence server for activation and renewal, and Arm notes this is the only licence server port users require. The default is TCP 7070, settable at install. One line usually closes the security review: during activation or renewal, the only personal data sent to the local licence server or the Arm licence server is the username.

Build networks with no internet access have a documented path rather than a workaround. Proxy activation needs one device with licence server or activation code network access to create the file, plus a way of copying it to the devices that need tool access. With a borrow period of up to 365 days and no renewal attempts by default, an isolated build farm can run on it.

Triage: symptom to cause

CodeSymptomWhat Arm says to do
100No licence found in the local cacheActivate with the information from your licence administrator. In CI, the cache usually did not reach the runner, or the process runs as a different user than you think
200Expired cached licenceThree causes: no network connection to the licence server, a cache that cannot be modified (disk space or permissions), or a changed licence server URL. Where network requirements cannot be met, the fallback is proxy activation
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 unsupportede.g. armclang: error: FuSa versions of the Product is not supported with this licenseOccurs with a licence server, or after upgrading to a tool version the cached licence does not support. If the licence was proxy-activated, proxy-activate again; otherwise the cache picks the change up on renewal

That last row is the one a pinned-toolchain pipeline hits: on a proxy-activated estate, tool upgrades and licence refreshes are the same change window.

The environment-variable surface

Arm publishes exactly five variables. Knowing the list is complete is as useful as knowing what is on it.

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, which is what makes a licensing failure visible in a CI log
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>. 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.

UBL does not apply to Arm development tools released before 2022. That migration is its own decision, covered in our FlexNet node-locked migration guide.

The checklist

  1. Run whoami and armlm inspect as the first step of a job. 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.
  3. If you go proxy, choose --borrow-period deliberately, remember --enable-renewal resets the window to 7 days, and schedule regeneration of the transfer file.
  4. Move the activation code and transfer file out of images and into your secret store.
  5. Send the network team hostnames, protocol and port with the two curl checks, not an IP range.
  6. Widen the search window to seven days when triaging a licensing failure.

How GSAS helps

GSAS Micro Systems is the authorized Arm development tools engineering partner for India, the UAE and Sri Lanka, and has worked with Arm since 2018 across Keil MDK, Arm Compiler for Embedded, Arm Compiler for Embedded FuSa and Arm Development Studio.

The value is engineering, not paperwork. Our field application engineers work in your time zone, on your build farm, starting from the shape of the log.

What you are facingWhat GSAS does
Pipeline failing on licensing, not reproducible locallyPipeline review: we read the job, the identity and the cache, and find where the entitlement stops reaching the compiler
Matrix outgrowing per-job activationMigration to proxy activation, including borrow period, regeneration schedule and ownership
Egress policy blocking activationProxy and firewall configuration with your network team, using the documented hostnames, ports and reachability checks
Air-gapped defence, aerospace and secure-medical buildsLocal License Server deployment alongside your IT security team
Uncertain seat countsSeat sizing against your real job and identity patterns rather than guesswork
Still on FlexNetMigration planning from FlexNet licensing to UBL, sequenced with your toolchain upgrades
Licence material in image layersContainer and runner standards so it stops happening

Our engineers work with teams across Bengaluru, Hyderabad, Chennai, Pune, Mumbai, Delhi NCR and Visakhapatnam, and across the UAE and Sri Lanka.

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 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.
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.

Stay in the Loop

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