Universal SSH remote deployment tool optimized for domestic network environment
A Python paramiko-based SSH deployment tool for managing Linux servers with support for:
- Multi-server management and batch operations
- Remote command execution and file transfer (SCP)
- Installation script templates for common software (Docker, MySQL, PostgreSQL, Nginx, Node.js, Redis, Python, Git)
- Automated domestic mirror configuration (Aliyun, Tsinghua, USTC)
- Direct integration with
~/.ssh/config - Group and tag-based server targeting
# 1. Navigate to the skill directory
cd /root/.openclaw/workspace/skills/ssh-deploy-skill
# 2. Install dependency
pip3 install paramiko
# 3. Add a server (three methods)
## Method A: Using inventory.py
python3 scripts/inventory.py add web-01 \
--host 192.168.1.101 \
--user root \
--ssh-key ~/.ssh/id_rsa \
--groups production,web
## Method B: Edit ~/.ssh-deploy/inventory.json manually
## Method C: Use existing ~/.ssh/config entries (no extra config needed!)
# 4. Execute commands
python3 scripts/deploy.py exec web-01 "uptime"
# 5. Batch operations by group/tag
python3 scripts/deploy.py exec group:production "docker ps"
python3 scripts/deploy.py exec tag:aliyun "systemctl status nginx"
# 6. File transfer
python3 scripts/deploy.py upload web-01 ./nginx.conf /etc/nginx/nginx.conf
python3 scripts/deploy.py download web-01 /var/log/app.log ./logs/
# 7. Use installation templates
cat templates/install_docker.sh | python3 scripts/deploy.py exec web-01 "bash -s"Enable strict SSH host key checking for production environments:
python3 scripts/deploy.py --strict exec web-01 "uptime"When --strict is used:
- Loads
~/.ssh/known_hosts - Rejects unknown or changed host keys
- First connection to a new server will fail; you must manually verify fingerprint first
- Prevents man-in-the-middle attacks
Default: --strict is OFF for convenience. Turn ON for production.
Custom configuration directory (default: ~/.ssh-deploy):
python3 scripts/deploy.py --config-dir /path/to/custom/config exec web-01 "uptime"~/.ssh-deploy/inventory.json- Server inventory (primary)~/.ssh-deploy/config.json- Reserved for future global settings (currently unused)
Note: config.json is a placeholder. It may be used in future versions for default timeout, mirror settings, or output formatting. You can safely ignore it for now.
- Multiple input methods: CLI (
inventory.py), JSON file (~/.ssh-deploy/inventory.json), or direct~/.ssh/configreading - Grouping: Organize servers by environment/role (production, staging, web, db)
- Tagging: Flexible labels for cloud provider, region, etc.
- Target syntax:
server-name,group:name,tag:name,*(all servers)
- Command execution: Parallel by default,
--sequentialfor controlled batches - File transfer: Upload/download with progress tracking
- Template system: Pre-built shell scripts with domestic mirror optimization
- Automatic mirror configuration for apt/yum, npm, pip, Docker, Go, Maven
- Works out-of-the-box for China mainland
- Mirror fallback switching
ssh-deploy-skill/
├── SKILL.md # Skill definition
├── README.md # This file (English - primary)
├── README.zh-CN.md # Full Chinese documentation
├── scripts/
│ ├── inventory.py # Server inventory management
│ ├── deploy.py # Core deployment engine
│ └── templates.py # Template utilities
├── templates/ # Installation script templates
│ ├── base_setup.sh # Base environment + mirrors
│ ├── install_git.sh
│ ├── install_docker.sh
│ ├── install_mysql.sh
│ ├── install_postgresql.sh
│ ├── install_nginx.sh
│ ├── install_nodejs.sh
│ ├── install_redis.sh
│ └── install_python.sh
└── references/ # Detailed documentation references
├── best-practices.md
├── mirrors.md
└── troubleshooting.md
# List all servers
python3 scripts/inventory.py list
# Filter by group
python3 scripts/inventory.py list --group production
# Filter by tag
python3 scripts/inventory.py list --tag "阿里云"
# Add server
python3 scripts/inventory.py add <name> \
--host <ip> \
--port <22> \
--user <username> \
--ssh-key <path> \
--groups <group1,group2> \
--tags <tag1,tag2> \
--desc "description"# Single server
python3 scripts/deploy.py exec <target> "<command>"
# Batch (parallel by default)
python3 scripts/deploy.py exec group:web "docker ps"
# Sequential (large batches)
python3 scripts/deploy.py exec group:large "apt upgrade -y" --sequential
# With environment variables
export MYSQL_ROOT_PASSWORD="secret"
cat templates/install_mysql.sh | python3 scripts/deploy.py exec db-01 "bash -s"# Upload
python3 scripts/deploy.py upload <target> <local_path> <remote_path>
# Download
python3 scripts/deploy.py download <target> <remote_path> <local_path_or_dir>All templates in templates/ are ready-to-use with domestic mirrors:
| Template | Software | Env Vars |
|---|---|---|
base_setup.sh |
Base system + mirrors | - |
install_docker.sh |
Docker CE + mirrors | - |
install_mysql.sh |
MySQL 8.0 | MYSQL_ROOT_PASSWORD |
install_postgresql.sh |
PostgreSQL 15 | PG_VERSION |
install_nginx.sh |
Nginx | - |
install_nodejs.sh |
Node.js | NODE_VERSION (default: 20) |
install_redis.sh |
Redis | - |
install_python.sh |
Python | PYTHON_VERSION (default: 3.10) |
install_git.sh |
Git | GIT_USER_NAME, GIT_USER_EMAIL |
If you already use ~/.ssh/config, no additional configuration needed:
# ~/.ssh/config
Host myserver
HostName 1.2.3.4
User deploy
IdentityFile ~/.ssh/id_rsa_myserver
Port 22Then use directly:
python3 scripts/deploy.py exec myserver "uptime"Note: SSH config hosts are read-only (dynamic). To add groups/tags, import to inventory:
python3 scripts/inventory.py add myserver --from-ssh-configEdit ~/.ssh-deploy/inventory.json:
{
"servers": {
"web-prod-01": {
"host": "1.2.3.101",
"port": 22,
"user": "deploy",
"ssh_key": "~/.ssh/id_rsa_prod",
"groups": ["web", "production"],
"tags": ["aliyun", "east-china"]
}
}
}The skill automatically configures mirrors for:
- apt (Ubuntu/Debian):
mirrors.aliyun.com - yum (CentOS/RHEL):
mirrors.aliyun.com - npm:
registry.npmmirror.com(Taobao) - pip:
mirrors.aliyun.com/pypi/simple - Docker: USTC, NetEase, Baidu mirrors
- Go modules:
goproxy.cn - Maven: Aliyun repository
For manual configuration details, see references/mirrors.md.
- Use SSH keys, disable password authentication
- Create dedicated deployment users (not root)
- Set key permissions to
600 - Configure
PermitRootLogin noandPasswordAuthentication noin/etc/ssh/sshd_config
By default, the tool automatically accepts new host keys (convenient for initial setup). For production environments, enable strict mode:
python3 scripts/deploy.py --strict exec web-01 "uptime"Strict mode behavior:
- Loads
~/.ssh/known_hosts - Rejects unknown or changed host keys
- First connection to a new server will fail with
BadHostKeyException - You must manually confirm the fingerprint once:
ssh root@server
When to use strict mode:
- Production environments
- Multi-tenant or untrusted networks
- Compliance requirements (audit trails)
When to skip:
- Initial server provisioning (auto-accept, then switch to strict)
- Isolated lab environments
- Ephemeral test servers
The tool detects if any server in your inventory uses password authentication (stored in plaintext in inventory.json). A warning will be displayed:
⚠️ 检测到以下服务器使用密码认证(不推荐):web-old, db-test
建议:使用 SSH 密钥认证,避免明文存储密码。
参考:https://docs.openclaw.ai/security/ssh-keys
Recommendation: Always use SSH key authentication. Remove password fields from inventory.json and migrate to key-based auth.
- Test on one server before batch operations
- Backup configurations before overwriting
- Use
--sequentialfor 20+ servers - Always check exit codes and outputs
- Maintain deployment logs
# Example: GitLab CI
deploy_production:
script:
- pip3 install paramiko
- export TARGET="group:production"
- cat deploy.sh | python3 skills/ssh-deploy-skill/scripts/deploy.py exec "$TARGET" "bash -s"For comprehensive best practices, see references/best-practices.md.
| Symptom | Solution |
|---|---|
Connection refused |
Check server status, SSH service, firewall, security group |
Permission denied (publickey) |
Verify key in ~/.ssh/authorized_keys, key permissions 600 |
Command not found |
Use absolute path or source ~/.bashrc && <command> |
sudo: a password is required |
Configure NOPASSWD sudo or use root user |
| Slow downloads in China | Run base_setup.sh to configure domestic mirrors |
Host key verification failed |
Remove old key from ~/.ssh/known_hosts |
No space left on device |
df -h, clean Docker/apt cache |
# 1. Network test
ping <host>
telnet <host> 22
# 2. Manual SSH test
ssh -i ~/.ssh/id_rsa <user>@<host> "uptime"
# 3. Check server info
python3 scripts/deploy.py exec <target> "uname -a && cat /etc/os-release"
# 4. Enable debug logging
export LOG_LEVEL=DEBUG
python3 scripts/deploy.py exec <target> "command"For complete troubleshooting guide, see references/troubleshooting.md.
# Add to inventory
python3 scripts/inventory.py add web-01 \
--host 1.2.3.101 \
--groups web,production \
--tags "aliyun"
# Base setup (mirrors, tools)
cat templates/base_setup.sh | python3 scripts/deploy.py exec web-01 "bash -s"
# Install Docker
cat templates/install_docker.sh | python3 scripts/deploy.py exec web-01 "bash -s"
# Upload config
python3 scripts/deploy.py upload web-01 ./app-config.json /opt/app/config.json
# Start app
python3 scripts/deploy.py exec web-01 "docker-compose up -d"# Add multiple servers to same group
for i in 1 2 3; do
python3 scripts/inventory.py add web-$i \
--host 1.2.3.10$i \
--groups web \
--tags "production"
done
# Deploy to all group members in parallel
cat templates/install_docker.sh | python3 scripts/deploy.py exec group:web "bash -s"
# Upload config to all at once
python3 scripts/deploy.py upload group:web ./nginx.conf /etc/nginx/nginx.conf
# Restart services sequentially (avoid thundering herd)
python3 scripts/deploy.py exec group:web "systemctl restart nginx" --sequential# Phase 1: Canary group
cat deploy-v2.sh | python3 scripts/deploy.py exec tag:"canary" "bash -s"
# Monitor metrics...
# Phase 2: Full rollout
cat deploy-v2.sh | python3 scripts/deploy.py exec tag:production "bash -s"# Collect status from all servers
python3 scripts/deploy.py exec "*" "uptime" > uptime-$(date +%F).log
python3 scripts/deploy.py exec "*" "df -h" > disk-$(date +%F).log
python3 scripts/deploy.py exec "*" "docker ps --format 'table {{.Names}}\t{{.Status}}'" > containers-$(date +%F).logfrom inventory import Inventory
from deploy import SSHDeployer
# Load inventory
inv = Inventory()
server = inv.get_server("web-prod-01")
# Create deployer (with strict mode for production)
deployer = SSHDeployer(strict_host_key=True)
# Execute command
result = deployer.execute(server, "docker ps")
print(f"Success: {result.success}, Output: {result.output}")
# Upload file
res = deployer.upload_file(server, "./local.conf", "/etc/conf.d/local.conf")
# Batch execute
servers = inv.get_servers_by_group("production")
results = deployer.execute_batch(servers, "systemctl status nginx", sequential=False)
# Always close connections (or use context manager)
deployer.close()Recommended: Context Manager (auto cleanup)
with SSHDeployer(strict_host_key=True) as deployer:
result = deployer.execute(server, "uptime")
# Connections automatically closed on exit- Python 3.8+
paramikopackage (pip3 install paramiko)- System bins:
python3,ssh,scp - Target servers: Any Linux distribution (Ubuntu, Debian, CentOS, RHEL, Alpine, etc.)
MIT
For detailed Chinese documentation including comprehensive examples, see:
README.zh-CN.md- Full Chinese manualreferences/best-practices.md- 最佳实践references/mirrors.md- 国内镜像源配置详解references/troubleshooting.md- 故障排查完整手册