improve linux-ssh-operator-skill: bug fixes, rsync support, tunneling docs
- Fix dry-run bug in ssh_alias_setup.sh (pub key check skipped when key was never generated) - Remove dead code in ssh_run.sh (-- check after host arg was never reachable) - Add --rsync/--delete flags to ssh_copy.sh for incremental rsync transfers - Add -C/--compress flag to ssh_run.sh for slow-link connections - Expand SKILL.md/SKILL.zh-CN.md: tunneling, jump host, SSH multiplexing, Docker ops - Expand ssh-playbook.md: port forwarding, bastion, ControlMaster, structured troubleshooting table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: linux-ssh-operator-skill
|
name: linux-ssh-operator-skill
|
||||||
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件(scp/rsync)、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
|
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件(scp/rsync)、配置 SSH 别名、安装公钥启用免密登录、SSH 隧道/端口转发、跳板机、内网穿透、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件、SSH 隧道、端口转发、跳板机、堡垒机、服务器上看一下、帮我连上服务器、查看服务器日志 等场景时使用。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Linux SSH Operator
|
# Linux SSH Operator
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Use SSH to connect to Linux servers and perform safe, repeatable remote operations (commands, logs, services, file transfer, alias bootstrap, passwordless login).
|
Use SSH to connect to Linux servers and perform safe, repeatable remote operations (commands, logs, services, file transfer, alias bootstrap, passwordless login, tunnels).
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -99,29 +100,85 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
|
|||||||
```bash
|
```bash
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --compress my-server -- cat /var/log/big.log
|
||||||
```
|
```
|
||||||
|
|
||||||
### Transfer files
|
### Transfer files
|
||||||
|
|
||||||
Upload:
|
Upload (scp):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Download:
|
Download (scp):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Incremental sync (rsync — preferred for directories or large files):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync pull my-server /var/log/ ./logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH Tunneling / Port Forwarding
|
||||||
|
|
||||||
|
Local forward (access remote service on a local port):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access remote MySQL (3306) at localhost:13306
|
||||||
|
ssh -L 13306:127.0.0.1:3306 my-server -N
|
||||||
|
```
|
||||||
|
|
||||||
|
Remote forward (expose a local port on the server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make local :8080 accessible on server's :18080
|
||||||
|
ssh -R 18080:127.0.0.1:8080 my-server -N
|
||||||
|
```
|
||||||
|
|
||||||
|
Jump host (bastion → target):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -J bastion-user@bastion:22 target-user@target-host
|
||||||
|
```
|
||||||
|
|
||||||
|
Or configure in `~/.ssh/config`:
|
||||||
|
|
||||||
|
```sshconfig
|
||||||
|
Host target-internal
|
||||||
|
HostName 10.0.0.50
|
||||||
|
User ubuntu
|
||||||
|
ProxyJump bastion
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH Multiplexing (faster repeated connections)
|
||||||
|
|
||||||
|
Add to `~/.ssh/config` to reuse the same TCP connection:
|
||||||
|
|
||||||
|
```sshconfig
|
||||||
|
Host *
|
||||||
|
ControlMaster auto
|
||||||
|
ControlPath ~/.ssh/ctrl-%h-%p-%r
|
||||||
|
ControlPersist 60s
|
||||||
|
```
|
||||||
|
|
||||||
|
The first `ssh my-server` opens a master connection; subsequent connections reuse it without re-handshaking.
|
||||||
|
|
||||||
## Common Ops (snippets)
|
## Common Ops (snippets)
|
||||||
|
|
||||||
- Disk: `df -h`, `du -sh /path/* | sort -h`
|
- Disk: `df -h`, `du -sh /path/* | sort -h`
|
||||||
- Memory/CPU: `free -h`, `top`, `ps aux --sort=-%mem | head`
|
- Memory/CPU: `free -h`, `top`, `ps aux --sort=-%mem | head`
|
||||||
- Logs: `journalctl -u SERVICE -n 200 --no-pager`, `tail -n 200 -f /path/log`
|
- Logs: `journalctl -u SERVICE -n 200 --no-pager`, `tail -n 200 -f /path/log`
|
||||||
- Services: `systemctl status|restart|stop SERVICE`
|
- Services: `systemctl status|restart|stop|enable SERVICE`
|
||||||
- Networking: `ss -lntp`, `ip a`, `ip r`, `curl -v http://127.0.0.1:PORT/`
|
- Networking: `ss -lntp`, `ip a`, `ip r`, `curl -v http://127.0.0.1:PORT/`
|
||||||
|
- Docker: `docker ps`, `docker logs -f CONTAINER`, `docker exec -it CONTAINER bash`
|
||||||
|
- Config test: `nginx -t`, `sshd -T`
|
||||||
|
- Open files: `lsof -i :PORT`, `fuser PORT/tcp`
|
||||||
|
|
||||||
## Safety
|
## Safety
|
||||||
|
|
||||||
@@ -136,5 +193,5 @@ Download:
|
|||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
- `scripts/ssh_alias_setup.sh`: create/update SSH aliases and optionally run `ssh-copy-id`.
|
- `scripts/ssh_alias_setup.sh`: create/update SSH aliases and optionally run `ssh-copy-id`.
|
||||||
- `scripts/ssh_run.sh`: run remote commands with consistent options.
|
- `scripts/ssh_run.sh`: run remote commands with consistent options (supports `--compress`, `--tty`, `--sudo`).
|
||||||
- `scripts/ssh_copy.sh`: push/pull files via scp with consistent options.
|
- `scripts/ssh_copy.sh`: push/pull files via scp or rsync (use `--rsync` for incremental transfers).
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: linux-ssh-operator-skill
|
name: linux-ssh-operator-skill
|
||||||
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件(scp/rsync)、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
|
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件(scp/rsync)、配置 SSH 别名、安装公钥启用免密登录、SSH 隧道/端口转发、跳板机、内网穿透、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件、SSH 隧道、端口转发、跳板机、堡垒机、服务器上看一下、帮我连上服务器、查看服务器日志 等场景时使用。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Linux SSH 运维助手(中文说明)
|
# Linux SSH 运维助手(中文说明)
|
||||||
@@ -14,9 +15,10 @@ description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、
|
|||||||
- 执行远程命令
|
- 执行远程命令
|
||||||
- 查看系统与服务日志
|
- 查看系统与服务日志
|
||||||
- 管理 `systemd` 服务
|
- 管理 `systemd` 服务
|
||||||
- 上传和下载文件
|
- 上传和下载文件(scp 或 rsync)
|
||||||
- 配置 SSH 别名
|
- 配置 SSH 别名
|
||||||
- 安装公钥并启用免密登录
|
- 安装公钥并启用免密登录
|
||||||
|
- SSH 隧道 / 端口转发 / 跳板机
|
||||||
- 进行常见运维排障
|
- 进行常见运维排障
|
||||||
|
|
||||||
适合以下场景:
|
适合以下场景:
|
||||||
@@ -25,9 +27,11 @@ description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、
|
|||||||
- 在服务器上运行命令
|
- 在服务器上运行命令
|
||||||
- 查看 `journalctl` / 应用日志
|
- 查看 `journalctl` / 应用日志
|
||||||
- 启动、停止、重启服务
|
- 启动、停止、重启服务
|
||||||
- 把文件部署到服务器
|
- 把文件部署到服务器(scp 或 rsync 增量同步)
|
||||||
- 从服务器拉取日志、配置或产物文件
|
- 从服务器拉取日志、配置或产物文件
|
||||||
- 按“别名 + IP + 密码”快速配置一键 SSH 免密登录
|
- 按"别名 + IP + 密码"快速配置一键 SSH 免密登录
|
||||||
|
- 配置端口转发访问内网服务
|
||||||
|
- 通过跳板机/堡垒机连接内网服务器
|
||||||
|
|
||||||
## 工作流程
|
## 工作流程
|
||||||
|
|
||||||
@@ -71,7 +75,7 @@ ssh my-server
|
|||||||
|
|
||||||
### 一键配置别名 + 免密登录
|
### 一键配置别名 + 免密登录
|
||||||
|
|
||||||
当用户提供“别名、主机/IP、密码”,并希望直接通过 `ssh 别名` 登录时,优先使用下面的流程:
|
当用户提供"别名、主机/IP、密码",并希望直接通过 `ssh 别名` 登录时,优先使用下面的流程:
|
||||||
|
|
||||||
1. 使用辅助脚本写入或更新 SSH 别名:
|
1. 使用辅助脚本写入或更新 SSH 别名:
|
||||||
|
|
||||||
@@ -118,22 +122,78 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
|
|||||||
```bash
|
```bash
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --compress my-server -- cat /var/log/big.log
|
||||||
```
|
```
|
||||||
|
|
||||||
### 传输文件
|
### 传输文件
|
||||||
|
|
||||||
上传文件:
|
上传文件(scp):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
下载文件:
|
下载文件(scp):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
|
||||||
```
|
```
|
||||||
|
|
||||||
|
增量同步(rsync,推荐用于目录或大文件):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 上传目录
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
|
||||||
|
# 上传并删除远端多余文件
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
|
||||||
|
# 下载目录
|
||||||
|
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync pull my-server /var/log/ ./logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH 隧道 / 端口转发
|
||||||
|
|
||||||
|
本地转发(将远程服务映射到本地端口):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 把远程 MySQL(3306) 映射到本地 13306
|
||||||
|
ssh -L 13306:127.0.0.1:3306 my-server -N
|
||||||
|
```
|
||||||
|
|
||||||
|
远程转发(将本地服务暴露到服务器端口):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 把本地 :8080 映射到服务器 :18080
|
||||||
|
ssh -R 18080:127.0.0.1:8080 my-server -N
|
||||||
|
```
|
||||||
|
|
||||||
|
跳板机 / 堡垒机:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -J bastion-user@bastion:22 target-user@target-host
|
||||||
|
```
|
||||||
|
|
||||||
|
或在 `~/.ssh/config` 中配置:
|
||||||
|
|
||||||
|
```sshconfig
|
||||||
|
Host target-internal
|
||||||
|
HostName 10.0.0.50
|
||||||
|
User ubuntu
|
||||||
|
ProxyJump bastion
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH 多路复用(加速重复连接)
|
||||||
|
|
||||||
|
在 `~/.ssh/config` 中添加,复用同一 TCP 连接:
|
||||||
|
|
||||||
|
```sshconfig
|
||||||
|
Host *
|
||||||
|
ControlMaster auto
|
||||||
|
ControlPath ~/.ssh/ctrl-%h-%p-%r
|
||||||
|
ControlPersist 60s
|
||||||
|
```
|
||||||
|
|
||||||
|
第一次 `ssh my-server` 建立主连接,后续连接直接复用,无需重新握手,速度更快。
|
||||||
|
|
||||||
## 常见运维操作
|
## 常见运维操作
|
||||||
|
|
||||||
- 磁盘空间:`df -h`
|
- 磁盘空间:`df -h`
|
||||||
@@ -142,8 +202,11 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
|
|||||||
- 进程查看:`ps aux --sort=-%mem | head`
|
- 进程查看:`ps aux --sort=-%mem | head`
|
||||||
- 日志查看:`journalctl -u SERVICE -n 200 --no-pager`
|
- 日志查看:`journalctl -u SERVICE -n 200 --no-pager`
|
||||||
- 日志跟踪:`tail -n 200 -f /path/log`
|
- 日志跟踪:`tail -n 200 -f /path/log`
|
||||||
- 服务管理:`systemctl status|restart|stop SERVICE`
|
- 服务管理:`systemctl status|restart|stop|enable SERVICE`
|
||||||
- 网络排查:`ss -lntp`、`ip a`、`ip r`、`curl -v http://127.0.0.1:PORT/`
|
- 网络排查:`ss -lntp`、`ip a`、`ip r`、`curl -v http://127.0.0.1:PORT/`
|
||||||
|
- Docker:`docker ps`、`docker logs -f CONTAINER`、`docker exec -it CONTAINER bash`
|
||||||
|
- 配置测试:`nginx -t`、`sshd -T`
|
||||||
|
- 端口占用:`lsof -i :PORT`、`fuser PORT/tcp`
|
||||||
|
|
||||||
## 安全注意事项
|
## 安全注意事项
|
||||||
|
|
||||||
@@ -170,7 +233,8 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
|
|||||||
|
|
||||||
- 指定用户、端口、私钥
|
- 指定用户、端口、私钥
|
||||||
- 设置连接超时
|
- 设置连接超时
|
||||||
- 启用 TTY
|
- 启用 TTY(`--tty`)
|
||||||
|
- 启用 SSH 压缩(`--compress`,适合慢速链路)
|
||||||
- 使用 `sudo` 或 `sudo -n`
|
- 使用 `sudo` 或 `sudo -n`
|
||||||
- 使用 `--accept-new` 处理首次连接主机
|
- 使用 `--accept-new` 处理首次连接主机
|
||||||
- 使用 `--dry-run` 预览实际将执行的命令
|
- 使用 `--dry-run` 预览实际将执行的命令
|
||||||
@@ -184,11 +248,13 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
|
|||||||
|
|
||||||
### `scripts/ssh_copy.sh`
|
### `scripts/ssh_copy.sh`
|
||||||
|
|
||||||
用于通过 `scp` 上传或下载文件,支持:
|
用于通过 `scp` 或 `rsync` 上传或下载文件,支持:
|
||||||
|
|
||||||
- `push` 上传文件到远程服务器
|
- `push` 上传文件到远程服务器
|
||||||
- `pull` 从远程服务器下载文件
|
- `pull` 从远程服务器下载文件
|
||||||
- 递归复制目录
|
- `--rsync`:使用 rsync 增量同步(显示进度,适合目录和大文件)
|
||||||
|
- `--delete`:配合 `--rsync push` 删除远端多余文件
|
||||||
|
- 递归复制目录(scp 模式用 `-r`)
|
||||||
- 指定用户、端口、私钥
|
- 指定用户、端口、私钥
|
||||||
- 使用 `--accept-new`
|
- 使用 `--accept-new`
|
||||||
- 使用 `--dry-run` 预览命令
|
- 使用 `--dry-run` 预览命令
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ ssh my-server "du -sh /var/log/* | sort -h | tail"
|
|||||||
```bash
|
```bash
|
||||||
ssh my-server "ps aux --sort=-%mem | head"
|
ssh my-server "ps aux --sort=-%mem | head"
|
||||||
ssh my-server "ss -lntp"
|
ssh my-server "ss -lntp"
|
||||||
|
ssh my-server "lsof -i :8080"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logs (systemd)
|
### Logs (systemd)
|
||||||
@@ -67,6 +68,7 @@ ssh my-server "ss -lntp"
|
|||||||
```bash
|
```bash
|
||||||
ssh my-server "journalctl -u SERVICE -n 200 --no-pager"
|
ssh my-server "journalctl -u SERVICE -n 200 --no-pager"
|
||||||
ssh my-server "journalctl -u SERVICE -f --no-pager"
|
ssh my-server "journalctl -u SERVICE -f --no-pager"
|
||||||
|
ssh my-server "journalctl -u SERVICE --since '1 hour ago' --no-pager"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Services (systemd)
|
### Services (systemd)
|
||||||
@@ -89,15 +91,137 @@ Non-interactive sudo (fails if a password prompt would be required):
|
|||||||
ssh my-server "sudo -n systemctl restart SERVICE"
|
ssh my-server "sudo -n systemctl restart SERVICE"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh my-server "docker ps"
|
||||||
|
ssh my-server "docker logs --tail=200 -f CONTAINER"
|
||||||
|
ssh my-server "docker exec -it CONTAINER bash"
|
||||||
|
ssh my-server "docker stats --no-stream"
|
||||||
|
```
|
||||||
|
|
||||||
|
### File editing shortcuts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View a config file
|
||||||
|
ssh my-server "cat /etc/nginx/nginx.conf"
|
||||||
|
|
||||||
|
# Quick in-place line replacement
|
||||||
|
ssh my-server "sed -i 's/old/new/g' /path/to/file"
|
||||||
|
|
||||||
|
# Append a line
|
||||||
|
ssh my-server "echo 'new line' >> /path/to/file"
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSH Tunneling / Port Forwarding
|
||||||
|
|
||||||
|
### Local forward
|
||||||
|
|
||||||
|
Access a remote service on a local port (server-side port does not need to be publicly open):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# remote MySQL → localhost:13306
|
||||||
|
ssh -L 13306:127.0.0.1:3306 my-server -N
|
||||||
|
|
||||||
|
# remote Redis → localhost:16379
|
||||||
|
ssh -L 16379:127.0.0.1:6379 my-server -N
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `-f` to background the tunnel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -fNL 13306:127.0.0.1:3306 my-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote forward
|
||||||
|
|
||||||
|
Expose a local service on the server (useful for demos or webhooks):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# local :8080 → server :18080
|
||||||
|
ssh -R 18080:127.0.0.1:8080 my-server -N
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jump host / bastion
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Single jump
|
||||||
|
ssh -J bastion user@target
|
||||||
|
|
||||||
|
# Multi-hop
|
||||||
|
ssh -J jump1,jump2 user@target
|
||||||
|
```
|
||||||
|
|
||||||
|
`~/.ssh/config` version:
|
||||||
|
|
||||||
|
```sshconfig
|
||||||
|
Host target-internal
|
||||||
|
HostName 10.0.0.50
|
||||||
|
User ubuntu
|
||||||
|
ProxyJump bastion
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSH Multiplexing
|
||||||
|
|
||||||
|
Reuse the same TCP connection for multiple SSH sessions — avoids repeated handshakes:
|
||||||
|
|
||||||
|
```sshconfig
|
||||||
|
Host *
|
||||||
|
ControlMaster auto
|
||||||
|
ControlPath ~/.ssh/ctrl-%h-%p-%r
|
||||||
|
ControlPersist 60s
|
||||||
|
```
|
||||||
|
|
||||||
|
Check active master sockets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls ~/.ssh/ctrl-*
|
||||||
|
```
|
||||||
|
|
||||||
|
Close a master socket manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -O exit my-server
|
||||||
|
```
|
||||||
|
|
||||||
## Safer host key handling
|
## Safer host key handling
|
||||||
|
|
||||||
- Prefer verifying the host key fingerprint out-of-band on first connect.
|
- Prefer verifying the host key fingerprint out-of-band on first connect.
|
||||||
- If you must automate first-connect for ephemeral hosts, use `StrictHostKeyChecking=accept-new` (OpenSSH 7.6+).
|
- If you must automate first-connect for ephemeral hosts, use `StrictHostKeyChecking=accept-new` (OpenSSH 7.6+).
|
||||||
- If you see a "host key changed" warning, treat it as a potential security incident until you confirm the change is expected.
|
- If you see a "host key changed" warning, treat it as a potential security incident until you confirm the change is expected.
|
||||||
|
|
||||||
|
Remove a stale known_hosts entry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-keygen -R SERVER_IP
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting quick hits
|
## Troubleshooting quick hits
|
||||||
|
|
||||||
- `Permission denied (publickey)`: wrong user, wrong key, server missing your public key, or `sshd` settings.
|
| Symptom | Likely cause | Fix |
|
||||||
- `Connection timed out`: routing/firewall/security group, wrong port, server down.
|
|---|---|---|
|
||||||
- `No route to host`: network path missing (VPN, subnet, ACL).
|
| `Permission denied (publickey)` | Wrong user/key, pub key not on server, sshd config | Check `~/.ssh/authorized_keys` on server; verify key matches |
|
||||||
|
| `Connection timed out` | Firewall/security group, wrong port, server down | Check port, security group, ping |
|
||||||
|
| `No route to host` | Network path missing (VPN, ACL, subnet) | Check VPN, routing table |
|
||||||
|
| `Host key changed` warning | Server rebuilt, or MITM | Verify with out-of-band channel; `ssh-keygen -R HOST` if expected |
|
||||||
|
| `Too many authentication failures` | SSH agent offering too many keys | Use `-o IdentitiesOnly=yes -i ~/.ssh/id_ed25519` |
|
||||||
|
| `sudo: a terminal is required` | No TTY allocated | Use `ssh -tt` or pass `-t` to wrapper |
|
||||||
|
|
||||||
|
### Debug connection verbosely
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -vvv my-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check sshd config on server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh my-server "sudo sshd -T | grep -E 'pubkeyauthentication|passwordauthentication|permitrootlogin'"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check authorized_keys permissions (common cause of publickey failure)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh my-server "ls -la ~/.ssh/ && cat ~/.ssh/authorized_keys"
|
||||||
|
# .ssh should be 700, authorized_keys should be 600
|
||||||
|
```
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ if [[ ! -f "$key" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$key_pub" ]]; then
|
if ! $dry_run && [[ ! -f "$key_pub" ]]; then
|
||||||
echo "Public key not found: $key_pub" >&2
|
echo "Public key not found: $key_pub" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'USAGE'
|
cat <<'USAGE'
|
||||||
Copy files via scp with consistent options.
|
Copy files via scp or rsync with consistent options.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
ssh_copy.sh [options] push HOST LOCAL_PATH REMOTE_PATH
|
ssh_copy.sh [options] push HOST LOCAL_PATH REMOTE_PATH
|
||||||
@@ -13,9 +13,11 @@ Options:
|
|||||||
-u, --user USER Override SSH user (or set REMOTE_USER)
|
-u, --user USER Override SSH user (or set REMOTE_USER)
|
||||||
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
|
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
|
||||||
-i, --key PATH Identity file (default: REMOTE_KEY)
|
-i, --key PATH Identity file (default: REMOTE_KEY)
|
||||||
-r, --recursive Copy directories recursively
|
-r, --recursive Copy directories recursively (scp mode only; rsync always recurses)
|
||||||
|
--rsync Use rsync instead of scp (incremental, shows progress)
|
||||||
|
--delete With --rsync push: delete remote files not present in source
|
||||||
--accept-new Set StrictHostKeyChecking=accept-new
|
--accept-new Set StrictHostKeyChecking=accept-new
|
||||||
--dry-run Print the scp command that would run
|
--dry-run Print the command that would run
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
|
|
||||||
Environment defaults:
|
Environment defaults:
|
||||||
@@ -23,6 +25,8 @@ Environment defaults:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
ssh_copy.sh push my-server ./app.tar.gz /tmp/app.tar.gz
|
ssh_copy.sh push my-server ./app.tar.gz /tmp/app.tar.gz
|
||||||
|
ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
|
||||||
|
ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
|
||||||
ssh_copy.sh --user ubuntu pull 10.0.0.1 /var/log/syslog ./syslog
|
ssh_copy.sh --user ubuntu pull 10.0.0.1 /var/log/syslog ./syslog
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
@@ -32,47 +36,28 @@ user="${REMOTE_USER:-}"
|
|||||||
key="${REMOTE_KEY:-}"
|
key="${REMOTE_KEY:-}"
|
||||||
|
|
||||||
recursive=false
|
recursive=false
|
||||||
|
use_rsync=false
|
||||||
|
delete_mode=false
|
||||||
accept_new=false
|
accept_new=false
|
||||||
dry_run=false
|
dry_run=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-u|--user)
|
-u|--user) user="${2:-}"; shift 2 ;;
|
||||||
user="${2:-}"
|
-p|--port) port="${2:-}"; shift 2 ;;
|
||||||
shift 2
|
-i|--key) key="${2:-}"; shift 2 ;;
|
||||||
;;
|
-r|--recursive) recursive=true; shift ;;
|
||||||
-p|--port)
|
--rsync) use_rsync=true; shift ;;
|
||||||
port="${2:-}"
|
--delete) delete_mode=true; shift ;;
|
||||||
shift 2
|
--accept-new) accept_new=true; shift ;;
|
||||||
;;
|
--dry-run) dry_run=true; shift ;;
|
||||||
-i|--key)
|
-h|--help) usage; exit 0 ;;
|
||||||
key="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-r|--recursive)
|
|
||||||
recursive=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--accept-new)
|
|
||||||
accept_new=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--dry-run)
|
|
||||||
dry_run=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
-*)
|
-*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
*)
|
*) break ;;
|
||||||
break
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -81,11 +66,8 @@ if [[ $# -lt 4 ]]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
direction="$1"
|
direction="$1"; shift
|
||||||
shift
|
host="$1"; shift
|
||||||
|
|
||||||
host="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
dest_host="$host"
|
dest_host="$host"
|
||||||
if [[ -n "$user" ]]; then
|
if [[ -n "$user" ]]; then
|
||||||
@@ -93,34 +75,70 @@ if [[ -n "$user" ]]; then
|
|||||||
dest_host="${user}@${host_no_user}"
|
dest_host="${user}@${host_no_user}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
scp_opts=(-P "$port" -p)
|
if $use_rsync; then
|
||||||
if [[ -n "$key" ]]; then
|
if ! command -v rsync >/dev/null 2>&1; then
|
||||||
scp_opts+=(-i "$key" -o "IdentitiesOnly=yes")
|
echo "rsync is required but not installed." >&2
|
||||||
fi
|
exit 1
|
||||||
if $recursive; then
|
fi
|
||||||
scp_opts+=(-r)
|
ssh_parts=("ssh" "-p" "$port")
|
||||||
fi
|
if [[ -n "$key" ]]; then
|
||||||
if $accept_new; then
|
ssh_parts+=("-i" "$key" "-o" "IdentitiesOnly=yes")
|
||||||
scp_opts+=(-o "StrictHostKeyChecking=accept-new")
|
fi
|
||||||
fi
|
if $accept_new; then
|
||||||
|
ssh_parts+=("-o" "StrictHostKeyChecking=accept-new")
|
||||||
|
fi
|
||||||
|
rsync_opts=(-avz --progress -e "${ssh_parts[*]}")
|
||||||
|
|
||||||
case "$direction" in
|
case "$direction" in
|
||||||
push)
|
push)
|
||||||
local_path="$1"
|
local_path="$1"
|
||||||
remote_path="$2"
|
remote_path="$2"
|
||||||
full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
|
if $delete_mode; then
|
||||||
;;
|
rsync_opts+=(--delete)
|
||||||
pull)
|
fi
|
||||||
remote_path="$1"
|
full_cmd=(rsync "${rsync_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
|
||||||
local_path="$2"
|
;;
|
||||||
full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
|
pull)
|
||||||
;;
|
remote_path="$1"
|
||||||
*)
|
local_path="$2"
|
||||||
echo "Unknown direction: $direction (expected: push|pull)" >&2
|
full_cmd=(rsync "${rsync_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
|
||||||
usage >&2
|
;;
|
||||||
exit 2
|
*)
|
||||||
;;
|
echo "Unknown direction: $direction (expected: push|pull)" >&2
|
||||||
esac
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
scp_opts=(-P "$port" -p)
|
||||||
|
if [[ -n "$key" ]]; then
|
||||||
|
scp_opts+=(-i "$key" -o "IdentitiesOnly=yes")
|
||||||
|
fi
|
||||||
|
if $recursive; then
|
||||||
|
scp_opts+=(-r)
|
||||||
|
fi
|
||||||
|
if $accept_new; then
|
||||||
|
scp_opts+=(-o "StrictHostKeyChecking=accept-new")
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$direction" in
|
||||||
|
push)
|
||||||
|
local_path="$1"
|
||||||
|
remote_path="$2"
|
||||||
|
full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
|
||||||
|
;;
|
||||||
|
pull)
|
||||||
|
remote_path="$1"
|
||||||
|
local_path="$2"
|
||||||
|
full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown direction: $direction (expected: push|pull)" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
if $dry_run; then
|
if $dry_run; then
|
||||||
printf '%q ' "${full_cmd[@]}"
|
printf '%q ' "${full_cmd[@]}"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Options:
|
|||||||
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
|
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
|
||||||
-i, --key PATH Identity file (default: REMOTE_KEY)
|
-i, --key PATH Identity file (default: REMOTE_KEY)
|
||||||
-t, --tty Force pseudo-tty allocation (useful for sudo prompts)
|
-t, --tty Force pseudo-tty allocation (useful for sudo prompts)
|
||||||
|
-C, --compress Enable SSH compression (useful on slow links)
|
||||||
--accept-new Set StrictHostKeyChecking=accept-new
|
--accept-new Set StrictHostKeyChecking=accept-new
|
||||||
--sudo Prefix command with sudo --
|
--sudo Prefix command with sudo --
|
||||||
--sudo-non-interactive Prefix command with sudo -n -- (fails if password needed)
|
--sudo-non-interactive Prefix command with sudo -n -- (fails if password needed)
|
||||||
@@ -36,6 +37,7 @@ key="${REMOTE_KEY:-}"
|
|||||||
connect_timeout="${REMOTE_CONNECT_TIMEOUT:-10}"
|
connect_timeout="${REMOTE_CONNECT_TIMEOUT:-10}"
|
||||||
|
|
||||||
tty=false
|
tty=false
|
||||||
|
compress=false
|
||||||
accept_new=false
|
accept_new=false
|
||||||
sudo_mode=""
|
sudo_mode=""
|
||||||
dry_run=false
|
dry_run=false
|
||||||
@@ -58,6 +60,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
tty=true
|
tty=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
-C|--compress)
|
||||||
|
compress=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--accept-new)
|
--accept-new)
|
||||||
accept_new=true
|
accept_new=true
|
||||||
shift
|
shift
|
||||||
@@ -126,14 +132,15 @@ if $accept_new; then
|
|||||||
ssh_opts+=(-o "StrictHostKeyChecking=accept-new")
|
ssh_opts+=(-o "StrictHostKeyChecking=accept-new")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if $compress; then
|
||||||
|
ssh_opts+=(-C)
|
||||||
|
fi
|
||||||
|
|
||||||
if $tty; then
|
if $tty; then
|
||||||
ssh_opts+=(-tt)
|
ssh_opts+=(-tt)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd=("$@")
|
cmd=("$@")
|
||||||
if [[ ${#cmd[@]} -gt 0 && "${cmd[0]}" == "--" ]]; then
|
|
||||||
cmd=("${cmd[@]:1}")
|
|
||||||
fi
|
|
||||||
if [[ -n "$sudo_mode" && ${#cmd[@]} -gt 0 ]]; then
|
if [[ -n "$sudo_mode" && ${#cmd[@]} -gt 0 ]]; then
|
||||||
if [[ "$sudo_mode" == "sudo-n" ]]; then
|
if [[ "$sudo_mode" == "sudo-n" ]]; then
|
||||||
cmd=("sudo" "-n" "--" "${cmd[@]}")
|
cmd=("sudo" "-n" "--" "${cmd[@]}")
|
||||||
|
|||||||
Reference in New Issue
Block a user