Repository navigation
OpenSSH for macOS with security keys built in: the relocatable client set, the agent, one aqua package - #33
Merged
Merged
Conversation
…ibfido2 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: closer-claudio <claudio@farcloser.world>
closer-claudio
force-pushed
the
claudio/20261010-openssh
branch
2 times, most recently
from
October 11, 2026 03:38
587f50d to
6888be4
Compare
closer-claudio
commented
Oct 11, 2026
closer-claudio
left a comment
Contributor
Author
There was a problem hiding this comment.
🛑 at 6888be4: the supply chain is exactly as described and the relocation patch is sound; two things stop it, both in the plumbing around the build and the agent, both small.
- 🛑
build.sh, the libfido2 step: configure fails on the runner with "could not find libcbor" (this head's build leg,CMakeLists.txt:231). libfido2's non-MSVC branch resolves libcbor, libcrypto and zlib through pkg-config (CMakeLists.txt:225-237of 1.17.0); the-DCBOR_*and-DCRYPTO_*hints only feedinclude_directories/link_directoriesafterwards,HAVE_CBOR_His probed on the default include path at line 97, andZLIB_FOUNDcan only ever come from pkg-config. On macos-15 CMake finds Homebrew's pkgconf through its own/opt/homebrewprefix search, not PATH, so the hermetic PATH does not shield this step: that pkgconf knows zlib (Homebrew ships a.pcfor the system zlib) and nothing of${deps}. Two consistent ways out:PKG_CONFIG_PATH="${deps}/lib/pkgconfig"on this step (libcbor and LibreSSL both install.pcfiles there) with the host's pkg-config stated as a build requirement; or a pinned pkgconf plus azlib.pcfor the SDK's zlib in the repository, pkg-config then ours end to end, the hermetic shape the rest of the script has. Either way the build leg is the proof, andtest-binary.shthen runs on the runner for the first time too. - 🛑
agent/ssh-agent-install: on an aqua machine the LaunchAgent it writes cannot start. The installer prefers aqua'sssh-agent-launchdproxy asProgramArguments, and launchd runs it withHOMEand the system PATH, none of the shell rc's exports. Reproduced here with aqua 2.64.0 against a global-config package (limen), cwd/: PATH/usr/bin:/bin:/usr/sbin:/sbin→aqua isn't found(the proxy locatesaquathrough PATH); aqua's bin dir on PATH → code 004command is not found(noAQUA_GLOBAL_CONFIG, and limen-install's global config is at~/.config/aqua/aqua.yaml, not aqua's default location); plusAQUA_GLOBAL_CONFIG→ code 002this package isn't allowed; plusAQUA_POLICY_CONFIG→ runs. WithKeepAlivelaunchd respawns it until throttled. Fix in the proxy branch: anEnvironmentVariablesdict in the plist withPATH(aqua's bin dir ahead of the system's),AQUA_GLOBAL_CONFIGandAQUA_POLICY_CONFIGtaken from the installing shell's environment, refusing to install the proxy form when they are unset; or point the plist at the package's own path and accept a re-run per upgrade.test-binary.shitem 6 runs the script directly under the system PATH, which is why it is green. ⚠️ test-binary.shitem 5:strings … | grep -q -E …insideifunderpipefail. When grep matches early and closes the pipe,stringsdies of SIGPIPE (141), the pipeline's status is 141, theifreads it as "no match", and a binary that does carry a build path passes. The same hazard item 4's comment describes. Drop-qand send grep's output to/dev/null(grep then reads to EOF), or writestringsto a file first.- 💡
ssh-agent-install:proxy="${AQUA_ROOT_DIR:-${HOME}/.local/share/aquaproj-aqua}/bin/…"skipsXDG_DATA_HOME; limen-install's expression is${AQUA_ROOT_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/aquaproj-aqua}. - 💡
config/ssh_config:KexAlgorithms curve25519-sha256,sntrup761x25519-sha512@openssh.comcarries the formula's list; OpenSSH 10 putsmlkem768x25519-sha256first by default (the NIST hybrid, sntrup761's successor). If the list is deliberate, fine; if it was carried over, the newer hybrid belongs in it. - 📝 The proxy form also presumes openssh sits in a global aqua config reachable from launchd. Today limen-install's global config holds limen alone and is rewritten on every run; whichever config is to carry openssh, its path is what the plist has to see.
- 📝 Conflicts with #32 on
renovate.jsonby construction, as the description says; the second to land rebases. closer-claudio has write on this repository now andmainrequires an approving review, so the ✅, when it comes, arms auto-merge as elsewhere. - 🔍 Checked: the four
pins.yamldigests at their sources by download, openssh-10.6p1 from cdn.openbsd.org (reachable from here; the same sha256 Homebrew core and the farcloser formula carry), libfido2 1.17.0 and libcbor v0.14.0 tag archives, libressl-4.3.3 release asset; the patch guard58ce223b…dfb2recomputed on the tarball's twelve files, equal;patches/relocate.patchdry-run and real apply on that tree: twelve files, no fuzz, no offsets; no_PATH_SSH_*/SSHDIRsite left in the twelve, and none of those macros in another client-side file. The patch itself:reloc_pathresolves the executable, strips two components, caches the prefix,xasprintfper call (a few bytes leaked per call inssh_keysign's three uses, a child process; fine). CMake 4.4.4: five checksums = the release's API digests =cmake-4.4.4-SHA-256.txt. Action pins: checkout v7.0.1, cache v6.1.0, upload-artifact v7.0.2, download-artifact v8.0.2, attest-build-provenance v4.2.2, each tag → the pinned commit.ci.yamlandrelease.yamlagainst curl's: one macos-15 leg instead of the matrix, abuild/dlcache onpins.yaml,--generate-notesadded, the rest the same shape (gate asserts all three,always(), env not interpolated).renovate.json: the forkcloser App identity is the one curl's carries; the preset'spins.yamlmanager regex matches each entry'srenovate:/extract-version/versioning/version:block by reading. The version guard inbuild.shaccepts10.6.1and10.6.1.N, refuses10.7.1,10.6.12and a fifth segment. shellcheck clean on the four scripts, no baregolangci-lintadded. Thetoolsleg installs CMake for real, the lint lane is green.
closer-claudio
force-pushed
the
claudio/20261010-openssh
branch
2 times, most recently
from
October 11, 2026 03:58
82638f7 to
26dea7f
Compare
…h the agent The repository becomes forkcloser/openssh: a package of OpenSSH portable for macOS arm64 built without OpenSSL, with libfido2 and libcbor linked statically and LibreSSL's libcrypto under libfido2 only, by Apple's clang on the macOS runner, released as one tarball per version with GitHub Artifact Attestations and a checksums.txt, for aqua. Apple's OpenSSH has no FIDO support, which is the reason it exists; the farcloser Homebrew formula it replaces built the same thing in a Cellar. Every source is a pins.yaml entry read back by build.sh and verified by sha256 before unpacking; OpenSSH comes from OpenBSD's CDN, watched by a custom Renovate datasource on the directory index since it is not on GitHub. The build keeps the client set (ssh, ssh-agent, ssh-add, ssh-keygen, scp, sftp, ssh-keyscan, the sk and pkcs11 helpers), drops the server, ssh-keysign and the man pages, writes the ed25519-only ssh_config the formula carried, and refuses a binary that links anything but the system. OpenSSH bakes its configuration directory and helper paths at build time. patches/relocate.patch resolves them from the running executable instead (the directory above bin/ or libexec/, symlinks resolved): the system ssh_config and the Include base, the global known_hosts files, the sk and pkcs11 helpers, ssh-keysign, ssh-askpass, and the ssh that scp and sftp execute. Twelve files, applied only to the exact sources it was written against (patches/relocate.sha256), so a bump that moves a site fails the build. The package then reads its own ssh_config and never Apple's, from wherever it is unpacked, aqua's versioned package directory included, and no environment variable is needed. Nothing shipped runs setuid or as root, so the executable's directory is the same trust domain as ~/.ssh/config. The agent files of farcloser/ssh-agent move into the package: the launch script finds the ssh-agent beside it, the installer writes the LaunchAgent pointing at aqua's proxy when there is one (an upgrade is then the next start) and retires Apple's agent for the next login. The copied logging library, the brew lookups and the tap-based test go. CI is curl's shape: lint, the one build leg on macos-15 followed by the smoke test, tools, gate. The smoke test unpacks the package elsewhere and checks the version line, the security-key algorithms and the absence of RSA, the shipped config read through a symlink, scp executing the package's ssh, no build path in the binaries, and the agent starting through its launch script under the system PATH, as launchd runs it. The release workflow builds on a v* tag, attests and publishes; the tag names the OpenSSH version with the portable level as the third segment (v10.6.1 for 10.6p1) and a fourth segment for a rebuild. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: closer-claudio <claudio@farcloser.world>
closer-claudio
force-pushed
the
claudio/20261010-openssh
branch
from
October 11, 2026 04:11
26dea7f to
82abaec
Compare
closer-claudio
commented
Oct 11, 2026
closer-claudio
left a comment
Contributor
Author
There was a problem hiding this comment.
✅ at 82abaec: the two blockers are closed the right way, the smoke test now proves both, and nothing in the supply chain moved since the first review.
- 🔍 Since 6888be4: the libfido2 step takes no pkg-config at all (
-DPKG_CONFIG_EXECUTABLE=/usr/bin/false, whose failing--versionmakes CMake drop it;-DCMAKE_REQUIRED_INCLUDESfor the header probes; the libraries by directory;-DZLIB_FOUND=ON -DZLIB_LIBRARIES=zfor the SDK's zlib). I reproduced the one point I doubted against the pinned cmake 4.4.4: a presetZLIB_FOUNDsurvivespkg_search_modulewhen the executable is gone, on a stripped PATH too, so the step no longer depends on what the host carries.-ffile-prefix-map=${work}=.on the three cmake builds and in OpenSSH'sCFLAGS(with-O2kept, since a setCFLAGSreplaces configure's default), which is what takes LibreSSL'sOPENSSL_assertsites out of the binaries; the inert fixed prefix/opt/forkcloser/opensshfor what the patch does not reach, installed throughDESTDIRand flattened (mv "${stage}${install_prefix}"/*,rm opt; thevar/emptythe install creates falls to the existingrm -rf var). The LaunchAgent names the launch script by the package's real path (pwd -P), the README and the installer say to re-run after an upgrade, and the registry entry no longer lists the launch script. Test item 5 readsstringsfrom a file; item 3 follows the new cipher line.config/ssh_config: every name is one this OpenSSH has (kex.h:mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com,curve25519-sha256;cipher.c:chacha20-poly1305@openssh.com,aes256-ctr;mac.c:hmac-sha2-512-etm@openssh.com;ssh-mldsa-eddsa.c,ssh-ed25519.c,ssh-ed25519-sk.cfor the key forms), the ML-DSA hybrids underUSE_MLDSA, which does not depend onWITH_OPENSSL;ssh -Gin item 3 would refuse an unknown name, so the smoke test is the proof on the built binary. Unchanged since the first review and re-checked as byte-identical in the diff:pins.yaml(four digests verified at source),patches/relocate.patchand its guard, the CMake checksums, the action pins,renovate.json, the workflows. No baregolangci-lintadded. Lint green; the build leg'sjust buildthenjust testat this head is the end-to-end proof the first review asked for. - 📝 Conflicts with #32 and now #34 on
renovate.json/the aqua files by construction; whichever lands second rebases. closer-claudio has write here andmainrequires an approving review. - 📝
securityis red at this head by the lycheelinksrecipe alone: github.com answered 503 on blob links in the canonicalAGENTS.md(the storm; farcloser/limen#344 widens the exclusion with the next release). Red is storm only, bypass-safe. Auto-merge is not armed for that reason.
1 task
apostasie
approved these changes
Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Why. Apple's OpenSSH is built without FIDO support, so an
sk-ssh-ed25519@openssh.comkey is unusable with the systemssh,ssh-agentandssh-keygen; the farcloser Homebrew formula built one that could, and the dev stack is leaving Homebrew. This repository (farcloser/ssh-agent until its transfer) becomes forkcloser/openssh: the package, on curl's model, with the agent's LaunchAgent that was this repository's whole content folded in. Design settled with @apostasie: no sshd, no ldns, LibreSSL under libfido2, relocation by patch, tagv10.6.1for 10.6p1.🛠️ What.
pins.yaml: OpenSSH 10.6p1 from OpenBSD's CDN (a custom Renovate datasource on the directory index, since it is not on GitHub), libfido2 1.17.0, libcbor 0.14.0, LibreSSL 4.3.3, each by sha256;build.shreads them back withlimen pins getand verifies before unpacking. CMake from aqua (just do tools add Kitware/CMake, the first commit); the compiler is Xcode's clang on macos-15, the SDK's libedit and zlib.--without-openssl --with-security-key-builtin --with-libedit --without-pam; the client set kept (ssh, ssh-agent, ssh-add, ssh-keygen, scp, sftp, ssh-keyscan, the sk and pkcs11 helpers), sshd, ssh-keysign and the man pages dropped; the ed25519-onlyssh_configthe formula carried; the licenses of everything linked; a binary that links anything but the system fails the build.patches/relocate.patch: OpenSSH bakes its config dir and helper paths at build time; the patch resolves them from the running executable (the directory abovebin/orlibexec/, symlinks resolved): the systemssh_configand theIncludebase, the global known_hosts, the sk and pkcs11 helpers, ssh-keysign, ssh-askpass, and thesshscp and sftp execute. Twelve files, 59 lines; applied only to the exact sources it was written against (patches/relocate.sha256), so a bump that moves a site fails the build. Nothing shipped is setuid or runs as root, so the executable's directory is the trust domain of~/.ssh/config.libexec/farcloser-ssh-agent(execs the ssh-agent beside it, writes~/.posh_ssh), the plist,bin/ssh-agent-install(points the LaunchAgent at aqua's proxy when there is one, retires Apple's agent for the next login,uninstallreverses it). The copied logging library, the brew lookups and the tap-based test go.ci.yamlon curl's shape (lint, build on macos-15 then the smoke test, tools, gate);release.yaml(v* tag: build, test, attest every asset, publish withchecksums.txt; dispatch is the dry run). README: the aqua registry entry, the tag mapping, the agent.🧪 How.
just lint16/16 rules and every lane green locally (links lane excepted: sandbox).test-binary.shgreen on a package assembled from the local proof build (the same sources, built the same way): version lineOpenSSH_10.6p1, without OpenSSL, sk-ed25519 present and no RSA, the shipped config read from the package through a symlink, scp executing the package's own ssh, no build path in the binaries, the agent starting through its launch script under the system PATH and answering ssh-add.build.shend to end. cdn.openbsd.org is unreachable from the sandbox, so the release tarball (pinned by the formula's sha256) was never downloaded here; the proof built the git tag's tree with host autoconf. The patch guard hash was taken on that tree's twelve files; the release tarball's copies are expected identical, and the leg says so./usr/liband/System/Library/Frameworks(otool -L).📝 Notes. Conflicts with #32 on
renovate.jsonby construction (both set theextendsline and the forkcloser App identity); whichever lands second rebases. The aqua registry entry in the README is the consumer's to copy into limen's canonical registry once a release exists; farcloser/ssh-agent's successor is this package'sssh-agent-install.