How actions/checkout Handles GitHub Token and SSH Authentication
The actions/checkout action secures repository access through src/git-auth-helper.ts by implementing token-based HTTP authentication using placeholder replacement in temporary credential files, and SSH key authentication via GIT_SSH_COMMAND with temporary key files scoped to the RUNNER_TEMP directory.
The actions/checkout action is the standard mechanism for accessing repository code in GitHub Actions workflows. Understanding how it handles actions/checkout authentication is crucial for securing private repositories and managing deployment keys. The action implements two distinct pathways in the GitAuthHelper class: one for HTTPS using personal access tokens or GITHUB_TOKEN, and one for SSH using private keys.
Token-Based HTTP Authentication
The token-based authentication flow in src/git-auth-helper.ts ensures that bearer tokens never appear in shell logs or process arguments. This mechanism intercepts HTTPS requests and injects authorization headers through Git's configuration system.
Constructing the Authorization Header
The GitAuthHelper builds a Basic authentication header using the provided token. In the constructor, the code creates a base64-encoded credential string from x-access-token and the supplied auth token, then immediately masks it using core.setSecret().
const basicCredential = Buffer.from(`x-access-token:${this.settings.authToken}`, 'utf8')
.toString('base64')
core.setSecret(basicCredential)
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`
this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`
This creates two values: a placeholder containing asterisks and the actual header containing the encoded token.
The Placeholder Replacement Strategy
To prevent the token from appearing in command-line arguments during git config operations, the helper employs a two-stage write process. First, it writes the placeholder value to a temporary credentials file using git config. Then it replaces the placeholder with the real header by reading the file, performing a string substitution, and writing it back.
// Write placeholder first
await this.git.config(this.tokenConfigKey,
this.tokenPlaceholderConfigValue,
false, false,
credentialsConfigPath)
// Replace with actual token
let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
content = content.replace(this.tokenPlaceholderConfigValue,
this.tokenConfigValue)
await fs.promises.writeFile(credentialsConfigPath, content)
Temporary Credential File Management
The temporary credentials file lives under RUNNER_TEMP (e.g., git-credentials-<uuid>.config). The repository's .git/config references this file through an includeIf.gitdir: directive, causing Git to include the authorization header on every HTTPS request to the remote. When the job completes, these temporary files are removed to prevent credential leakage.
SSH Key Authentication
When the ssh-key input is provided in action.yml, the action switches to SSH authentication. This pathway creates temporary key files and configures Git to use them via environment variables and configuration entries.
Secure Key File Creation
The configureSsh() method writes the SSH private key to a unique file path within RUNNER_TEMP with strict permissions (mode 0600). This ensures only the current process can read the key material.
this.sshKeyPath = path.join(runnerTemp, uniqueId)
await fs.promises.writeFile(this.sshKeyPath,
this.settings.sshKey.trim() + '\n',
{mode: 0o600})
Known Hosts Configuration
The action constructs a known-hosts file by concatenating existing entries from ~/.ssh/known_hosts, the optional ssh-known-hosts input, and built-in GitHub host keys. This file is also stored in RUNNER_TEMP to prevent man-in-the-middle attacks while maintaining isolation between workflow runs.
GIT_SSH_COMMAND Configuration
Rather than modifying system SSH configuration, the action sets the GIT_SSH_COMMAND environment variable to point to the temporary key and known-hosts files. This command is constructed in configureSsh() and includes strict host checking options when ssh-strict is enabled.
this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`
if (this.settings.sshStrict) {
// Add strict checking options
}
this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
When persist-credentials is true, this command is also stored in the repository's Git config as core.sshCommand, ensuring that subsequent Git operations like submodules inherit the same authentication.
Authentication Orchestration and Cleanup
The configureAuth() method in src/git-auth-helper.ts orchestrates the setup by clearing previous authentication state and invoking both configureToken() and configureSsh() as needed. For workflows running in Docker containers or requiring global configuration, configureGlobalAuth() copies the host's .gitconfig to a temporary location and applies credentials globally.
For repositories with submodules, configureSubmoduleAuth() propagates the temporary credentials to nested repository configurations. This ensures that git submodule update commands authenticate correctly using the same token or SSH key as the parent repository.
The removeSsh() and removeToken() methods perform cleanup by deleting temporary files and unsetting configuration entries, preventing credential persistence across job steps.
Configuration Examples
Configure HTTPS authentication using a Personal Access Token:
# .github/workflows/token-auth.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
persist-credentials: true
Configure SSH authentication with a deployment key:
# .github/workflows/ssh-auth.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-known-hosts: |
github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+P...
ssh-strict: true
persist-credentials: true
Summary
- actions/checkout authentication implements two distinct pathways: token-based HTTPS using
AUTHORIZATIONheaders and SSH using temporary key files. - The placeholder replacement technique in
configureToken()ensures tokens never appear in process arguments or logs during Git configuration. - SSH keys are written to
RUNNER_TEMPwith0600permissions and referenced via theGIT_SSH_COMMANDenvironment variable set inconfigureSsh(). - Temporary credential files are scoped to the repository using
includeIf.gitdir:directives and cleaned up after execution. - The
GitAuthHelperclass insrc/git-auth-helper.tscoordinates authentication throughconfigureAuth(), whileconfigureSubmoduleAuth()ensures nested repositories inherit credentials.
Frequently Asked Questions
How does actions/checkout prevent tokens from appearing in logs?
The action uses a placeholder mechanism where it first writes AUTHORIZATION: basic *** to a temporary config file using standard Git commands, then replaces the asterisks with the actual base64-encoded token by directly modifying the file. This prevents the token from appearing in shell history or process listings, and core.setSecret() masks the token in GitHub Actions logs.
What is the difference between token and SSH authentication in actions/checkout?
Token authentication modifies HTTPS URLs to include x-access-token credentials via Git's extraheader configuration, while SSH authentication uses the GIT_SSH_COMMAND environment variable to specify a private key file. Token auth is suitable for GITHUB_TOKEN or PATs, whereas SSH auth is required for deployment keys or when organizations mandate SSH protocols.
How does persist-credentials work with SSH keys?
When persist-credentials is set to true, the action stores the constructed SSH command in the repository's Git configuration as core.sshCommand. This persists the GIT_SSH_COMMAND equivalent across subsequent steps, allowing later Git operations like git fetch or git submodule update to use the same temporary key without requiring reinjection.
Where are temporary authentication files stored?
All temporary files—including credential configs, SSH keys, and known-hosts files—are stored in the directory specified by the RUNNER_TEMP environment variable. These files are created with restrictive permissions (mode 0600 for SSH keys) and are explicitly deleted during the cleanup phase to prevent credential leakage between jobs or steps.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →