From 40d9cdda57d11b7158c53fb17a27e6d1dc9c0aa7 Mon Sep 17 00:00:00 2001
From: Bolke de Bruin <bolke@xs4all.nl>
Date: Tue, 16 Aug 2022 14:52:22 +0200
Subject: [PATCH] Make config more docker friendly

---
 README.md                           |  2 +-
 cmd/rdpgw/config/configuration.go   | 41 +++++++++++++++++++++++----
 cmd/rdpgw/main.go                   | 44 +++++++++++++++++------------
 cmd/rdpgw/security/string.go        | 39 +++++++++++++++++++++++++
 dev/docker/docker-compose-arm64.yml |  2 ++
 dev/docker/docker-readme.md         | 36 +++++++++++++++++++++++
 6 files changed, 140 insertions(+), 24 deletions(-)
 create mode 100644 cmd/rdpgw/security/string.go
 create mode 100644 dev/docker/docker-readme.md

diff --git a/README.md b/README.md
index 1bad7d2..2f64d6a 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 4974449..9a485e9 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 4c482ed..d07bff7 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 0000000..dcf5821
--- /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 f41985a..0ec46e7 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 0000000..e41b94f
--- /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
-- 
GitLab