Commit 7f5c45d0af

7f5c45d0af0bd378a22019a6c85c29f261484071

parent: cab50f5004

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:29 UTC

sshd: refuse expired keys at auth and per exec; expiring keys cannot mint

Ref #277

Layout: unified · split

cmd/gitbayd/system.go +7 −2
@@ -3,6 +3,7 @@ package main
33import (
44 "fmt"
55 "os"
6 "time"
67
78 "github.com/spf13/cobra"
89 "golang.org/x/crypto/ssh"
@@ -43,8 +44,8 @@ func authorizedKeysCmd() *cobra.Command {
4344 return nil // unparseable key: no output, auth fails
4445 }
4546 key, err := st.SSHKeyByFingerprint(ssh.FingerprintSHA256(pub))
46 if err != nil {
47 return nil // unknown key: no output, auth fails
47 if err != nil || key.Expired(time.Now()) {
48 return nil // unknown or expired key: no output, auth fails
4849 }
4950 self, err := os.Executable()
5051 if err != nil {
@@ -82,6 +83,10 @@ func shellCmd() *cobra.Command {
8283 fmt.Fprintln(os.Stderr, "key no longer registered")
8384 os.Exit(protocol.ExitDenied)
8485 }
86 if key.Expired(time.Now()) {
87 fmt.Fprintln(os.Stderr, "this key has expired; remove it and add a new one")
88 os.Exit(protocol.ExitDenied)
89 }
8590 user, err := st.UserByID(key.UserID)
8691 if err != nil {
8792 fmt.Fprintln(os.Stderr, "account no longer exists")
internal/sshd/revoke_test.go +57
@@ -2,12 +2,16 @@ package sshd
22
33import (
44 "bytes"
5 "crypto/ed25519"
6 "crypto/rand"
57 "errors"
68 "strings"
79 "testing"
810 "time"
911
1012 "golang.org/x/crypto/ssh"
13
14 "gitbay.org/gitbay/internal/store"
1115)
1216
1317// execStatus runs cmd on a new session and returns its exit status and
@@ -110,3 +114,56 @@ func TestSweepCutsOutOfProcessRevocation(t *testing.T) {
110114 ts.srv.sweepOnce()
111115 waitClosed(t, ts.client)
112116}
117
118// A key that expires while connected: the next exec is refused, and
119// the sweep closes the connection.
120func TestExpiredKeyRefusedAndCut(t *testing.T) {
121 ts := newTestServer(t)
122 past := time.Now().Add(-time.Second).UTC().Format("2006-01-02T15:04:05.000Z")
123 if _, err := ts.st.DB.Exec("UPDATE ssh_keys SET expires_at = ? WHERE id = ?", past, ts.keyID); err != nil {
124 t.Fatal(err)
125 }
126 if code, errOut := execStatus(ts.client, "whoami"); code != 4 || !strings.Contains(errOut, "expired") {
127 t.Fatalf("whoami with an expired key: %d %q", code, errOut)
128 }
129 ts.srv.sweepOnce()
130 waitClosed(t, ts.client)
131}
132
133// An expiring key may not mint.
134func TestExpiringKeyCannotMint(t *testing.T) {
135 ts := newTestServer(t)
136 future := time.Now().Add(time.Hour).UTC().Format("2006-01-02T15:04:05.000Z")
137 if _, err := ts.st.DB.Exec("UPDATE ssh_keys SET expires_at = ? WHERE id = ?", future, ts.keyID); err != nil {
138 t.Fatal(err)
139 }
140 if code, errOut := execStatus(ts.client, "token create --name x"); code != 4 || !strings.Contains(errOut, "expires") {
141 t.Fatalf("token create with an expiring key: %d %q", code, errOut)
142 }
143}
144
145func TestExpiredKeyRefusedAtAuth(t *testing.T) {
146 ts := newTestServer(t)
147 _, priv, err := ed25519.GenerateKey(rand.Reader)
148 if err != nil {
149 t.Fatal(err)
150 }
151 signer, err := ssh.NewSignerFromKey(priv)
152 if err != nil {
153 t.Fatal(err)
154 }
155 pub := signer.PublicKey()
156 past := time.Now().Add(-time.Minute)
157 if err := ts.st.AddSSHKeyFrom(ts.uid, ssh.FingerprintSHA256(pub), pub.Type(), pub.Marshal(), "full", "", store.KeyOrigin{ExpiresAt: &past}); err != nil {
158 t.Fatal(err)
159 }
160 _, err = ssh.Dial("tcp", ts.client.RemoteAddr().String(), &ssh.ClientConfig{
161 User: "git",
162 Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
163 HostKeyCallback: ssh.InsecureIgnoreHostKey(),
164 Timeout: 5 * time.Second,
165 })
166 if err == nil {
167 t.Fatal("an expired key authenticated")
168 }
169}
internal/sshd/sshd.go +9
@@ -162,6 +162,10 @@ func (s *Server) authenticate(meta ssh.ConnMetadata, pub ssh.PublicKey) (*ssh.Pe
162162 s.st.Audit(0, "auth.failed", map[string]any{"ip": ip, "fingerprint": fp})
163163 return nil, fmt.Errorf("unknown key %s", fp)
164164 }
165 if key.Expired(time.Now()) {
166 s.st.Audit(key.UserID, "auth.expired", map[string]any{"ip": ip, "fingerprint": fp})
167 return nil, fmt.Errorf("key %s has expired", fp)
168 }
165169 s.authLimiter.success(ip)
166170 return &ssh.Permissions{Extensions: map[string]string{
167171 "user-id": strconv.FormatInt(key.UserID, 10),
@@ -407,6 +411,10 @@ func (s *Server) runExec(c *conn, sconn *ssh.ServerConn, ch ssh.Channel, term co
407411 fmt.Fprintln(ch.Stderr(), "authentication temporarily unavailable")
408412 return protocol.ExitFailure
409413 }
414 if key.Expired(time.Now()) {
415 fmt.Fprintln(ch.Stderr(), "this key has expired; remove it and add a new one")
416 return protocol.ExitDenied
417 }
410418 user, err := s.st.UserByID(userID)
411419 if err != nil {
412420 fmt.Fprintln(ch.Stderr(), "account no longer exists")
@@ -484,6 +492,7 @@ func Exec(cfg config.Config, st *store.Store, user store.User, key store.SSHKey,
484492 Stderr: stderr,
485493 Done: done,
486494 Stopping: stopping,
495 Expires: key.ExpiresAt,
487496 }
488497 return control.Dispatch(ctx, argv)
489498}