feat: initial release — Setup SSH Client GitHub Action\n\nInitial implementation of the action to configure an SSH client and an optional remote shell wrapper.\n- Creates ~/.ssh, adds identity and known_hosts\n- Adds ssh config for 'github-action-host'\n- Optional SSH remote shell wrapper using mktemp\n- Fails when strict-host-key-checking is 'yes' and no hosts are provided\n\nThis is the initial creation of the action.,
Some checks are pending
Deploy Example / Deploy using ssh-remote shell (push) Waiting to run
Deploy Example / Deploy using direct SSH (push) Waiting to run

This commit is contained in:
Karolis2011 2025-11-19 23:18:55 +02:00
commit a5027e414c
5 changed files with 518 additions and 0 deletions

68
.github/workflows/example.yml vendored Normal file
View file

@ -0,0 +1,68 @@
# Example workflow demonstrating the setup-ssh-client action
name: Deploy Example
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy-with-shell-wrapper:
name: Deploy using ssh-remote shell
if: always() == 'false'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH
uses: ./
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.SSH_HOST }}
ssh-user: ${{ secrets.SSH_USER }}
- name: Check system info
shell: ssh-remote
run: |
echo "=== System Information ==="
whoami
hostname
uname -a
pwd
- name: Run deployment script
shell: ssh-remote
run: |
echo "=== Starting Deployment ==="
cd /var/www || cd ~
# Example deployment commands
echo "Current directory: $(pwd)"
echo "Disk usage:"
df -h | head -n 5
echo "✅ Deployment completed"
deploy-direct-ssh:
name: Deploy using direct SSH
if: always() == 'false'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH (without shell wrapper)
uses: ./
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.SSH_HOST }}
ssh-user: ${{ secrets.SSH_USER }}
use-shell-wrapper: 'false'
- name: Run direct SSH commands
run: |
echo "Running remote commands via SSH..."
ssh github-action-host "whoami"
ssh github-action-host "pwd"
ssh github-action-host "uname -a"

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Ignore OS files
.DS_Store
Thumbs.db
# Ignore editor files
.vscode/
.idea/
*.swp
*.swo
*~
# Ignore test files
test/
*.test.yml

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Karolis Kundrotas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

254
README.md Normal file
View file

@ -0,0 +1,254 @@
# Setup SSH Client
A composite GitHub Action that configures SSH client for running remote commands securely.
## Features
- 🔐 Secure SSH key management
- 🔧 Automatic SSH configuration
- 🔑 Known hosts setup with ssh-keyscan fallback
- ✅ Connection validation
- 🎯 Simple, reusable configuration
- 🚀 Custom shell wrapper for seamless remote command execution
## Usage
### Basic Example with Custom Shell
```yaml
name: Deploy Application
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH
uses: your-username/setup-ssh-client@v1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.SSH_HOST }}
ssh-user: ${{ secrets.SSH_USER }}
- name: Run remote commands with custom shell
shell: ssh-remote
run: |
cd /var/www
git pull origin main
npm install
systemctl restart myapp
- name: Or use direct SSH commands
run: |
ssh github-action-host "uptime"
ssh github-action-host "df -h"
```
### Basic Example (Direct SSH)
```yaml
name: Deploy Application
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH
uses: your-username/setup-ssh-client@v1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.SSH_HOST }}
ssh-user: ${{ secrets.SSH_USER }}
- name: Run remote commands
run: |
ssh github-action-host "cd /var/www && git pull"
ssh github-action-host "systemctl restart myapp"
```
### Advanced Example
```yaml
- name: Setup SSH with custom configuration
uses: your-username/setup-ssh-client@v1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: example.com
ssh-user: deploy
ssh-port: '2222'
strict-host-key-checking: 'yes'
ssh-known-hosts: |
example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...
- name: Deploy application
run: |
ssh github-action-host "cd /var/www/app && ./deploy.sh"
```
### Using Different Remote Shells
```yaml
- name: Setup SSH with zsh
uses: your-username/setup-ssh-client@v1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: example.com
ssh-user: deploy
remote-shell: 'zsh'
- name: Run commands in remote zsh
shell: ssh-remote
run: |
source ~/.zshrc
echo "Running in zsh: $SHELL"
```
### Disable Shell Wrapper (SSH Direct Only)
```yaml
- name: Setup SSH without shell wrapper
uses: your-username/setup-ssh-client@v1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: example.com
ssh-user: deploy
use-shell-wrapper: 'false'
- name: Use direct SSH commands
run: |
ssh github-action-host "command1"
ssh github-action-host "command2"
```
## Inputs
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `ssh-private-key` | SSH private key for authentication | Yes | - |
| `ssh-host` | SSH host to connect to | Yes | - |
| `ssh-user` | SSH username | Yes | - |
| `ssh-port` | SSH port | No | `22` |
| `ssh-known-hosts` | Known hosts content (uses ssh-keyscan if not provided) | No | `''` |
| `strict-host-key-checking` | Enable strict host key checking (`yes`/`no`/`accept-new`) | No | `accept-new` |
| `use-shell-wrapper` | Create shell wrapper for remote execution (enables `shell: ssh-remote`) | No | `true` |
| `remote-shell` | Shell to use on remote server (`bash`, `sh`, `zsh`, etc.) | No | `bash` |
## Outputs
| Output | Description |
|--------|-------------|
| `ssh-config-path` | Path to the SSH config file |
| `ssh-key-path` | Path to the SSH private key |
| `shell-wrapper-path` | Path to the SSH remote shell wrapper script |
## Remote Command Execution
This action provides two ways to execute commands remotely:
### 1. Custom Shell Wrapper (Recommended)
Use `shell: ssh-remote` in any step to execute the entire script on the remote server:
```yaml
- name: Deploy application
shell: ssh-remote
run: |
cd /var/www/myapp
git pull origin main
npm install --production
pm2 restart myapp
```
**Benefits:**
- Natural multi-line script syntax
- Automatic error handling with `set -e`
- Works like a local shell
- No need to wrap commands in SSH
### 2. SSH Host Alias (Direct)
Use the `github-action-host` alias for direct SSH commands:
```yaml
- name: Run single commands
run: |
ssh github-action-host "uptime"
ssh github-action-host "df -h"
```
This eliminates the need to specify the host, user, port, and key path in every SSH command.
## Security Best Practices
### Generating SSH Keys
```bash
# Generate a dedicated SSH key pair for GitHub Actions
ssh-keygen -t ed25519 -C "github-actions" -f github_actions_key
# Or use RSA if ed25519 is not supported
ssh-keygen -t rsa -b 4096 -C "github-actions" -f github_actions_key
```
### Setting up Secrets
1. Copy your private key content:
```bash
cat github_actions_key
```
2. Add it to GitHub Secrets:
- Go to your repository → Settings → Secrets and variables → Actions
- Click "New repository secret"
- Name: `SSH_PRIVATE_KEY`
- Value: Paste the entire private key content
3. Add the public key to your server:
```bash
cat github_actions_key.pub >> ~/.ssh/authorized_keys
```
### Getting Known Hosts
To pre-populate known hosts (recommended for security):
```bash
ssh-keyscan -H your-server.com
```
Add the output to a GitHub secret named `SSH_KNOWN_HOSTS`.
## Troubleshooting
### Connection Timeout
If the connection test fails with a timeout:
- Verify the host and port are correct
- Check firewall rules allow connections from GitHub Actions IPs
- Ensure SSH service is running on the remote server
### Permission Denied
If you get "Permission denied":
- Verify the public key is in `~/.ssh/authorized_keys` on the remote server
- Check the private key format is correct (should include header/footer)
- Ensure the SSH user has appropriate permissions
### Host Key Verification Failed
If you encounter host key verification issues:
- Set `strict-host-key-checking: 'no'` (not recommended for production)
- Or provide `ssh-known-hosts` input with the correct host key
## License
MIT License - see [LICENSE](LICENSE) file for details
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.

161
action.yml Normal file
View file

@ -0,0 +1,161 @@
name: 'Setup SSH Client'
description: 'Configure SSH client for running remote commands with custom shell wrapper'
author: 'Karolis Kundrotas'
branding:
icon: 'terminal'
color: 'blue'
inputs:
ssh-private-key:
description: 'SSH private key for authentication'
required: true
ssh-host:
description: 'SSH host to connect to'
required: true
ssh-user:
description: 'SSH username'
required: true
ssh-port:
description: 'SSH port'
required: false
default: '22'
ssh-known-hosts:
description: 'Known hosts content (optional, will use ssh-keyscan if not provided)'
required: false
default: ''
strict-host-key-checking:
description: 'Enable strict host key checking (yes/no/accept-new)'
required: false
default: 'accept-new'
use-shell-wrapper:
description: 'Create shell wrapper for remote execution (enables shell: ssh-remote)'
required: false
default: 'true'
remote-shell:
description: 'Shell to use on remote server (bash, sh, zsh, etc.)'
required: false
default: 'bash'
outputs:
ssh-config-path:
description: 'Path to the SSH config file'
value: ${{ steps.setup-ssh.outputs.ssh-config-path }}
ssh-key-path:
description: 'Path to the SSH private key'
value: ${{ steps.setup-ssh.outputs.ssh-key-path }}
shell-wrapper-path:
description: 'Path to the SSH remote shell wrapper script'
value: ${{ steps.setup-shell-wrapper.outputs.shell-wrapper-path }}
runs:
using: 'composite'
steps:
- name: Setup SSH
id: setup-ssh
shell: bash
run: |
# Create SSH directory
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Setup SSH private key
SSH_KEY_PATH="$HOME/.ssh/github_action_key"
echo "${{ inputs.ssh-private-key }}" > "$SSH_KEY_PATH"
chmod 600 "$SSH_KEY_PATH"
echo "ssh-key-path=$SSH_KEY_PATH" >> $GITHUB_OUTPUT
# Setup known hosts
if [ -n "${{ inputs.ssh-known-hosts }}" ]; then
echo "${{ inputs.ssh-known-hosts }}" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
else
if [ "${{ inputs.strict-host-key-checking }}" = "yes" ]; then
echo "❌ strict-host-key-checking is 'yes' but no ssh-known-hosts provided. Refusing to auto-add host key via ssh-keyscan." >&2
echo "Provide the host's key (e.g. from a trusted source) via 'ssh-known-hosts' input." >&2
exit 1
fi
echo "Scanning host keys for ${{ inputs.ssh-host }}..."
ssh-keyscan -p ${{ inputs.ssh-port }} -H "${{ inputs.ssh-host }}" >> ~/.ssh/known_hosts 2>/dev/null || true
chmod 644 ~/.ssh/known_hosts
fi
# Create SSH config
SSH_CONFIG_PATH="$HOME/.ssh/config"
cat >> "$SSH_CONFIG_PATH" << EOF
Host github-action-host
HostName ${{ inputs.ssh-host }}
User ${{ inputs.ssh-user }}
Port ${{ inputs.ssh-port }}
IdentityFile $SSH_KEY_PATH
StrictHostKeyChecking ${{ inputs.strict-host-key-checking }}
UserKnownHostsFile ~/.ssh/known_hosts
EOF
chmod 600 "$SSH_CONFIG_PATH"
echo "ssh-config-path=$SSH_CONFIG_PATH" >> $GITHUB_OUTPUT
echo "✅ SSH client configured successfully"
echo "Connect using: ssh github-action-host"
- name: Test SSH Connection
shell: bash
run: |
echo "Testing SSH connection..."
if ssh -o ConnectTimeout=10 -o BatchMode=yes github-action-host "echo 'SSH connection successful'" 2>&1; then
echo "✅ SSH connection test passed"
else
echo "⚠️ SSH connection test failed - please verify your credentials and host"
exit 1
fi
- name: Create SSH Remote Shell Wrapper
id: setup-shell-wrapper
if: inputs.use-shell-wrapper == 'true'
shell: bash
run: |
# Create a unique temporary wrapper script. Prefer RUNNER_TEMP if set.
WRAPPER_DIR="${RUNNER_TEMP:-/tmp}"
mkdir -p "$WRAPPER_DIR"
WRAPPER_PATH="$(mktemp "$WRAPPER_DIR/ssh-remote-shell-XXXXXX.sh")"
cat << 'WRAPPER_EOF' > "$WRAPPER_PATH"
#!/bin/bash
set -e
# Check if input file is provided
if [ -z "$1" ]; then
echo "Error: No script file provided" >&2
exit 1
fi
SCRIPT_FILE="$1"
# Check if script file exists
if [ ! -f "$SCRIPT_FILE" ]; then
echo "Error: Script file '$SCRIPT_FILE' not found" >&2
exit 1
fi
# Execute script on remote server
# Use BatchMode to prevent interactive prompts
# ConnectTimeout to fail fast if connection issues
ssh -o BatchMode=yes \
-o ConnectTimeout=10 \
github-action-host \
"${{ inputs.remote-shell }} -s" < "$SCRIPT_FILE"
WRAPPER_EOF
chmod +x "$WRAPPER_PATH"
echo "shell-wrapper-path=$WRAPPER_PATH" >> $GITHUB_OUTPUT
echo "✅ SSH remote shell wrapper created at $WRAPPER_PATH"
echo ""
echo "To use the remote shell in subsequent steps, add:"
echo " shell: ssh-remote"
echo ""
echo "The 'ssh-remote' shell is now available for use."
- name: Register Custom Shell
if: inputs.use-shell-wrapper == 'true'
shell: bash
run: |
echo "ssh-remote=${{ steps.setup-shell-wrapper.outputs.shell-wrapper-path }} {0}" >> "$GITHUB_ENV"
echo "✅ Custom shell 'ssh-remote' registered for this job"