pico

created pr with 84.1 on 2025-12-01T05:25:52Z · by c8ef7d19
added 84.2 on 2025-12-13T00:51:45Z · by c8ef7d19
1: 659964e ! 1: af4cda9 feat: access control using ssh certs
-: ------- > 2: d9a74df chore: logging and cleanup
added 84.3 on 2025-12-13T02:02:13Z · by c8ef7d19
1: af4cda9 < -: ------- feat: access control using ssh certs
2: d9a74df ! 1: bb8ef88 feat: access control using ssh certs
cmds
checkout latest patchset:
ssh pr.pico.sh print 84 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 84.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 84
+2 -1 Makefile #
......@@ -142,10 +142,11 @@ migrate:
142142 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250320_add_tunnel_id_to_tuns_event_logs_table.sql
143143 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250410_add_index_analytics_visits_host_list.sql
144144 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250418_add_project_post_idx_analytics.sql
145+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20251130_add_expires_at_to_public_keys.sql
145146 .PHONY: migrate
146147
147148 latest:
148- $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250418_add_project_post_idx_analytics.sql
149+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20251130_add_expires_at_to_public_keys.sql
149150 .PHONY: latest
150151
151152 psql:
+1 -1 pkg/apps/feeds/ssh.go #
......@@ -46,7 +46,7 @@ func StartSshServer() {
4646 }
4747 handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
4848
49- sshAuth := shared.NewSshAuthHandler(dbh, logger)
49+ sshAuth := shared.NewSshAuthHandler(dbh, logger, "feeds")
5050
5151 // Create a new SSH server
5252 server, err := pssh.NewSSHServerWithConfig(
+1 -1 pkg/apps/pastes/ssh.go #
......@@ -45,7 +45,7 @@ func StartSshServer() {
4545 "fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
4646 }
4747 handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
48- sshAuth := shared.NewSshAuthHandler(dbh, logger)
48+ sshAuth := shared.NewSshAuthHandler(dbh, logger, "pastes")
4949
5050 // Create a new SSH server
5151 server, err := pssh.NewSSHServerWithConfig(
+1 -1 pkg/apps/pgs/ssh.go #
......@@ -34,7 +34,7 @@ func StartSshServer(cfg *PgsConfig, killCh chan error) {
3434 ctx,
3535 )
3636
37- sshAuth := shared.NewSshAuthHandler(cfg.DB, logger)
37+ sshAuth := shared.NewSshAuthHandler(cfg.DB, logger, "pgs")
3838
3939 webTunnel := &tunkit.WebTunnelHandler{
4040 Logger: logger,
+1 -1 pkg/apps/pico/ssh.go #
......@@ -64,7 +64,7 @@ func StartSshServer() {
6464 DBPool: dbpool,
6565 }
6666
67- sshAuth := shared.NewSshAuthHandler(dbpool, logger)
67+ sshAuth := shared.NewSshAuthHandler(dbpool, logger, "pico")
6868
6969 // Create a new SSH server
7070 server, err := pssh.NewSSHServerWithConfig(
+1 -1 pkg/apps/pipe/ssh.go #
......@@ -46,7 +46,7 @@ func StartSshServer() {
4646 Access: syncmap.New[string, []string](),
4747 }
4848
49- sshAuth := shared.NewSshAuthHandler(dbh, logger)
49+ sshAuth := shared.NewSshAuthHandler(dbh, logger, "pipe")
5050
5151 // Create a new SSH server
5252 server, err := pssh.NewSSHServerWithConfig(
+1 -1 pkg/apps/prose/ssh.go #
......@@ -59,7 +59,7 @@ func StartSshServer() {
5959 }
6060 handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
6161
62- sshAuth := shared.NewSshAuthHandler(dbh, logger)
62+ sshAuth := shared.NewSshAuthHandler(dbh, logger, "prose")
6363
6464 // Create a new SSH server
6565 server, err := pssh.NewSSHServerWithConfig(
+9 -0 pkg/db/db.go #
......@@ -36,6 +36,15 @@ type PublicKey struct {
3636 Name string `json:"name" db:"name"`
3737 Key string `json:"public_key" db:"public_key"`
3838 CreatedAt *time.Time `json:"created_at" db:"created_at"`
39+ ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
40+}
41+
42+func (pk *PublicKey) IsValid() bool {
43+ if pk.ExpiresAt == nil {
44+ return true
45+ }
46+ now := time.Now()
47+ return pk.ExpiresAt.After(now)
3948 }
4049
4150 type User struct {
+7 -7 pkg/db/postgres/storage.go #
......@@ -135,11 +135,11 @@ var (
135135 )
136136
137137 const (
138- sqlSelectPublicKey = `SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE public_key = $1`
139- sqlSelectPublicKeys = `SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE user_id = $1 ORDER BY created_at ASC`
138+ sqlSelectPublicKey = `SELECT id, user_id, name, public_key, created_at, expires_at FROM public_keys WHERE public_key = $1`
139+ sqlSelectPublicKeys = `SELECT id, user_id, name, public_key, created_at, expires_at FROM public_keys WHERE user_id = $1 ORDER BY created_at ASC`
140140 sqlSelectUser = `SELECT id, name, created_at FROM app_users WHERE id = $1`
141141 sqlSelectUserForName = `SELECT id, name, created_at FROM app_users WHERE name = $1`
142- sqlSelectUserForNameAndKey = `SELECT app_users.id, app_users.name, app_users.created_at, public_keys.id as pk_id, public_keys.public_key, public_keys.created_at as pk_created_at FROM app_users LEFT JOIN public_keys ON public_keys.user_id = app_users.id WHERE app_users.name = $1 AND public_keys.public_key = $2`
142+ sqlSelectUserForNameAndKey = `SELECT app_users.id, app_users.name, app_users.created_at, public_keys.id as pk_id, public_keys.public_key, public_keys.created_at as pk_created_at, public_keys.expires_at FROM app_users LEFT JOIN public_keys ON public_keys.user_id = app_users.id WHERE app_users.name = $1 AND public_keys.public_key = $2`
143143 sqlSelectUsers = `SELECT id, name, created_at FROM app_users ORDER BY name ASC`
144144
145145 sqlSelectUserForToken = `
......@@ -450,7 +450,7 @@ func (me *PsqlDB) FindPublicKeyForKey(key string) (*db.PublicKey, error) {
450450
451451 for rs.Next() {
452452 pk := &db.PublicKey{}
453- err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt)
453+ err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt)
454454 if err != nil {
455455 return nil, err
456456 }
......@@ -486,7 +486,7 @@ func (me *PsqlDB) FindPublicKey(pubkeyID string) (*db.PublicKey, error) {
486486
487487 for rs.Next() {
488488 pk := &db.PublicKey{}
489- err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt)
489+ err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt)
490490 if err != nil {
491491 return nil, err
492492 }
......@@ -513,7 +513,7 @@ func (me *PsqlDB) FindKeysForUser(user *db.User) ([]*db.PublicKey, error) {
513513 }
514514 for rs.Next() {
515515 pk := &db.PublicKey{}
516- err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt)
516+ err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt)
517517 if err != nil {
518518 return keys, err
519519 }
......@@ -678,7 +678,7 @@ func (me *PsqlDB) FindUserForNameAndKey(name string, key string) (*db.User, erro
678678 pk := &db.PublicKey{}
679679
680680 r := me.Db.QueryRow(sqlSelectUserForNameAndKey, strings.ToLower(name), key)
681- err := r.Scan(&user.ID, &user.Name, &user.CreatedAt, &pk.ID, &pk.Key, &pk.CreatedAt)
681+ err := r.Scan(&user.ID, &user.Name, &user.CreatedAt, &pk.ID, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt)
682682 if err != nil {
683683 return nil, err
684684 }
+52 -9 pkg/shared/ssh.go #
......@@ -4,6 +4,7 @@ import (
44 "fmt"
55 "log/slog"
66 "strings"
7+ "time"
78
89 "github.com/picosh/pico/pkg/db"
910 "github.com/picosh/utils"
......@@ -13,8 +14,9 @@ import (
1314 const adminPrefix = "admin__"
1415
1516 type SshAuthHandler struct {
16- DB AuthFindUser
17- Logger *slog.Logger
17+ DB AuthFindUser
18+ Logger *slog.Logger
19+ Principal string
1820 }
1921
2022 type AuthFindUser interface {
......@@ -23,18 +25,54 @@ type AuthFindUser interface {
2325 FindFeature(userID, name string) (*db.FeatureFlag, error)
2426 }
2527
26-func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger) *SshAuthHandler {
28+func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger, principal string) *SshAuthHandler {
2729 return &SshAuthHandler{
28- DB: dbh,
29- Logger: logger,
30+ DB: dbh,
31+ Logger: logger,
32+ Principal: principal,
3033 }
3134 }
3235
3336 func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
34- pubkey := utils.KeyForKeyText(key)
35- user, err := r.DB.FindUserByPubkey(pubkey)
37+ log := r.Logger
38+ var user *db.User
39+ var err error
40+ pubkey := ""
41+
42+ cert, ok := key.(*ssh.Certificate)
43+ if ok {
44+ if cert.CertType != ssh.UserCert {
45+ return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
46+ }
47+
48+ found := false
49+ for _, princ := range cert.ValidPrincipals {
50+ if princ == "admin" || princ == r.Principal {
51+ found = true
52+ break
53+ }
54+ }
55+ if !found {
56+ return nil, fmt.Errorf("ssh: principals not valid")
57+ }
58+
59+ clock := time.Now
60+ unixNow := clock().Unix()
61+ if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
62+ return nil, fmt.Errorf("ssh: cert is not yet valid")
63+ }
64+ if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) {
65+ return nil, fmt.Errorf("ssh: cert has expired")
66+ }
67+
68+ pubkey = utils.KeyForKeyText(cert.SignatureKey)
69+ } else {
70+ pubkey = utils.KeyForKeyText(key)
71+ }
72+
73+ user, err = r.DB.FindUserByPubkey(pubkey)
3674 if err != nil {
37- r.Logger.Error(
75+ log.Error(
3876 "could not find user for key",
3977 "keyType", key.Type(),
4078 "key", string(key.Marshal()),
......@@ -43,8 +81,13 @@ func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.Public
4381 return nil, err
4482 }
4583
84+ // TODO: fix since we don't always have access to public key record here
85+ if !user.PublicKey.IsValid() {
86+ return nil, fmt.Errorf("public key has been revoked")
87+ }
88+
4689 if user.Name == "" {
47- r.Logger.Error("username is not set")
90+ log.Error("username is not set")
4891 return nil, fmt.Errorf("username is not set")
4992 }
5093
+1 -0 sql/migrations/20251130_add_expires_at_to_public_keys.sql #
......@@ -0,0 +1,1 @@
1+ALTER TABLE public_keys ADD COLUMN expires_at timestamp without time zone NOT NULL DEFAULT NOW();
Back to top