How actions/checkout Handles SSH Authentication with the ssh-key Input

The actions/checkout action implements SSH authentication by collecting the ssh-key input, securely writing it to a temporary file with restricted permissions, constructing a custom GIT_SSH_COMMAND with host verification settings, and injecting it into the Git environment.

When you configure the actions/checkout action with the ssh-key input, it initiates a multi-stage authentication pipeline written in TypeScript. This process securely manages private keys, handles host verification through known_hosts, and ensures credentials are cleaned up after the job completes.

Parsing SSH Inputs in src/input-helper.ts

The authentication process begins in src/input-helper.ts, where the action reads the workflow inputs and validates SSH-specific settings. The getInputs() function collects four SSH-related parameters:

  • ssh-key: The private key content for authentication
  • ssh-known-hosts: Optional custom host entries
  • ssh-strict: Boolean flag for host key checking (defaults to true)
  • ssh-user: Optional SSH username override
result.sshKey = core.getInput('ssh-key')
result.sshKnownHosts = core.getInput('ssh-known-hosts')
result.sshStrict = (core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE'
result.sshUser = core.getInput('ssh-user')

This input collection occurs at lines 59–65, parsing the raw workflow values into a structured settings object that subsequent modules consume.

Secure Key Storage and Git Environment Configuration

The src/git-auth-helper.ts module implements the core security logic. It transforms the input settings into temporary SSH artifacts and configures Git to use them for all remote operations.

Writing the Private Key to Temporary Storage

To prevent credential leakage, the action writes the private key to a randomly named file within the $RUNNER_TEMP directory with mode 0600 (read/write for owner only). This ensures no other users or processes can access the key material.

this.sshKeyPath = path.join(runnerTemp, uniqueId);
await fs.promises.writeFile(this.sshKeyPath,
                            this.settings.sshKey.trim() + '\n',
                            {mode: 0o600});

Lines 56–66 handle this atomic write operation, ensuring the key exists only for the duration of the job.

Building the Known Hosts File

The action constructs a comprehensive known_hosts file to prevent man-in-the-middle attacks. At lines 78–99, it merges three sources:

  1. The runner's existing ~/.ssh/known_hosts
  2. User-provided entries from the ssh-known-hosts input
  3. A hard-coded entry for github.com

This merged file is written to $RUNNER_TEMP alongside the private key.

Constructing the GIT_SSH_COMMAND

At lines 101–108, the action assembles a custom SSH command string that Git will use for all network operations. This command explicitly references the temporary key and known hosts files:

this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`;
if (this.settings.sshStrict) {
    this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no';
}
this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`;

When ssh-strict is enabled (the default), the command enforces strict host key checking while disabling IP checks to accommodate GitHub's load balancing.

Persisting Credentials in Git Config

The action exports the SSH command through two mechanisms at lines 111–118:

this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand);
if (this.settings.persistCredentials) {
    await this.git.config(SSH_COMMAND_KEY, this.sshCommand);
}

First, it sets the GIT_SSH_COMMAND environment variable for immediate use. If persist-credentials is true, it also writes the command to the repository's local Git config under core.sshCommand, ensuring submodules and subsequent Git operations inherit the authentication settings.

HTTPS Fallback When SSH Keys Are Unavailable

In src/git-source-provider.ts (lines 37–47), the action implements an authentication fallback strategy. When no ssh-key is provided, it configures Git to rewrite SSH URLs to HTTPS using the insteadOf mechanism:

if (!this.settings.sshKey) {
    for (const insteadOfValue of this.insteadOfValues) {
        await this.git.config(this.insteadOfKey, insteadOfValue, true, true);
    }
}

This ensures that repositories referencing submodules or remotes via SSH still function when the workflow uses HTTPS-based authentication instead.

Post-Job Cleanup of SSH Artifacts

After the job completes, the GitAuthHelper.removeAuth() method (implemented in src/git-auth-helper.ts) performs sanitation:

  • Removes temporary files: Deletes the private key and known_hosts files from $RUNNER_TEMP
  • Clears Git configuration: Removes the core.sshCommand entry if persist-credentials was enabled
  • Unsets environment variables: Cleans up GIT_SSH_COMMAND

This ensures credentials do not persist on the runner for subsequent jobs.

Implementing SSH Authentication in Workflows

To authenticate with private repositories using SSH, provide the private key through a repository secret:


# .github/workflows/checkout-ssh.yml

name: Checkout with SSH
on: [push]

jobs:
  checkout:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout private repo via SSH
        uses: actions/checkout@v4
        with:
          repository: my-org/private-repo
          ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
          ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
          ssh-strict: true
          persist-credentials: true

For repositories with private submodules, enable recursive checkout and persist credentials:


# .github/workflows/checkout-submodule.yml

name: Checkout with submodule SSH
on: [pull_request]

jobs:
  checkout:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
          ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
          persist-credentials: true

Summary

  • Input parsing: src/input-helper.ts collects ssh-key, ssh-known-hosts, ssh-strict, and ssh-user inputs from the workflow.
  • Secure storage: src/git-auth-helper.ts writes the private key to $RUNNER_TEMP with 0600 permissions and constructs a known_hosts file.
  • Git integration: The action builds a GIT_SSH_COMMAND that references the temporary files and exports it to the environment and Git config.
  • Fallback behavior: Without an SSH key, src/git-source-provider.ts configures HTTPS URL rewriting via insteadOf settings.
  • Cleanup: Temporary keys and configuration entries are removed after job completion to prevent credential leakage.

Frequently Asked Questions

What file permissions does actions/checkout set for SSH private keys?

The action writes SSH private keys to the runner's temporary directory with mode 0600 (owner read/write only). This permission mask prevents group or other users from reading the key material, as implemented in src/git-auth-helper.ts at lines 56–66.

Can I use actions/checkout with SSH for private submodules?

Yes. Set submodules: recursive or submodules: true in the workflow, provide the ssh-key input, and ensure persist-credentials: true is set. The persisted core.sshCommand configuration allows submodule initialization commands to inherit the SSH authentication settings automatically.

How does actions/checkout handle SSH host key verification?

By default, the action enables strict host key checking (StrictHostKeyChecking=yes) when ssh-strict is true (the default). It builds a temporary known_hosts file in $RUNNER_TEMP that combines the runner's existing entries, user-provided ssh-known-hosts input, and a hard-coded GitHub host key entry to verify server identities.

What happens if I don't provide an ssh-key input?

When no ssh-key is provided, the action skips SSH configuration and falls back to HTTPS authentication. In src/git-source-provider.ts, it configures Git URL rewriting via the insteadOf mechanism to translate SSH URLs to HTTPS equivalents, allowing repositories to clone using the built-in GITHUB_TOKEN or other HTTPS credentials.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →