mirror of
https://github.com/bolkedebruin/rdpgw.git
synced 2025-08-15 05:13:47 +02:00
Make config more docker friendly
This commit is contained in:
parent
3ca05cbf16
commit
40d9cdda57
6 changed files with 140 additions and 24 deletions
|
@ -51,7 +51,7 @@ Server:
|
||||||
# TLS certificate files
|
# TLS certificate files
|
||||||
CertFile: server.pem
|
CertFile: server.pem
|
||||||
KeyFile: key.pem
|
KeyFile: key.pem
|
||||||
# gateway address advertised in the rdp files
|
# gateway address advertised in the rdp files and browser
|
||||||
GatewayAddress: localhost
|
GatewayAddress: localhost
|
||||||
# port to listen on (change to 80 or equivalent if not using TLS)
|
# port to listen on (change to 80 or equivalent if not using TLS)
|
||||||
Port: 443
|
Port: 443
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
||||||
"github.com/knadh/koanf"
|
"github.com/knadh/koanf"
|
||||||
"github.com/knadh/koanf/parsers/yaml"
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
"github.com/knadh/koanf/providers/confmap"
|
"github.com/knadh/koanf/providers/confmap"
|
||||||
|
@ -30,7 +31,7 @@ type ServerConfig struct {
|
||||||
SessionStore string `koanf:"sessionstore"`
|
SessionStore string `koanf:"sessionstore"`
|
||||||
SendBuf int `koanf:"sendbuf"`
|
SendBuf int `koanf:"sendbuf"`
|
||||||
ReceiveBuf int `koanf:"recievebuf"`
|
ReceiveBuf int `koanf:"recievebuf"`
|
||||||
DisableTLS bool `koanf:"disabletls"`
|
DisableTLS bool `koanf:"disabletls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenIDConfig struct {
|
type OpenIDConfig struct {
|
||||||
|
@ -114,13 +115,13 @@ func Load(configFile string) Configuration {
|
||||||
k.Load(confmap.Provider(map[string]interface{}{
|
k.Load(confmap.Provider(map[string]interface{}{
|
||||||
"Server.CertFile": "server.pem",
|
"Server.CertFile": "server.pem",
|
||||||
"Server.KeyFile": "key.pem",
|
"Server.KeyFile": "key.pem",
|
||||||
"Server.TlsDisabled": false,
|
"Server.TlsDisabled": false,
|
||||||
"Server.Port": 443,
|
"Server.Port": 443,
|
||||||
"Server.SessionStore": "cookie",
|
"Server.SessionStore": "cookie",
|
||||||
"Client.NetworkAutoDetect": 1,
|
"Client.NetworkAutoDetect": 1,
|
||||||
"Client.BandwidthAutoDetect": 1,
|
"Client.BandwidthAutoDetect": 1,
|
||||||
"Security.VerifyClientIp": true,
|
"Security.VerifyClientIp": true,
|
||||||
"Caps.TokenAuth": true,
|
"Caps.TokenAuth": true,
|
||||||
}, "."), nil)
|
}, "."), nil)
|
||||||
|
|
||||||
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != 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("Security", &Conf.Security, koanfTag)
|
||||||
k.UnmarshalWithConf("Client", &Conf.Client, 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
|
return Conf
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"github.com/thought-machine/go-flags"
|
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/api"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/api"
|
||||||
"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"
|
||||||
|
@ -11,9 +10,11 @@ import (
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/thought-machine/go-flags"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
@ -50,18 +51,25 @@ func main() {
|
||||||
}
|
}
|
||||||
verifier := provider.Verifier(oidcConfig)
|
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{
|
oauthConfig := oauth2.Config{
|
||||||
ClientID: conf.OpenId.ClientId,
|
ClientID: conf.OpenId.ClientId,
|
||||||
ClientSecret: conf.OpenId.ClientSecret,
|
ClientSecret: conf.OpenId.ClientSecret,
|
||||||
RedirectURL: "https://" + conf.Server.GatewayAddress + "/callback",
|
RedirectURL: url.String(),
|
||||||
Endpoint: provider.Endpoint(),
|
Endpoint: provider.Endpoint(),
|
||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
}
|
}
|
||||||
security.OIDCProvider = provider
|
security.OIDCProvider = provider
|
||||||
security.Oauth2Config = oauthConfig
|
security.Oauth2Config = oauthConfig
|
||||||
|
|
||||||
api := &api.Config{
|
api := &api.Config{
|
||||||
GatewayAddress: conf.Server.GatewayAddress,
|
GatewayAddress: url.Host,
|
||||||
OAuth2Config: &oauthConfig,
|
OAuth2Config: &oauthConfig,
|
||||||
OIDCTokenVerifier: verifier,
|
OIDCTokenVerifier: verifier,
|
||||||
PAATokenGenerator: security.GeneratePAAToken,
|
PAATokenGenerator: security.GeneratePAAToken,
|
||||||
|
@ -69,7 +77,7 @@ func main() {
|
||||||
EnableUserToken: conf.Security.EnableUserToken,
|
EnableUserToken: conf.Security.EnableUserToken,
|
||||||
SessionKey: []byte(conf.Server.SessionKey),
|
SessionKey: []byte(conf.Server.SessionKey),
|
||||||
SessionEncryptionKey: []byte(conf.Server.SessionEncryptionKey),
|
SessionEncryptionKey: []byte(conf.Server.SessionEncryptionKey),
|
||||||
SessionStore: conf.Server.SessionStore,
|
SessionStore: conf.Server.SessionStore,
|
||||||
Hosts: conf.Server.Hosts,
|
Hosts: conf.Server.Hosts,
|
||||||
NetworkAutoDetect: conf.Client.NetworkAutoDetect,
|
NetworkAutoDetect: conf.Client.NetworkAutoDetect,
|
||||||
UsernameTemplate: conf.Client.UsernameTemplate,
|
UsernameTemplate: conf.Client.UsernameTemplate,
|
||||||
|
@ -84,7 +92,7 @@ func main() {
|
||||||
cfg := &tls.Config{}
|
cfg := &tls.Config{}
|
||||||
|
|
||||||
if conf.Server.DisableTLS {
|
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 {
|
} else {
|
||||||
if conf.Server.CertFile == "" || conf.Server.KeyFile == "" {
|
if conf.Server.CertFile == "" || conf.Server.KeyFile == "" {
|
||||||
log.Fatal("Both certfile and keyfile need to be specified")
|
log.Fatal("Both certfile and keyfile need to be specified")
|
||||||
|
@ -108,24 +116,24 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Addr: ":" + strconv.Itoa(conf.Server.Port),
|
Addr: ":" + strconv.Itoa(conf.Server.Port),
|
||||||
TLSConfig: cfg,
|
TLSConfig: cfg,
|
||||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // disable http2
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // disable http2
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the gateway
|
// create the gateway
|
||||||
handlerConfig := protocol.ServerConf{
|
handlerConfig := protocol.ServerConf{
|
||||||
IdleTimeout: conf.Caps.IdleTimeout,
|
IdleTimeout: conf.Caps.IdleTimeout,
|
||||||
TokenAuth: conf.Caps.TokenAuth,
|
TokenAuth: conf.Caps.TokenAuth,
|
||||||
SmartCardAuth: conf.Caps.SmartCardAuth,
|
SmartCardAuth: conf.Caps.SmartCardAuth,
|
||||||
RedirectFlags: protocol.RedirectFlags{
|
RedirectFlags: protocol.RedirectFlags{
|
||||||
Clipboard: conf.Caps.EnableClipboard,
|
Clipboard: conf.Caps.EnableClipboard,
|
||||||
Drive: conf.Caps.EnableDrive,
|
Drive: conf.Caps.EnableDrive,
|
||||||
Printer: conf.Caps.EnablePrinter,
|
Printer: conf.Caps.EnablePrinter,
|
||||||
Port: conf.Caps.EnablePort,
|
Port: conf.Caps.EnablePort,
|
||||||
Pnp: conf.Caps.EnablePnp,
|
Pnp: conf.Caps.EnablePnp,
|
||||||
DisableAll: conf.Caps.DisableRedirect,
|
DisableAll: conf.Caps.DisableRedirect,
|
||||||
EnableAll: conf.Caps.RedirectAll,
|
EnableAll: conf.Caps.RedirectAll,
|
||||||
},
|
},
|
||||||
VerifyTunnelCreate: security.VerifyPAAToken,
|
VerifyTunnelCreate: security.VerifyPAAToken,
|
||||||
VerifyServerFunc: security.VerifyServerFunc,
|
VerifyServerFunc: security.VerifyServerFunc,
|
||||||
|
|
39
cmd/rdpgw/security/string.go
Normal file
39
cmd/rdpgw/security/string.go
Normal 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
|
||||||
|
}
|
|
@ -44,6 +44,8 @@ services:
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
- keycloak
|
- keycloak
|
||||||
|
environment:
|
||||||
|
RDPGW_SERVER__SESSION_STORE: file
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
|
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
36
dev/docker/docker-readme.md
Normal file
36
dev/docker/docker-readme.md
Normal 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
|
||||||
|
```
|
Loading…
Add table
Add a link
Reference in a new issue