Compare commits

...

12 Commits
develop ... v2

Author SHA1 Message Date
Matthew Penner
ee280f1deb
Merge branch 'develop' into v2 2022-04-01 09:48:24 -06:00
Dane Everitt
355c10c1e4 Update lockfile 2022-02-13 13:17:26 -05:00
Dane Everitt
04933c153b Merge branch 'develop' into v2 2022-02-13 13:10:36 -05:00
Matthew Penner
f1344f1a82
Merge branch 'develop' into v2 2022-01-18 13:12:09 -07:00
Matthew Penner
d874af85db
tweaks to websocket event handling 2021-11-15 10:13:31 -07:00
Matthew Penner
54b6033392
update dependencies 2021-11-15 10:13:19 -07:00
Matthew Penner
10a2ffc0a7
Merge branch 'develop' into v2 2021-10-28 23:58:12 -06:00
Matthew Penner
ede1cdc76f
Merge branch 'develop' into v2 2021-10-05 19:01:34 -06:00
Dane Everitt
9e98287172 Merge branch 'develop' into v2 2021-09-19 11:17:05 -07:00
Matthew Penner
6653466ca8
Merge branch 'develop' into v2 2021-09-01 16:33:32 -06:00
Matthew Penner
f54a736353 add support for systemd notify 2021-08-02 15:17:22 -06:00
Matthew Penner
d3360f0fd9 sftp: add support for ssh keys 2021-08-02 15:17:22 -06:00
8 changed files with 353 additions and 79 deletions

View File

@ -9,11 +9,13 @@ import (
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"os/signal"
"path" "path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"github.com/NYTimes/logrotate" "github.com/NYTimes/logrotate"
@ -28,6 +30,7 @@ import (
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/internal/notify"
"github.com/pterodactyl/wings/loggers/cli" "github.com/pterodactyl/wings/loggers/cli"
"github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/router" "github.com/pterodactyl/wings/router"
@ -324,43 +327,57 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
} }
// Check if the server should run with TLS but using autocert. // Check if the server should run with TLS but using autocert.
if autotls { go func(s *http.Server, api config.ApiConfiguration, sys config.SystemConfiguration, autotls bool, tlshostname string) {
m := autocert.Manager{ if autotls {
Prompt: autocert.AcceptTOS, m := autocert.Manager{
Cache: autocert.DirCache(path.Join(sys.RootDirectory, "/.tls-cache")), Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(tlshostname), Cache: autocert.DirCache(path.Join(sys.RootDirectory, "/.tls-cache")),
} HostPolicy: autocert.HostWhitelist(tlshostname),
log.WithField("hostname", tlshostname).Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt")
// Hook autocert into the main http server.
s.TLSConfig.GetCertificate = m.GetCertificate
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, acme.ALPNProto) // enable tls-alpn ACME challenges
// Start the autocert server.
go func() {
if err := http.ListenAndServe(":http", m.HTTPHandler(nil)); err != nil {
log.WithError(err).Error("failed to serve autocert http server")
} }
}()
// Start the main http server with TLS using autocert. log.WithField("hostname", tlshostname).Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt")
if err := s.ListenAndServeTLS("", ""); err != nil {
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlshostname, "error": err}).Fatal("failed to configure HTTP server using auto-tls") // Hook autocert into the main http server.
s.TLSConfig.GetCertificate = m.GetCertificate
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, acme.ALPNProto) // enable tls-alpn ACME challenges
// Start the autocert server.
go func() {
if err := http.ListenAndServe(":http", m.HTTPHandler(nil)); err != nil {
log.WithError(err).Error("failed to serve autocert http server")
}
}()
// Start the main http server with TLS using autocert.
if err := s.ListenAndServeTLS("", ""); err != nil {
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlshostname, "error": err}).Fatal("failed to configure HTTP server using auto-tls")
}
return
} }
return
// Check if main http server should run with TLS. Otherwise reset the TLS
// config on the server and then serve it over normal HTTP.
if api.Ssl.Enabled {
if err := s.ListenAndServeTLS(api.Ssl.CertificateFile, api.Ssl.KeyFile); err != nil {
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
}
return
}
s.TLSConfig = nil
if err := s.ListenAndServe(); err != nil {
log.WithField("error", err).Fatal("failed to configure HTTP server")
}
}(s, api, sys, autotls, tlshostname)
if err := notify.Readiness(); err != nil {
log.WithField("error", err).Error("failed to notify systemd of readiness state")
} }
// Check if main http server should run with TLS. Otherwise reset the TLS c := make(chan os.Signal, 1)
// config on the server and then serve it over normal HTTP. signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
if api.Ssl.Enabled { <-c
if err := s.ListenAndServeTLS(api.Ssl.CertificateFile, api.Ssl.KeyFile); err != nil {
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server") if err := notify.Stopping(); err != nil {
} log.WithField("error", err).Error("failed to notify systemd of stopping state")
return
}
s.TLSConfig = nil
if err := s.ListenAndServe(); err != nil {
log.WithField("error", err).Fatal("failed to configure HTTP server")
} }
} }

19
internal/notify/notify.go Normal file
View File

@ -0,0 +1,19 @@
// Package notify handles notifying the operating system of the program's state.
//
// For linux based operating systems, this is done through the systemd socket
// set by "NOTIFY_SOCKET" environment variable.
//
// Currently, no other operating systems are supported.
package notify
func Readiness() error {
return readiness()
}
func Reloading() error {
return reloading()
}
func Stopping() error {
return stopping()
}

View File

@ -0,0 +1,48 @@
package notify
import (
"io"
"net"
"os"
"strings"
)
func notify(path string, r io.Reader) error {
s := &net.UnixAddr{
Name: path,
Net: "unixgram",
}
c, err := net.DialUnix(s.Net, nil, s)
if err != nil {
return err
}
defer c.Close()
if _, err := io.Copy(c, r); err != nil {
return err
}
return nil
}
func socketNotify(payload string) error {
v, ok := os.LookupEnv("NOTIFY_SOCKET")
if !ok || v == "" {
return nil
}
if err := notify(v, strings.NewReader(payload)); err != nil {
return err
}
return nil
}
func readiness() error {
return socketNotify("READY=1")
}
func reloading() error {
return socketNotify("RELOADING=1")
}
func stopping() error {
return socketNotify("STOPPING=1")
}

View File

@ -0,0 +1,16 @@
//go:build !linux
// +build !linux
package notify
func readiness() error {
return nil
}
func reloading() error {
return nil
}
func stopping() error {
return nil
}

View File

@ -71,6 +71,7 @@ type SftpAuthRequest struct {
IP string `json:"ip"` IP string `json:"ip"`
SessionID []byte `json:"session_id"` SessionID []byte `json:"session_id"`
ClientVersion []byte `json:"client_version"` ClientVersion []byte `json:"client_version"`
Type string `json:"type"`
} }
// SftpAuthResponse is returned by the Panel when a pair of SFTP credentials // SftpAuthResponse is returned by the Panel when a pair of SFTP credentials
@ -78,8 +79,8 @@ type SftpAuthRequest struct {
// matched as well as the permissions that are assigned to the authenticated // matched as well as the permissions that are assigned to the authenticated
// user for the SFTP subsystem. // user for the SFTP subsystem.
type SftpAuthResponse struct { type SftpAuthResponse struct {
SSHKeys []string `json:"ssh_keys"`
Server string `json:"server"` Server string `json:"server"`
Token string `json:"token"`
Permissions []string `json:"permissions"` Permissions []string `json:"permissions"`
} }

View File

@ -164,6 +164,5 @@ func (h *Handler) listenForServerEvents(ctx context.Context) error {
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil return nil
} }

View File

@ -125,7 +125,7 @@ func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) {
return f, nil return f, nil
} }
// Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading // Filecmd handler for basic SFTP system calls related to files, but not anything to do with reading
// or writing to those files. // or writing to those files.
func (h *Handler) Filecmd(request *sftp.Request) error { func (h *Handler) Filecmd(request *sftp.Request) error {
if h.ro { if h.ro {

View File

@ -1,11 +1,17 @@
package sftp package sftp
import ( import (
"bytes"
"context" "context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"io" "io"
"io/ioutil"
"net" "net"
"os" "os"
"path" "path"
@ -16,7 +22,6 @@ import (
"emperror.dev/errors" "emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
@ -51,37 +56,28 @@ func New(m *server.Manager) *SFTPServer {
// SFTP connections. This will automatically generate an ED25519 key if one does // SFTP connections. This will automatically generate an ED25519 key if one does
// not already exist on the system for host key verification purposes. // not already exist on the system for host key verification purposes.
func (c *SFTPServer) Run() error { func (c *SFTPServer) Run() error {
if _, err := os.Stat(c.PrivateKeyPath()); os.IsNotExist(err) { keys, err := c.loadPrivateKeys()
if err := c.generateED25519PrivateKey(); err != nil {
return err
}
} else if err != nil {
return errors.Wrap(err, "sftp: could not stat private key file")
}
pb, err := os.ReadFile(c.PrivateKeyPath())
if err != nil {
return errors.Wrap(err, "sftp: could not read private key file")
}
private, err := ssh.ParsePrivateKey(pb)
if err != nil { if err != nil {
return err return err
} }
conf := &ssh.ServerConfig{ conf := &ssh.ServerConfig{
NoClientAuth: false, NoClientAuth: false,
MaxAuthTries: 6, MaxAuthTries: 6,
PasswordCallback: c.passwordCallback, PasswordCallback: c.passwordCallback,
PublicKeyCallback: c.publicKeyCallback,
}
for _, k := range keys {
conf.AddHostKey(k)
} }
conf.AddHostKey(private)
listener, err := net.Listen("tcp", c.Listen) listener, err := net.Listen("tcp", c.Listen)
if err != nil { if err != nil {
return err return err
} }
public := string(ssh.MarshalAuthorizedKey(private.PublicKey())) log.WithField("listen", c.Listen).Info("sftp server listening for connections")
log.WithField("listen", c.Listen).WithField("public_key", strings.Trim(public, "\n")).Info("sftp server listening for connections")
for { for {
if conn, _ := listener.Accept(); conn != nil { if conn, _ := listener.Accept(); conn != nil {
go func(conn net.Conn) { go func(conn net.Conn) {
@ -107,7 +103,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
// If its not a session channel we just move on because its not something we // If its not a session channel we just move on because its not something we
// know how to handle at this point. // know how to handle at this point.
if ch.ChannelType() != "session" { if ch.ChannelType() != "session" {
ch.Reject(ssh.UnknownChannelType, "unknown channel type") _ = ch.Reject(ssh.UnknownChannelType, "unknown channel type")
continue continue
} }
@ -121,7 +117,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
// Channels have a type that is dependent on the protocol. For SFTP // Channels have a type that is dependent on the protocol. For SFTP
// this is "subsystem" with a payload that (should) be "sftp". Discard // this is "subsystem" with a payload that (should) be "sftp". Discard
// anything else we receive ("pty", "shell", etc) // anything else we receive ("pty", "shell", etc)
req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil) _ = req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
} }
}(requests) }(requests)
@ -139,6 +135,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
return s.ID() == uuid return s.ID() == uuid
}) })
if srv == nil { if srv == nil {
_ = conn.Close()
continue continue
} }
@ -146,37 +143,160 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
// them access to the underlying filesystem. // them access to the underlying filesystem.
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv).Handlers()) handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv).Handlers())
if err := handler.Serve(); err == io.EOF { if err := handler.Serve(); err == io.EOF {
handler.Close() _ = handler.Close()
} }
} }
} }
// Generates a new ED25519 private key that is used for host authentication when func (c *SFTPServer) loadPrivateKeys() ([]ssh.Signer, error) {
// a user connects to the SFTP server. if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_rsa")); err != nil {
func (c *SFTPServer) generateED25519PrivateKey() error { if !os.IsNotExist(err) {
_, priv, err := ed25519.GenerateKey(rand.Reader) return nil, err
if err != nil { }
return errors.Wrap(err, "sftp: failed to generate ED25519 private key")
if err := c.generateRSAPrivateKey(); err != nil {
return nil, err
}
} }
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath()), 0o755); err != nil { rsaBytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_rsa"))
return errors.Wrap(err, "sftp: could not create internal sftp data directory")
}
o, err := os.OpenFile(c.PrivateKeyPath(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil { if err != nil {
return errors.WithStack(err) return nil, errors.Wrap(err, "sftp/server: could not read private key file")
}
rsaPrivateKey, err := ssh.ParsePrivateKey(rsaBytes)
if err != nil {
return nil, err
}
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_ecdsa")); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err := c.generateECDSAPrivateKey(); err != nil {
return nil, err
}
}
ecdsaBytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_ecdsa"))
if err != nil {
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
}
ecdsaPrivateKey, err := ssh.ParsePrivateKey(ecdsaBytes)
if err != nil {
return nil, err
}
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_ed25519")); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err := c.generateEd25519PrivateKey(); err != nil {
return nil, err
}
}
ed25519Bytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_ed25519"))
if err != nil {
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
}
ed25519PrivateKey, err := ssh.ParsePrivateKey(ed25519Bytes)
if err != nil {
return nil, err
}
return []ssh.Signer{
rsaPrivateKey,
ecdsaPrivateKey,
ed25519PrivateKey,
}, nil
}
// generateRSAPrivateKey generates a RSA-4096 private key that will be used by the SFTP server.
func (c *SFTPServer) generateRSAPrivateKey() error {
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath("rsa")), 0o755); err != nil {
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
}
o, err := os.OpenFile(c.PrivateKeyPath("rsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
} }
defer o.Close() defer o.Close()
b, err := x509.MarshalPKCS8PrivateKey(priv) if err := pem.Encode(o, &pem.Block{
if err != nil { Type: "RSA PRIVATE KEY",
return errors.Wrap(err, "sftp: failed to marshal private key into bytes") Bytes: x509.MarshalPKCS1PrivateKey(key),
} }); err != nil {
if err := pem.Encode(o, &pem.Block{Type: "PRIVATE KEY", Bytes: b}); err != nil { return err
return errors.Wrap(err, "sftp: failed to write ED25519 private key to disk")
} }
return nil return nil
} }
// generateECDSAPrivateKey generates a ECDSA-P256 private key that will be used by the SFTP server.
func (c *SFTPServer) generateECDSAPrivateKey() error {
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
return err
}
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath("ecdsa")), 0o755); err != nil {
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
}
o, err := os.OpenFile(c.PrivateKeyPath("ecdsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
defer o.Close()
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
if err := pem.Encode(o, &pem.Block{
Type: "PRIVATE KEY",
Bytes: privBytes,
}); err != nil {
return err
}
return nil
}
// generateEd25519PrivateKey generates an ed25519 private key that will be used by the SFTP server.
func (c *SFTPServer) generateEd25519PrivateKey() error {
_, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return err
}
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath("ed25519")), 0o755); err != nil {
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
}
o, err := os.OpenFile(c.PrivateKeyPath("ed25519"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
defer o.Close()
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
if err := pem.Encode(o, &pem.Block{
Type: "PRIVATE KEY",
Bytes: privBytes,
}); err != nil {
return err
}
return nil
}
// PrivateKeyPath returns the path the host private key for this server instance.
func (c *SFTPServer) PrivateKeyPath(name string) string {
return path.Join(c.BasePath, ".sftp", "id_"+name)
}
// A function capable of validating user credentials with the Panel API. // A function capable of validating user credentials with the Panel API.
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
request := remote.SftpAuthRequest{ request := remote.SftpAuthRequest{
@ -185,6 +305,7 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
IP: conn.RemoteAddr().String(), IP: conn.RemoteAddr().String(),
SessionID: conn.SessionID(), SessionID: conn.SessionID(),
ClientVersion: conn.ClientVersion(), ClientVersion: conn.ClientVersion(),
Type: "password",
} }
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()}) logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
@ -195,6 +316,11 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
return nil, &remote.SftpInvalidCredentialsError{} return nil, &remote.SftpInvalidCredentialsError{}
} }
if len(pass) < 1 {
logger.Warn("failed to validate user credentials (invalid format)")
return nil, &remote.SftpInvalidCredentialsError{}
}
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request) resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
if err != nil { if err != nil {
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok { if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
@ -217,7 +343,55 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
return sshPerm, nil return sshPerm, nil
} }
// PrivateKeyPath returns the path the host private key for this server instance. func (c *SFTPServer) publicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
func (c *SFTPServer) PrivateKeyPath() string { request := remote.SftpAuthRequest{
return path.Join(c.BasePath, ".sftp/id_ed25519") User: conn.User(),
Pass: "KEKW",
IP: conn.RemoteAddr().String(),
SessionID: conn.SessionID(),
ClientVersion: conn.ClientVersion(),
Type: "publicKey",
}
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
logger.Debug("validating public key for SFTP connection")
if !validUsernameRegexp.MatchString(request.User) {
logger.Warn("failed to validate user credentials (invalid format)")
return nil, &remote.SftpInvalidCredentialsError{}
}
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
if err != nil {
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
logger.Warn("failed to validate user credentials (invalid username or password)")
} else {
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
}
return nil, err
}
if len(resp.SSHKeys) < 1 {
return nil, &remote.SftpInvalidCredentialsError{}
}
for _, k := range resp.SSHKeys {
storedPublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
if err != nil {
return nil, err
}
if !bytes.Equal(key.Marshal(), storedPublicKey.Marshal()) {
continue
}
return &ssh.Permissions{
Extensions: map[string]string{
"uuid": resp.Server,
"user": conn.User(),
"permissions": strings.Join(resp.Permissions, ","),
},
}, nil
}
return nil, &remote.SftpInvalidCredentialsError{}
} }