Go · 5114 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package main
4
5 import (
6 "context"
7 "fmt"
8 "net/netip"
9 "os"
10 "strings"
11 "time"
12
13 "github.com/spf13/cobra"
14
15 "github.com/tenseleyFlow/shithub/internal/infra/config"
16 "github.com/tenseleyFlow/shithub/internal/infra/db"
17 usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
18 )
19
20 // sshAuthkeysCmd implements sshd's AuthorizedKeysCommand contract:
21 //
22 // - On a known fingerprint, write a single authorized_keys line on stdout
23 // with a forced command and restrictive options.
24 // - On an unknown fingerprint OR any error, write nothing and exit 0.
25 // sshd uses STDOUT as the auth answer; non-zero exit is a config error,
26 // not a deny. Failing closed is the right model: better to deny a
27 // legitimate connection than accidentally authorize the wrong user.
28 //
29 // Latency is critical — every SSH connection waits on this. The pool is
30 // sized small (max 4 conns) to bound startup cost and tail-latency.
31 var sshAuthkeysCmd = &cobra.Command{
32 Use: "ssh-authkeys <fingerprint>",
33 Short: "AuthorizedKeysCommand handler for sshd",
34 Args: cobra.ExactArgs(1),
35 Hidden: true, // not for direct human use
36 RunE: func(cmd *cobra.Command, args []string) error {
37 // Fail-closed wrapper: anything below that returns an error or
38 // panics writes nothing to stdout. The exit code stays 0.
39 defer func() {
40 _ = recover()
41 }()
42 fp := strings.TrimSpace(args[0])
43 if !isWellFormedFingerprint(fp) {
44 return nil
45 }
46
47 cfg, err := config.Load(nil)
48 if err != nil || cfg.DB.URL == "" {
49 return nil
50 }
51
52 ctx, cancel := context.WithTimeout(cmd.Context(), 1500*time.Millisecond)
53 defer cancel()
54
55 pool, err := db.Open(ctx, db.Config{
56 URL: cfg.DB.URL, MaxConns: 4, MinConns: 0,
57 ConnectTimeout: 750 * time.Millisecond,
58 })
59 if err != nil {
60 return nil
61 }
62 defer pool.Close()
63
64 q := usersdb.New()
65 row, err := q.GetUserSSHKeyByFingerprint(ctx, pool, fp)
66 if err != nil {
67 // pgx.ErrNoRows or any other error → silently empty.
68 return nil
69 }
70
71 _, _ = fmt.Fprintln(cmd.OutOrStdout(), authorizedKeysLine(row))
72
73 // Best-effort last-used update. 500ms cap; any error is dropped.
74 updateCtx, updateCancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
75 defer updateCancel()
76 _ = q.TouchSSHKeyLastUsed(updateCtx, pool, usersdb.TouchSSHKeyLastUsedParams{
77 ID: row.ID,
78 LastUsedIp: clientAddrFromEnv(),
79 })
80 return nil
81 },
82 }
83
84 // sshShellCmd is the placeholder for the forced-command target. S13 swaps
85 // this for the real git-over-SSH dispatcher; for S07 we just log the
86 // inbound command and exit non-zero with a friendly message so an
87 // operator (or test) can confirm the wiring works end-to-end.
88 var sshShellCmd = &cobra.Command{
89 Use: "ssh-shell <user_id>",
90 Short: "Forced-command target invoked by sshd via AuthorizedKeysCommand",
91 Args: cobra.ExactArgs(1),
92 Hidden: true,
93 RunE: func(cmd *cobra.Command, args []string) error {
94 userID := args[0]
95 original := os.Getenv("SSH_ORIGINAL_COMMAND")
96 // Log to stderr so it's captured by sshd's session log without
97 // polluting the (silent-on-empty) stdout contract.
98 _, _ = fmt.Fprintf(cmd.ErrOrStderr(),
99 "shithubd ssh-shell: user_id=%s original_command=%q (git-over-SSH lands in S13)\n",
100 userID, original)
101 return fmt.Errorf("git over SSH not enabled yet")
102 },
103 }
104
105 // authorizedKeysLine builds the single line sshd consumes. The forced
106 // command runs `shithubd ssh-shell <user_id>`; the option set strips
107 // every interactive affordance.
108 func authorizedKeysLine(row usersdb.UserSshKey) string {
109 binary := os.Args[0]
110 // Quote-escape only the binary path; user_id is a digit string so it
111 // can never contain shell metacharacters.
112 cmd := fmt.Sprintf(`%s ssh-shell %d`, binary, row.UserID)
113 options := strings.Join([]string{
114 fmt.Sprintf(`command="%s"`, cmd),
115 "no-port-forwarding",
116 "no-X11-forwarding",
117 "no-agent-forwarding",
118 "no-pty",
119 }, ",")
120 return options + " " + row.PublicKey
121 }
122
123 // clientAddrFromEnv extracts the connecting client's address from
124 // $SSH_CONNECTION (sshd sets it to "<client> <cport> <server> <sport>").
125 // Returns nil when unavailable, which sqlc encodes as a SQL NULL.
126 func clientAddrFromEnv() *netip.Addr {
127 conn := os.Getenv("SSH_CONNECTION")
128 if conn == "" {
129 return nil
130 }
131 parts := strings.Fields(conn)
132 if len(parts) < 1 {
133 return nil
134 }
135 addr, err := netip.ParseAddr(parts[0])
136 if err != nil {
137 return nil
138 }
139 return &addr
140 }
141
142 // isWellFormedFingerprint accepts only the canonical SHA256:<b64> shape
143 // our codebase emits. Defense against an attacker passing crafted strings
144 // to influence the SQL plan.
145 func isWellFormedFingerprint(s string) bool {
146 if !strings.HasPrefix(s, "SHA256:") {
147 return false
148 }
149 rest := s[len("SHA256:"):]
150 if len(rest) < 30 || len(rest) > 80 {
151 return false
152 }
153 for _, r := range rest {
154 switch {
155 case r >= 'A' && r <= 'Z',
156 r >= 'a' && r <= 'z',
157 r >= '0' && r <= '9',
158 r == '+', r == '/', r == '=':
159 default:
160 return false
161 }
162 }
163 return true
164 }
165
166 func init() {
167 rootCmd.AddCommand(sshAuthkeysCmd)
168 rootCmd.AddCommand(sshShellCmd)
169 }
170