krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

ca9fd6021b3910ce1ae78fb6f433760e9b7daf74

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-23T23:23:44Z

M9: ssh.mode = system (host sshd)

- forged authorized-keys: AuthorizedKeysCommand backend; prints a
  restrict,command="forged shell --key-id N" line for registered keys
  and nothing for unknown ones, so strangers fail auth inside sshd
- forged shell: forced command; reads SSH_ORIGINAL_COMMAND and runs the
  same dispatch path as the embedded listener (sshd.Exec, extracted);
  opens the store directly (WAL) while hooks still reach the daemon
  over the unix socket
- serve skips the SSH listener in system mode; http/git-daemon/hookd
  unchanged
- gitutil.Transport takes separate stdin/stdout for process stdio
- e2e against a real unprivileged /usr/sbin/sshd: whoami --json,
  unknown-key auth failure, git-scope control denial, private repo
  create/push, bob not-found denial, protected-branch force-push
  refused through the full forced-command -> hook -> daemon path;
  AuthorizedKeysCommand routed via root-owned /bin/sh wrapper (sshd
  requires the command binary be root-owned)
 cmd/forged/main.go          |  32 ++++---
 cmd/forged/system.go        | 105 ++++++++++++++++++++++
 e2e/system_test.go          | 211 ++++++++++++++++++++++++++++++++++++++++++++
 internal/gitutil/gitutil.go |  10 +--
 internal/sshd/sshd.go       |  49 +++++-----
 internal/store/users.go     |  11 +++
 6 files changed, 379 insertions(+), 39 deletions(-)

diff --git a/cmd/forged/main.go b/cmd/forged/main.go
index a7dbbe9..ce02986 100644
--- a/cmd/forged/main.go
+++ b/cmd/forged/main.go
@@ -53,6 +53,8 @@ func main() {
 		migrateCmd(),
 		adminCmd(),
 		hookCmd(),
+		authorizedKeysCmd(),
+		shellCmd(),
 	)
 
 	if err := root.Execute(); err != nil {
@@ -99,10 +101,6 @@ func serveCmd() *cobra.Command {
 			}
 			defer st.Close()
 
-			if cfg.SSH.Mode != "embedded" {
-				return fmt.Errorf("ssh.mode = %q not implemented (M9)", cfg.SSH.Mode)
-			}
-
 			// Regenerate hook scripts so a moved binary self-heals, then
 			// start the hook policy socket.
 			self, err := os.Executable()
@@ -118,18 +116,24 @@ func serveCmd() *cobra.Command {
 			}
 			defer stopHookd()
 
-			srv, err := sshd.New(cfg, st)
-			if err != nil {
-				return err
-			}
-			ln, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(cfg.SSH.Port)))
-			if err != nil {
-				return err
+			errCh := make(chan error, 3)
+			if cfg.SSH.Mode == "embedded" {
+				srv, err := sshd.New(cfg, st)
+				if err != nil {
+					return err
+				}
+				ln, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(cfg.SSH.Port)))
+				if err != nil {
+					return err
+				}
+				slog.Info("ssh listening", "addr", ln.Addr())
+				go func() { errCh <- srv.Serve(ln) }()
+			} else {
+				// system mode: the host sshd owns the SSH port and invokes
+				// this binary via AuthorizedKeysCommand + forced command.
+				slog.Info("ssh handled by host sshd (ssh.mode = system)")
 			}
-			slog.Info("ssh listening", "addr", ln.Addr())
 
-			errCh := make(chan error, 3)
-			go func() { errCh <- srv.Serve(ln) }()
 
 			web := httpd.New(cfg, st)
 			hs := &http.Server{Addr: cfg.HTTP.Addr, Handler: web.Handler()}
diff --git a/cmd/forged/system.go b/cmd/forged/system.go
new file mode 100644
index 0000000..5f589c5
--- /dev/null
+++ b/cmd/forged/system.go
@@ -0,0 +1,105 @@
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+	"golang.org/x/crypto/ssh"
+
+	"github.com/krazywarez/forge/internal/config"
+	"github.com/krazywarez/forge/internal/protocol"
+	"github.com/krazywarez/forge/internal/sshd"
+)
+
+// authorizedKeysCmd backs sshd's AuthorizedKeysCommand in system mode:
+//
+//	AuthorizedKeysCommand /usr/bin/forged --config /etc/forge/config.toml authorized-keys %t %k
+//	AuthorizedKeysCommandUser git
+//
+// It prints a forced-command authorized_keys line for registered keys and
+// nothing for unknown ones — so unknown keys fail authentication inside
+// sshd, before any forge code runs. That is why system mode requires
+// registration = "closed".
+func authorizedKeysCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:    "authorized-keys <key-type> <key-base64>",
+		Hidden: true,
+		Args:   cobra.ExactArgs(2),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cfg, err := config.Load(configPath)
+			if err != nil {
+				return err
+			}
+			st, err := openStore(cfg)
+			if err != nil {
+				return err
+			}
+			defer st.Close()
+
+			pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(args[0] + " " + args[1]))
+			if err != nil {
+				return nil // unparseable key: no output, auth fails
+			}
+			key, err := st.SSHKeyByFingerprint(ssh.FingerprintSHA256(pub))
+			if err != nil {
+				return nil // unknown key: no output, auth fails
+			}
+			self, err := os.Executable()
+			if err != nil {
+				return err
+			}
+			fmt.Printf("restrict,command=\"%s --config %s shell --key-id %d\" %s %s\n",
+				self, configPath, key.ID, args[0], args[1])
+			return nil
+		},
+	}
+}
+
+// shellCmd is the forced command sshd runs for an authenticated key. The
+// original client command arrives in SSH_ORIGINAL_COMMAND; dispatch is the
+// same code path as the embedded listener.
+func shellCmd() *cobra.Command {
+	var keyID int64
+	cmd := &cobra.Command{
+		Use:    "shell",
+		Hidden: true,
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cfg, err := config.Load(configPath)
+			if err != nil {
+				return err
+			}
+			st, err := openStore(cfg)
+			if err != nil {
+				return err
+			}
+			defer st.Close()
+
+			key, err := st.SSHKeyByID(keyID)
+			if err != nil {
+				fmt.Fprintln(os.Stderr, "key no longer registered")
+				os.Exit(protocol.ExitDenied)
+			}
+			user, err := st.UserByID(key.UserID)
+			if err != nil {
+				fmt.Fprintln(os.Stderr, "account no longer exists")
+				os.Exit(protocol.ExitDenied)
+			}
+			_ = st.TouchSSHKey(key.ID)
+
+			cmdline := os.Getenv("SSH_ORIGINAL_COMMAND")
+			if cmdline == "" {
+				fmt.Fprintf(os.Stderr, "forge control plane: interactive shells are not available.\nTry: ssh <host> help\n")
+				os.Exit(protocol.ExitUsage)
+			}
+			code := sshd.Exec(cfg, st, user, key.Scope, cmdline, os.Stdin, os.Stdout, os.Stderr)
+			st.Close()
+			os.Exit(code)
+			return nil
+		},
+	}
+	cmd.Flags().Int64Var(&keyID, "key-id", 0, "registered key id (set by authorized-keys)")
+	cmd.MarkFlagRequired("key-id")
+	return cmd
+}
diff --git a/e2e/system_test.go b/e2e/system_test.go
new file mode 100644
index 0000000..906ceab
--- /dev/null
+++ b/e2e/system_test.go
@@ -0,0 +1,211 @@
+package e2e
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+)
+
+// TestSystemSSHMode runs the M1/M2 scenarios against a real host sshd using
+// AuthorizedKeysCommand + forced command instead of the embedded listener.
+func TestSystemSSHMode(t *testing.T) {
+	sshdBin := "/usr/sbin/sshd"
+	if _, err := os.Stat(sshdBin); err != nil {
+		t.Skipf("no host sshd at %s", sshdBin)
+	}
+	me, err := user.Current()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// forged in system mode: no embedded SSH listener; hookd + http still run.
+	inst := startInstanceWith(t, "") // placeholder to reuse helpers; killed below
+	inst.proc.Process.Kill()
+	inst.proc.Wait()
+	cfg := fmt.Sprintf(`
+[server]
+root = %q
+site_url = "https://forge.test"
+[ssh]
+mode = "system"
+[http]
+addr = "127.0.0.1:%d"
+tls = "off"
+`, inst.root, inst.httpPort)
+	if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil {
+		t.Fatal(err)
+	}
+	inst.proc = exec.Command(inst.forged, "--config", inst.config, "serve")
+	inst.proc.Stderr = os.Stderr
+	if err := inst.proc.Start(); err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() { inst.proc.Process.Kill(); inst.proc.Wait() })
+
+	aliceKey := inst.newKey(t, "alice")
+	bobKey := inst.newKey(t, "bob")
+	strangerKey := inst.newKey(t, "stranger")
+	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
+	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
+
+	// Host sshd on a high port as the current user.
+	sshdDir := t.TempDir()
+	hostKey := filepath.Join(sshdDir, "host_ed25519")
+	if out, err := exec.Command("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", hostKey).CombinedOutput(); err != nil {
+		t.Fatalf("host keygen: %v\n%s", err, out)
+	}
+	// sshd requires the AuthorizedKeysCommand program itself to be owned by
+	// root; a test-built binary is not. Use root-owned /bin/sh with a
+	// wrapper script argument — only the command path is ownership-checked.
+	wrapper := filepath.Join(sshdDir, "akc.sh")
+	script := fmt.Sprintf("#!/bin/sh\nexec %q --config %q authorized-keys \"$1\" \"$2\"\n",
+		inst.forged, inst.config)
+	if err := os.WriteFile(wrapper, []byte(script), 0o755); err != nil {
+		t.Fatal(err)
+	}
+
+	sshdPort := freePort(t)
+	sshdConf := filepath.Join(sshdDir, "sshd_config")
+	conf := fmt.Sprintf(`Port %d
+ListenAddress 127.0.0.1
+HostKey %s
+PasswordAuthentication no
+KbdInteractiveAuthentication no
+PubkeyAuthentication yes
+AuthorizedKeysFile none
+AuthorizedKeysCommand /bin/sh %s %%t %%k
+AuthorizedKeysCommandUser %s
+StrictModes no
+UsePAM no
+PidFile %s
+LogLevel ERROR
+`, sshdPort, hostKey, wrapper, me.Username, filepath.Join(sshdDir, "sshd.pid"))
+	if err := os.WriteFile(sshdConf, []byte(conf), 0o600); err != nil {
+		t.Fatal(err)
+	}
+	sshd := exec.Command(sshdBin, "-D", "-e", "-f", sshdConf)
+	sshd.Stderr = os.Stderr
+	if err := sshd.Start(); err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() { sshd.Process.Kill(); sshd.Wait() })
+
+	deadline := time.Now().Add(10 * time.Second)
+	for {
+		conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", sshdPort), 200*time.Millisecond)
+		if err == nil {
+			conn.Close()
+			break
+		}
+		if time.Now().After(deadline) {
+			t.Fatal("sshd did not start")
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+
+	// ssh helper against the host sshd (login user = current user; identity
+	// still comes from the key).
+	sysSSH := func(key, stdin string, args ...string) (string, string, int) {
+		base := []string{
+			"-p", fmt.Sprint(sshdPort),
+			"-i", key,
+			"-o", "IdentitiesOnly=yes",
+			"-o", "StrictHostKeyChecking=no",
+			"-o", "UserKnownHostsFile=" + filepath.Join(sshdDir, "kh"),
+			"-o", "BatchMode=yes",
+			me.Username + "@127.0.0.1",
+		}
+		cmd := exec.Command("ssh", append(base, args...)...)
+		if stdin != "" {
+			cmd.Stdin = strings.NewReader(stdin)
+		}
+		var out, errOut strings.Builder
+		cmd.Stdout = &out
+		cmd.Stderr = &errOut
+		err := cmd.Run()
+		code := 0
+		if ee, ok := err.(*exec.ExitError); ok {
+			code = ee.ExitCode()
+		} else if err != nil {
+			t.Fatalf("ssh: %v", err)
+		}
+		return out.String(), errOut.String(), code
+	}
+
+	// M1: whoami over the host sshd.
+	out, errOut, code := sysSSH(aliceKey, "", "whoami", "--json")
+	if code != 0 {
+		t.Fatalf("whoami via sshd: exit %d\nstdout: %s\nstderr: %s", code, out, errOut)
+	}
+	if !strings.Contains(out, `"username":"alice"`) || !strings.Contains(out, `"protocol_version":1`) {
+		t.Fatalf("whoami output: %s", out)
+	}
+
+	// Unknown key: authentication fails inside sshd (authorized-keys emits
+	// nothing), before any forge code runs.
+	_, _, code = sysSSH(strangerKey, "", "whoami")
+	if code == 0 {
+		t.Fatal("stranger authenticated via host sshd")
+	}
+
+	// Scoped key: registered with git-only scope, denied control commands.
+	scopedKey := inst.newKey(t, "scoped")
+	pub, _ := os.ReadFile(scopedKey + ".pub")
+	if _, errOut, code := sysSSH(aliceKey, string(pub), "keys", "add", "--scope", "git"); code != 0 {
+		t.Fatalf("keys add: %s", errOut)
+	}
+	_, errOut, code = sysSSH(scopedKey, "", "whoami")
+	if code != 4 || !strings.Contains(errOut, "does not allow control commands") {
+		t.Fatalf("scoped denial via sshd: exit %d, %s", code, errOut)
+	}
+
+	// M2: private repo, push, denial, protected branch — through host sshd.
+	if _, errOut, code = sysSSH(aliceKey, "", "repo", "create", "alice/proj", "--private"); code != 0 {
+		t.Fatalf("repo create: %s", errOut)
+	}
+	sysGitEnv := func(key string) []string {
+		return append(os.Environ(),
+			fmt.Sprintf("GIT_SSH_COMMAND=ssh -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=%s -o BatchMode=yes",
+				key, filepath.Join(sshdDir, "kh")),
+			"GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null",
+			"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
+			"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test",
+		)
+	}
+	urlFor := func(repo string) string {
+		return fmt.Sprintf("ssh://%s@127.0.0.1:%d/%s.git", me.Username, sshdPort, repo)
+	}
+
+	work := t.TempDir()
+	aliceEnv := sysGitEnv(aliceKey)
+	mustGit(t, work, aliceEnv, "clone", urlFor("alice/proj"), "w")
+	dir := filepath.Join(work, "w")
+	os.WriteFile(filepath.Join(dir, "f"), []byte("x\n"), 0o644)
+	mustGit(t, dir, aliceEnv, "checkout", "-q", "-b", "main")
+	mustGit(t, dir, aliceEnv, "add", ".")
+	mustGit(t, dir, aliceEnv, "commit", "-q", "-m", "init")
+	mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
+
+	// Bob: authenticated but no access — not-found, not permission-denied.
+	cloneOut, cloneCode := gitRun(t, t.TempDir(), sysGitEnv(bobKey), "clone", urlFor("alice/proj"))
+	if cloneCode == 0 || !strings.Contains(cloneOut, "repository not found") {
+		t.Fatalf("bob clone via sshd: %d\n%s", cloneCode, cloneOut)
+	}
+
+	// Protected branch: the hook path (forced command -> git -> pre-receive
+	// -> daemon unix socket) refuses the force-push.
+	if _, errOut, code = sysSSH(aliceKey, "", "repo", "settings", "protect", "alice/proj", "main"); code != 0 {
+		t.Fatalf("protect: %s", errOut)
+	}
+	mustGit(t, dir, aliceEnv, "commit", "-q", "--amend", "-m", "rewritten")
+	pushOut, pushCode := gitRun(t, dir, aliceEnv, "push", "--force", "origin", "main")
+	if pushCode == 0 || !strings.Contains(pushOut, "force-push refused") {
+		t.Fatalf("force-push via sshd: %d\n%s", pushCode, pushOut)
+	}
+}
diff --git a/internal/gitutil/gitutil.go b/internal/gitutil/gitutil.go
index 639189b..b773b37 100644
--- a/internal/gitutil/gitutil.go
+++ b/internal/gitutil/gitutil.go
@@ -29,9 +29,9 @@ func InitBare(path, defaultBranch, hooksPath string) error {
 }
 
 // Transport streams one git transport service (upload-pack, receive-pack,
-// upload-archive) over rw. extraEnv entries are appended to the daemon's
-// environment; hooks read the FORGE_* variables from it.
-func Transport(service, repoPath string, rw io.ReadWriter, errW io.Writer, extraEnv []string) error {
+// upload-archive). extraEnv entries are appended to the process environment;
+// hooks read the FORGE_* variables from it.
+func Transport(service, repoPath string, stdin io.Reader, stdout, errW io.Writer, extraEnv []string) error {
 	var args []string
 	switch service {
 	case "git-upload-pack", "git-receive-pack", "git-upload-archive":
@@ -41,8 +41,8 @@ func Transport(service, repoPath string, rw io.ReadWriter, errW io.Writer, extra
 	}
 	cmd := exec.Command("git", args...)
 	cmd.Env = append(os.Environ(), extraEnv...)
-	cmd.Stdin = rw
-	cmd.Stdout = rw
+	cmd.Stdin = stdin
+	cmd.Stdout = stdout
 	cmd.Stderr = errW
 	return cmd.Run()
 }
diff --git a/internal/sshd/sshd.go b/internal/sshd/sshd.go
index ff9fbb0..4057c4a 100644
--- a/internal/sshd/sshd.go
+++ b/internal/sshd/sshd.go
@@ -8,6 +8,7 @@ import (
 	"encoding/pem"
 	"errors"
 	"fmt"
+	"io"
 	"log/slog"
 	"net"
 	"os"
@@ -184,70 +185,78 @@ func (s *Server) runExec(sconn *ssh.ServerConn, ch ssh.Channel, cmdline string)
 		return protocol.ExitDenied
 	}
 	_ = s.st.TouchSSHKey(keyID)
+	return Exec(s.cfg, s.st, user, ext["scope"], cmdline, ch, ch, ch.Stderr())
+}
 
+// Exec runs one SSH exec command line for an authenticated key. It is the
+// single dispatch path shared by the embedded listener and the system-sshd
+// forced command (forged shell).
+func Exec(cfg config.Config, st *store.Store, user store.User, scope, cmdline string,
+	stdin io.Reader, stdout, stderr io.Writer) int {
 	argv, err := protocol.Tokenize(cmdline)
 	if err != nil {
-		fmt.Fprintf(ch.Stderr(), "cannot parse command: %v\n", err)
+		fmt.Fprintf(stderr, "cannot parse command: %v\n", err)
 		return protocol.ExitUsage
 	}
 	if len(argv) > 0 {
 		switch argv[0] {
 		case "git-upload-pack", "git-receive-pack", "git-upload-archive":
-			return s.runGit(ch, user, ext["scope"], argv)
+			return runGit(cfg, st, user, scope, argv, stdin, stdout, stderr)
 		}
 	}
 	ctx := &control.Ctx{
 		User:   user,
-		Scope:  ext["scope"],
-		Store:  s.st,
-		Cfg:    s.cfg,
-		Stdin:  ch,
-		Stdout: ch,
-		Stderr: ch.Stderr(),
+		Scope:  scope,
+		Store:  st,
+		Cfg:    cfg,
+		Stdin:  stdin,
+		Stdout: stdout,
+		Stderr: stderr,
 	}
 	return control.Dispatch(ctx, argv)
 }
 
 // runGit streams a git transport service after access checks.
-func (s *Server) runGit(ch ssh.Channel, user store.User, scope string, argv []string) int {
+func runGit(cfg config.Config, st *store.Store, user store.User, scope string, argv []string,
+	stdin io.Reader, stdout, stderr io.Writer) int {
 	service := argv[0]
 	if len(argv) != 2 {
-		fmt.Fprintf(ch.Stderr(), "usage: %s <path>\n", service)
+		fmt.Fprintf(stderr, "usage: %s <path>\n", service)
 		return protocol.ExitUsage
 	}
 	write := service == "git-receive-pack"
 
-	repo, err := s.st.RepoByPath(argv[1])
+	repo, err := st.RepoByPath(argv[1])
 	if err != nil {
-		fmt.Fprintln(ch.Stderr(), "repository not found")
+		fmt.Fprintln(stderr, "repository not found")
 		return protocol.ExitNotFound
 	}
-	grant, err := s.st.AccessRole(repo.ID, user.ID)
+	grant, err := st.AccessRole(repo.ID, user.ID)
 	if err != nil {
-		fmt.Fprintln(ch.Stderr(), "internal error")
+		fmt.Fprintln(stderr, "internal error")
 		return protocol.ExitFailure
 	}
 	if !policy.CanRead(user, repo, grant) {
 		// Same answer as nonexistence: private repos must not be enumerable.
-		fmt.Fprintln(ch.Stderr(), "repository not found")
+		fmt.Fprintln(stderr, "repository not found")
 		return protocol.ExitNotFound
 	}
 	if !policy.ScopeAllowsGit(scope, repo.Path(), write) {
-		fmt.Fprintf(ch.Stderr(), "this key's scope (%s) does not allow %s on %s\n", scope, service, repo.Path())
+		fmt.Fprintf(stderr, "this key's scope (%s) does not allow %s on %s\n", scope, service, repo.Path())
 		return protocol.ExitDenied
 	}
 	if write && !policy.CanWrite(user, repo, grant) {
-		fmt.Fprintf(ch.Stderr(), "write access to %s denied\n", repo.Path())
+		fmt.Fprintf(stderr, "write access to %s denied\n", repo.Path())
 		return protocol.ExitDenied
 	}
 
-	dir := control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)
+	dir := control.RepoDir(cfg.Server.Root, repo.OwnerName, repo.Name)
 	env := []string{
-		hookd.EnvSocket + "=" + hookd.SocketPath(s.cfg.Server.Root),
+		hookd.EnvSocket + "=" + hookd.SocketPath(cfg.Server.Root),
 		hookd.EnvRepoID + "=" + strconv.FormatInt(repo.ID, 10),
 		hookd.EnvUserID + "=" + strconv.FormatInt(user.ID, 10),
 	}
-	if err := gitutil.Transport(service, dir, ch, ch.Stderr(), env); err != nil {
+	if err := gitutil.Transport(service, dir, stdin, stdout, stderr, env); err != nil {
 		return protocol.ExitFailure
 	}
 	return protocol.ExitOK
diff --git a/internal/store/users.go b/internal/store/users.go
index 93f847a..b671f50 100644
--- a/internal/store/users.go
+++ b/internal/store/users.go
@@ -198,3 +198,14 @@ func boolInt(b bool) int {
 func isUniqueErr(err error) bool {
 	return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed")
 }
+
+func (s *Store) SSHKeyByID(id int64) (SSHKey, error) {
+	var k SSHKey
+	err := s.DB.QueryRow(
+		"SELECT id, user_id, fingerprint, algo, blob, scope FROM ssh_keys WHERE id = ?",
+		id).Scan(&k.ID, &k.UserID, &k.Fingerprint, &k.Algo, &k.Blob, &k.Scope)
+	if errors.Is(err, sql.ErrNoRows) {
+		return k, ErrNotFound
+	}
+	return k, err
+}