Applications
View as MarkdownSSH deploy keys for git sources
A git service can clone over SSH instead of HTTPS: Settings → Source → Repository accepts ssh://git@host[:port]/org/repo.git and the scp form git@host:org/repo.git. An SSH URL needs a deploy key and a pinned host key; both are managed per service under Source. They apply immediately and are never staged.
Deploy key#
- Use SSH: generate a key creates an Ed25519 key pair on the control plane. The private key is stored encrypted (AES-256-GCM, like every secret), is write-only (no endpoint returns it) and travels to the agent as a send-time secret, never in a command payload, release or log. The public key is shown with a copy button, its fingerprint, and, for github.com, gitlab.com, bitbucket.org, codeberg.org and gitea.com, a link to the repository's "add deploy key" page. Read-only access is enough.
- Upload your own key stores an unencrypted OpenSSH or PEM private key (Ed25519, ECDSA, or RSA of at least 2048 bits). Passphrase-protected keys are rejected.
- Generate new key / Upload a key on an existing key is a rotation: the old key stops being used on the next deploy. Add the new public key to the repository first.
- Remove key also drops the pinned host keys. A service with an SSH URL and no key fails its deploys with "has no deploy key".
- A pull-request preview of a service copies its key and pins (same repository). Previews from forks do not get them.
Host key pinning (trust on first use)#
The first fetch (a plan, a deploy, Test connection) reads the git host's SSH host key and records it per service and host (host or [host]:port). The control plane and the agent then only accept that key:
- The agent trusts nothing else: it is sent the pinned
known_hostsline with every deploy and never falls back tossh-agentor~/.ssh/known_hosts. A host that is not pinned fails the clone. - If the host later presents a different key, the deploy fails with "SSH host key for … changed", the refused fingerprint is recorded and shown under Host key with Accept new host key. Accepting asks for confirmation and pins exactly the fingerprint you reviewed; if the server presents yet another key in between, nothing is pinned (409).
- Accepting is audited (
service.ssh_host.accept), as are first-use pins (service.ssh_host.pin), refused keys (service.ssh_host.mismatch) and key changes (service.ssh_key.create,.rotate,.delete).
A mismatch seen only by the agent (for example, interception on the agent's network path only) fails that deploy with the same message in the build log; the control plane's own view of the key is unchanged, so the Host key row does not show it.
Submodules#
The deploy key and the pinned host key are also used for ssh:// and git@host: submodules on the same host and port as the repository. A submodule on another host or port is not fetched (no key, no pinned host key for it). Hosts that scope a deploy key to one repository (GitHub) need the key added to the submodule repository as well, or a machine-user key that can read both. See git submodules.
API#
GET /services/{id}/ssh (projects:read), POST /services/{id}/ssh/key (generate, or upload with privateKey; rotate: true to replace), DELETE /services/{id}/ssh/key, POST /services/{id}/ssh/test, POST /services/{id}/ssh/host-keys/accept {fingerprint} (all projects:write).
Limitations#
- One key per service; there is no team-level key shared by several services (upload the same key to each).
- One pinned key per host and service. SSH certificates,
@cert-authorityand hashed known_hosts entries are not supported. - Only
ssh://and scp-style URLs; passwords inside the URL are rejected. SSH through a proxy or jump host is not supported. - Webhook-driven auto-deploy and commit statuses need a git connection (HTTPS API); an SSH service can still be deployed by hand, by CLI or API.