dashboard / erock/pico / feat: access control using ssh certs #84 rss

accepted · opened on 2025-12-01T05:25:52Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-84 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 84
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 84
accept PR:
ssh pr.pico.sh pr accept 84
close PR:
ssh pr.pico.sh pr close 84
Timeline Patchsets
+1 -1 pkg/apps/feeds/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/pkg/apps/feeds/ssh.go b/pkg/apps/feeds/ssh.go
index 37ed52a..548f1a8 100644
--- a/pkg/apps/feeds/ssh.go
+++ b/pkg/apps/feeds/ssh.go
@@ -46,7 +46,7 @@ func StartSshServer() {
 	}
 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 
-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
+	sshAuth := shared.NewSshAuthHandler(dbh, logger, "feeds")
 
 	// Create a new SSH server
 	server, err := pssh.NewSSHServerWithConfig(
+1 -1 pkg/apps/pastes/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/pkg/apps/pastes/ssh.go b/pkg/apps/pastes/ssh.go
index 71ed1b8..86a6f42 100644
--- a/pkg/apps/pastes/ssh.go
+++ b/pkg/apps/pastes/ssh.go
@@ -45,7 +45,7 @@ func StartSshServer() {
 		"fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 	}
 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
+	sshAuth := shared.NewSshAuthHandler(dbh, logger, "pastes")
 
 	// Create a new SSH server
 	server, err := pssh.NewSSHServerWithConfig(
+1 -1 pkg/apps/pgs/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/pkg/apps/pgs/ssh.go b/pkg/apps/pgs/ssh.go
index 30ddbeb..98d302d 100644
--- a/pkg/apps/pgs/ssh.go
+++ b/pkg/apps/pgs/ssh.go
@@ -34,7 +34,7 @@ func StartSshServer(cfg *PgsConfig, killCh chan error) {
 		ctx,
 	)
 
-	sshAuth := shared.NewSshAuthHandler(cfg.DB, logger)
+	sshAuth := shared.NewSshAuthHandler(cfg.DB, logger, "pgs")
 
 	webTunnel := &tunkit.WebTunnelHandler{
 		Logger:      logger,
+3 -2 pkg/apps/pico/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
diff --git a/pkg/apps/pico/ssh.go b/pkg/apps/pico/ssh.go
index 2050e28..8794470 100644
--- a/pkg/apps/pico/ssh.go
+++ b/pkg/apps/pico/ssh.go
@@ -64,7 +64,7 @@ func StartSshServer() {
 		DBPool: dbpool,
 	}
 
-	sshAuth := shared.NewSshAuthHandler(dbpool, logger)
+	sshAuth := shared.NewSshAuthHandler(dbpool, logger, "pico")
 
 	// Create a new SSH server
 	server, err := pssh.NewSSHServerWithConfig(
@@ -76,7 +76,8 @@ func StartSshServer() {
 		promPort,
 		"ssh_data/term_info_ed25519",
 		func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
-			perms, _ := sshAuth.PubkeyAuthHandler(conn, key)
+			perms, err := sshAuth.PubkeyAuthHandler(conn, key)
+			logger.Warn("pubkey auth handler", "err", err)
 			if perms == nil {
 				perms = &ssh.Permissions{
 					Extensions: map[string]string{
+1 -1 pkg/apps/pipe/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/pkg/apps/pipe/ssh.go b/pkg/apps/pipe/ssh.go
index 9ddf122..af7312f 100644
--- a/pkg/apps/pipe/ssh.go
+++ b/pkg/apps/pipe/ssh.go
@@ -46,7 +46,7 @@ func StartSshServer() {
 		Access:  syncmap.New[string, []string](),
 	}
 
-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
+	sshAuth := shared.NewSshAuthHandler(dbh, logger, "pipe")
 
 	// Create a new SSH server
 	server, err := pssh.NewSSHServerWithConfig(
+1 -1 pkg/apps/prose/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/pkg/apps/prose/ssh.go b/pkg/apps/prose/ssh.go
index 94dd7b9..709f8d8 100644
--- a/pkg/apps/prose/ssh.go
+++ b/pkg/apps/prose/ssh.go
@@ -59,7 +59,7 @@ func StartSshServer() {
 	}
 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 
-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
+	sshAuth := shared.NewSshAuthHandler(dbh, logger, "prose")
 
 	// Create a new SSH server
 	server, err := pssh.NewSSHServerWithConfig(
+47 -9 pkg/shared/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
diff --git a/pkg/shared/ssh.go b/pkg/shared/ssh.go
index 30840c5..234c0bc 100644
--- a/pkg/shared/ssh.go
+++ b/pkg/shared/ssh.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"log/slog"
 	"strings"
+	"time"
 
 	"github.com/picosh/pico/pkg/db"
 	"github.com/picosh/utils"
@@ -13,8 +14,9 @@ import (
 const adminPrefix = "admin__"
 
 type SshAuthHandler struct {
-	DB     AuthFindUser
-	Logger *slog.Logger
+	DB        AuthFindUser
+	Logger    *slog.Logger
+	Principal string
 }
 
 type AuthFindUser interface {
@@ -23,18 +25,54 @@ type AuthFindUser interface {
 	FindFeature(userID, name string) (*db.FeatureFlag, error)
 }
 
-func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger) *SshAuthHandler {
+func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger, principal string) *SshAuthHandler {
 	return &SshAuthHandler{
-		DB:     dbh,
-		Logger: logger,
+		DB:        dbh,
+		Logger:    logger,
+		Principal: principal,
 	}
 }
 
 func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
-	pubkey := utils.KeyForKeyText(key)
-	user, err := r.DB.FindUserByPubkey(pubkey)
+	log := r.Logger
+	var user *db.User
+	var err error
+	pubkey := ""
+
+	cert, ok := key.(*ssh.Certificate)
+	if ok {
+		if cert.CertType != ssh.UserCert {
+			return nil, fmt.Errorf("ssh-cert has type %d", cert.CertType)
+		}
+
+		found := false
+		for _, princ := range cert.ValidPrincipals {
+			if princ == "admin" || princ == r.Principal {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return nil, fmt.Errorf("ssh-cert principals not valid")
+		}
+
+		clock := time.Now
+		unixNow := clock().Unix()
+		if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
+			return nil, fmt.Errorf("ssh-cert is not yet valid")
+		}
+		if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) {
+			return nil, fmt.Errorf("ssh-cert has expired")
+		}
+
+		pubkey = utils.KeyForKeyText(cert.SignatureKey)
+	} else {
+		pubkey = utils.KeyForKeyText(key)
+	}
+
+	user, err = r.DB.FindUserByPubkey(pubkey)
 	if err != nil {
-		r.Logger.Error(
+		log.Error(
 			"could not find user for key",
 			"keyType", key.Type(),
 			"key", string(key.Marshal()),
@@ -44,7 +82,7 @@ func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.Public
 	}
 
 	if user.Name == "" {
-		r.Logger.Error("username is not set")
+		log.Error("username is not set")
 		return nil, fmt.Errorf("username is not set")
 	}