mirror of
https://github.com/bolkedebruin/rdpgw.git
synced 2026-03-31 07:56:35 +00:00
Add kdcproxy to support spnego
This commit is contained in:
@@ -21,13 +21,15 @@ const (
|
|||||||
SessionStoreCookie = "cookie"
|
SessionStoreCookie = "cookie"
|
||||||
SessionStoreFile = "file"
|
SessionStoreFile = "file"
|
||||||
|
|
||||||
AuthenticationOpenId = "openid"
|
AuthenticationOpenId = "openid"
|
||||||
AuthenticationBasic = "local"
|
AuthenticationBasic = "local"
|
||||||
|
AuthenticationKerberos = "kerberos"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Server ServerConfig `koanf:"server"`
|
Server ServerConfig `koanf:"server"`
|
||||||
OpenId OpenIDConfig `koanf:"openid"`
|
OpenId OpenIDConfig `koanf:"openid"`
|
||||||
|
Kerberos KerberosConfig `koanf:"kerberos"`
|
||||||
Caps RDGCapsConfig `koanf:"caps"`
|
Caps RDGCapsConfig `koanf:"caps"`
|
||||||
Security SecurityConfig `koanf:"security"`
|
Security SecurityConfig `koanf:"security"`
|
||||||
Client ClientConfig `koanf:"client"`
|
Client ClientConfig `koanf:"client"`
|
||||||
@@ -50,6 +52,11 @@ type ServerConfig struct {
|
|||||||
AuthSocket string `koanf:"authsocket"`
|
AuthSocket string `koanf:"authsocket"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KerberosConfig struct {
|
||||||
|
Keytab string `koanf:"keytab"`
|
||||||
|
Krb5Conf string `koanf:"krb5conf"`
|
||||||
|
}
|
||||||
|
|
||||||
type OpenIDConfig struct {
|
type OpenIDConfig struct {
|
||||||
ProviderUrl string `koanf:"providerurl"`
|
ProviderUrl string `koanf:"providerurl"`
|
||||||
ClientId string `koanf:"clientid"`
|
ClientId string `koanf:"clientid"`
|
||||||
@@ -161,6 +168,7 @@ func Load(configFile string) Configuration {
|
|||||||
k.UnmarshalWithConf("Caps", &Conf.Caps, koanfTag)
|
k.UnmarshalWithConf("Caps", &Conf.Caps, koanfTag)
|
||||||
k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
|
k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
|
||||||
k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
|
k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
|
||||||
|
k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag)
|
||||||
|
|
||||||
if len(Conf.Security.PAATokenEncryptionKey) != 32 {
|
if len(Conf.Security.PAATokenEncryptionKey) != 32 {
|
||||||
Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32)
|
Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32)
|
||||||
@@ -206,6 +214,10 @@ func Load(configFile string) Configuration {
|
|||||||
log.Fatalf("openid is configured but tokenauth disabled")
|
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
|
// prepend '//' if required for URL parsing
|
||||||
if !strings.Contains(Conf.Server.GatewayAddress, "//") {
|
if !strings.Contains(Conf.Server.GatewayAddress, "//") {
|
||||||
Conf.Server.GatewayAddress = "//" + Conf.Server.GatewayAddress
|
Conf.Server.GatewayAddress = "//" + Conf.Server.GatewayAddress
|
||||||
|
|||||||
157
cmd/rdpgw/kdcproxy/proxy.go
Normal file
157
cmd/rdpgw/kdcproxy/proxy.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -4,8 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"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/common"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/config"
|
"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/protocol"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/web"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/web"
|
||||||
@@ -204,6 +208,19 @@ func main() {
|
|||||||
if conf.Server.Authentication == config.AuthenticationBasic {
|
if conf.Server.Authentication == config.AuthenticationBasic {
|
||||||
h := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket}
|
h := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket}
|
||||||
http.Handle("/remoteDesktopGateway/", common.EnrichContext(h.BasicAuth(gw.HandleGatewayProtocol)))
|
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 {
|
} else {
|
||||||
// openid
|
// openid
|
||||||
oidc := initOIDC(url, store)
|
oidc := initOIDC(url, store)
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ func (g *Gateway) HandleGatewayProtocol(w http.ResponseWriter, r *http.Request)
|
|||||||
t = &Tunnel{
|
t = &Tunnel{
|
||||||
RDGId: connId,
|
RDGId: connId,
|
||||||
RemoteAddr: ctx.Value(common.ClientIPCtx).(string),
|
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)
|
username := ctx.Value(common.UsernameCtx)
|
||||||
if username != nil {
|
if username != nil {
|
||||||
t.UserName = username.(string)
|
t.UserName = username.(string)
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -3,18 +3,20 @@ module github.com/bolkedebruin/rdpgw
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/bolkedebruin/gokrb5/v8 v8.5.0
|
||||||
github.com/coreos/go-oidc/v3 v3.2.0
|
github.com/coreos/go-oidc/v3 v3.2.0
|
||||||
github.com/fatih/structs v1.1.0
|
github.com/fatih/structs v1.1.0
|
||||||
github.com/go-jose/go-jose/v3 v3.0.0
|
github.com/go-jose/go-jose/v3 v3.0.0
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
|
github.com/jcmturner/gofork v1.7.6
|
||||||
github.com/knadh/koanf v1.4.2
|
github.com/knadh/koanf v1.4.2
|
||||||
github.com/msteinert/pam v1.0.0
|
github.com/msteinert/pam v1.0.0
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/prometheus/client_golang v1.12.1
|
github.com/prometheus/client_golang v1.12.1
|
||||||
github.com/thought-machine/go-flags v1.6.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
|
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
|
||||||
google.golang.org/grpc v1.49.0
|
google.golang.org/grpc v1.49.0
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
@@ -26,6 +28,11 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // 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/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.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/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/stretchr/testify v1.7.1 // indirect
|
golang.org/x/net v0.0.0-20220725212005-46097bf591d3 // indirect
|
||||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user