From df175da330966b1c96c31698f41d4d7f91518d42 Mon Sep 17 00:00:00 2001 From: Bolke de Bruin <bolke@xs4all.nl> Date: Wed, 12 Oct 2022 16:32:05 +0200 Subject: [PATCH] Add kdcproxy to support spnego --- cmd/rdpgw/config/configuration.go | 16 ++- cmd/rdpgw/kdcproxy/proxy.go | 157 ++++++++++++++++++++++++++++++ cmd/rdpgw/main.go | 17 ++++ cmd/rdpgw/protocol/gateway.go | 4 +- go.mod | 16 ++- 5 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 cmd/rdpgw/kdcproxy/proxy.go diff --git a/cmd/rdpgw/config/configuration.go b/cmd/rdpgw/config/configuration.go index 673a9eb..92d08eb 100644 --- a/cmd/rdpgw/config/configuration.go +++ b/cmd/rdpgw/config/configuration.go @@ -21,13 +21,15 @@ const ( SessionStoreCookie = "cookie" SessionStoreFile = "file" - AuthenticationOpenId = "openid" - AuthenticationBasic = "local" + AuthenticationOpenId = "openid" + AuthenticationBasic = "local" + AuthenticationKerberos = "kerberos" ) type Configuration struct { Server ServerConfig `koanf:"server"` OpenId OpenIDConfig `koanf:"openid"` + Kerberos KerberosConfig `koanf:"kerberos"` Caps RDGCapsConfig `koanf:"caps"` Security SecurityConfig `koanf:"security"` Client ClientConfig `koanf:"client"` @@ -50,6 +52,11 @@ type ServerConfig struct { AuthSocket string `koanf:"authsocket"` } +type KerberosConfig struct { + Keytab string `koanf:"keytab"` + Krb5Conf string `koanf:"krb5conf"` +} + type OpenIDConfig struct { ProviderUrl string `koanf:"providerurl"` ClientId string `koanf:"clientid"` @@ -161,6 +168,7 @@ func Load(configFile string) Configuration { k.UnmarshalWithConf("Caps", &Conf.Caps, koanfTag) k.UnmarshalWithConf("Security", &Conf.Security, koanfTag) k.UnmarshalWithConf("Client", &Conf.Client, koanfTag) + k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag) if len(Conf.Security.PAATokenEncryptionKey) != 32 { Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32) @@ -206,6 +214,10 @@ func Load(configFile string) Configuration { log.Fatalf("openid is configured but tokenauth disabled") } + if Conf.Server.Authentication == AuthenticationKerberos && Conf.Kerberos.Keytab == "" { + log.Fatalf("kerberos is configured but no keytab was specified") + } + // prepend '//' if required for URL parsing if !strings.Contains(Conf.Server.GatewayAddress, "//") { Conf.Server.GatewayAddress = "//" + Conf.Server.GatewayAddress diff --git a/cmd/rdpgw/kdcproxy/proxy.go b/cmd/rdpgw/kdcproxy/proxy.go new file mode 100644 index 0000000..86e796e --- /dev/null +++ b/cmd/rdpgw/kdcproxy/proxy.go @@ -0,0 +1,157 @@ +package kdcproxy + +import ( + "fmt" + krbconfig "github.com/bolkedebruin/gokrb5/v8/config" + "github.com/jcmturner/gofork/encoding/asn1" + "io" + "log" + "net" + "net/http" + "time" +) + +const ( + maxLength = 128 * 1024 + systemConfigPath = "/etc/krb5.conf" + timeout = 5 * time.Second +) + +type KdcProxyMsg struct { + Message []byte `asn1:"tag:0,explicit"` + Realm string `asn1:"tag:1,optional"` + Flags int `asn1:"tag:2,optional"` +} + +type KerberosProxy struct { + krb5Config *krbconfig.Config +} + +func InitKdcProxy(krb5Conf string) KerberosProxy { + path := systemConfigPath + if krb5Conf != "" { + path = krb5Conf + } + cfg, err := krbconfig.Load(path) + if err != nil { + log.Fatalf("Cannot load krb5 config %s due to %s", path, err) + } + + return KerberosProxy{ + krb5Config: cfg, + } +} + +func (k KerberosProxy) Handler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + length := r.ContentLength + if length == -1 { + http.Error(w, "Content length required", http.StatusLengthRequired) + return + } + + if length > maxLength { + http.Error(w, "Request entity too large", http.StatusRequestEntityTooLarge) + return + } + + data := make([]byte, length) + _, err := io.ReadFull(r.Body, data) + if err != nil { + log.Printf("Error reading from stream: %s", err) + http.Error(w, "Error reading from stream", http.StatusInternalServerError) + return + } + + msg, err := decode(data) + if err != nil { + log.Printf("Cannot unmarshal: %s", err) + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + krb5resp, err := k.forward(msg.Realm, msg.Message) + if err != nil { + log.Printf("cannot forward to kdc due to %s", err) + http.Error(w, "Service unavailable", http.StatusServiceUnavailable) + return + } + + reply, err := encode(krb5resp) + if err != nil { + log.Printf("unable to encode krb5 message due to %s", err) + http.Error(w, "encoding error", http.StatusInternalServerError) + } + + w.Header().Set("Content-Type", "application/kerberos") + w.Write(reply) +} + +func (k *KerberosProxy) forward(realm string, data []byte) (resp []byte, err error) { + if realm == "" { + realm = k.krb5Config.LibDefaults.DefaultRealm + } + + // load udp first as is the default for kerberos + c, kdcs, err := k.krb5Config.GetKDCs(realm, false) + if err != nil || c < 1 { + return nil, fmt.Errorf("cannot get kdc for realm %s due to %s", realm, err) + } + + for i := range kdcs { + conn, err := net.Dial("tcp", kdcs[i]) + if err != nil { + log.Printf("error connecting to %s due to %s, trying next if available", kdcs[i], err) + continue + } + conn.SetDeadline(time.Now().Add(timeout)) + + _, err = conn.Write(data) + if err != nil { + log.Printf("cannot write packet data to %s due to %s, trying next if available", kdcs[i], err) + conn.Close() + continue + } + + // todo check header + resp, err = io.ReadAll(conn) + if err != nil { + log.Printf("error reading from kdc %s due to %s, trying next if available", kdcs[i], err) + conn.Close() + continue + } + conn.Close() + + return resp, nil + } + + return nil, fmt.Errorf("no kdcs found for realm %s", realm) +} + +func decode(data []byte) (msg *KdcProxyMsg, err error) { + var m KdcProxyMsg + rest, err := asn1.Unmarshal(data, &m) + if err != nil { + return nil, err + } + + if len(rest) > 0 { + return nil, fmt.Errorf("trailing data in request") + } + + return &m, nil +} + +func encode(krb5data []byte) (r []byte, err error) { + m := KdcProxyMsg{Message: krb5data} + enc, err := asn1.Marshal(m) + if err != nil { + log.Printf("cannot marshal due to %s", err) + return nil, err + } + return enc, nil +} diff --git a/cmd/rdpgw/main.go b/cmd/rdpgw/main.go index 4ab1d75..6d91b79 100644 --- a/cmd/rdpgw/main.go +++ b/cmd/rdpgw/main.go @@ -4,8 +4,12 @@ import ( "context" "crypto/tls" "fmt" + "github.com/bolkedebruin/gokrb5/v8/keytab" + "github.com/bolkedebruin/gokrb5/v8/service" + "github.com/bolkedebruin/gokrb5/v8/spnego" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/common" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/config" + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/kdcproxy" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/security" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/web" @@ -204,6 +208,19 @@ func main() { if conf.Server.Authentication == config.AuthenticationBasic { h := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket} http.Handle("/remoteDesktopGateway/", common.EnrichContext(h.BasicAuth(gw.HandleGatewayProtocol))) + } else if conf.Server.Authentication == config.AuthenticationKerberos { + keytab, err := keytab.Load(conf.Kerberos.Keytab) + if err != nil { + log.Fatalf("Cannot load keytab: %s", err) + } + http.Handle("/remoteDesktopGateway/", common.EnrichContext( + spnego.SPNEGOKRB5Authenticate( + http.HandlerFunc(gw.HandleGatewayProtocol), + keytab, + service.Logger(log.Default()))), + ) + k := kdcproxy.InitKdcProxy(conf.Kerberos.Krb5Conf) + http.HandleFunc("/KdcProxy", k.Handler) } else { // openid oidc := initOIDC(url, store) diff --git a/cmd/rdpgw/protocol/gateway.go b/cmd/rdpgw/protocol/gateway.go index 4e17dc5..2a4646b 100644 --- a/cmd/rdpgw/protocol/gateway.go +++ b/cmd/rdpgw/protocol/gateway.go @@ -68,9 +68,9 @@ func (g *Gateway) HandleGatewayProtocol(w http.ResponseWriter, r *http.Request) t = &Tunnel{ RDGId: connId, RemoteAddr: ctx.Value(common.ClientIPCtx).(string), - UserName: ctx.Value(common.UsernameCtx).(string), } - // username can be nil with openid as it's only available later + // username can be nil with openid & kerberos as it's only available later + // todo grab kerberos principal now? username := ctx.Value(common.UsernameCtx) if username != nil { t.UserName = username.(string) diff --git a/go.mod b/go.mod index 5998bd2..ad14f4b 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,20 @@ module github.com/bolkedebruin/rdpgw go 1.19 require ( + github.com/bolkedebruin/gokrb5/v8 v8.5.0 github.com/coreos/go-oidc/v3 v3.2.0 github.com/fatih/structs v1.1.0 github.com/go-jose/go-jose/v3 v3.0.0 github.com/google/uuid v1.1.2 github.com/gorilla/sessions v1.2.1 github.com/gorilla/websocket v1.5.0 + github.com/jcmturner/gofork v1.7.6 github.com/knadh/koanf v1.4.2 github.com/msteinert/pam v1.0.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.12.1 github.com/thought-machine/go-flags v1.6.1 - golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 @@ -26,6 +28,11 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gorilla/securecookie v1.1.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -34,12 +41,11 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/stretchr/testify v1.7.1 // indirect - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/net v0.0.0-20220725212005-46097bf591d3 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect - gopkg.in/yaml.v3 v3.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) -- GitLab