ssh-key-action: installing a private key inside a GitHub Actions runner
GitHub Action that installs SSH key to .ssh
At a glance
- What is it?
- shimataro/ssh-key-action writes an SSH private key into the .ssh directory of a GitHub Actions runner so later steps can scp, sftp or rsync over SSH, and it handles the parts that usually break, which are known_hosts and the removal of the key at the end of the job.
- Who is it for?
- Adopt ssh-key-action if your deployment pipeline copies files to a host over SSH and you want host key verification and key cleanup handled for you. Do not adopt it if your transfer is to GitHub itself, where the built in checkout already handles tokens, or if you need an encrypted key installed directly, since the action states it does not support encrypted keys.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 122 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The gap is that runners start with no key at all
The action installs an SSH key in ~/.ssh, and the README states the purpose plainly: it is useful for SCP, SFTP and rsync over SSH in a deployment script. The problem it solves is specific to CI. A GitHub-hosted runner is a fresh machine every time, so a private key that lives on a developer's laptop is not available, and copying it into the workflow as a plain step means handling the file, the permissions and the cleanup by hand. This action does that in one step. The audience is anyone writing a deployment workflow that pushes files to a server, and the design assumption is that the key arrives through the repository or organisation secrets rather than being committed. The action is published on the GitHub Marketplace as install-ssh-key, and it is MIT licensed, with the scripts and documentation released under that licence.
The workflow snippet is the whole API
Usage is a normal step in a workflow file, with the key coming from a secret you add under Settings, Secrets, Add a new secret beforehand. The README notes that PEM in RSA format, PKCS8 and RFC4716 in OpenSSH format are all accepted.
runs-on: ubuntu-latest
steps:
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
name: id_rsa # optional
known_hosts: ${{ secrets.KNOWN_HOSTS }}
config: ${{ secrets.CONFIG }} # ssh_config; optional
if_key_exists: fail # replace / ignore / fail; optional (defaults to fail)
- name: rsync over SSH
run: rsync -r ./foo/ user@remote:bar/The optional parameters are the interesting part. name sets the key file name, so a key installed as id_rsa is found at the default path without further configuration. known_hosts supplies the host keys, which is what makes host key verification work. config is an ssh_config fragment for hosts and options. And if_key_exists decides what happens when a key with that name is already there, with fail as the default.
The key is removed when the job ends
Two behaviours in the README are worth internalising because they affect how you write the workflow around it. The server key of github.com will always be set to known_hosts, so the action manages that entry itself and you do not have to. And SSH keys will be removed at the end of the workflow, which is the security property that makes it safe to install a real key on a shared runner. That is also why the action is usable on a hosted runner at all: the private key exists only for the duration of the job. The second behaviour, the append semantics, is a consequence of the same design. When the action is called multiple times, the contents of known_hosts and config will be appended, and the key must be saved under a different name using the name option so the calls do not collide.
Multiple keys, port forwarding, and the appending rule
Calling the action more than once is how you set up a bastion and a target, and the README notes it is useful for port forwarding. The example installs a bastion key and a target key, with separate known_hosts and separate config blocks, and the second config uses a ProxyCommand through the bastion.
runs-on: ubuntu-latest
steps:
- name: Install SSH key of bastion
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY_OF_BASTION }}
name: id_rsa-bastion
known_hosts: ${{ secrets.KNOWN_HOSTS_OF_BASTION }}
config: |
Host bastion
HostName xxx.xxx.xxx.xxx
User user-of-bastion
IdentityFile ~/.ssh/id_rsa-bastion
- name: Install SSH key of target
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY_OF_TARGET }}
name: id_rsa-target
known_hosts: ${{ secrets.KNOWN_HOSTS_OF_TARGET }} # will be appended to existing .ssh/known_hosts
config: | # will be appended to existing .ssh/config
Host target
HostName yyy.yyy.yyy.yyy
User user-of-target
IdentityFile ~/.ssh/id_rsa-target
ProxyCommand ssh -W %h:%p bastion
- name: SCP via port-forwarding
run: scp ./foo/ target:bar/The comments in that example are the documentation of the appending behaviour. Each known_hosts and each config fragment is added to what is already there, which is what makes two different hosts work from one .ssh directory, and the IdentityFile lines point at the two different key names the name option created.
Known hosts, and the case for not omitting it
The troubleshooting section is unusually opinionated, and for good reason. The README states that it is NOT secure to SSH with no known_hosts and using the StrictHostKeyChecking=no option. It then separates the reasons for wanting to omit it. If the reason is not understanding what known_hosts does, or it being a bother to fetch the server key, you should not omit it. If the reason is that the server is created dynamically and the key is hard to prefetch, the answer is to use a bastion server. And if the reason is that you are using a secure method such as SSHFP or a signed server key, there is a special value to omit known_hosts, and the README says you should use it only in that case, with the value known_hosts: unnecessary. That is a narrow exception with a stated condition, not a general escape hatch, and the Q and A also ties it to the failure mode you will hit first, which is a host key verification failed error, whose fix is to set known_hosts correctly using ssh-keyscan.
Encrypted keys and the HOME problem on self-hosted runners
Two practical failure modes are documented. The first is encrypted private keys, which this action does not support directly. The README lists four workarounds in order of preference: decrypting the key beforehand, described as the best bet because it works on any VM, the sshpass command, described as the next best bet but not supported on Windows, the expect command with a caution about exposing the passphrase to the console, and the SSH_ASKPASS environment variable, described as potentially troublesome. The second is the error HOME is not defined, which the README says has been reported on several self-hosted runners, and the fix is to set the environment variable explicitly on the step.
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
env:
HOME: /root
with:
.....The README points at issue 184 for that one. The if_key_exists parameter covers a third case, with replace replacing the key, ignore doing nothing and fail failing, and fail being the default.
Where ssh-key-action is the wrong tool
Two boundaries stand out. The first is that this is a file transfer helper, not a deployment system. It installs a key and gets out of the way, so everything about how you authenticate, what you copy and what you do afterwards is still your workflow. The second is the encrypted key limitation, which pushes decryption into the job, and that step is where secrets tend to leak. The alternative worth naming is a dedicated deploy action that handles key installation, host verification and file transfer together, which is fewer moving parts but couples your transfer to one vendor's conventions. There is also the option of using a short lived token or an OIDC based access mechanism instead of a static key, which removes the private key from the workflow entirely, and that is the direction the industry has moved for exactly this reason, but it requires the target server to support the federation rather than accepting a key file. For an existing pipeline that already uses rsync over SSH and just needs the key in the right place, this action is the smallest possible change, and the rsync via bastion recommendation in the README is a good default to follow.
How the action is built and maintained
The implementation is TypeScript compiled to a bundled JavaScript, with the dist/ directory committed so the action runs without an install step on the runner. The build script in package.json uses esbuild to bundle src/main.ts and src/post.ts for the node platform with minification and source maps, and the package requires Node 24 or newer with npm 11. The lint scripts are thorough for a project of this size, covering eslint on the TypeScript sources, a tsc no-emit type check of the two entry points, markdownlint on the documentation, and yamllint on the workflow and YAML files, with the aggregate run through npm-run-all. There is also an npm-check-updates script, which is how a dependency bump gets proposed, and an eslint config with the jsdoc plugin, which suggests the code is expected to be documented. The release history shows v2.8.0 in February 2026 and v2.8.1 in March 2026 after v2.7.0 in February 2024, the default branch is v2, and the last push to the repository was on 2026-06-23.
Editorial conclusion
Adopt ssh-key-action if your deployment pipeline copies files to a host over SSH and you want host key verification and key cleanup handled for you. Do not adopt it if your transfer is to GitHub itself, where the built in checkout already handles tokens, or if you need an encrypted key installed directly, since the action states it does not support encrypted keys. Verify first that you can populate known_hosts, since the action fails loudly on host key verification and the documented escape hatch for omitting it should be a last resort.
Frequently asked questions
How do I use shimataro/ssh-key-action in a GitHub Actions workflow?
Add your key to the repository or organisation secrets first, then add a step that uses shimataro/ssh-key-action@v2 with key, and optionally name, known_hosts, config and if_key_exists. Later steps can then run scp, sftp or rsync over SSH using the installed key.
Does ssh-key-action support encrypted private keys?
No, the action states it does not support encrypted keys directly. The README lists workarounds, with decrypting the key beforehand as the best bet since it works on any VM, then sshpass, expect and the SSH_ASKPASS environment variable.
What happens if the SSH key already exists in the runner?
The if_key_exists parameter decides: replace replaces the key, ignore does nothing and fail fails the step, with fail being the default. It is also relevant when the action is called multiple times, where known_hosts and config are appended and each key needs a distinct name option.
How do I fix a host key verification failed error in ssh-key-action?
Set the known_hosts parameter correctly, using the ssh-keyscan command to collect the server key. The README is explicit that SSH with no known_hosts and StrictHostKeyChecking=no is not secure, and that the only supported reason to omit known_hosts is a secure method such as SSHFP or a signed server key, using the value known_hosts: unnecessary.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/shimataro-ssh-key-action)Was this useful?