Range-diff rd-160
- title
- feat: access control using ssh certs
- description
-
Patch changed - old #1
af4cda9- new #1
bb8ef88
- title
- chore: logging and cleanup
- description
-
Patch removed - old #2
d9a74df- new #0
(none)
1: af4cda9 ! 1: bb8ef88 feat: access control using ssh certs
old
old:
Makefile
new:Makefile
$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250320_add_tunnel_id_to_tuns_event_logs_table.sql $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250410_add_index_analytics_visits_host_list.sql $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250418_add_project_post_idx_analytics.sql + $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20251130_add_expires_at_to_public_keys.sql .PHONY: migrate latest: - $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250418_add_project_post_idx_analytics.sql + $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20251130_add_expires_at_to_public_keys.sql .PHONY: latest psql:
new
old
old:
pkg/apps/pico/ssh.go
new:pkg/apps/pico/ssh.go
DBPool: dbpool, } - sshAuth := shared.NewSshAuthHandler(dbpool, logger) + sshAuth := shared.NewSshAuthHandler(dbpool, logger, "pico") // Create a new SSH server server, err := pssh.NewSSHServerWithConfig(
new
old:
pkg/apps/pico/ssh.go
new:pkg/apps/pico/ssh.go
DBPool: dbpool, } - sshAuth := shared.NewSshAuthHandler(dbpool, logger) + sshAuth := shared.NewSshAuthHandler(dbpool, logger, "pico") // Create a new SSH server server, err := pssh.NewSSHServerWithConfig( 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{
old
old:
pkg/db/db.go
new:pkg/db/db.go
Name string `json:"name" db:"name"` Key string `json:"public_key" db:"public_key"` CreatedAt *time.Time `json:"created_at" db:"created_at"` + ExpiresAt *time.Time `json:"expires_at" db:"expires_at"` +} + +func (pk *PublicKey) IsValid() bool { + if pk.ExpiresAt == nil { + return true + } + now := time.Now() + return pk.ExpiresAt.After(now) } type User struct {
new
old
old:
pkg/db/postgres/storage.go
new:pkg/db/postgres/storage.go
) const ( - sqlSelectPublicKey = `SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE public_key = $1` - sqlSelectPublicKeys = `SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE user_id = $1 ORDER BY created_at ASC` + sqlSelectPublicKey = `SELECT id, user_id, name, public_key, created_at, expires_at FROM public_keys WHERE public_key = $1` + sqlSelectPublicKeys = `SELECT id, user_id, name, public_key, created_at, expires_at FROM public_keys WHERE user_id = $1 ORDER BY created_at ASC` sqlSelectUser = `SELECT id, name, created_at FROM app_users WHERE id = $1` sqlSelectUserForName = `SELECT id, name, created_at FROM app_users WHERE name = $1` - 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` + 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` sqlSelectUsers = `SELECT id, name, created_at FROM app_users ORDER BY name ASC` sqlSelectUserForToken = ` for rs.Next() { pk := &db.PublicKey{} - err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt) + err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt) if err != nil { return nil, err } for rs.Next() { pk := &db.PublicKey{} - err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt) + err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt) if err != nil { return nil, err } } for rs.Next() { pk := &db.PublicKey{} - err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt) + err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt) if err != nil { return keys, err } pk := &db.PublicKey{} r := me.Db.QueryRow(sqlSelectUserForNameAndKey, strings.ToLower(name), key) - err := r.Scan(&user.ID, &user.Name, &user.CreatedAt, &pk.ID, &pk.Key, &pk.CreatedAt) + err := r.Scan(&user.ID, &user.Name, &user.CreatedAt, &pk.ID, &pk.Key, &pk.CreatedAt, &pk.ExpiresAt) if err != nil { return nil, err }
new
old
old:
pkg/shared/ssh.go
new:pkg/shared/ssh.go
"fmt" "log/slog" "strings" + "time" "github.com/picosh/pico/pkg/db" "github.com/picosh/utils" const adminPrefix = "admin__" type SshAuthHandler struct { - DB AuthFindUser - Logger *slog.Logger + DB AuthFindUser + Logger *slog.Logger + Principal string } 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: 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()), return nil, err } + // TODO: fix since we don't always have access to public key record here + if !user.PublicKey.IsValid() { + return nil, fmt.Errorf("public key has been revoked") + } + if user.Name == "" { - r.Logger.Error("username is not set") + log.Error("username is not set") return nil, fmt.Errorf("username is not set") }
new
old:
pkg/shared/ssh.go
new:pkg/shared/ssh.go
"fmt" "log/slog" "strings" + "time" "github.com/picosh/pico/pkg/db" "github.com/picosh/utils" const adminPrefix = "admin__" type SshAuthHandler struct { - DB AuthFindUser - Logger *slog.Logger + DB AuthFindUser + Logger *slog.Logger + Principal string } 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()), } if user.Name == "" { - r.Logger.Error("username is not set") + log.Error("username is not set") return nil, fmt.Errorf("username is not set") }
old
new:
sql/migrations/20251130_add_expires_at_to_public_keys.sql
+ALTER TABLE public_keys ADD COLUMN expires_at timestamp without time zone NOT NULL DEFAULT NOW();
new
2: d9a74df < -: ------- chore: logging and cleanup
old
old:
pkg/apps/pico/ssh.go
new:pkg/apps/pico/ssh.go
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{
new
old
old:
pkg/shared/ssh.go
new:pkg/shared/ssh.go
cert, ok := key.(*ssh.Certificate) if ok { if cert.CertType != ssh.UserCert { - return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType) + return nil, fmt.Errorf("ssh-cert has type %d", cert.CertType) } found := false } } if !found { - return nil, fmt.Errorf("ssh: principals not valid") + 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") + 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") + return nil, fmt.Errorf("ssh-cert has expired") } pubkey = utils.KeyForKeyText(cert.SignatureKey) } // TODO: fix since we don't always have access to public key record here - if !user.PublicKey.IsValid() { - return nil, fmt.Errorf("public key has been revoked") - } + // if !user.PublicKey.IsValid() { + // return nil, fmt.Errorf("public key has been revoked") + // } if user.Name == "" { log.Error("username is not set")