Skip to content
LatchkeyLatchkey home

Submodule "Permission denied (publickey)" over SSH in CI

A submodule uses an SSH remote (git@github.com:...) and the runner has no private key SSH can present, so GitHub rejects the connection with "Permission denied (publickey)". HTTPS auth does not apply to SSH remotes.

What this error means

The parent repo checks out, then submodule update fails with "git@github.com: Permission denied (publickey). fatal: Could not read from remote repository." The .gitmodules URL is an git@ SSH URL.

git
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights and the repository exists.
fatal: clone of 'git@github.com:acme/shared.git' into submodule path 'vendor/shared' failed

Diagnose it: depth, refs, or credentials?

Terminal
git rev-parse --is-shallow-repository
git rev-parse --abbrev-ref HEAD    # prints HEAD when detached
git log --oneline -3
git remote -v

Common causes

No SSH deploy key is loaded on the runner

The submodule remote is SSH, but nothing added a private key to an agent, so SSH offers no key and the server denies publickey auth.

The token-based checkout does not cover SSH submodules

actions/checkout injects an HTTPS token that rewrites github.com HTTPS URLs. An git@ SSH submodule URL is not rewritten by that token, so it still needs a key.

How to fix it

Load a deploy key with ssh-key on checkout

  1. Add the submodule repo a read-only deploy key; store the private key as a secret.
  2. Pass it to actions/checkout via ssh-key so the agent can authenticate SSH submodules.
  3. Keep submodules: recursive so nested submodules are fetched too.
.github/workflows/ci.yml
- uses: actions/checkout@v4
  with:
    submodules: recursive
    ssh-key: ${{ secrets.SUBMODULE_DEPLOY_KEY }}

Rewrite SSH submodule URLs to authenticated HTTPS

If you prefer a token over a key, rewrite the SSH remote to token HTTPS before updating submodules.

Terminal
git config --global url."https://x-access-token:${{ secrets.REPO_READ_PAT }}@github.com/".insteadOf "git@github.com:"
git submodule update --init --recursive

How to prevent it

  • Pick one auth style for submodules: deploy key for SSH URLs, or token plus insteadOf for HTTPS.
  • Keep .gitmodules URLs consistent so one credential covers them all.
  • Give submodule deploy keys read-only scope.

Frequently asked questions

What causes Submodule "Permission denied (publickey)" over SSH in CI?
There are 2 common causes: no ssh deploy key is loaded on the runner and the token-based checkout does not cover ssh submodules. The submodule remote is SSH, but nothing added a private key to an agent, so SSH offers no key and the server denies publickey auth.
How do I fix Submodule "Permission denied (publickey)" over SSH in CI?
There are 2 fixes depending on which cause you have: load a deploy key with ssh-key on checkout and rewrite ssh submodule urls to authenticated https. Work through them in order, since the first is the most common.
What does Submodule "Permission denied (publickey)" over SSH in CI actually mean?
The parent repo checks out, then submodule update fails with "git@github.com: Permission denied (publickey).
How do I stop Submodule "Permission denied (publickey)" over SSH in CI happening again?
Pick one auth style for submodules: deploy key for SSH URLs, or token plus insteadOf for HTTPS. The prevention section lists 3 changes that keep it from recurring.

Related guides

References

Not every red build is your code. Latchkey repairs the ones that are not, on the runner. Start free → 30-day trial · No credit card