# Sign commits on managed computers

Store a GPG or SSH signing key as a runtime secret, load it in Droid Computer and Environment sessions, and push signed commits to self-managed source control.

Factory does not create, store, or manage commit-signing keys for you. To sign commits on a Factory-managed computer, store your own signing key as a runtime secret, load it into `gpg` or `ssh-keygen` inside the session, and point Git at it. This is the supported path for both persistent [Droid Computers](/droid-computers/overview) and instances started from an [Environment](/environments/overview), because both read the same secrets from **Settings → Account → Secrets**.

<Note>
  No secret name is special to Factory. `GPG_SIGNING_KEY_B64`, `SSH_SIGNING_KEY_B64`, and `GPG_SIGNING_PASSPHRASE` on this page are examples that the setup script below reads. You can use any name that is not [runtime-reserved](/environments/prerequisites#reserved-secret-names), as long as the script reads the same name.
</Note>

## How it works

1. You create a dedicated signing key on your own workstation and register its public half with your source-control server.
2. You base64-encode the private key so it fits in one line, and save it as a runtime secret.
3. When a session starts, the secret is available as an environment variable. A setup script imports the key and sets `user.signingkey`, `gpg.format`, and `commit.gpgsign` in the computer's global Git config.
4. Every commit Droid makes is signed. Your server verifies the signature against the public key you registered.

Runtime secrets are **not available during an Environment build**, so signing setup always runs inside a session, never in the Environment's setup script. See [where to run the setup](#where-to-run-the-setup).

## Choose a signature format

| Format | Git setting | Use it when |
| --- | --- | --- |
| GPG (OpenPGP) | `gpg.format=openpgp` | Your server or a custom pre-receive hook requires GPG signatures specifically. |
| SSH | `gpg.format=ssh` | Your server accepts SSH signatures and you prefer to manage SSH keys. |

GitLab's **Reject unsigned commits** push rule accepts GPG, SSH, and X.509 signatures. A custom server hook can be stricter; for example, an error such as `Commit must be signed with a GPG key` can come from a GPG-only hook. Check which rule your project uses before you choose. GitHub Enterprise Server also verifies both GPG and SSH signatures. X.509 signing (`gpg.format=x509`) needs `gpgsm` and a certificate setup that this page does not cover.

## Before you start

- **Use a dedicated key.** Create a key used only for commits that Droid makes on Factory computers. Do not reuse a personal key that signs other work, so that you can revoke it without affecting anything else.
- **Match the commit email.** Your server marks a signature as verified only when the commit's committer email matches a verified email on the account that owns the key. For GPG, that email must also be a user ID on the key. See [commit identity](#commit-identity-with-self-managed-source-control).
- **Plan for no prompts.** Sessions cannot answer a passphrase prompt. Use a key without a passphrase, or store the passphrase as a second secret, which the setup script passes to `gpg` in loopback mode.
- **Check the size limit.** A secret value can hold at most 10,000 characters. Ed25519 keys are well under that limit after encoding. See [secret limits](/environments/prerequisites#secret-limits).

## Create and encode the key

Run these commands on your own workstation, not in a Droid session, so the private key never passes through a session transcript.

<Tabs>
  <Tab title="GPG">
    Create a signing-only Ed25519 key. Use the email that your commits will carry.

    ```bash
    gpg --quick-gen-key "Factory Droid <droid@example.com>" ed25519 sign 1y
    ```

    Export the secret key in binary form and encode it as one base64 line. Binary export is smaller than ASCII armor.

    ```bash
    gpg --export-secret-keys droid@example.com | base64 | tr -d '\n' > gpg-signing-key.b64
    ```

    Export the public key and add it to the source-control account that will own the commits. In GitLab, open **Preferences → GPG keys**. In GitHub Enterprise Server, open **Settings → SSH and GPG keys**.

    ```bash
    gpg --armor --export droid@example.com
    ```
  </Tab>
  <Tab title="SSH">
    Create an Ed25519 key without a passphrase, used only for signing.

    ```bash
    ssh-keygen -t ed25519 -C "droid@example.com" -N "" -f factory-signing-key
    ```

    Encode the private key as one base64 line.

    ```bash
    base64 < factory-signing-key | tr -d '\n' > ssh-signing-key.b64
    ```

    Add `factory-signing-key.pub` to the source-control account that will own the commits. In GitLab, open **Preferences → SSH Keys** and set **Usage type** to **Signing** or **Authentication & Signing**. In GitHub Enterprise Server, add it as a **Signing key**.
  </Tab>
</Tabs>

`tr -d '\n'` removes the line breaks that `base64` adds on Linux, so the same command works on Linux and macOS. Delete the `.b64` file after you save the secret.

## Save the key as a runtime secret

1. Open **Settings → Account → Secrets**.
2. Choose **Personal** for a key that signs only your sessions. A manager or owner can choose **Organization** for a key that every member and service account shares, though a per-person or per-service-account key keeps attribution clearer.
3. Click **Add secret**. Name it `GPG_SIGNING_KEY_B64` or `SSH_SIGNING_KEY_B64` and paste the contents of the `.b64` file as the value.
4. If your GPG key has a passphrase, add a second secret named `GPG_SIGNING_PASSPHRASE`.

To give a service account its own key, for example for [Delegations](/delegations), save the secret while acting as that service account or as an organization secret. A session that runs as a service account does not see the configuring user's personal secrets.

## Add the setup script

The script below reads whichever key secret is present, imports it, and configures Git. It does nothing when neither secret is set, so it is safe to commit to a repository that people also use locally.

```bash title=".factory/hooks/setup-commit-signing.sh"
#!/usr/bin/env bash
# Configures Git commit signing from a Factory runtime secret.
# Safe to run repeatedly. Does nothing when no signing secret is set.
set -euo pipefail

if [ -n "${GPG_SIGNING_KEY_B64:-}" ]; then
  install -d -m 700 "$HOME/.gnupg"
  key_fpr="$(printf '%s' "$GPG_SIGNING_KEY_B64" | base64 -d \
    | gpg --batch --with-colons --import-options show-only --import 2>/dev/null \
    | awk -F: '/^fpr:/ { print $10; exit }')"
  printf '%s' "$GPG_SIGNING_KEY_B64" | base64 -d | gpg --batch --quiet --import 2>/dev/null
  git config --global gpg.format openpgp
  git config --global user.signingkey "$key_fpr"
  if [ -n "${GPG_SIGNING_PASSPHRASE:-}" ]; then
    install -d -m 700 "$HOME/.local/bin"
    cat > "$HOME/.local/bin/gpg-loopback" <<'WRAPPER'
#!/usr/bin/env bash
exec gpg --batch --pinentry-mode loopback --passphrase-fd 3 "$@" 3<<<"$GPG_SIGNING_PASSPHRASE"
WRAPPER
    chmod 700 "$HOME/.local/bin/gpg-loopback"
    git config --global gpg.program "$HOME/.local/bin/gpg-loopback"
  fi
elif [ -n "${SSH_SIGNING_KEY_B64:-}" ]; then
  install -d -m 700 "$HOME/.ssh"
  (umask 077 && printf '%s' "$SSH_SIGNING_KEY_B64" | base64 -d > "$HOME/.ssh/factory_signing_key")
  ssh-keygen -y -f "$HOME/.ssh/factory_signing_key" > "$HOME/.ssh/factory_signing_key.pub"
  git config --global gpg.format ssh
  git config --global user.signingkey "$HOME/.ssh/factory_signing_key"
  printf '%s namespaces="git" %s\n' "$(git config --global user.email)" \
    "$(cat "$HOME/.ssh/factory_signing_key.pub")" > "$HOME/.ssh/allowed_signers"
  git config --global gpg.ssh.allowedSignersFile "$HOME/.ssh/allowed_signers"
else
  exit 0
fi

git config --global commit.gpgsign true
git config --global tag.gpgSign true
echo "Git commit signing is configured ($(git config --global gpg.format))."
```

The passphrase wrapper reads `GPG_SIGNING_PASSPHRASE` from the environment each time Git signs, so the passphrase is never written to disk. `gpg` and `ssh-keygen` are preinstalled on Factory-managed computers.

## Where to run the setup

| Target | What persists | When to run the script |
| --- | --- | --- |
| Droid Computer | The imported key and Git config stay on the computer across sessions. | Once, then again after you rotate the key. |
| Environment instance | Each instance starts from the build snapshot, which never contains runtime secrets. | At the start of every session on a new instance. |

To run it automatically, commit the script and a project [`SessionStart` hook](/harness/hooks) to the repository:

```json title=".factory/hooks.json"
{
  "SessionStart": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "\"$FACTORY_PROJECT_DIR\"/.factory/hooks/setup-commit-signing.sh",
          "timeout": 30
        }
      ]
    }
  ]
}
```

Make the script executable (`chmod +x`) before you commit it. Without a hook, run the script yourself through `droid computer ssh`, or ask Droid to run it before it commits.

Do not put the key, the passphrase, or the decoding commands in an Environment setup script. Secrets are not available during the build, and anything the build writes becomes part of the shared snapshot.

## Commit identity with self-managed source control

Factory writes a Git identity into each managed computer's global Git config and can add per-host identities for connected source-control servers. Which identity applies depends on who runs the session:

| Session runs as | Commits are authored as | Key to register |
| --- | --- | --- |
| Human user (organization OAuth) | The identity Factory configures for that user on the computer. | Add the public key to the server account whose verified email matches that identity. |
| Service account, GitLab Self-Hosted | The commit author name and email set on the service account's GitLab token, or the token owner's email when unset. | Add the public key to the GitLab user that owns that email. |
| Service account, GitHub Enterprise Server | The `factory-droid[bot]` identity of the GitHub App. | A bot email cannot own your key. Set a repository `user.email` for the machine user that owns the key, as shown below. |

Check the identity a repository will use before the first signed commit:

```bash
git config user.name
git config user.email
```

If the email does not match the key, set it for that repository. Repository config takes precedence over the global and per-host identities that Factory manages, and Factory never changes it:

```bash
git config user.email droid@example.com
```

[Self-Managed Source Control](/enterprise/self-managed-scm) describes how human users and service accounts get repository credentials on GitLab Self-Hosted and GitHub Enterprise Server.

## Verify a signed commit

After the setup script runs, make a commit and check it locally before you push:

```bash
git log --show-signature -1
git cat-file commit HEAD | head -n 8
```

A GPG commit shows `Good signature from` and a `gpgsig` header. An SSH commit shows `Good "git" signature for`. After you push, the commit shows **Verified** in GitLab or GitHub Enterprise Server when the email and registered public key match.

## Rotate or revoke a key

1. Create a new key and register its public half with your server, as in [create and encode the key](#create-and-encode-the-key).
2. In **Settings → Account → Secrets**, open the secret's menu, select **Edit**, and paste the new encoded value. Saved values cannot be viewed again.
3. New Environment instances get the new value right away. An existing Droid Computer keeps its current environment until credentials refresh and its processes restart; restart the computer if a session still sees the old value.
4. Run the setup script again. It sets `user.signingkey` to the newly imported key.
5. Remove the old public key from your server and, for GPG, delete it from the computer's keyring with `gpg --delete-secret-and-public-keys <old-fingerprint>`.

Deleting a personal secret lets an organization secret with the same name apply after the next refresh.

## Keep the key out of commits and transcripts

- Never commit the private key, the `.b64` file, or `~/.gnupg`. [Droid Shield](/autonomy-and-safety/droid-shield) blocks commits that add PGP and SSH private keys, but it cannot recognize a base64-encoded key reliably, so do not rely on it for this.
- Do not ask Droid to print or `echo` the secret. Command output becomes part of the session transcript.
- Signing does not change approvals. If your autonomy level requires approval for `git commit` or `git push`, Droid still asks before it runs them.

## Troubleshooting

<Troubleshooting>
  <TroubleshootingItem title="The secret variable is empty in the session">
    Check that the secret exists in the scope the session uses. Service-account sessions do not see personal secrets, and a personal secret overrides an organization secret with the same name. On an existing Droid Computer, restart it after you add or change the secret.
  </TroubleshootingItem>
  <TroubleshootingItem title="Saving the secret fails">
    The value must be a single line of at most 10,000 characters, and the name must not be runtime-reserved. Re-encode the key with `base64 | tr -d '\n'`, and use an Ed25519 key if an RSA key is too large. See [secret limits](/environments/prerequisites#secret-limits).
  </TroubleshootingItem>
  <TroubleshootingItem title="gpg fails with Inappropriate ioctl for device">
    The key has a passphrase and no prompt is available. Add `GPG_SIGNING_PASSPHRASE` and run the setup script again so Git uses the loopback wrapper, or use a key without a passphrase.
  </TroubleshootingItem>
  <TroubleshootingItem title="The commit is signed but shows Unverified">
    The committer email does not match a verified email on the server account that owns the public key, or the key is registered on a different account. Compare `git config user.email` with the key's user ID and the account's verified emails.
  </TroubleshootingItem>
  <TroubleshootingItem title="The server rejects the push as unsigned">
    Run `git log --show-signature` on every commit being pushed; commits made before the setup script ran are unsigned. Amend or rebase them with `git rebase --exec 'git commit --amend --no-edit -S' <base>`. If only SSH-signed commits are rejected, the project uses a GPG-only hook; switch to a GPG key.
  </TroubleshootingItem>
</Troubleshooting>

<RelatedLinks>
  <RelatedLink href="/environments/prerequisites" title="Environment prerequisites">
    Secret limits, multi-line values, and reserved names.
  </RelatedLink>
  <RelatedLink href="/enterprise/self-managed-scm" title="Self-Managed Source Control">
    Connect GitLab Self-Hosted or GitHub Enterprise Server.
  </RelatedLink>
  <RelatedLink href="/droid-computers/overview" title="Droid Computers">
    Create and manage persistent managed computers.
  </RelatedLink>
  <RelatedLink href="/harness/hooks" title="Hooks">
    Run commands automatically when a session starts.
  </RelatedLink>
</RelatedLinks>
