kin
KIN Is Enough. Read the other way, the same letters are Key Is the Name: the enrolled key is who you are, and that is enough to get in.
kin is a remote login protocol with a client and a daemon. Both are installed on the machines that use them. The daemon does not go through sshd. Authentication is a signature from an enrolled SSH key. The protocol has no username field and no password field.
A client that proves it holds a private key enrolled on the host gets a session. Any other client has the connection closed. The host does not ask who is connecting.
Programs
kin is the client and the daemon, one binary. kin work2 connects. kin serve listens. kin hostkey prints the host key fingerprints on the machine it runs on. A second binary named kind would share its name with the Kubernetes tool.
kin [-p port] [-i identity] [-t] host [command ...]
kin serve [--listen addr]... [--dir dir] [--init]
kin hostkey
kin version
With a command, kin runs it and exits with its status. Without one, kin opens a login shell on a pty when standard input is a terminal, and otherwise runs the account's shell reading standard input. -t puts a command on a pty. The exit status is 255 when kin itself fails.
The daemon runs on Linux. The client runs on Linux and macOS.
Identity
The client identity on the wire is the public key. The client sends the key and a signature over the handshake. The server looks that key up in its own file.
A match opens a session as the Unix account recorded beside the key. A miss closes the socket. There is no method list and no prompt.
The account name is server configuration, written when the key was enrolled. The client has no field to put a name in, and kin user@host is an error.
On a single-person machine, drop the per-key account and run the daemon as that person (see docs/usermode.md). Every accepted session runs as that account. An unknown key is still a closed connection.
Enrollment
Someone who already has access to the host writes /etc/kin/keys:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... james-laptop uid=cjames
The comment is a label for the administrator. uid is the account the session process runs as. Two keys can share an account. A key line without uid= is refused.
The file is read again for every connection, so a new or removed key applies to the next login without a restart. kin refuses the file, and the directory holding it, when another account owns it or can write to it.
The first key is installed from the console, from the machine image, or from a session that an already enrolled key opened. Enrollment over the protocol, by password, does not exist.
A lost key is replaced from the console, or with a second key that was enrolled earlier.
Ports
The default port is 2288, for the daemon and the client. It needs no privilege. IANA lists 2288 for netml; 2222 is avoided because scanners try it and many machines already use it for a second sshd or a VM forward.
The daemon takes its addresses from the first of these that is set:
--listen ADDR, which can be given more than once- the
listenfile in the state directory (/etc/kin/listen, or~/.local/share/kin/listenfor an account), one address per line,#starting a comment :2288
An address is 2288, :2288, 192.0.2.10:2288 or [2001:db8::1]:2288. A bare port listens on every IPv4 and IPv6 address. Several lines let a daemon listen on 2288 and 443 at once while clients move from one to the other.
The client uses port from ~/.kin/config, or -p, and 2288 otherwise.
Privileged ports
The root daemon binds any port. Put :443 or :22 in /etc/kin/listen.
An account daemon cannot bind a port below net.ipv4.ip_unprivileged_port_start, which is 1024 on most systems. kin tries the bind and lets the kernel decide, so a machine that lowers that value (containers often set it to 0) works as configured. When the kernel refuses, kin exits with a message naming the port, the current value and the two routes below. Both need an administrator once. kin does not add capabilities to itself, and setcap on the binary is not a supported route because replacing the binary clears the capability.
Socket activation
systemd binds the port as root and passes the open socket to kin, which runs as the account and holds no privilege. kin reads LISTEN_FDS and serves those sockets in place of its own addresses. As root, for the account cjames on 443:
# /etc/systemd/system/kin-cjames.socket
[Unit]
Description=kin for cjames, port 443
[Socket]
ListenStream=443
[Install]
WantedBy=sockets.target
# /etc/systemd/system/kin-cjames.service
[Unit]
Description=kin for cjames
[Service]
User=cjames
ExecStart=/home/cjames/.local/bin/kin serve
KillMode=process
systemctl daemon-reload
systemctl enable --now kin-cjames.socket
The first connection starts the service. It reads /home/cjames/.local/share/kin as usual. Copies of both units are in contrib/.
Port redirect
The daemon stays on 2288 and nftables sends a low port to it. kin needs no change, and the client connects to and pins the low port.
table inet kin {
chain prerouting {
type nat hook prerouting priority dstnat;
tcp dport 443 redirect to :2288
}
}
The redirect applies to connections from other machines. A connection from the host to itself goes through the output hook and needs a second chain.
Port 443 shared with a web server
kin offers the ALPN name kin/1 in every handshake. A TLS proxy that routes by ALPN, such as nginx with ssl_preread or HAProxy, can pass kin connections to the daemon and HTTPS to the web server on the same port.
Handshake
The handshake is TLS 1.3. TLS authenticates with a signature and agrees session keys from fresh ephemeral keys, using Go's default groups, which include the X25519MLKEM768 hybrid. An SSH key held by the agent can sign. It cannot perform Diffie-Hellman. A hardware-backed key can only sign. The handshake has to match that.
The user's ssh-ed25519 key signs the client side. A host key, generated when the host is set up, signs the server side. The host key is its own key, separate from the user keys.
The certificates carry the public keys. Certificate-authority checks are off. The client certificate is signed by a throwaway key, since the server reads only its public key; the signature that proves the user holds the key is the TLS CertificateVerify, made by the agent or from the key file. A connection costs one agent signature.
Both sides offer ALPN kin/1 and refuse a connection that does not agree on it.
The client accepts the server only when the server public key matches the pin stored for that host. The server accepts the client only when the client public key is in the keys file and the signature checks. Names inside the certificate are ignored.
The client checks the pin before it sends its certificate. A server that fails the pin never receives the client's key. TLS 1.3 encrypts the client certificate, so the key is not visible on the network either.
The client offers one key, the one named for that host. A client that offered every key in the agent would show a hostile server the full set.
The server allows 30 seconds for a handshake, which leaves time for an agent that asks before each signature (ssh-add -c), and at most 64 handshakes in progress. Session resumption is off, so every connection signs again.
Host pins
The pin file is ~/.kin/hosts, one line per pin:
work2:2288 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
The pin is the host together with the port. A system daemon and an account daemon on the same machine have different host keys and different ports, and the client stores a pin for each.
On the first connection to a host and port, kin makes a handshake that sends an empty client certificate. The server proves it holds its key, the client learns that key, and the server learns nothing about the client. kin then shows the fingerprint and writes the pin only after the person accepts it. When the same key is already pinned for that host on another port, the prompt says so, which is the usual case after a daemon moves port. kin hostkey on the host prints the fingerprint to compare.
A later change of host key fails the connection before the client's key is sent. The error names both fingerprints and the line to delete if the change is expected. A host and port may have more than one pin line; any of them matches, which lets a host key be replaced without a gap.
Session
Frames after the handshake are a type byte, a four-byte length, and the payload.
exec: a command, run asshell -c commandwith pipes. An empty command runs the shell reading standard input.shell: a pty with its size andTERM, running a login shell, or a command when one is given.stdin,eof, andwinch(a new window size) from the client.stdout,stderr,exit(the status, or 128 plus the signal number), anderror(the session could not start) from the server.
When the client goes away, the server hangs up the session: a pty session gets the terminal hangup, and an exec command's process group gets SIGHUP.
Port forwarding, file transfer, and agent forwarding wait. They are a later version.
Processes
The listener takes the socket and checks the key. It starts the session as the mapped uid; the child process sets its groups and ids before it changes to the home directory and runs the shell. The listener keeps its own privileges. A key with no uid mapping is refused. An unmapped key does not run as root.
A static binary cannot call PAM, so kin checks the account itself: an account that has expired in /etc/shadow, or whose shell is nologin or false, is refused. A locked password (! in /etc/shadow) is not a refusal, because key-only accounts are often locked that way on purpose. Accounts come from /etc/passwd and /etc/group; an account that exists only in LDAP or sssd is not found.
The session environment is HOME, USER, LOGNAME, SHELL, PATH, TERM on a pty, the system locale, KIN_CONNECTION (client address and port, server address and port, as in SSH_CONNECTION) and KIN_KEY (the fingerprint of the key that logged in). XDG_RUNTIME_DIR and the session bus are set when /run/user/UID already exists for the account. kin does not open a logind session, so it does not create that directory, and sessions do not appear in who or last.
The pty belongs to the account, group tty, mode 0620.
Sessions run inside the daemon's process, so restarting the daemon ends every open session, including the one that ran the restart (the restart itself completes). Without logind, session processes also stay in the daemon's cgroup. The units in contrib/ set KillMode=process so that stopping the daemon does not kill tmux servers and nohup jobs started from a session.
Keys
The first version takes ssh-ed25519 only. RSA and ed25519-sk use other signature layouts in the agent protocol. Add them once the ed25519 path is in use.
identity names a private key file or its .pub. kin reads the public key from the .pub, or from the unencrypted header of the private key file. When ssh-agent (SSH_AUTH_SOCK) holds that key, the agent signs and the private key file is not read. Otherwise kin reads the private key file and asks for its passphrase on the terminal. A key held only by the agent is named by its .pub file.
Client configuration
The client config is ~/.kin/config. identity is the key to offer for that host. The default identity is ~/.ssh/id_ed25519.
host work2
hostname work2.example
identity ~/.ssh/id_ed25519
host arm64box
hostname 192.0.2.20
port 443
A host line takes one or more glob patterns. Lines before the first host apply to every host. As in ssh_config, the first value found for a keyword wins, so specific blocks go above general ones. An unknown keyword is an error, and so is user.
Install
./build.sh builds static binaries (CGO_ENABLED=0) for linux/amd64, linux/arm64, darwin/arm64 and darwin/amd64 into dist/, and a bundle per target, dist/kin-OS-ARCH.tar.gz, holding the binary, install.sh, uninstall.sh and this README. Copy a bundle to the machine, unpack it, and run the installer from inside it. A source checkout works too, after ./build.sh.
install.sh [client|host|both] [--port N] [--key KEY] [--account NAME] [--no-start] [--binary PATH]
both is the default. client installs the binary and, for an account, ~/.kin with a config template. host installs the binary, the state directory, the host key, the keys file and the systemd service, and starts the service. On macOS, where the daemon does not run, the default installs the client.
Run as root, the installer sets up the system daemon: /usr/local/bin/kin, /etc/kin, and kin.service. Run as any other account, it sets up that account's daemon under ~/.local with a user service (see docs/usermode.md).
--port Nwrites the listen file. An account install refuses a port the kernel will not let it bind, before changing anything.--keyenrolls a public key, given as a.pubfile or as thessh-ed25519 AAAA...line. On a root install the key logs in as--account, which defaults to the account that ransudo.--no-startinstalls and enables the service without starting it.
sudo ./install.sh --key ~/.ssh/id_ed25519.pub # system daemon on 2288, key logs in as you
sudo ./install.sh host --port 443 # move it to 443
./install.sh --port 2299 --key "$(cat ~/.ssh/id_ed25519.pub)" # account daemon beside it
./install.sh client # client only
The installer refuses a port another process already holds, which catches an account daemon left on 2288 next to a system one. A rerun replaces the binary, keeps the host key, the keys and the client config, and restarts a running daemon as its last step, so a rerun from a kin session through that daemon prints its whole report before the session ends. A key that is already enrolled is not added twice.
uninstall.sh [client|host|both] [--purge]
uninstall.sh stops and removes the service and the binary. host leaves the binary for the client, and client leaves it while the daemon uses it. The host key, the keys file and the client's pins stay unless --purge is given.
go test ./... runs the tests. They start account daemons on loopback and need no root.
Nearby tools
mosh and Eternal Terminal open their own session after login. The login still goes through sshd.
WireGuard authenticates with public keys and has no usernames. The keys are WireGuard keys, and the result is a tunnel.
A daemon that speaks SSH, including one that is not OpenSSH, still receives a username because SSH clients send one.
Build order
- Host-key generation,
/etc/kin/keys, and a TLS handshake that accepts a pinned host key and an enrolled client key. execof one command as the mapped uid, with stdout, stderr, and an exit code.- A pty shell, and window-size changes.
- Further key types, port forwarding, and file transfer, after the first three are in use.
Steps 1 to 3 are in this version.