diff --git a/README.md b/README.md index 1bad7d23367f54e6436afeee2cd489b002f24309..2f64d6aef4ec179f46825cd00b8e1ffe64ff25b6 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Server: # TLS certificate files CertFile: server.pem KeyFile: key.pem - # gateway address advertised in the rdp files + # gateway address advertised in the rdp files and browser GatewayAddress: localhost # port to listen on (change to 80 or equivalent if not using TLS) Port: 443 diff --git a/cmd/rdpgw/config/configuration.go b/cmd/rdpgw/config/configuration.go index 4974449f1e829ea567546351685cae2524f395e9..9a485e9c7a385e1f9897b80670cf08a2589d5842 100644 --- a/cmd/rdpgw/config/configuration.go +++ b/cmd/rdpgw/config/configuration.go @@ -1,6 +1,7 @@ package config import ( + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/security" "github.com/knadh/koanf" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/confmap" @@ -30,7 +31,7 @@ type ServerConfig struct { SessionStore string `koanf:"sessionstore"` SendBuf int `koanf:"sendbuf"` ReceiveBuf int `koanf:"recievebuf"` - DisableTLS bool `koanf:"disabletls"` + DisableTLS bool `koanf:"disabletls"` } type OpenIDConfig struct { @@ -114,13 +115,13 @@ func Load(configFile string) Configuration { k.Load(confmap.Provider(map[string]interface{}{ "Server.CertFile": "server.pem", "Server.KeyFile": "key.pem", - "Server.TlsDisabled": false, + "Server.TlsDisabled": false, "Server.Port": 443, - "Server.SessionStore": "cookie", + "Server.SessionStore": "cookie", "Client.NetworkAutoDetect": 1, "Client.BandwidthAutoDetect": 1, "Security.VerifyClientIp": true, - "Caps.TokenAuth": true, + "Caps.TokenAuth": true, }, "."), nil) if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil { @@ -142,6 +143,36 @@ func Load(configFile string) Configuration { k.UnmarshalWithConf("Security", &Conf.Security, koanfTag) k.UnmarshalWithConf("Client", &Conf.Client, koanfTag) + if len(Conf.Security.PAATokenEncryptionKey) != 32 { + Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32) + log.Printf("No valid `security.paatokenencryptionkey` specified (empty or not 32 characters). Setting to random") + } + + if len(Conf.Security.PAATokenSigningKey) != 32 { + Conf.Security.PAATokenSigningKey, _ = security.GenerateRandomString(32) + log.Printf("No valid `security.paatokensigningkey` specified (empty or not 32 characters). Setting to random") + } + + if len(Conf.Security.UserTokenEncryptionKey) != 32 { + Conf.Security.UserTokenEncryptionKey, _ = security.GenerateRandomString(32) + log.Printf("No valid `security.usertokenencryptionkey` specified (empty or not 32 characters). Setting to random") + } + + if len(Conf.Security.UserTokenSigningKey) != 32 { + Conf.Security.UserTokenSigningKey, _ = security.GenerateRandomString(32) + log.Printf("No valid `security.usertokensigningkey` specified (empty or not 32 characters). Setting to random") + } + + if len(Conf.Server.SessionKey) != 32 { + Conf.Server.SessionKey, _ = security.GenerateRandomString(32) + log.Printf("No valid `server.sessionkey` specified (empty or not 32 characters). Setting to random") + } + + if len(Conf.Server.SessionEncryptionKey) != 32 { + Conf.Server.SessionEncryptionKey, _ = security.GenerateRandomString(32) + log.Printf("No valid `server.sessionencryptionkey` specified (empty or not 32 characters). Setting to random") + } + return Conf -} \ No newline at end of file +} diff --git a/cmd/rdpgw/main.go b/cmd/rdpgw/main.go index 4c482ed4b85b52943ab71bd4ee1e081569de04db..d07bff7ad51a2c5f973491d85d92a1d746a11599 100644 --- a/cmd/rdpgw/main.go +++ b/cmd/rdpgw/main.go @@ -3,7 +3,6 @@ package main import ( "context" "crypto/tls" - "github.com/thought-machine/go-flags" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/api" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/common" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/config" @@ -11,9 +10,11 @@ import ( "github.com/bolkedebruin/rdpgw/cmd/rdpgw/security" "github.com/coreos/go-oidc/v3/oidc" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/thought-machine/go-flags" "golang.org/x/oauth2" "log" "net/http" + "net/url" "os" "strconv" ) @@ -50,18 +51,25 @@ func main() { } verifier := provider.Verifier(oidcConfig) + // get callback url and external advertised gateway address + url, err := url.Parse(conf.Server.GatewayAddress) + if url.Scheme == "" { + url.Scheme = "https" + } + url.Path = "callback" + oauthConfig := oauth2.Config{ - ClientID: conf.OpenId.ClientId, + ClientID: conf.OpenId.ClientId, ClientSecret: conf.OpenId.ClientSecret, - RedirectURL: "https://" + conf.Server.GatewayAddress + "/callback", - Endpoint: provider.Endpoint(), - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + RedirectURL: url.String(), + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } security.OIDCProvider = provider security.Oauth2Config = oauthConfig api := &api.Config{ - GatewayAddress: conf.Server.GatewayAddress, + GatewayAddress: url.Host, OAuth2Config: &oauthConfig, OIDCTokenVerifier: verifier, PAATokenGenerator: security.GeneratePAAToken, @@ -69,7 +77,7 @@ func main() { EnableUserToken: conf.Security.EnableUserToken, SessionKey: []byte(conf.Server.SessionKey), SessionEncryptionKey: []byte(conf.Server.SessionEncryptionKey), - SessionStore: conf.Server.SessionStore, + SessionStore: conf.Server.SessionStore, Hosts: conf.Server.Hosts, NetworkAutoDetect: conf.Client.NetworkAutoDetect, UsernameTemplate: conf.Client.UsernameTemplate, @@ -84,7 +92,7 @@ func main() { cfg := &tls.Config{} if conf.Server.DisableTLS { - log.Printf("TLS disabled - rdp gw connections require tls make sure to have a terminator") + log.Printf("TLS disabled - rdp gw connections require tls, make sure to have a terminator") } else { if conf.Server.CertFile == "" || conf.Server.KeyFile == "" { log.Fatal("Both certfile and keyfile need to be specified") @@ -108,24 +116,24 @@ func main() { } server := http.Server{ - Addr: ":" + strconv.Itoa(conf.Server.Port), - TLSConfig: cfg, + Addr: ":" + strconv.Itoa(conf.Server.Port), + TLSConfig: cfg, TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // disable http2 } // create the gateway handlerConfig := protocol.ServerConf{ - IdleTimeout: conf.Caps.IdleTimeout, - TokenAuth: conf.Caps.TokenAuth, + IdleTimeout: conf.Caps.IdleTimeout, + TokenAuth: conf.Caps.TokenAuth, SmartCardAuth: conf.Caps.SmartCardAuth, RedirectFlags: protocol.RedirectFlags{ - Clipboard: conf.Caps.EnableClipboard, - Drive: conf.Caps.EnableDrive, - Printer: conf.Caps.EnablePrinter, - Port: conf.Caps.EnablePort, - Pnp: conf.Caps.EnablePnp, + Clipboard: conf.Caps.EnableClipboard, + Drive: conf.Caps.EnableDrive, + Printer: conf.Caps.EnablePrinter, + Port: conf.Caps.EnablePort, + Pnp: conf.Caps.EnablePnp, DisableAll: conf.Caps.DisableRedirect, - EnableAll: conf.Caps.RedirectAll, + EnableAll: conf.Caps.RedirectAll, }, VerifyTunnelCreate: security.VerifyPAAToken, VerifyServerFunc: security.VerifyServerFunc, diff --git a/cmd/rdpgw/security/string.go b/cmd/rdpgw/security/string.go new file mode 100644 index 0000000000000000000000000000000000000000..dcf5821c357a464f68daf71d15491d3f23258238 --- /dev/null +++ b/cmd/rdpgw/security/string.go @@ -0,0 +1,39 @@ +package security + +import ( + "crypto/rand" + "math/big" +) + +// GenerateRandomBytes returns securely generated random bytes. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil +} + +// GenerateRandomString returns a securely generated random string. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomString(n int) (string, error) { + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" + ret := make([]byte, n) + for i := 0; i < n; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + ret = append(ret, letters[num.Int64()]) + } + + return string(ret), nil +} diff --git a/dev/docker/docker-compose-arm64.yml b/dev/docker/docker-compose-arm64.yml index f41985ac7081812888cc9bbb58ba1bfb83adbd46..0ec46e711d974aee2ade3735b1f63782b74bbc71 100644 --- a/dev/docker/docker-compose-arm64.yml +++ b/dev/docker/docker-compose-arm64.yml @@ -44,6 +44,8 @@ services: restart: on-failure depends_on: - keycloak + environment: + RDPGW_SERVER__SESSION_STORE: file healthcheck: test: ["CMD", "curl", "-f", "http://keycloak:8080"] interval: 30s diff --git a/dev/docker/docker-readme.md b/dev/docker/docker-readme.md new file mode 100644 index 0000000000000000000000000000000000000000..e41b94fed2adb44b5c58d09c577ebc71f23dbcad --- /dev/null +++ b/dev/docker/docker-readme.md @@ -0,0 +1,36 @@ +# RDPGW +## What is RDPGW? +Remote Desktop Gateway (RDPGW, RDG or RD Gateway) provides a secure encrypted connection +to user desktops via RDP. It enhances control by removing all remote user direct access to +your system and replaces it with a point-to-point remote desktop connection. + +## How to use this image +The remote desktop gateway relies on an OpenID Connect authentication service, such as Keycloak, +Azure AD or Google, and a backend remote desktop service such as XRDP, gnome-remote-desktop, or +Windows VMs. Make sure that these services have been properly setup and can be reached from +where you will run this image. + +This image works stateless, which means it does not store any state by default. In case you configure +the session store to be a `filestore` a little bit of session information is stored temporarily. This means +that a load balancer would need to maintain state for a while, which typically is the case. + +Session and token encryption keys will be randomized on startup. As a consequence sessions will be +invalidated on restarts and if you are load balancing the different instances will not be able to share +user sessions. Make sure to set these encryption keys to something static, so they can be shared +across the different instances if this is not what you want. + +## Configuration through environment variables +```bash +docker --run name rdpgw bolkedebruin/rdpgw:latest \ + -e RDPGW_SERVER__SSL_CERT_FILE=/etc/rdpgw/cert.pem + -e RDPGW_SERVER__SSL_KEY_FILE=/etc/rdpgw.cert.pem + -e SSL_KEY=<SSL_KEY> + -e RDPGW_SERVER__GATEWAY_ADDRESS=https://localhost:443 + -e RDPGW_SERVER__SESSION_KEY=thisisasessionkeyreplacethisjetz # 32 characters + -e RDPGW_SERVER__SESSION_ENCRYPTION_KEY=thisisasessionkeyreplacethisnunu # 32 characters + -e RDPGW_OPENID__PROVIDER_URL=http://keycloak:8080/auth/realms/rdpgw + -e RDPGW_OPENID__CLIENT_ID=rdpgw + -e RDPGW_OPENID__CLIENT_SECRET=01cd304c-6f43-4480-9479-618eb6fd578f + -e RDPGW_SECURITY__SECURITY_PAA_TOKEN_SIGNING_KEY=prettypleasereplacemeinproductio # 32 characters + -v conf:/etc/rdpgw +``` \ No newline at end of file