Local PEM Signing
Generate a local key pair and configure signed OTA deployments
Set Up Local Signing
Review the signing rollout before adding a signer to an existing Hot Updater project.
Use the built-in local config when you accept responsibility for an exportable private key. Enable signing and provide the private key path:
import { defineConfig } from "hot-updater";
export default defineConfig({
signing: {
enabled: true,
privateKeyPath:
process.env.HOT_UPDATER_SIGNING_PRIVATE_KEY_PATH ?? "./keys/private-key.pem",
},
// ...other config
});Omit signing or set signing: { enabled: false } to disable signing. A local
config must set enabled: true; other shapes are rejected. Local signing
derives its public identity only from privateKeyPath.
Generate the key pair, then back up and protect the exportable private key:
npx hot-updater keys generate --output ./keys --key-size 4096The command creates private-key.pem with owner-only permissions and
public-key.pem. It refuses to overwrite either existing file; use a fresh
output directory for an intentional rotation. Keep the private file out of
Git, EAS build inputs, artifacts, caches, and logs. Commit the public file and
reference it from the Expo app plugin when using Expo.
Keep the Public Key with the Project
Newly generated keys inside the project add only the private-key path to
.gitignore. If .gitignore contains keys/, replace that whole-directory rule
with rules that include only the public file before committing it:
keys/
keys/*
!keys/public-key.pemFor EAS uploads, copy your existing .gitignore rules into .easignore, then
append these rules to include only the public key (EAS ignore-file
documentation):
# Keep your existing ignore rules above
!/keys/
/keys/*
!/keys/public-key.pemExisting ignore rules are not rewritten automatically.
Embed the Public Key and Release the App
Follow the shared steps for Expo and EAS Build or React Native CLI. Build and release the native app before deploying its first signed OTA bundle.
When replacing an existing key, follow Key Rotation before changing the signer used for installed apps.
Deploy from CI
For CI, use a protected secret manager and write the private file only outside the workspace for the duration of the trusted signing job. A PEM retrieved from a secret manager is still available to that process and is less isolated than a KMS or HSM-backed remote signer.
If local PEM custody is required in GitHub Actions, store it as a secret in a
protected GitHub Environment with required reviewers. Write the secret beneath
runner.temp, keep the checked-in public key path explicit, and remove the
temporary file even when deployment fails. Avoid a broadly available
repository or organization secret for production signing authority.
The config above reads HOT_UPDATER_SIGNING_PRIVATE_KEY_PATH so this job can
use its temporary private-key file.
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
steps:
- name: Set the temporary signing-key path
run: echo "HOT_UPDATER_SIGNING_PRIVATE_KEY_PATH=$RUNNER_TEMP/hot-updater-signing/private-key.pem" >> "$GITHUB_ENV"
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 22
- run: npm ci
- name: Materialize the signing key
env:
HOT_UPDATER_SIGNING_PRIVATE_KEY: ${{ secrets.HOT_UPDATER_SIGNING_PRIVATE_KEY }}
run: |
install -m 700 -d "$(dirname "$HOT_UPDATER_SIGNING_PRIVATE_KEY_PATH")"
printf '%s' "$HOT_UPDATER_SIGNING_PRIVATE_KEY" > "$HOT_UPDATER_SIGNING_PRIVATE_KEY_PATH"
chmod 600 "$HOT_UPDATER_SIGNING_PRIVATE_KEY_PATH"
- run: npx --no-install hot-updater deploy -p ios
- name: Remove the signing key
if: always()
run: rm -f "$HOT_UPDATER_SIGNING_PRIVATE_KEY_PATH"Prefer GitHub OIDC with a KMS signer when available because the workflow then receives short-lived signing authorization instead of exportable key material.