How do you decide between SSH keys and HTTPS tokens for Git access across many CI jobs and machines?
answer
- Four axes, not a preference
- How long does the secret live?
- How far does a leak reach?
- Where does the secret rest during the job?
- Forwarding an agent is lending your identity
basics
~20 sJudge each option on lifetime, scope, how the secret reaches the git process, and what happens when a machine is compromised. Tokens favour short-lived, narrowly scoped, easily rotated automation; keys favour stable identities where port 443 is unavailable or an agent already exists.
solid answer
~50 sFrame it on four axes. **Lifetime and rotation**: tokens usually carry an expiry and are trivial to reissue, while an SSH key tends to live until someone remembers to remove it. **Scope**: prefer credentials narrow enough that a leak affects one repository, not the estate. **Delivery**: an HTTPS token can arrive as an environment variable consumed by an askpass hook or an inline credential helper, so nothing is written to disk; an SSH key needs a file with strict permissions, a `known_hosts` entry, and usually an agent — more moving parts in a minimal container. **Reachability**: HTTPS uses 443 and traverses proxies; SSH needs port 22 open. Then set the guardrails: never embed a secret in a remote URL, never forward an agent into a shared runner, and normalize the transport with `url.<base>.insteadOf` so submodules do not silently need the credential you deliberately excluded.
code
bash · 5 lines# token stays in memory: no ~/.git-credentials, no key file
export CI_TOKEN="..."
git -c credential.helper='!f() { echo "username=ci"; echo "password=$CI_TOKEN"; }; f' \
-c url."https://example.com/".insteadOf="ssh://[email protected]/" \
clone https://example.com/acme/app.gitgo deeper
Know that CI needs its own credential rather than an engineer's, and that secrets should be supplied through the environment rather than written into a remote URL.
Explain how a token reaches Git without touching disk, using an askpass program or an inline credential helper, and what an SSH key needs in a container by comparison.
Design a working scheme for a fleet: narrow scope, short lifetime, no on-disk persistence on ephemeral runners, seeded host verification, and transport normalization so submodules still work.
Own the estate-wide policy and its failure modes — blast radius per credential, rotation that actually takes effect, attribution for automated actions, and the review cadence for long-lived keys.
## Decide on properties, not preference Both transports authenticate fine. The interesting question is what the credential *is* as an operational object, and that reduces to four axes. **Lifetime.** A short-lived credential limits the value of a leak by construction. Tokens commonly expire; SSH keys typically do not, so their security depends on an offboarding process actually running. If your answer to "what happens when this leaks" is "we notice and revoke", you have chosen detection over containment. **Scope.** The blast radius of a credential is the set of repositories and operations it can reach. A read-only credential bound to a single repository is a fundamentally different incident from an identity that can write anywhere. Whichever transport you pick, the design goal is the same: one credential per automation purpose, with the minimum permissions the job needs. **Delivery and rest.** Ask where the secret sits while the job runs. An HTTPS token can be handed to Git purely in memory: an askpass program named by `GIT_ASKPASS`, or an inline helper such as `credential.helper '!f() { echo "password=$CI_TOKEN"; }; f'`, so nothing lands in `~/.git-credentials` or a container layer. An SSH key must exist as a file with restrictive permissions, plus a `known_hosts` entry, and ideally be unlocked into an agent. On a workstation that is well-trodden; in an ephemeral container it is extra surface. **Reachability.** HTTPS on 443 traverses corporate proxies that block 22. This alone decides the question in many environments and is not a security argument at all. ## Attribution and auditability Automation should not act as a person. A pipeline authenticating with an engineer's personal credential means the audit trail names that engineer, the job breaks when they leave, and their access defines the job's access. Use a dedicated automation identity or a per-repository credential, whichever the host provides, and make the mapping from job to identity explicit. ## The guardrails that matter more than the choice - **Never embed a secret in a remote URL.** It is written into `.git/config`, echoed in errors, visible in process listings, and captured by log shipping. Use a helper or askpass instead. - **Never forward an SSH agent into a shared runner.** Agent forwarding lets anything with access to the socket — including another tenant or a compromised build step — authenticate as you for as long as the connection lives. It is a lateral-movement primitive. - **Seed host verification properly.** Provisioning `known_hosts` is a one-time job; disabling host-key checking to make a pipeline green removes the protection against connecting to an impostor. - **Normalize transports.** If jobs get HTTPS tokens, ensure submodules and vendored manifests do not silently require SSH. `url.<base>.insteadOf` is the tool; make it explicit configuration rather than something inherited from a base image. - **Prefer no persistence on ephemeral machines.** A helper that persists to disk in a container image can leak the secret into caches and artifacts. ## A defensible default For CI: short-lived, narrowly scoped HTTPS credentials delivered through the environment into an askpass or inline helper, with no on-disk persistence, plus a transport rewrite so every URL the job touches uses that credential. For humans on workstations: SSH keys in an agent, or HTTPS with the OS keychain helper — both acceptable, chosen for ergonomics and network reality rather than dogma. For long-lived deploy targets that cannot receive rotating secrets, a per-repository read-only key is a reasonable compromise, provided its existence is inventoried and reviewed. The answer an interviewer is listening for is not "SSH" or "HTTPS". It is that you reason about lifetime, scope, delivery and blast radius, and that you know which Git-level mechanisms implement each choice.
- Why is putting a token directly in the remote URL a poor practice?The URL is persisted in `.git/config`, printed in error messages, visible in process listings, and frequently captured by CI log collection. The secret therefore spreads to places you never audited and is painful to rotate. Deliver it through a credential helper or `GIT_ASKPASS` so it exists only in the process's memory for the run.
- What is the specific risk of SSH agent forwarding onto a shared runner?Forwarding exposes a socket on the remote machine that can sign authentication challenges with your key. Anyone with sufficient privilege there — another tenant, a compromised build step, an administrator — can use it to act as you against any host that trusts your key, for the life of the session. The key never leaves your laptop, which is exactly why this is easy to underestimate.
- When is a long-lived SSH key still the right answer?When the environment cannot receive rotating secrets, when only port 22 is reachable, or when the identity must be stable for a system that is provisioned once and rarely touched. Make it per-repository and read-only where possible, inventory it, and pair it with a review cadence — the risk is not the key, it is the key nobody remembers exists.
- How do you keep submodules from defeating a credential decision?Submodule URLs come from `.gitmodules` and may name a transport the job has no credential for. Normalize them at job scope with `url.<base>.insteadOf` so every URL resolves to the transport you provisioned, and set the rule explicitly in the pipeline rather than relying on one inherited from a base image.
saying these in an interview costs you the question
- Argues SSH is inherently more secure without naming a property
- Uses one shared credential for the whole CI estate
- Puts tokens in remote URLs for convenience
- Forwards an SSH agent into shared build machines
- Lets pipelines authenticate as an individual engineer