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.,
This commit is contained in:
commit
a5027e414c
5 changed files with 518 additions and 0 deletions
68
.github/workflows/example.yml
vendored
Normal file
68
.github/workflows/example.yml
vendored
Normal 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
14
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
254
README.md
Normal 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
161
action.yml
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue