How actions/checkout Manages Known Hosts for SSH Connections

actions/checkout constructs a temporary known-hosts file that isolates SSH trust by combining the runner's default keys, workflow-supplied entries, and a built-in GitHub host key, then injects this configuration via the GIT_SSH_COMMAND environment variable.

When cloning repositories over SSH in GitHub Actions, the actions/checkout action implements a secure, ephemeral approach to host key verification. Rather than modifying the runner's global SSH configuration, it creates temporary credential files that exist only for the job duration. This article examines the source code implementation to explain how the action manages known hosts without compromising runner security.

Input Configuration for SSH Connections

The process begins in src/input-helper.ts, where the action reads SSH-specific workflow inputs. At line 161, the helper retrieves the ssh-known-hosts input using result.sshKnownHosts = core.getInput('ssh-known-hosts').

  • ssh-key: The private key used for authentication, stored as a repository secret.
  • ssh-known-hosts: Additional host entries provided by the workflow author, typically containing public keys for self-hosted Git servers.
  • ssh-strict: An optional boolean that enforces strict host key checking when enabled.

The GitAuthHelper.configureSsh() Method

Core orchestration occurs in src/git-auth-helper.ts within the configureSsh() method. This function handles both key preparation and known-hosts assembly while ensuring no credentials leak between jobs.

Writing the Private Key

When ssh-key is provided, the helper writes the key to a temporary file under RUNNER_TEMP with restrictive permissions (mode 600). The path is persisted using stateHelper.setSshKeyPath() for later cleanup. This prevents unauthorized read access while the key remains on the filesystem during the job execution.

Assembling the Known Hosts Payload

The action constructs a comprehensive known-hosts file by aggregating three distinct sources:

  1. Runner defaults: The existing contents of ~/.ssh/known_hosts from the GitHub-hosted runner image.
  2. User inputs: Entries supplied via the ssh-known-hosts workflow input.
  3. Built-in GitHub key: An implicit entry for github.com containing the public RSA key shipped with the action.

This combined payload is written to a uniquely named file (<uniqueId>_known_hosts) inside RUNNER_TEMP. The path is saved via stateHelper.setSshKnownHostsPath(), ensuring the post-action cleanup phase can locate and remove the file.

Injecting SSH Configuration via GIT_SSH_COMMAND

Rather than modifying ~/.ssh/config, the action uses the GIT_SSH_COMMAND environment variable to override SSH behavior exclusively for Git operations. In src/git-auth-helper.ts (lines 101-114), the helper constructs a command string:

const sshPath = await io.which('ssh', true)
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)}"`

The command explicitly:

  • Points to the temporary private key using the -i flag.
  • References the temporary known-hosts file via UserKnownHostsFile.
  • Enforces strict checking when ssh-strict is enabled, while disabling IP checks to avoid mismatches in dynamic cloud environments.

This command is exposed to Git through this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand).

Persisting Credentials for Submodules

When the persist-credentials input is set to true, the action stores the same SSH command in Git's global configuration at core.sshCommand (lines 115-119 in src/git-auth-helper.ts). This ensures that subsequent submodule operations inherit the identical host verification policy without re-executing the checkout action.

Secure Cleanup and State Management

Security isolation relies on complete removal of temporary files after job completion. The src/state-helper.ts module manages this through its cleanup functions (lines 43-52). During the post-action phase, the helper retrieves the stored paths for both the SSH key and known-hosts files, then deletes them from RUNNER_TEMP. This guarantees no sensitive material persists on the runner for subsequent jobs.

Practical Implementation Examples

Configure SSH-based checkout in your workflow by supplying the appropriate inputs:

name: Checkout with SSH Authentication
on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout via SSH with custom hosts
        uses: actions/checkout@v4
        with:
          ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
          ssh-known-hosts: |
            git.mycompany.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...
            10.0.0.5 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE...
          ssh-strict: true

For advanced scenarios requiring programmatic control, use the internal helper directly:

import { createAuthHelper } from '@actions/checkout/lib/git-auth-helper';

const authHelper = createAuthHelper(gitCommandManager, {
  sshKey: process.env.SSH_PRIVATE_KEY,
  sshKnownHosts: process.env.SSH_KNOWN_HOSTS,
  sshStrict: true,
  persistCredentials: true
});

await authHelper.configureSsh();

Summary

  • Temporary isolation: actions/checkout never modifies the runner's global ~/.ssh/known_hosts file.
  • Multi-source validation: The src/git-auth-helper.ts module combines runner defaults, user inputs, and a built-in GitHub key into a single temporary file.
  • Environment injection: SSH configuration uses GIT_SSH_COMMAND with explicit UserKnownHostsFile and identity file paths rather than system configuration.
  • Automatic cleanup: The src/state-helper.ts module guarantees removal of all temporary files and keys after job completion.
  • Submodule support: Persistent credentials store the SSH command in Git config for nested repository operations.

Frequently Asked Questions

Does actions/checkout modify the runner's permanent SSH configuration?

No. According to the actions/checkout source code, the action exclusively uses temporary files within RUNNER_TEMP and environment variables. The cleanup routine in src/state-helper.ts (lines 43-52) ensures complete removal of keys and known-hosts files after the job completes, leaving no trace on the runner.

Why does the action include a built-in GitHub host key?

The implicit github.com entry ensures immediate compatibility with GitHub-hosted repositories without requiring users to manually specify GitHub's public RSA key. This key is hardcoded in the action and appended to any user-supplied known-hosts entries in src/git-auth-helper.ts, providing a seamless experience for the primary use case while still allowing custom host definitions.

How does strict host key checking work with actions/checkout?

When ssh-strict: true is set in the workflow, the action appends -o StrictHostKeyChecking=yes to the GIT_SSH_COMMAND constructed in src/git-auth-helper.ts. This forces SSH to reject connections where the host key is not found in the temporary known-hosts file, while the accompanying -o CheckHostIP=no prevents IP address mismatches that commonly occur in dynamic cloud environments.

Can I use actions/checkout with self-hosted runners that have existing SSH configurations?

Yes. The action safely reads the existing ~/.ssh/known_hosts from the runner and includes those entries in the temporary file created in RUNNER_TEMP. However, it still isolates the configuration via GIT_SSH_COMMAND, ensuring your job trusts only the explicit key set without affecting other processes running concurrently on the self-hosted runner.

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 →