CI Server Authentication
Major orgs
To automate deployments from major branches to their related org, you need to configure secure authentication from your CI server to an External Client App.
Note: You need openssl installed on your computer (available in
Git bash)
- Remain in your initialization branch
cicd, or a sub-branch of your lowest level major branch (usuallyintegration). - For each major branch to link to an org, run the sfdx-hardis command Configuration ->
(sf hardis:project:configure:auth)
For example, run the command for integration, uat, preprod and production major branches.
If messages ask you to run twice the same command, it's normal, it's for technical reasons :)
If you have errors in your apex tests classes, you may not be able to deploy the app to the Production org automatically. You will need to create the app manually by following the instructions in yellow in the error message, or follow the Additional information below. You can do it later, after having succeeded to merge the first merge request in the lower major branch (usually
integration).
The command
sf hardis:project:configure:authwill create/update:
.sfdx-hardis.ymlbranch configuration file (committed to repo)- A self-signed SSL certificate (
server.key/server.crt)- An External Client App deployed to the target org via metadata API
- CI environment variables (manually set in CI/CD server UIs)
At runtime, sfdx-hardis uses the OAuth 2.0 JSON Web Token (JWT) bearer flow with the Consumer Key stored as a secured CI/CD variable and the certificate key decrypted on the fly using an AES passphrase stored as a secured CI/CD variable.
See how to configure pipelines and CI/CD variables on different Git providers:
- Gitlab tutorial
- Azure tutorial, with Pipeline setup instructions
- GitHub tutorial
- BitBucket tutorial
- Jenkins tutorial
Additional information
The sections below cover background information, advanced scenarios, and reference details. You do not need them for the standard configuration described above.
External Client App
sfdx-hardis uses an External Client App (metadata type ExternalClientApplication, managed in Setup > External Client App Manager) to authenticate from CI to the target Salesforce org via JWT bearer flow.
It is a good practice to use one dedicated External Client App per use case (one for CI/CD, another one for Monitoring, etc.). This way, if you ever need to investigate or rotate credentials, you can identify exactly which application is involved.
Certificate storage modes
When the wizard generates the SSL certificate, it asks where you want to store the encrypted private key:
- Encrypted file in repo (default): the encrypted
<branchName>.keyfile is committed toconfig/branches/.jwt/. OnlySFDX_CLIENT_ID_<ALIAS>andSFDX_CLIENT_KEY_<ALIAS>need to be set as CI variables (the latter being the AES passphrase used to decrypt the file at runtime). - CI variable: nothing is committed to the repo. You set three CI variables:
SFDX_CLIENT_ID_<ALIAS>,SFDX_CLIENT_KEY_<ALIAS>, andSFDX_CLIENT_CERT_<ALIAS>(which holds the encrypted key content directly).
When you bring your own CA-signed certificate (see Use a CA-signed certificate below), a third mode is available: store the raw PEM private key directly in SFDX_CLIENT_CERT_<ALIAS>, no AES passphrase needed.
The variable-storage modes are useful when your security policy forbids committing any key material (even encrypted) to git.
CI environment variables
sfdx-hardis resolves the JWT credentials in this priority order:
| Variable | Required | Description |
|---|---|---|
SFDX_CLIENT_ID_<ALIAS> |
Yes | The Consumer Key of the External Client App. |
SFDX_CLIENT_CERT_<ALIAS> |
Required in CI-variable storage modes; ignored when the encrypted key file is committed in config/branches/.jwt/ |
Either the raw PEM private key (a PEM block whose header matches -----BEGIN ... PRIVATE KEY-----, no decryption needed) or the sfdx-hardis-encrypted key content (<iv-hex>:<encrypted-hex>, needs SFDX_CLIENT_KEY_<ALIAS>). sfdx-hardis auto-detects the format. |
SFDX_CLIENT_KEY_<ALIAS> |
Only with encrypted storage (file or variable) | The AES-256 passphrase used by sfdx-hardis to decrypt the encrypted private key (32 hex characters). |
SFDX_CLIENT_ID / SFDX_CLIENT_CERT / SFDX_CLIENT_KEY |
Fallback | Same as above, but without the _<ALIAS> suffix. Only useful if you have a single org alias. |
<ALIAS> is the uppercased branch name (for example INTEGRATION, UAT, PREPROD, PRODUCTION). For the Dev Hub, the alias is the value of devHubAlias from .sfdx-hardis.yml (often DEVHUB_<PROJECTNAME>).
⚠️ Less secure alternative: sfdx-hardis also recognizes
SFDX_AUTH_URL_<ALIAS>andSFDX_AUTH_URL_DEV_HUBfor the SFDX auth URL flow. When set, JWT is skipped andsf org login sfdx-urlis used instead. Do not use this for major orgs (integration / UAT / preprod / production). An SFDX auth URL embeds a long-lived OAuth refresh token that grants full org access if leaked; unlike JWT, it cannot be tied to a specific signing certificate and cannot be rotated without re-authenticating manually. Reserve it for scratch orgs and Dev Hub scenarios where JWT cannot be set up.
Use a CA-signed certificate
If your organization requires a certificate signed by an internal or public Certificate Authority instead of the self-signed certificate generated by sfdx-hardis, the setup is straightforward:
-
Generate your key pair, get
server.crtsigned by your CA, and create the External Client App manually in Setup withserver.crtuploaded as Digital Signature, Permitted Users = Admin approved users are pre-authorized, and the CI user's profile assigned. -
Set two CI/CD variables for the matching branch:
-
SFDX_CLIENT_ID_<ALIAS>= the Consumer Key of the External Client App. -SFDX_CLIENT_CERT_<ALIAS>= the full PEM content of your private key file (server.key), including the-----BEGIN ... PRIVATE KEY-----and-----END ... PRIVATE KEY-----lines. The key must be unencrypted (no passphrase) and in a supported PEM private key format such as-----BEGIN PRIVATE KEY-----or-----BEGIN RSA PRIVATE KEY-----. Do not use-----BEGIN ENCRYPTED PRIVATE KEY-----or-----BEGIN OPENSSH PRIVATE KEY-----.
That's it. Do not set SFDX_CLIENT_KEY_<ALIAS> in this mode: sfdx-hardis auto-detects that SFDX_CLIENT_CERT_<ALIAS> starts with -----BEGIN and uses the key as-is, with no decryption step. Because of that, passphrase-protected keys and unsupported private key formats cannot be loaded for JWT signing.
💡 Fetching the key from a vault at runtime: because sfdx-hardis only reads
SFDX_CLIENT_CERT_<ALIAS>from the environment whenhardis:auth:loginruns, you do not have to store the key as a static CI/CD variable. You can add your own pipeline step beforesf hardis:auth:loginthat retrieves the PEM key from your secrets backend (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, CyberArk Conjur, etc.) and exports it asSFDX_CLIENT_CERT_<ALIAS>. Example:# Fetch the private key from Vault, then run sfdx-hardis as usual export SFDX_CLIENT_ID_INTEGRATION="$(vault kv get -field=consumer_key secret/sf/integration)" export SFDX_CLIENT_CERT_INTEGRATION="$(vault kv get -field=private_key secret/sf/integration)" sf hardis:auth:login --target-org integration
💡 Bring your own authentication script: if neither flow above fits your security policy, you can skip sfdx-hardis's authentication helper entirely. Authenticate however you want (custom
sf org logininvocation, short-lived OAuth token from your IdP, JWT minted by a privileged service, etc.) and just make sure thesfCLI ends up with a defaulttarget-org(ortarget-dev-hub) pointing at the right org before anyhardis:*command runs. The prerun auth hook detects that an org is already connected (viasf org display) and skips its own login flow, so the rest of the pipeline runs unchanged. Example:# Custom step: authenticate however you want, then set the default org ./my-company-auth.sh --output sfdx-auth-url.txt sf org login sfdx-url --sfdx-url-file sfdx-auth-url.txt --alias integration --set-default # sfdx-hardis picks up the already-connected org and runs normally sf hardis:project:deploy:smart --check
To rotate the certificate later, generate a new key/CSR pair, upload the new server.crt to the same External Client App, and update SFDX_CLIENT_CERT_<ALIAS> (or the vault entry it is fetched from) with the new key content. The Consumer Key does not change.
Curious about how sfdx-hardis resolves credentials at runtime (alias lookup order, JWT private key format auto-detection, key file lookup paths, etc.)? See the Technical explanations section of
hardis:auth:login.
Dev Hub
If you are using scratch orgs, you also need to configure authentication for the Dev Hub (even if you already configured authentication for the production org).
To do that, run the following command:
sf hardis:project:configure:auth --devhub
This stores the Dev Hub alias / username / instance URL in the project-level .sfdx-hardis.yml and generates a dedicated key pair under config/.jwt/. Set SFDX_CLIENT_ID_<DEVHUB_ALIAS> plus either:
SFDX_CLIENT_KEY_<DEVHUB_ALIAS>(and optionallySFDX_CLIENT_CERT_<DEVHUB_ALIAS>with the encrypted key) for the self-signed flow, orSFDX_CLIENT_CERT_<DEVHUB_ALIAS>with the raw PEM key content for the CA-signed flow.
As a less secure last resort for scratch-org workflows where JWT cannot be set up, you can set SFDX_AUTH_URL_DEV_HUB with the output of sf org auth show-sfdx-auth-url --target-org <devhub-alias> --no-prompt --json | jq -r .result.sfdxAuthUrl. Be aware that this value contains a long-lived OAuth refresh token granting full Dev Hub access if leaked; prefer JWT whenever possible.
