From a5027e414cd48ac6f08d7d8eafa31064977fda25 Mon Sep 17 00:00:00 2001 From: Karolis2011 Date: Wed, 19 Nov 2025 23:18:55 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20release=20=E2=80=94=20Setup?= =?UTF-8?q?=20SSH=20Client=20GitHub=20Action\n\nInitial=20implementation?= =?UTF-8?q?=20of=20the=20action=20to=20configure=20an=20SSH=20client=20and?= =?UTF-8?q?=20an=20optional=20remote=20shell=20wrapper.\n-=20Creates=20~/.?= =?UTF-8?q?ssh,=20adds=20identity=20and=20known=5Fhosts\n-=20Adds=20ssh=20?= =?UTF-8?q?config=20for=20'github-action-host'\n-=20Optional=20SSH=20remot?= =?UTF-8?q?e=20shell=20wrapper=20using=20mktemp\n-=20Fails=20when=20strict?= =?UTF-8?q?-host-key-checking=20is=20'yes'=20and=20no=20hosts=20are=20prov?= =?UTF-8?q?ided\n\nThis=20is=20the=20initial=20creation=20of=20the=20actio?= =?UTF-8?q?n.,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/example.yml | 68 +++++++++ .gitignore | 14 ++ LICENSE | 21 +++ README.md | 254 ++++++++++++++++++++++++++++++++++ action.yml | 161 +++++++++++++++++++++ 5 files changed, 518 insertions(+) create mode 100644 .github/workflows/example.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 action.yml diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml new file mode 100644 index 0000000..729daf9 --- /dev/null +++ b/.github/workflows/example.yml @@ -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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5818c14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Ignore OS files +.DS_Store +Thumbs.db + +# Ignore editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Ignore test files +test/ +*.test.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2a301cb --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c39a67 --- /dev/null +++ b/README.md @@ -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. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..27fb301 --- /dev/null +++ b/action.yml @@ -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"