Skip to content

Commit c4c2321

Browse files
committed
release: v0.3.0 — add shimkit ssh; ship three host-tools as 0.3.0
Final of three new tools for v0.3.0 (ports → hosts → ssh per docs/plans/v0.3.0-new-tools.md). All three together: - `shimkit ports` (commit 6cf38f4 + b43450c) - `shimkit hosts` (commit 2b4c7bf) - `shimkit ssh` (this commit) Each follows the same five architecture rules and the same MODERATE prompt + SEVERE token pattern from the v0.2.x tools. None ships an optional dependency extra; everything uses CommandRunner + stdlib. ssh-specific: - Surface: keys list/generate/rotate, agent status/add, known-hosts audit/prune, perms audit/fix, config show [HOST]. 11 leaf subcommands under 5 sub-groups. - Passphrase isolation: keys generate calls `ssh-keygen` with capture_output=False so the passphrase prompt lands on the user's TTY. shimkit never sees, captures, or logs the passphrase. - Permission audit is asymmetric: a path with a STRICTER mode than expected (e.g. 0400 private key vs 0600 expected) is fine. Only laxer modes are flagged. - known_hosts duplicate-prune preserves comments + blank lines verbatim; first occurrence wins. - Pure scanner module (`scanner.py`) is unit-testable without shelling out. - Config: SshConfig + SshPermsConfig added to typed schema. - `--ssh-dir PATH` flag on every subcommand for tests + chroot use. Test count 252 → 295 (23 new). Gates: ruff clean, mypy strict clean, bandit medium=0. Cut as v0.3.0 (not 0.2.4) because the surface change is large — three new public top-level subcommands.
1 parent 2b4c7bf commit c4c2321

14 files changed

Lines changed: 1740 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,27 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
66

77
## [Unreleased]
88

9+
## [0.3.0] — 2026-05-15
10+
11+
Three new host-machine dev-workflow tools, each shipped under the
12+
existing five architecture rules (CommandRunner chokepoint, UI
13+
chokepoint, config-driven values, builder pattern, fluent self).
14+
Test count 252 → 295. No new optional dependency extras — all
15+
three use only baseline binaries that ship with the OS.
16+
917
### Added
1018

19+
- `shimkit ssh` — SSH key + agent + known_hosts + perms hygiene.
20+
`keys list/generate/rotate`, `agent status/add`,
21+
`known-hosts audit/prune`, `perms audit/fix`, `config show
22+
[HOST]`. No third-party deps; passphrases stay in `ssh-keygen`'s
23+
TTY prompt (never logged or captured). Permission matrix is
24+
config-driven (`tools.ssh.perms`); audit flags only laxer-than-
25+
expected modes — stricter modes pass. 23 tests covering scanner
26+
units (list_keys / parse_agent_keys /
27+
find_known_host_duplicates / audit_perms), every CLI subcommand,
28+
dry-run no-op assertions, and the moderate-prompt refusal under
29+
`--no-input`. [`docs/tools/ssh.md`](docs/tools/ssh.md).
1130
- `shimkit ports` — cross-platform TCP/UDP port owner inspector +
1231
killer. `shimkit ports show [PORT]` lists every listening socket
1332
via `lsof` on macOS or `ss` on Linux; `shimkit ports kill PORT`

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ behaviour: [`docs/installation.md`](docs/installation.md).
4747
- **[`shimkit hosts`](docs/tools/hosts.md)**`/etc/hosts` editor
4848
with atomic-write + timestamped backups. add / remove / block /
4949
unblock / apply-list (severe) / rollback. macOS + Linux.
50+
- **[`shimkit ssh`](docs/tools/ssh.md)** — SSH key + agent +
51+
known_hosts + perms hygiene. keys list/generate/rotate, agent
52+
status/add, known-hosts audit/prune, perms audit/fix, config
53+
show. No third-party deps; passphrases handled by ssh-keygen.
5054

5155
Plus three utilities:
5256

docs/tools/ssh.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# shimkit ssh
2+
3+
SSH key + agent + known_hosts + perms hygiene. No third-party deps —
4+
every operation shells out to baseline `ssh-keygen`, `ssh-add`,
5+
`ssh-agent`, or `ssh`. Passphrases are never handled by shimkit:
6+
`ssh-keygen` prompts the user directly.
7+
8+
Pure scanner logic lives in `src/shimkit/tools/ssh/scanner.py`
9+
(filesystem read + known_hosts parser + perms audit). The manager
10+
owns CommandRunner shell-outs.
11+
12+
## Commands
13+
14+
| Command | Purpose |
15+
|--------------------------------------------------|--------------------------------------------------------|
16+
| `shimkit ssh` | Interactive menu (read-only paths only). |
17+
| `shimkit ssh keys list` | List every recognised private key. |
18+
| `shimkit ssh keys generate NAME` | Generate a new key (MODERATE). |
19+
| `shimkit ssh keys rotate NAME` | Back up the old key + regenerate (MODERATE). |
20+
| `shimkit ssh agent status` | Show ssh-agent state + loaded keys. |
21+
| `shimkit ssh agent add KEY` | Pass-through to `ssh-add`. |
22+
| `shimkit ssh known-hosts audit` | Find duplicate entries. |
23+
| `shimkit ssh known-hosts prune` | Remove later duplicates (MODERATE). |
24+
| `shimkit ssh perms audit` | Check ~/.ssh modes against the config matrix. |
25+
| `shimkit ssh perms fix` | chmod every offender (MODERATE). |
26+
| `shimkit ssh config show [HOST]` | Print ~/.ssh/config; with HOST, expand via `ssh -G`. |
27+
28+
Universal flags (`--quiet`, `--verbose`, `--log-file`, `--no-color`,
29+
`--color`, `--no-input`) go before any subcommand. Per-command flags
30+
(`--dry-run`, `--yes`, `--force`, `--json`, `--ssh-dir`,
31+
`--type`, `--comment`) go after.
32+
33+
## Keys
34+
35+
```bash
36+
shimkit ssh keys list # list every recognised key
37+
shimkit ssh keys list --json # machine-readable
38+
39+
shimkit ssh keys generate id_work --type ed25519 --yes
40+
shimkit ssh keys generate id_work -C work@example # custom comment
41+
shimkit ssh keys generate id_work --dry-run # show command without running
42+
43+
shimkit ssh keys rotate id_ed25519 --yes # backup + regenerate
44+
```
45+
46+
`rotate` moves the old key to `<name>.bak-YYYYMMDDHHMMSS` and the old
47+
`.pub` alongside, then runs `ssh-keygen` for a fresh pair. You're
48+
responsible for syncing the new public key to your authorized_keys
49+
on each server — shimkit prints the steps but pushes nowhere.
50+
51+
## ssh-agent
52+
53+
```bash
54+
shimkit ssh agent status # what's loaded
55+
shimkit ssh agent status --json # parses cleanly
56+
shimkit ssh agent add ~/.ssh/id_ed25519
57+
```
58+
59+
`status` differentiates "agent not running" (exit 1, warning) from
60+
"agent running, no keys" (exit 0, info).
61+
62+
## known_hosts hygiene
63+
64+
```bash
65+
shimkit ssh known-hosts audit --json
66+
shimkit ssh known-hosts prune --yes
67+
```
68+
69+
`audit` reports any `(host, key_blob)` pair seen more than once.
70+
`prune` keeps the first occurrence and drops the rest; comments and
71+
blank lines are preserved verbatim.
72+
73+
## Permission matrix
74+
75+
`audit` flags any path whose mode is **laxer** than the configured
76+
expected mode. Stricter modes pass — a `0400` key is fine even though
77+
expected is `600`.
78+
79+
Default matrix (`tools.ssh.perms`):
80+
81+
| Path | Expected |
82+
|----------------------------|----------|
83+
| `~/.ssh` (the dir) | `700` |
84+
| Private keys | `600` |
85+
| `.pub` files | `644` |
86+
| `~/.ssh/config` | `644` |
87+
| `~/.ssh/known_hosts` | `644` |
88+
| `~/.ssh/authorized_keys` | `644` |
89+
90+
```bash
91+
shimkit ssh perms audit --json # report only
92+
shimkit ssh perms fix --yes # chmod each offender
93+
shimkit ssh perms fix --dry-run # what would change
94+
```
95+
96+
## ~/.ssh/config
97+
98+
```bash
99+
shimkit ssh config show # print ~/.ssh/config verbatim
100+
shimkit ssh config show gh # `ssh -G gh` — effective expansion
101+
```
102+
103+
The effective-expansion form is useful when `Include`s + multiple
104+
`Host` blocks make it non-obvious what options actually apply.
105+
106+
## Configuration
107+
108+
```json
109+
{
110+
"tools": {
111+
"ssh": {
112+
"ssh_dir": "~/.ssh",
113+
"default_key_type": "ed25519",
114+
"perms": {
115+
"dir": "700",
116+
"private_key": "600",
117+
"public_key": "644",
118+
"config": "644",
119+
"known_hosts": "644",
120+
"authorized_keys": "644"
121+
}
122+
}
123+
}
124+
}
125+
```
126+
127+
`--ssh-dir PATH` overrides `ssh_dir` for one invocation — used by
128+
tests, also useful for editing a chroot's keys from outside.
129+
130+
## Exit codes
131+
132+
| Code | Meaning |
133+
|-----:|-------------------------------------------------------------|
134+
| 0 | success / no-op |
135+
| 1 | generic failure (overwrite refused, ssh-keygen non-zero, prompt cancelled, agent unreachable) |
136+
| 2 | Typer usage error |
137+
| 69 | EX_UNAVAILABLE — wrong platform |
138+
| 130 | SIGINT |
139+
140+
## Platform support
141+
142+
| Platform | Status |
143+
|----------|--------|
144+
| macOS | ✓ — OpenSSH 9.x ships with macOS; `ssh-keygen` / `ssh-add` baseline. |
145+
| Linux | ✓ — `openssh-client` package, also baseline. |
146+
| WSL | ✓ (Linux path). |
147+
| Windows | ✗ — out of charter. |
148+
149+
## Security notes
150+
151+
- **Passphrases never pass through shimkit.** `ssh-keygen` reads them
152+
interactively from the TTY; our `CommandRunner.run` call uses
153+
`capture_output=False` for that step. Nothing involving the
154+
passphrase is logged, captured, or echoed.
155+
- **No automatic key push.** `keys rotate` does *not* upload the new
156+
public key anywhere. You sync `authorized_keys` on each server
157+
yourself.
158+
- **`config show` is read-only.** `~/.ssh/config` is your domain;
159+
shimkit reads it but doesn't write to it.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "shimkit"
7-
version = "0.2.3"
7+
version = "0.3.0"
88
description = "A toolkit of developer utilities — Java version manager, shell upgrader, and more. Python tools, shimmed by bash."
99
readme = "README.md"
1010
license = { file = "LICENSE" }

src/shimkit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
Python tools, shimmed by bash.
44
"""
55

6-
__version__ = "0.2.3"
6+
__version__ = "0.3.0"
77
__all__ = ["__version__"]

src/shimkit/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from shimkit.tools.java.commands import java_app
2222
from shimkit.tools.ports.commands import ports_app
2323
from shimkit.tools.shell.commands import shell_app
24+
from shimkit.tools.ssh.commands import ssh_app
2425

2526
app = typer.Typer(
2627
name="shimkit",
@@ -38,6 +39,7 @@
3839
app.add_typer(docker_clean_app)
3940
app.add_typer(ports_app)
4041
app.add_typer(hosts_app)
42+
app.add_typer(ssh_app)
4143

4244

4345
# --- config -----------------------------------------------------------------

src/shimkit/config/defaults.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@
120120
"apply_list_severe_token": "APPLY-LIST",
121121
"max_entries_per_apply": 5000,
122122
"managed_block_marker": "# === shimkit-managed ==="
123+
},
124+
"ssh": {
125+
"ssh_dir": "~/.ssh",
126+
"default_key_type": "ed25519",
127+
"perms": {
128+
"dir": "700",
129+
"private_key": "600",
130+
"public_key": "644",
131+
"config": "644",
132+
"known_hosts": "644",
133+
"authorized_keys": "644"
134+
}
123135
}
124136
},
125137
"package_managers": {

src/shimkit/config/schema.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,25 @@ class PortsConfig(_StrictModel):
163163
system_pid_threshold: int = Field(default=100, ge=1, le=65535)
164164

165165

166+
class SshPermsConfig(_StrictModel):
167+
"""File-mode matrix for `shimkit ssh perms`."""
168+
169+
dir: str = "700"
170+
private_key: str = "600"
171+
public_key: str = "644"
172+
config: str = "644"
173+
known_hosts: str = "644"
174+
authorized_keys: str = "644"
175+
176+
177+
class SshConfig(_StrictModel):
178+
"""SSH key + agent + perms hygiene — `shimkit ssh`."""
179+
180+
ssh_dir: str = "~/.ssh"
181+
default_key_type: str = "ed25519"
182+
perms: SshPermsConfig = Field(default_factory=SshPermsConfig)
183+
184+
166185
class HostsConfig(_StrictModel):
167186
"""/etc/hosts editor — `shimkit hosts`."""
168187

@@ -185,6 +204,7 @@ class ToolsConfig(_StrictModel):
185204
docker_clean: DockerCleanConfig = Field(default_factory=DockerCleanConfig)
186205
ports: PortsConfig = Field(default_factory=PortsConfig)
187206
hosts: HostsConfig = Field(default_factory=HostsConfig)
207+
ssh: SshConfig = Field(default_factory=SshConfig)
188208

189209

190210
class PackageManagerEntry(_StrictModel):

src/shimkit/tools/ssh/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""SSH key + agent + perms hygiene — ``shimkit ssh``.
2+
3+
Generate / rotate keys, manage ``ssh-agent``, audit + prune
4+
``known_hosts``, audit + fix ``~/.ssh`` permissions, and read your
5+
``~/.ssh/config``. No third-party deps — every operation shells out
6+
to baseline ``ssh-keygen`` / ``ssh-add`` / ``ssh-agent`` /
7+
``ssh-keyscan``.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from .manager import SshManager
13+
from .models import AgentKey, KeyEntry, PermIssue
14+
15+
__all__ = ["AgentKey", "KeyEntry", "PermIssue", "SshManager"]

0 commit comments

Comments
 (0)