Make config more docker friendly

This commit is contained in:
Bolke de Bruin 2022-08-16 14:52:22 +02:00
parent 3ca05cbf16
commit 40d9cdda57
6 changed files with 140 additions and 24 deletions

View file

@ -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

View file

@ -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
}
}

View file

@ -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,

View file

@ -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
}

View file

@ -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

View file

@ -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
```