Skip to content

docs: update OpenWiki - #94

Closed
github-actions[bot] wants to merge 13 commits into
mainfrom
openwiki/update
Closed

docs: update OpenWiki#94
github-actions[bot] wants to merge 13 commits into
mainfrom
openwiki/update

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Automated OpenWiki documentation update.

This PR was generated by the scheduled OpenWiki workflow.

@github-actions
github-actions Bot force-pushed the openwiki/update branch 5 times, most recently from 599034f to 19fc009 Compare August 11, 2026 09:10
petruha and others added 13 commits August 12, 2026 00:49
Closes #85 (Phase 1, single host). Scripts had to rebuild the ssh command
line by hand — `sshub host resolve` plus manual -p/-i/-J/-o assembly —
which threw away the stored identity, the askpass wiring and ProxyJump
the launcher already knows. `sshub exec web-01 -- systemctl is-active
nginx` uses all of it and hands back the remote exit code.

stdio passes through (stdin inherited, so pipes work), `-T` by default
and `-tt` behind --tty, --timeout kills and exits 124 like timeout(1),
--format json buffers the run into one record. A command given on the
exec line overrides the host's stored remote_command. Mosh is refused:
it has no one-shot command mode, and silently opening an interactive
session is a hang, not an error, for a script.

The run is audited as `via exec` — filterable with
`sshub audit list --via exec`, and excluded from `--via connect`, which
now means interactive connects only. The command string itself is never
logged: it can carry a password in an argument.

Tested: unit tests for the argv shape (command after the target, --tty
maps to -tt, stored remote_command overridden), an oracle test that runs
the argv we build through the real `ssh -G` and checks it still resolves
to the intended host/user/port with the right requesttty (seen red first:
moving the command before the target makes OpenSSH reject it with
"hostname contains invalid characters"), and smoke tests for the parse
failures, the unknown host and the mosh refusal.
Adversarial and security review of the first commit found three ways a
scripted `sshub exec` could hang or run the wrong thing. All reproduced
first, then fixed:

- `--timeout` never fired with `--format json` over a ProxyJump. Killing
  only ssh leaves the `ssh -W` helper holding the output pipes, so the
  drain threads never saw EOF. The run now gets its own process group
  and the timeout kills the group — which also stops the helper
  outliving the deadline as an orphan. The group is created only when a
  timeout was asked for: it takes the child out of the terminal's
  foreground group, and without a deadline Ctrl-C must still reach ssh.

- exec could sit on an ssh prompt forever, which issue #85 forbids and
  the help text denied. ssh reads prompts from /dev/tty, so redirecting
  stdin does not help. Without a staged secret the run is now
  `BatchMode=yes`; with one it cannot be, because BatchMode also
  switches off the askpass helper.

- A host whose `~/.ssh/config` sets `RemoteCommand` made ssh refuse the
  run: "Cannot execute command-line and remote command", exit 255. The
  argv-tail truncation only covered launcher-managed hosts, so
  ssh_config hosts had no override at all. `-o RemoteCommand=none` now
  covers both.

Smaller, same review: a remote command's own flags no longer reach the
global profile parser (`sshub exec web -- aws --profile prod s3 ls`
failed with "unknown profile 'prod'"); a `-flag` without `--` is a usage
error instead of being silently dropped; `--timeout` with no value is a
usage error rather than a silently disabled timeout, and 0 is refused;
a spawn failure emits a JSON record instead of empty stdout.

Tests: a second oracle case asks real ssh about the RemoteCommand
conflict — with a config that sets it, our argv is accepted and the same
argv without `RemoteCommand=none` is refused, so the option cannot be
dropped unnoticed. The override unit test now builds its input through
the real `build_ssh_argv` instead of a hand-written copy of its output.
Smoke tests cover the three new usage errors. openwiki/workflows/cli.md
documents the command and the new exit code 124.
Closes #101.

A host stored with an address like `-oProxyCommand=id` was pushed onto
the argv verbatim, so ssh read it as a flag instead of a hostname and ran
`id` locally on the next connect. Verified against the real ssh:

    $ sshub host resolve evil --format json
    argv: ['ssh', '-o', 'ForwardAgent=no', '-oProxyCommand=id']
    $ ssh -F /dev/null -G -T "-oProxyCommand=id" -- uptime
    hostname uptime
    proxycommand id

This is not the user's own typo: `address` is written verbatim from
imported PuTTY, Termius and mRemoteNG files, so a crafted export was
local code execution on whoever imported it and connected. Reachable
through connect, exec and tunnels alike, which is why the guard goes
where they all pass rather than into any one of them: every target now
goes through `safe_ssh_target`, which rewrites a leading-dash target into
the `ssh://` URI form. OpenSSH refuses that outright instead of parsing
it as a flag, so a hostile row fails loudly; ordinary targets are
untouched. `ssh -- <host>` is not an alternative — the words after `--`
become the remote command.

The oracle test asks OpenSSH both halves: that it still reads the old
form as `proxycommand id` (so the guard cannot quietly become obsolete or
stop firing), and that it refuses the guarded one.

Not fixed here, noted in #101: rejecting such a value at write time so a
hostile row never lands at all, with an error naming the field. This
commit is the part that also protects rows already in a database.
fix(ssh): a leading-dash host target is no longer an ssh option
feat(cli): sshub exec runs one command on a saved host
Closes #101 — the write-side half. #102 stopped such a value from being
executed at connect time; this stops it from being stored at all, which
is where the user (or an import of a file someone else wrote) finds out.

`create_host` and `update_host` reject a `name`, `address` or `username`
starting with `-`: those three make up the connection target, and ssh
reads a leading dash as a flag. Nothing legitimate looks like this.

The three importers skip a refused row and count it instead of aborting
on `?`: one poisoned entry in a mRemoteNG/PuTTY/Termius export must not
cost the rest of the file, and the summary line says how many were
dropped. The TUI form checks the same rule before writing, because a
store error travelling out of `save_host_form` would leave `handle_key`
and end the run loop — the e2e test here fails exactly that way when the
check is removed.

ssh_config-sourced rows are deliberately left alone: that address comes
from the user's own ~/.ssh/config, refusing to sync it would silently
drop a host they can see in their config, and the connect-time guard
from #102 already covers it.
fix(store): refuse a host field ssh would read as an option
The examples block gained `sshub exec` with the feature, but the
paragraph above it still listed the CLI as "hosts, groups, identities,
tunnels, SFTP, and the audit log" and the exit codes as 0/1/2 — so the
one command a script reaches for first was invisible to anyone reading
the section rather than scanning the code block, and 124 was
undocumented.
docs(readme): name exec in the headless CLI intro
The Features list carried **Audit** and **Settings overlay** twice each,
in both cases as a short version and a longer one that said everything
the short one did plus more (the session-log path on the audit row,
the session-logging toggle on the overlay row). Kept the fuller of each.

It also had no bullet for the headless CLI at all — the half of SSHub a
script uses, `sshub exec` included — while listing every TUI feature.
docs(readme): drop two duplicated feature bullets, add the CLI
@Petyok

Petyok commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closing: this one can never merge. It was opened by `app/github-actions`, and a PR created with `GITHUB_TOKEN` does not trigger workflows — the commit has zero checks, while `main` requires Lint + both Test jobs, so `mergeStateStatus` stays BLOCKED forever. It also targets `main`, which only ever receives release merges.

The cause is fixed: `openwiki-update.yml` sets `base: development`, and that reached `main` with v0.14.2, so scheduled runs now open against `development` (as #106 did). Nothing here is lost — the next run regenerates it.

Written by Claude Opus 5 (Claude Code) on behalf of the maintainer.

@Petyok Petyok closed this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant