The production VPS pulls the repository via a deploy key — good. Except it was generated on the CTO's laptop, copied to /root/.ssh, and reused for staging and production. When staging is compromised via a vulnerable WordPress plugin, the attacker clones the private API repository too — same key, read access to the whole attached monorepo.
Deploy keys are the passport between your host and Git. Poorly scoped, they turn an application incident into intellectual property leakage.
SSH deploy key: minimal scope
On GitHub or GitLab: deploy key per repository, read-only for a server that only pulls code.
| Approach | Advantage | Risk |
|---|---|---|
| Read-only deploy key | Access limited to one repo | Key duplicated across environments |
| Machine user PAT | Rich API | Scope too broad, no rotation |
| OAuth app | Centralized | Complexity, audit |
Create a Linux user deploy without login shell (/usr/sbin/nologin), with ForceCommand if you need to restrict to git-shell.
A key on the production server is worth whatever that server is worth — treat it as a tier-one secret.
CI vs runtime: two distinct identities
The CI pipeline (GitHub Actions, GitLab CI) uses a token with read_repository scope or a deploy key with write access only if you automate tags and releases — never with admin rights on the organization.
The production server only pulls code. CI builds and pushes the artifact (image, archive); the server does not necessarily clone on every deployment if you ship immutable images.
Separating build credentials and deployment credentials limits the attack chain.
Rotation, audit and inventory
Keep a registry: key → repository → environment → creation date → next rotation. Automate alerts at 90 days.
Standard procedure: generate a new ed25519 pair, add the public key on the Git host with brief coexistence, validate deployment on staging then production, revoke the old key. After an incident: revoke immediately, not "next Monday".
Hosts and alternative Git access
On shared hosting without SSH Git: deploy via FTP or rsync from CI — no long-lived key on shared hosting. On VPS and bare metal: standard deploy key.
Some PaaS platforms (Clever Cloud, etc.) use webhooks — no key on the machine; verify who can trigger a deployment. On bare metal, restrict the deploy user to git pull via forced command and disable interactive shell entirely.
Document which team member owns each key rotation. When the person who generated the original key leaves, orphaned credentials often survive for months because nobody knows which pipeline still references them.
Common mistakes
Private key committed in the repository — scan Git history. StrictHostKeyChecking no eases MITM attacks: pin known_hosts. Private submodule without dedicated deploy key: parent clone succeeds, submodule fails or forces an overly broad key.
The peak: read-only that reads too much
Automating without blocking means short-lived machine identities, minimal scope, documented rotation — not an immortal key in root.
Decide and move forward without blind spots
Create one deploy key per repository and per environment, a dedicated deploy user, separate CI credentials, quarterly rotation, and an audit of known_hosts. Compare VPS and PaaS with secure deployment via the directory and comparator. The guides cover pipelines in detail.
Run an immediate audit: list active Git keys and accessible repositories — remove orphans.
Frequently asked questions
Deploy key or CI token?
Use a read-only SSH deploy key per repository for a server that only pulls code — that limits blast radius. A CI token suits API calls such as pull requests and releases, with minimal scopes, short expiry, and never the lead developer's personal account. Mixing both roles on one credential defeats the purpose.
Why forbid write access on a deploy key?
A compromised server with write access can push code or tags — a direct supply chain risk. Reserve pull-only on the application repository; controlled pipelines handle push. Write keys belong in CI with narrow scope and audit logs, not on long-lived production VMs.
How do you rotate without cutting production?
Add the new key on the remote, validate deployment on staging then production, then remove the old key. Document date and owner, and alert on keys older than ninety days. Brief coexistence avoids Friday-night outages when rotation is overdue.
One shared git user on the server?
Anti-pattern — logs become indistinguishable and one shared key spreads compromise. Prefer a dedicated deploy system user and one deploy key per environment or server. Each environment should be revocable without touching the others.
A clean deploy key can clone one repository — not your morning after a compromised staging environment.