Skip to content

Quiet keyless SSH links, add aether link --key, share one auth resolver - #32

Merged
ilovecrayons merged 5 commits into
mainfrom
not-varram/fix-ssh-auth-ux
Sep 4, 2026
Merged

Quiet keyless SSH links, add aether link --key, share one auth resolver#32
ilovecrayons merged 5 commits into
mainfrom
not-varram/fix-ssh-auth-ux

Conversation

@not-varram

Copy link
Copy Markdown
Contributor

Summary

  • Quiet on keyless connections. A Tailscale link or aether gui no longer prints parse ssh key ... passphrase protected when the server accepts the connection without a key. cli.ResolveAuth records what it examined and only speaks when the handshake is refused.
  • Actionable auth failures. The error now lists the ssh-agent outcome, each default key file (id_ed25519, id_ecdsa, id_rsa) and what stopped it, the server's banner (no Aether member for this key was previously dropped by the client), and the fixes: ssh-add <key> or --key <private-key>.
  • aether link --key <private-key>. Deterministic: only that key is offered; an agent key counts only when it is the same key (matched by public key, so a passphrase-protected --key works after ssh-add). .pub files are refused with a clear message. The absolute path is saved with the default or named profile and kept when relinking that profile without --key.
  • Common-key discovery. Without --key: every agent key, then ~/.ssh/id_ed25519, id_ecdsa, id_rsa. A missing, malformed, or locked candidate no longer blocks a later usable one. Keys already offered by the agent are not offered twice.
  • Daemon unification. aether daemon run uses the same resolver instead of parsing one file; host-key policy stays strict there (no TOFU for an unattended process).
  • macOS test isolation. New internal/testhome.Isolate sets HOME, USERPROFILE, XDG_CONFIG_HOME, AppData, clears SSH_AUTH_SOCK, and fails the test if os.UserConfigDir/os.UserHomeDir resolve outside the scratch dir. Previously tests on macOS overwrote the real ~/Library/Application Support/aether/config.json.
  • Docs. networking, quickstart, install, testing: macOS config path, discovery order, --key takes the private key, updated troubleshooting rows.

Out of scope, unchanged: TOFU host-key behavior, keyless/tailnet semantics, OpenSSH config (Include, Match, ProxyJump).

Test plan

  • make fmt-check, make vet, make lint (with GOTOOLCHAIN=go1.25.13), make public-audit pass.
  • make test: all packages pass except internal/coord, internal/mcpbridge, internal/protocol, internal/scheduler, which fail identically on origin/main on this macOS host (temp-path symlink / socket-length issues, unrelated).
  • go test -race -tags integration for internal/cli, internal/syncd, internal/localops, cmd/aether, internal/testhome passes. Docker is not available here, so the Docker-backed integration suite was not run.
  • New regression coverage: reported case (locked id_ed25519, usable id_rsa, no agent, keyless server, no stderr beyond the TOFU notice); discovery reaching a later candidate; explicit key ignoring unrelated agent keys; locked explicit key unlocked by a matching agent key; failure diagnostics including the server banner; invite with no key; CheckKey public-key rejection; link key persistence in top-level and named profiles plus relink; daemon discovery and daemon actionable error; testhome isolation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HaEkHna6S1R7uyf4xBoWti

not-varram and others added 5 commits September 2, 2026 22:56
…e one resolver

A successful Tailscale link printed "parse ssh key ... passphrase
protected" because the client parsed ~/.ssh/id_ed25519 up front and
reported every problem even when the server never asked for a key. The
new cli.ResolveAuth records what it examined and speaks only when the
handshake is refused: the error then lists the ssh-agent outcome, each
default key file (id_ed25519, id_ecdsa, id_rsa) and what stopped it, the
server's banner, and the two fixes (ssh-add, --key).

aether link --key <private-key> selects one key deterministically: only
that key is offered, an agent copy counts only when it is the same key,
and a .pub file is refused with a clear message. The absolute path is
saved with the default or named link and kept on relink. The sync
daemon now uses the same resolver instead of parsing one file itself.

Test helpers set only XDG_CONFIG_HOME and AppData, so on macOS
os.UserConfigDir still pointed at the real ~/Library/Application
Support and tests overwrote the developer's config. internal/testhome
sets every home and config variable and asserts the lookup stays inside
the scratch directory.

Docs now give the macOS config path, describe key discovery and --key,
and call the key a private key throughout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaEkHna6S1R7uyf4xBoWti
… the saved key applies

TestLinkKeyPersistsAndRelinks built a relative --key with filepath.Rel
from the checkout to the temp dir, which fails on Windows CI where the
two sit on different drives. The test now runs from the key's directory
and passes a bare file name.

aether link --key auto forgets a saved key and returns the link, or the
named profile, to ssh-agent and default-file discovery; before, the only
way back was editing config.json. The auth error for a refused explicit
key now names that way out and lists only the chosen file.

Config.Named no longer overlays the top-level key onto a profile that
saved none. A profile linked by discovery stayed on discovery until the
default link chose a file, at which point gui --server <profile> offered
only that file and was refused by a server that never saw it; it also
made --key auto on a named profile a no-op.

Docs no longer say later commands inherit the saved key. It covers
Aether's own SSH connections and the daemon event channel when the same
file is passed to daemon run --key; git push, the fetch behind aether
pull, and the daemon's git traffic run the system git over OpenSSH,
which needs ssh-add or an IdentityFile entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mjx8gvQA4YnryGieZTfM87
…heritance

Commit 7b727a2 made Named ignore the top-level key for every profile
without one. Config files written before per-profile keys existed have
empty profile keys that relied on inheriting that field, so the change
would have silently switched them to discovery.

NamedLink gains AutoKey, written as auto_key only when true. Named
clears the effective key for a profile carrying the marker and keeps the
non-empty overlay for everything else, so legacy files resolve exactly as
before. aether link --name saves the marker when the profile used
discovery, including --key auto, and clears it when --key names a file.
savedKey resolves through Named, so relinking a legacy profile without
--key keeps the inherited key in effect and writes it into the profile,
while a discovering profile stays on discovery.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mjx8gvQA4YnryGieZTfM87
…annot reach the real config

The localops link test overwrote the developer's real config on macOS
when run from a checkout whose helper set only XDG_CONFIG_HOME and
AppData; os.UserConfigDir reads HOME there. testhome.Isolate fixed that
on this branch, but any helper that misses one platform's variable makes
the same mistake again.

cli.Path now honors AETHER_CONFIG_DIR first: one variable, the same on
every platform, with no test-only hook in production code. Isolate sets
it beside the home variables, and both useTempConfigDir helpers keep
asserting the resolved path landed in the scratch directory.
TestLinkRepoLeavesRealConfigAlone writes a sentinel where the platform
lookup says the real config lives (a scratch stand-in), points HOME,
XDG_CONFIG_HOME, and AppData back at it after isolation, and proves
LinkRepo writes only the pinned path; the cli package proves the
override wins and that clearing it restores the platform lookup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mjx8gvQA4YnryGieZTfM87
@ilovecrayons
ilovecrayons merged commit 3d4a2e5 into main Sep 4, 2026
6 checks passed
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.

2 participants