11 KiB
11 KiB
sshctl
SSH configuration management toolkit — parse, validate, generate, and manage SSH configs at scale.
Features
- Parse — Full SSH config parser supporting
Host,Include, glob patterns, and continuation lines - Generate — Programmatic SSH config generation with deterministic option ordering
- Validate — Validate SSH config structure, check required options and missing includes
- Check — Parallel DNS/TCP/SSH connectivity checking across all hosts
- Exec — Run commands concurrently across all hosts via SSH
- Export — Export host inventory to CSV
- Ansible — Generate Ansible inventory from your SSH config directory tree
- Permissions — Fix or verify SSH directory permissions (0700 dirs, 0600 keys, 0644 public keys)
- CLI — Full-featured
typerCLI with rich output (tables, trees, status)
Installation
pip (wheel)
pip install sshctl # once published to PyPI
# or install the built wheel directly from a release
pip install ./sshctl-0.1.2-py3-none-any.whl
dpkg (Debian/Ubuntu)
# Download from latest release
wget https://src.panahifar.ir/ahp/sshctl/releases/download/v0.1.2/sshctl_0.1.2-1_all.deb
sudo dpkg -i sshctl_0.1.2-1_all.deb
From source
git clone https://src.panahifar.ir/ahp/sshctl.git
cd sshctl
pip install -e .
Download the latest .whl or .deb from the releases page.
Quick Start
# Initialize SSH config directory structure
sshctl init
# List all hosts
sshctl host list
# Show a specific host
sshctl host show server1
# Validate config
sshctl validate
# Check host connectivity
sshctl check
# Run a command across all hosts in parallel
sshctl exec "uptime"
# Export inventory as CSV
sshctl csv --output hosts.csv
# Generate Ansible inventory
sshctl ansible
# Fix SSH directory permissions
sshctl chmod
# Only check permissions (no fixes)
sshctl chmod --check
# Show conf.d directory tree
sshctl tree
Directory Structure
~/.ssh/
├── config # Main SSH config (Include conf.d/)
├── conf.d/ # Modular host configurations
│ ├── internal/ # Internal infrastructure universe
│ │ ├── AZ/ # Sector (e.g., region)
│ │ │ └── cluster1/
│ │ │ └── 8xx-serverfarm/
│ │ │ └── 82-k8s/
│ │ │ └── hosts.conf
│ │ └── ...
│ └── external/ # External/dedicated universe
│ └── ...
├── control.d/ # SSH control master sockets
├── keys.d/ # Private keys (auto-protected by sshctl chmod)
├── keys.pub/ # Public keys
├── report/ # Generated reports
└── inventory.d/ # Generated Ansible inventory
└── hosts.yml
ZonePath Convention
The conf.d/ hierarchy follows a ZonePath addressing model:
conf.d/{universe}/{sector}/{cluster}/{zone}/{vlan}
| Level | Description | Example |
|---|---|---|
| universe | internal or external |
internal |
| sector | Geographic/segment | AZ |
| cluster | Data center / DC | cluster1 |
| zone | Server farm / role | 8xx-serverfarm |
| vlan | Network segment | 82-k8s |
CLI Reference
Global Options
| Flag | Description |
|---|---|
-V, --version |
Show version |
-v, --verbose |
Verbose logging |
-c, --config |
Path to SSH config file |
Commands
| Command | Description |
|---|---|
init |
Initialize SSH config directory structure |
host list |
List all host aliases |
host show |
Show details for a specific host |
host add |
Add a new host to the SSH config |
check |
Check connectivity to all hosts |
exec |
Run a command in parallel across hosts |
csv |
Export host inventory to CSV |
chmod |
Fix or check SSH directory permissions |
validate |
Validate SSH config structure |
diff |
Show differences between generated and current config |
backup |
Create timestamped backup of SSH config directory |
restore <path> |
Restore SSH config from a backup |
ansible |
Generate Ansible inventory from SSH config |
tree |
Show the conf.d directory tree |
install-completion |
Install shell completions (bash/zsh/fish) |
Python API
from pathlib import Path
from sshctl.parser import parse_file, resolve_includes
from sshctl.models import HostBlock, SshConfig
from sshctl.generator import generate_config_text, write_config_file
from sshctl.validator import validate_config
from sshctl.resolver import resolve_host, resolve_all_hosts
from sshctl.exporter import export_csv
from sshctl.permissions import fix_ssh_permissions, check_permissions
from sshctl.inventory import generate_inventory
# Parse an SSH config
config = parse_file(Path("~/.ssh/config").expanduser())
# Resolve includes recursively
full_config = resolve_includes(Path("~/.ssh/config"))
# Find hosts
block = config.find("server1")
blocks = config.find_all("web*")
# Validate
result = validate_config(Path("~/.ssh/config"))
if not result.passed:
for issue in result.issues:
print(f"[{issue.severity}] {issue.message}")
# Resolve a host (uses ssh -G)
resolved = resolve_host("server1", Path("~/.ssh/config"))
# Generate config text
text = generate_config_text(full_config)
# Generate Ansible inventory
inventory = generate_inventory(Path("~/.ssh"))
# Fix permissions
counts = fix_ssh_permissions(Path("~/.ssh"))
Configuration
sshctl can be configured via YAML or environment variables.
YAML (~/.ssh/sshctl.yaml)
ssh_dir: ~/.ssh
config_file: config
verbose: false
timeout: 10
Environment Variables
All settings can be overridden with SSHCTL_ prefix:
export SSHCTL_VERBOSE=true
export SSHCTL_TIMEOUT=30
Development
Setup
git clone https://src.panahifar.ir/ahp/sshctl.git
cd sshctl
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Run Tests
pytest
Lint & Type Check
ruff check src/ tests/
ruff format --check src/ tests/
mypy src/ tests/
Project Structure
sshctl/
├── src/
│ └── sshctl/
│ ├── __init__.py # Package init, re-exports from core
│ ├── __main__.py # Entry point for python -m sshctl
│ │
│ ├── cli/ # CLI layer (user-facing)
│ │ ├── __init__.py
│ │ ├── app.py # Typer CLI application
│ │ └── __main__.py # Module entry point
│ │
│ ├── core/ # Core domain (shared foundation)
│ │ ├── __init__.py # Exports __version__
│ │ ├── _version.py # Version string
│ │ ├── config.py # Pydantic settings (SshctlConfig)
│ │ ├── exceptions.py # Exception hierarchy
│ │ ├── logutil.py # Structured logging (structlog)
│ │ └── models.py # Data models (HostBlock, SshConfig, ZonePath)
│ │
│ ├── config/ # SSH config lifecycle
│ │ ├── __init__.py
│ │ ├── parser.py # SSH config parser + include resolution
│ │ ├── generator.py # SSH config text generation
│ │ ├── resolver.py # Host resolution via ssh -G
│ │ └── validator.py # Config validation
│ │
│ ├── operations/ # Active operations
│ │ ├── __init__.py
│ │ ├── checker.py # DNS/TCP/SSH connectivity checks
│ │ └── executor.py # Parallel SSH command execution
│ │
│ └── export/ # Export/integration
│ ├── __init__.py
│ ├── exporter.py # CSV export
│ ├── inventory.py # Ansible inventory generation
│ └── permissions.py # SSH directory permissions
│
├── tests/
│ ├── conftest.py # Pytest fixtures
│ ├── test_config.py
│ ├── test_exceptions.py
│ ├── test_exporter.py
│ ├── test_generator.py
│ ├── test_inventory.py
│ ├── test_models.py
│ ├── test_parser.py
│ ├── test_permissions.py
│ └── test_validator.py
│
├── pyproject.toml # Build config, ruff, mypy, pytest
├── .editorconfig
├── .gitignore
├── LICENSE
└── README.md
Architecture
sshctl follows a modular layered architecture organized into subpackages:
┌─────────────────────────────────────────────────────────────┐
│ cli/ (app.py) │
│ Typer CLI, Rich output, orchestration │
└──────────────────────┬──────────────────────────────────────┘
│
┌────────────┼────────────────┬────────────────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌───────────┐ ┌──────────────┐ ┌──────────────┐
│ config/ │ │operations │ │ export/ │ │ core/ │
│ │ │ │ │ │ │ │
│ parser │ │ checker │ │ exporter │ │ models │
│ generator│ │ executor │ │ inventory │ │ exceptions │
│ resolver │ │ │ │ permissions │ │ config │
│ validator│ │ │ │ │ │ logutil │
└──────────┘ └───────────┘ └──────────────┘ └──────────────┘
▲
│
(shared foundation)
Key design decisions:
- Models first —
HostBlock,SshConfig,ZonePathare plain dataclasses; the entire pipeline operates on them - Parser is lossy by design — it extracts enough structure for management, not a perfect SSH config round-trip
- Resolver delegates to
ssh -G— rather than reimplementing SSH's full inheritance logic, sshctl shells out for the canonical resolved view - Async everywhere — checker and executor use
asynciowith semaphore-controlled concurrency
License
MIT — see LICENSE