Skip to main content
A pipeline signs with the same commands you use locally. The difference is where the key lives: it can’t sit in Keychain, so you generate a movable key, encrypt it with a passphrase, and store both in CI secrets. apps sign reads the passphrase from COGNITE_KEY_PASSPHRASE instead of prompting.
Read Signing Flows custom apps from the CLI first. This page only covers the developer signature, and only on projects with Dev status.

Responsibility of the key owner

A signing key identifies a person, not a repository. Whoever can change what your pipeline runs can sign as you. So as the key owner, you vouch for the pipeline: that it’s healthy, that it hasn’t been tampered with, and that changes to it get reviewed. Restricting what can trigger signing is part of setting this up, not an optional hardening step you get to later.

Prerequisites

Set up the pipeline

1

Generate a passphrase

Run the following command to generate a passphrase of 15 characters or more and copy it to the clipboard. The passphrase protects the key once it’s in CI.
Paste the passphrase into your password manager before you continue. You’ll need it again when you store the secret, and you can’t recover it from the key.
2

Generate a CI/CD key

Run the following command to generate a key you can copy into CI:
--no-keychain gives you a key you can move. The private key goes in a file instead of Keychain.When the CLI prompts, enter a key ID (kid), an email, an expiry, and the passphrase you just generated. The command writes the encrypted private key to ~/.cognite-cli/keys/<kid>.key.jwe and the public key to <kid>.pub.pem.Set the kid to a name for this pipeline, such as acme-ci-001. Keep this key separate from your local signing key so that a compromise of the pipeline exposes only the CI/CD key.
Never commit the .key.jwe file or the passphrase.
3

Register the public key

Submit the Zendesk form from the link keys generate shows in the terminal. The link opens a pre-filled form. Choose Open in browser, or pass --copy-link and submit the form later.In the ticket, state that this is a CI/CD key. Most builders already have a local key registered, and without that note the request looks like a duplicate.
Wait until Cognite confirms that the key is registered, then run the signing job for the first time. Until then, apps sign fails in CI with a 403 and the job stops before apps activate. While you wait, you can store the secrets, pin the CLI, and add the workflow.
4

Store the secrets

In the app repository, store the signing key, the passphrase, and the key ID:
gh secret set COGNITE_KEY_PASSPHRASE prompts for the passphrase, which keeps it out of your shell history.Store the deploy client secret named by deploySecretName in app.json the same way:
For additional security, consider putting the signing secrets in a separate GitHub environment with required reviewers, so signing waits for an approval.
5

Pin the CLI

In the app repository, install an exact version of the CLI and commit package.json and package-lock.json:
The workflow runs npx cognite, which uses this install. --save-exact records 1.12.0 rather than ^1.12.0, and the lockfile pins the bytes as well as the version. npm ci checks each tarball against that hash and fails the job on a mismatch. To upgrade the CLI, change the pinned version in a pull request. Dependabot or Renovate can open that pull request.
The signing job holds your key, so it must run the CLI version you committed. npx @cognite/cli@latest fetches whatever was published most recently.
6

Add the workflow

Save the sample below as .github/workflows/sign-and-publish.yml.
  1. In the pull request, bump versionTag in app.json. Merging without a bump redeploys a version that’s already published.
  2. Open the draft deploy on the pull request and check it. Merging to main signs that draft and activates it.
If you edit the workflow:
  • Pass secrets through env so they stay out of the generated shell script.
  • Write the key with printf '%s' so the bytes stay unchanged.
  • Remove the key file with trap, including when signing fails.
  • Leave cancel-in-progress set to false so two merges cannot sign two versions into the same slot.
GitHub doesn’t pass secrets to workflows triggered from a fork, so the draft deploy fails on pull requests from forks. If you accept outside contributions, gate the deploy step on github.event.pull_request.head.repo.fork == false.
The app’s dependencies are installed and built on the same runner that holds the key. Review the dependency tree, or split the build into a separate job that passes the bundle as an artifact.
7

Restrict what can trigger signing

  1. In the repository, open Settings > Rules > Rulesets and add a ruleset for main. Signing runs when a commit reaches main, so this ruleset is what keeps an unreviewed commit from being signed.
  2. Require a pull request with at least one approving review. A commit can reach main only after someone approves the pull request. A direct push is blocked.
  3. Turn on Dismiss stale approvals. If someone pushes new commits after an approval, that approval no longer counts. The new commits need a new review.
The workflow pins each action to a commit SHA. Keep those pins. A moved tag would change what runs in the job that holds your key.
Leave administrator bypass off. With it on, an administrator can merge without the pull request and approval rules.
8

Confirm the signature

After the pipeline runs on main, check the version:
The version is ACTIVE and your developer signature reads VALID for the kid the pipeline used.

Renew the key before it expires

Signing keys are valid for 12 months by default. When the key expires, the pipeline fails. The person who created the key may no longer maintain the repository. Set a shared calendar reminder for nine months before the key expires, so the repository maintainers can renew it. To keep the key longer, ask Cognite in a Zendesk ticket to extend its validity. Cognite usually completes that request quickly.

Rotate a key used by a pipeline

Rotate when a key expires or you suspect it’s compromised, and also when someone with access to the repository secrets leaves the team.
  1. Generate a replacement with keys generate --no-keychain --interactive.
  2. Register the new public key.
  3. Update the COGNITE_SIGNING_KEY_JWE and COGNITE_KEY_PASSPHRASE secrets and the COGNITE_SIGNING_KID variable.
  4. Ask your Cognite contact to revoke the old key.
Versions already published stay published.

Other CI systems

The sample workflow is GitHub Actions. The signing steps are the same on Azure DevOps or another CI system:
  • Store the encrypted key and its passphrase as secrets.
  • Write the key to a temporary file, set COGNITE_KEY_PASSPHRASE, and run apps sign and apps activate.
  • Limit who can change the pipeline and what can trigger it. Use the controls that system provides.

Troubleshooting

The key reached the CLI and the passphrase didn’t. Confirm COGNITE_KEY_PASSPHRASE is set on the step that runs apps sign, and that the secret has a value. A blank value counts as unset. Secrets aren’t passed to workflows triggered from forks, so a run from a fork sees an empty value.
The passphrase reached the CLI but doesn’t match the key. Check that COGNITE_SIGNING_KEY_JWE and COGNITE_KEY_PASSPHRASE come from the same keys generate run. Rotating one without the other is the usual cause. Verify the pair locally before updating the secrets:
The passphrase in the secret is too short. This is enforced on decryption as well as encryption, so a truncated or hand-typed value fails with this error rather than at key generation. Rotate the key with a generated passphrase.
apps sign --interactive prompts for the passphrase and ignores COGNITE_KEY_PASSPHRASE. Drop --interactive from the pipeline.
The job fails at the sign step with:
The first line is the cause: Cognite hasn’t registered the CI/CD key’s kid yet. The second sentence is a generic message the CLI shows for any 403. It is not an access problem.Confirm with your Cognite representative that the CI/CD key has been added, then re-run the job. Each run starts without the .dev.sig file that apps sign writes before it sends the signature, so the new run signs the bundle and activates it again. See 403: signing key is not in the public key registry.
Last modified on November 5, 2026