mirror of
https://github.com/bolkedebruin/rdpgw.git
synced 2026-03-27 14:36:36 +00:00
Add header authentication
This commit is contained in:
139
README.md
139
README.md
@@ -28,7 +28,8 @@ to connect.
|
||||
|
||||
The gateway has several security phases. In the authentication phase the client's credentials are
|
||||
verified. Depending the authentication mechanism used, the client's credentials are verified against
|
||||
an OpenID Connect provider, Kerberos, a local PAM service or a local database.
|
||||
an OpenID Connect provider, Kerberos, a local PAM service, a local database, or extracted from HTTP headers
|
||||
provided by upstream proxy services.
|
||||
|
||||
If OpenID Connect is used the user will
|
||||
need to connect to a webpage provided by the gateway to authenticate, which in turn will redirect
|
||||
@@ -61,7 +62,7 @@ settings.
|
||||
## Authentication
|
||||
|
||||
RDPGW wants to be secure when you set it up from the start. It supports several authentication
|
||||
mechanisms such as OpenID Connect, Kerberos, PAM or NTLM.
|
||||
mechanisms such as OpenID Connect, Kerberos, PAM, NTLM, and header-based authentication for proxy integration.
|
||||
|
||||
Technically, cookies are encrypted and signed on the client side relying
|
||||
on [Gorilla Sessions](https://www.gorillatoolkit.org/pkg/sessions). PAA tokens (gateway access tokens)
|
||||
@@ -78,142 +79,28 @@ if you want.
|
||||
It is technically possible to mix authentication mechanisms. Currently, you can mix local with Kerberos or NTLM. If you enable
|
||||
OpenID Connect it is not possible to mix it with local or Kerberos at the moment.
|
||||
|
||||
### Open ID Connect
|
||||

|
||||
### OpenID Connect
|
||||
|
||||
To use OpenID Connect make sure you have properly configured your OpenID Connect provider, and you have a client id
|
||||
and secret. The client id and secret are used to authenticate the gateway to the OpenID Connect provider. The provider
|
||||
will then authenticate the user and provide the gateway with a token. The gateway will then use this token to generate
|
||||
a PAA token that is used to connect to the RDP host.
|
||||
|
||||
To enable OpenID Connect make sure to set the following variables in the configuration file.
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- openid
|
||||
OpenId:
|
||||
ProviderUrl: http://<provider_url>
|
||||
ClientId: <your client id>
|
||||
ClientSecret: <your-secret>
|
||||
Caps:
|
||||
TokenAuth: true
|
||||
```
|
||||
|
||||
As you can see in the flow diagram when using OpenID Connect the user will use a browser to connect to the gateway first at
|
||||
https://your-gateway/connect. If authentication is successful the browser will download a RDP file with temporary credentials
|
||||
that allow the user to connect to the gateway by using a remote desktop client.
|
||||
For detailed OpenID Connect setup with providers like Keycloak, Azure AD, Google, and others, see the [OpenID Connect Authentication Documentation](docs/openid-authentication.md).
|
||||
|
||||
### Kerberos
|
||||

|
||||
|
||||
__NOTE__: Kerberos is heavily reliant on DNS (forward and reverse). Make sure that your DNS is properly configured.
|
||||
Next to that, its errors are not always very descriptive. It is beyond the scope of this project to provide a full
|
||||
Kerberos tutorial.
|
||||
|
||||
To use Kerberos make sure you have a keytab and krb5.conf file. The keytab is used to authenticate the gateway to the KDC
|
||||
and the krb5.conf file is used to configure the KDC. The keytab needs to contain a valid principal for the gateway.
|
||||
|
||||
Use `ktutil` or a similar tool provided by your Kerberos server to create a keytab file for the newly created service principal.
|
||||
Place this keytab file in a secure location on the server and make sure that the file is only readable by the user that runs
|
||||
the gateway.
|
||||
|
||||
```plaintext
|
||||
ktutil
|
||||
addent -password -p HTTP/rdpgw.example.com@YOUR.REALM -k 1 -e aes256-cts-hmac-sha1-96
|
||||
wkt rdpgw.keytab
|
||||
```
|
||||
|
||||
Then set the following in the configuration file.
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- kerberos
|
||||
Kerberos:
|
||||
Keytab: /etc/keytabs/rdpgw.keytab
|
||||
Krb5conf: /etc/krb5.conf
|
||||
Caps:
|
||||
TokenAuth: false
|
||||
```
|
||||
|
||||
The client can then connect directly to the gateway without the need for a RDP file.
|
||||
For detailed Kerberos setup including keytab generation, DNS requirements, and KDC proxy configuration, see the [Kerberos Authentication Documentation](docs/kerberos-authentication.md).
|
||||
|
||||
|
||||
### PAM / Local (Basic Auth)
|
||||

|
||||
### PAM/Local Authentication
|
||||
|
||||
The gateway can also support authentication against PAM. Sometimes this is referred to as local or passwd authentication,
|
||||
but it also supports LDAP authentication or even Active Directory if you have the correct modules installed. Typically
|
||||
(for passwd), PAM requires that it is accessed as root. Therefore, the gateway comes with a small helper program called
|
||||
`rdpgw-auth` that is used to authenticate the user. This program needs to be run as root or setuid.
|
||||
For detailed PAM setup including LDAP integration, container deployment, and compatible clients, see the [PAM Authentication Documentation](docs/pam-authentication.md).
|
||||
|
||||
__NOTE__: The default windows client ``mstsc`` does not support basic auth. You will need to use a different client or
|
||||
switch to OpenID Connect, Kerberos or NTLM authentication.
|
||||
### NTLM Authentication
|
||||
|
||||
__NOTE__: Using PAM for passwd (i.e. LDAP is fine) within a container is not recommended. It is better to use OpenID
|
||||
Connect or Kerberos. If you do want to use it within a container you can choose to run the helper program outside the
|
||||
container and have the socket available within. Alternatively, you can mount all what is needed into the container but
|
||||
PAM is quite sensitive to the environment.
|
||||
For detailed NTLM setup including user management, security considerations, and deployment options, see the [NTLM Authentication Documentation](docs/ntlm-authentication.md).
|
||||
|
||||
Ensure you have a PAM service file for the gateway, `/etc/pam.d/rdpgw`. For authentication against local accounts on the
|
||||
host located in `/etc/passwd` and `/etc/shadow` you can use the following.
|
||||
### Header Authentication (Proxy Integration)
|
||||
|
||||
```plaintext
|
||||
auth required pam_unix.so
|
||||
account required pam_unix.so
|
||||
```
|
||||
RDPGW supports header-based authentication for integration with reverse proxy services (Azure App Proxy, Google IAP, AWS ALB, etc.) that handle authentication upstream and pass user identity via HTTP headers.
|
||||
|
||||
Then set the following in the configuration file.
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- local
|
||||
AuthSocket: /tmp/rdpgw-auth.sock
|
||||
Caps:
|
||||
TokenAuth: false
|
||||
```
|
||||
|
||||
Make sure to run both the gateway and `rdpgw-auth`. The gateway will connect to the socket to authenticate the user.
|
||||
|
||||
```bash
|
||||
# ./rdpgw-auth -n rdpgw -s /tmp/rdpgw-auth.sock
|
||||
```
|
||||
|
||||
The client can then connect to the gateway directly by using a remote desktop client.
|
||||
|
||||
### NTLM
|
||||
|
||||
The gateway can also support NTLM authentication.
|
||||
Currently, only the configuration file is supported as a database for credential lookup.
|
||||
In the future, support for real databases (e.g. sqlite) may be added.
|
||||
|
||||
NTLM authentication has the advantage that it is easy to setup, especially in case the gateway is used for a limited number of users.
|
||||
Unlike PAM / local, NTLM authentication supports the default windows client ``mstsc``.
|
||||
|
||||
__WARNING__: The password is currently saved in plain text. So, you should keep the config file as secure as possible and avoid
|
||||
reusing the same password for other applications. The password is stored in plain text to support the NTLM authentication protocol.
|
||||
|
||||
To enable NTLM authentication make sure to set the following variables in the configuration file.
|
||||
|
||||
Configuration file for `rdpgw`:
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- ntlm
|
||||
Caps:
|
||||
TokenAuth: false
|
||||
```
|
||||
|
||||
Configuration file for `rdpgw-auth`:
|
||||
````yaml
|
||||
Users:
|
||||
- {Username: "my_username", Password: "my_secure_password"} # Modify this password!
|
||||
````
|
||||
|
||||
The client can then connect to the gateway directly by using a remote desktop client using the gateway credentials
|
||||
configured in the YAML configuration file.
|
||||
For detailed configuration and examples, see the [Header Authentication Documentation](docs/header-authentication.md).
|
||||
|
||||
## TLS
|
||||
|
||||
|
||||
@@ -26,12 +26,14 @@ const (
|
||||
AuthenticationOpenId = "openid"
|
||||
AuthenticationBasic = "local"
|
||||
AuthenticationKerberos = "kerberos"
|
||||
AuthenticationHeader = "header"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Server ServerConfig `koanf:"server"`
|
||||
OpenId OpenIDConfig `koanf:"openid"`
|
||||
Kerberos KerberosConfig `koanf:"kerberos"`
|
||||
Header HeaderConfig `koanf:"header"`
|
||||
Caps RDGCapsConfig `koanf:"caps"`
|
||||
Security SecurityConfig `koanf:"security"`
|
||||
Client ClientConfig `koanf:"client"`
|
||||
@@ -67,6 +69,13 @@ type OpenIDConfig struct {
|
||||
ClientSecret string `koanf:"clientsecret"`
|
||||
}
|
||||
|
||||
type HeaderConfig struct {
|
||||
UserHeader string `koanf:"userheader"`
|
||||
UserIdHeader string `koanf:"useridheader"`
|
||||
EmailHeader string `koanf:"emailheader"`
|
||||
DisplayNameHeader string `koanf:"displaynameheader"`
|
||||
}
|
||||
|
||||
type RDGCapsConfig struct {
|
||||
SmartCardAuth bool `koanf:"smartcardauth"`
|
||||
TokenAuth bool `koanf:"tokenauth"`
|
||||
@@ -183,6 +192,7 @@ func Load(configFile string) Configuration {
|
||||
koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
|
||||
k.UnmarshalWithConf("Server", &Conf.Server, koanfTag)
|
||||
k.UnmarshalWithConf("OpenId", &Conf.OpenId, koanfTag)
|
||||
k.UnmarshalWithConf("Header", &Conf.Header, koanfTag)
|
||||
k.UnmarshalWithConf("Caps", &Conf.Caps, koanfTag)
|
||||
k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
|
||||
k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
|
||||
@@ -235,6 +245,10 @@ func Load(configFile string) Configuration {
|
||||
log.Fatalf("kerberos is configured but no keytab was specified")
|
||||
}
|
||||
|
||||
if Conf.Server.HeaderEnabled() && Conf.Header.UserHeader == "" {
|
||||
log.Fatalf("header authentication is configured but no user header was specified")
|
||||
}
|
||||
|
||||
// prepend '//' if required for URL parsing
|
||||
if !strings.Contains(Conf.Server.GatewayAddress, "//") {
|
||||
Conf.Server.GatewayAddress = "//" + Conf.Server.GatewayAddress
|
||||
@@ -259,6 +273,10 @@ func (s *ServerConfig) NtlmEnabled() bool {
|
||||
return s.matchAuth("ntlm")
|
||||
}
|
||||
|
||||
func (s *ServerConfig) HeaderEnabled() bool {
|
||||
return s.matchAuth("header")
|
||||
}
|
||||
|
||||
func (s *ServerConfig) matchAuth(needle string) bool {
|
||||
for _, q := range s.Authentication {
|
||||
if q == needle {
|
||||
|
||||
89
cmd/rdpgw/config/configuration_test.go
Normal file
89
cmd/rdpgw/config/configuration_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHeaderEnabled(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
authentication []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "header_enabled",
|
||||
authentication: []string{"header"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "header_with_others",
|
||||
authentication: []string{"openid", "header", "local"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "header_not_enabled",
|
||||
authentication: []string{"openid", "local"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty_authentication",
|
||||
authentication: []string{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config := &ServerConfig{
|
||||
Authentication: tc.authentication,
|
||||
}
|
||||
|
||||
result := config.HeaderEnabled()
|
||||
if result != tc.expected {
|
||||
t.Errorf("expected HeaderEnabled(): %v, got: %v", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationConstants(t *testing.T) {
|
||||
// Test that the header authentication constant is correct
|
||||
if AuthenticationHeader != "header" {
|
||||
t.Errorf("incorrect authentication header constant: %v", AuthenticationHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderConfigValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
headerConf HeaderConfig
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "valid_config",
|
||||
headerConf: HeaderConfig{
|
||||
UserHeader: "X-Forwarded-User",
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "missing_user_header",
|
||||
headerConf: HeaderConfig{
|
||||
EmailHeader: "X-Forwarded-Email",
|
||||
},
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test the configuration struct
|
||||
if tc.headerConf.UserHeader == "" && !tc.shouldError {
|
||||
t.Error("expected user header to be set")
|
||||
}
|
||||
if tc.headerConf.UserHeader != "" && tc.shouldError {
|
||||
t.Error("expected configuration to be invalid")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,25 @@ func main() {
|
||||
r.HandleFunc("/callback", o.HandleCallback)
|
||||
|
||||
// only enable un-auth endpoint for openid only config
|
||||
if !conf.Server.KerberosEnabled() && !conf.Server.BasicAuthEnabled() && !conf.Server.NtlmEnabled() {
|
||||
if !conf.Server.KerberosEnabled() && !conf.Server.BasicAuthEnabled() && !conf.Server.NtlmEnabled() && !conf.Server.HeaderEnabled() {
|
||||
rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
// header auth (configurable proxy)
|
||||
if conf.Server.HeaderEnabled() {
|
||||
log.Printf("enabling header authentication with user header: %s", conf.Header.UserHeader)
|
||||
headerConfig := &web.HeaderConfig{
|
||||
UserHeader: conf.Header.UserHeader,
|
||||
UserIdHeader: conf.Header.UserIdHeader,
|
||||
EmailHeader: conf.Header.EmailHeader,
|
||||
DisplayNameHeader: conf.Header.DisplayNameHeader,
|
||||
}
|
||||
headerAuth := headerConfig.New()
|
||||
r.Handle("/connect", headerAuth.Authenticated(http.HandlerFunc(h.HandleDownload)))
|
||||
|
||||
// only enable un-auth endpoint for header only config
|
||||
if !conf.Server.KerberosEnabled() && !conf.Server.BasicAuthEnabled() && !conf.Server.NtlmEnabled() && !conf.Server.OpenIDEnabled() {
|
||||
rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
83
cmd/rdpgw/web/header.go
Normal file
83
cmd/rdpgw/web/header.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
userHeader string
|
||||
userIdHeader string
|
||||
emailHeader string
|
||||
displayNameHeader string
|
||||
}
|
||||
|
||||
type HeaderConfig struct {
|
||||
UserHeader string
|
||||
UserIdHeader string
|
||||
EmailHeader string
|
||||
DisplayNameHeader string
|
||||
}
|
||||
|
||||
func (c *HeaderConfig) New() *Header {
|
||||
return &Header{
|
||||
userHeader: c.UserHeader,
|
||||
userIdHeader: c.UserIdHeader,
|
||||
emailHeader: c.EmailHeader,
|
||||
displayNameHeader: c.DisplayNameHeader,
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticated middleware that extracts user identity from configurable proxy headers
|
||||
func (h *Header) Authenticated(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := identity.FromRequestCtx(r)
|
||||
|
||||
// Check if user is already authenticated
|
||||
if id.Authenticated() {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract username from configured user header
|
||||
userName := r.Header.Get(h.userHeader)
|
||||
if userName == "" {
|
||||
http.Error(w, "No authenticated user from proxy", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Set identity for downstream processing
|
||||
id.SetUserName(userName)
|
||||
id.SetAuthenticated(true)
|
||||
id.SetAuthTime(time.Now())
|
||||
|
||||
// Set optional user attributes from headers
|
||||
if h.userIdHeader != "" {
|
||||
if userId := r.Header.Get(h.userIdHeader); userId != "" {
|
||||
id.SetAttribute("user_id", userId)
|
||||
}
|
||||
}
|
||||
|
||||
if h.emailHeader != "" {
|
||||
if email := r.Header.Get(h.emailHeader); email != "" {
|
||||
id.SetEmail(email)
|
||||
}
|
||||
}
|
||||
|
||||
if h.displayNameHeader != "" {
|
||||
if displayName := r.Header.Get(h.displayNameHeader); displayName != "" {
|
||||
id.SetDisplayName(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the session identity
|
||||
if err := SaveSessionIdentity(r, w, id); err != nil {
|
||||
http.Error(w, "Failed to save session: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
318
cmd/rdpgw/web/header_test.go
Normal file
318
cmd/rdpgw/web/header_test.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize session store for testing
|
||||
sessionKey := []byte("thisisasessionkeyreplacethisjetzt")
|
||||
encryptionKey := []byte("thisisasessionencryptionkey12345")
|
||||
InitStore(sessionKey, encryptionKey, "cookie", 8192)
|
||||
}
|
||||
|
||||
func TestHeaderAuthenticated(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
headers map[string]string
|
||||
expectedStatusCode int
|
||||
expectedAuth bool
|
||||
expectedUser string
|
||||
expectedEmail string
|
||||
expectedDisplayName string
|
||||
expectedUserId string
|
||||
}{
|
||||
{
|
||||
name: "ms_app_proxy_headers",
|
||||
headers: map[string]string{
|
||||
"X-MS-CLIENT-PRINCIPAL-NAME": "user@domain.com",
|
||||
"X-MS-CLIENT-PRINCIPAL-ID": "12345-abcdef",
|
||||
"X-MS-CLIENT-PRINCIPAL-EMAIL": "user@domain.com",
|
||||
},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedAuth: true,
|
||||
expectedUser: "user@domain.com",
|
||||
expectedEmail: "user@domain.com",
|
||||
expectedUserId: "12345-abcdef",
|
||||
},
|
||||
{
|
||||
name: "google_iap_headers",
|
||||
headers: map[string]string{
|
||||
"X-Goog-Authenticated-User-Email": "testuser@example.org",
|
||||
"X-Goog-Authenticated-User-ID": "google-user-123",
|
||||
},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedAuth: true,
|
||||
expectedUser: "testuser@example.org",
|
||||
expectedEmail: "testuser@example.org",
|
||||
expectedUserId: "google-user-123",
|
||||
},
|
||||
{
|
||||
name: "aws_alb_headers",
|
||||
headers: map[string]string{
|
||||
"X-Amzn-Oidc-Subject": "aws-user-456",
|
||||
"X-Amzn-Oidc-Email": "awsuser@company.com",
|
||||
"X-Amzn-Oidc-Name": "AWS User",
|
||||
},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedAuth: true,
|
||||
expectedUser: "aws-user-456",
|
||||
expectedEmail: "awsuser@company.com",
|
||||
expectedDisplayName: "AWS User",
|
||||
},
|
||||
{
|
||||
name: "custom_headers",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-User": "customuser",
|
||||
"X-Forwarded-Email": "custom@example.com",
|
||||
"X-Forwarded-Name": "Custom User",
|
||||
},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedAuth: true,
|
||||
expectedUser: "customuser",
|
||||
expectedEmail: "custom@example.com",
|
||||
expectedDisplayName: "Custom User",
|
||||
},
|
||||
{
|
||||
name: "missing_user_header",
|
||||
headers: map[string]string{"X-Some-Other-Header": "value"},
|
||||
expectedStatusCode: http.StatusUnauthorized,
|
||||
expectedAuth: false,
|
||||
expectedUser: "",
|
||||
},
|
||||
{
|
||||
name: "empty_headers",
|
||||
headers: map[string]string{},
|
||||
expectedStatusCode: http.StatusUnauthorized,
|
||||
expectedAuth: false,
|
||||
expectedUser: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create a test handler that checks the identity
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := identity.FromRequestCtx(r)
|
||||
if id.Authenticated() != tc.expectedAuth {
|
||||
t.Errorf("expected authenticated: %v, got: %v", tc.expectedAuth, id.Authenticated())
|
||||
}
|
||||
if id.UserName() != tc.expectedUser {
|
||||
t.Errorf("expected username: %v, got: %v", tc.expectedUser, id.UserName())
|
||||
}
|
||||
if tc.expectedEmail != "" && id.Email() != tc.expectedEmail {
|
||||
t.Errorf("expected email: %v, got: %v", tc.expectedEmail, id.Email())
|
||||
}
|
||||
if tc.expectedDisplayName != "" && id.DisplayName() != tc.expectedDisplayName {
|
||||
t.Errorf("expected display name: %v, got: %v", tc.expectedDisplayName, id.DisplayName())
|
||||
}
|
||||
if tc.expectedUserId != "" {
|
||||
if userId := id.GetAttribute("user_id"); userId != tc.expectedUserId {
|
||||
t.Errorf("expected user_id: %v, got: %v", tc.expectedUserId, userId)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Determine header config based on test case
|
||||
var headerConfig *HeaderConfig
|
||||
switch tc.name {
|
||||
case "ms_app_proxy_headers":
|
||||
headerConfig = &HeaderConfig{
|
||||
UserHeader: "X-MS-CLIENT-PRINCIPAL-NAME",
|
||||
UserIdHeader: "X-MS-CLIENT-PRINCIPAL-ID",
|
||||
EmailHeader: "X-MS-CLIENT-PRINCIPAL-EMAIL",
|
||||
DisplayNameHeader: "",
|
||||
}
|
||||
case "google_iap_headers":
|
||||
headerConfig = &HeaderConfig{
|
||||
UserHeader: "X-Goog-Authenticated-User-Email",
|
||||
UserIdHeader: "X-Goog-Authenticated-User-ID",
|
||||
EmailHeader: "X-Goog-Authenticated-User-Email",
|
||||
}
|
||||
case "aws_alb_headers":
|
||||
headerConfig = &HeaderConfig{
|
||||
UserHeader: "X-Amzn-Oidc-Subject",
|
||||
EmailHeader: "X-Amzn-Oidc-Email",
|
||||
DisplayNameHeader: "X-Amzn-Oidc-Name",
|
||||
}
|
||||
case "custom_headers":
|
||||
headerConfig = &HeaderConfig{
|
||||
UserHeader: "X-Forwarded-User",
|
||||
EmailHeader: "X-Forwarded-Email",
|
||||
DisplayNameHeader: "X-Forwarded-Name",
|
||||
}
|
||||
default:
|
||||
headerConfig = &HeaderConfig{
|
||||
UserHeader: "X-Forwarded-User",
|
||||
}
|
||||
}
|
||||
|
||||
headerAuth := headerConfig.New()
|
||||
|
||||
// Wrap test handler with authentication
|
||||
authHandler := headerAuth.Authenticated(testHandler)
|
||||
|
||||
// Create test request
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
|
||||
// Add headers from test case
|
||||
for header, value := range tc.headers {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
|
||||
// Add identity to request context (simulating middleware)
|
||||
testId := identity.NewUser()
|
||||
req = identity.AddToRequestCtx(testId, req)
|
||||
|
||||
// Create response recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Execute the handler
|
||||
authHandler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if rr.Code != tc.expectedStatusCode {
|
||||
t.Errorf("expected status code: %v, got: %v", tc.expectedStatusCode, rr.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderAlreadyAuthenticated(t *testing.T) {
|
||||
// Create a test handler that checks the identity
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := identity.FromRequestCtx(r)
|
||||
if !id.Authenticated() {
|
||||
t.Error("expected user to remain authenticated")
|
||||
}
|
||||
if id.UserName() != "existing_user" {
|
||||
t.Errorf("expected username to remain: existing_user, got: %v", id.UserName())
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Create header auth handler
|
||||
headerConfig := &HeaderConfig{
|
||||
UserHeader: "X-Forwarded-User",
|
||||
}
|
||||
headerAuth := headerConfig.New()
|
||||
|
||||
// Wrap test handler with authentication
|
||||
authHandler := headerAuth.Authenticated(testHandler)
|
||||
|
||||
// Create test request
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("X-Forwarded-User", "new_user@domain.com")
|
||||
|
||||
// Add pre-authenticated identity to request context
|
||||
testId := identity.NewUser()
|
||||
testId.SetUserName("existing_user")
|
||||
testId.SetAuthenticated(true)
|
||||
testId.SetAuthTime(time.Now())
|
||||
req = identity.AddToRequestCtx(testId, req)
|
||||
|
||||
// Create response recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Execute the handler
|
||||
authHandler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("expected status code: %v, got: %v", http.StatusOK, rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderConfigValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
config *HeaderConfig
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "valid_config",
|
||||
config: &HeaderConfig{
|
||||
UserHeader: "X-Forwarded-User",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "full_config",
|
||||
config: &HeaderConfig{
|
||||
UserHeader: "X-MS-CLIENT-PRINCIPAL-NAME",
|
||||
UserIdHeader: "X-MS-CLIENT-PRINCIPAL-ID",
|
||||
EmailHeader: "X-MS-CLIENT-PRINCIPAL-EMAIL",
|
||||
DisplayNameHeader: "X-MS-CLIENT-PRINCIPAL-NAME",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
header := tc.config.New()
|
||||
if header == nil && tc.valid {
|
||||
t.Error("expected valid header instance")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderConfig(t *testing.T) {
|
||||
config := &HeaderConfig{}
|
||||
header := config.New()
|
||||
|
||||
if header == nil {
|
||||
t.Error("expected non-nil Header instance")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the authentication flow sets the correct attributes
|
||||
func TestHeaderAttributesSetting(t *testing.T) {
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := identity.FromRequestCtx(r)
|
||||
|
||||
// Check that auth time is set and recent
|
||||
authTime := id.AuthTime()
|
||||
if authTime.IsZero() {
|
||||
t.Error("expected auth time to be set")
|
||||
}
|
||||
if time.Since(authTime) > time.Minute {
|
||||
t.Error("auth time should be recent")
|
||||
}
|
||||
|
||||
// Check that user_id attribute is set
|
||||
if userId := id.GetAttribute("user_id"); userId != "test-id-123" {
|
||||
t.Errorf("expected user_id: test-id-123, got: %v", userId)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
headerConfig := &HeaderConfig{
|
||||
UserHeader: "X-Forwarded-User",
|
||||
UserIdHeader: "X-Forwarded-User-Id",
|
||||
}
|
||||
headerAuth := headerConfig.New()
|
||||
authHandler := headerAuth.Authenticated(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("X-Forwarded-User", "user@domain.com")
|
||||
req.Header.Set("X-Forwarded-User-Id", "test-id-123")
|
||||
|
||||
testId := identity.NewUser()
|
||||
req = identity.AddToRequestCtx(testId, req)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
authHandler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("expected status code: %v, got: %v", http.StatusOK, rr.Code)
|
||||
}
|
||||
}
|
||||
111
docs/header-authentication.md
Normal file
111
docs/header-authentication.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Header Authentication
|
||||
|
||||
RDPGW supports header-based authentication for integration with reverse proxy services that handle authentication upstream.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- header
|
||||
Tls: disable # Proxy handles TLS termination
|
||||
|
||||
Header:
|
||||
UserHeader: "X-Forwarded-User" # Required: Username header
|
||||
UserIdHeader: "X-Forwarded-User-Id" # Optional: User ID header
|
||||
EmailHeader: "X-Forwarded-Email" # Optional: Email header
|
||||
DisplayNameHeader: "X-Forwarded-Name" # Optional: Display name header
|
||||
|
||||
Caps:
|
||||
TokenAuth: true
|
||||
|
||||
Security:
|
||||
VerifyClientIp: false # Requests come through proxy
|
||||
```
|
||||
|
||||
## Proxy Service Examples
|
||||
|
||||
### Microsoft Azure Application Proxy
|
||||
|
||||
```yaml
|
||||
Header:
|
||||
UserHeader: "X-MS-CLIENT-PRINCIPAL-NAME"
|
||||
UserIdHeader: "X-MS-CLIENT-PRINCIPAL-ID"
|
||||
EmailHeader: "X-MS-CLIENT-PRINCIPAL-EMAIL"
|
||||
```
|
||||
|
||||
**Setup**: Configure App Proxy to publish RDPGW with pre-authentication enabled. Users authenticate via Azure AD before reaching RDPGW.
|
||||
|
||||
### Google Cloud Identity-Aware Proxy (IAP)
|
||||
|
||||
```yaml
|
||||
Header:
|
||||
UserHeader: "X-Goog-Authenticated-User-Email"
|
||||
UserIdHeader: "X-Goog-Authenticated-User-ID"
|
||||
EmailHeader: "X-Goog-Authenticated-User-Email"
|
||||
```
|
||||
|
||||
**Setup**: Enable IAP on your Cloud Load Balancer pointing to RDPGW. Configure OAuth consent screen and authorized users/groups.
|
||||
|
||||
### AWS Application Load Balancer (ALB) with Cognito
|
||||
|
||||
```yaml
|
||||
Header:
|
||||
UserHeader: "X-Amzn-Oidc-Subject"
|
||||
EmailHeader: "X-Amzn-Oidc-Email"
|
||||
DisplayNameHeader: "X-Amzn-Oidc-Name"
|
||||
```
|
||||
|
||||
**Setup**: Configure ALB with Cognito User Pool authentication. Enable OIDC headers forwarding to RDPGW target group.
|
||||
|
||||
### Traefik with ForwardAuth
|
||||
|
||||
```yaml
|
||||
Header:
|
||||
UserHeader: "X-Forwarded-User"
|
||||
EmailHeader: "X-Forwarded-Email"
|
||||
DisplayNameHeader: "X-Forwarded-Name"
|
||||
```
|
||||
|
||||
**Setup**: Use Traefik ForwardAuth middleware with external auth service (e.g., OAuth2 Proxy, Authelia) that sets headers.
|
||||
|
||||
### nginx with auth_request
|
||||
|
||||
```yaml
|
||||
Header:
|
||||
UserHeader: "X-Auth-User"
|
||||
EmailHeader: "X-Auth-Email"
|
||||
```
|
||||
|
||||
**nginx config**:
|
||||
```nginx
|
||||
location /auth {
|
||||
internal;
|
||||
proxy_pass http://auth-service;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
}
|
||||
|
||||
location / {
|
||||
auth_request /auth;
|
||||
auth_request_set $user $upstream_http_x_auth_user;
|
||||
auth_request_set $email $upstream_http_x_auth_email;
|
||||
proxy_set_header X-Auth-User $user;
|
||||
proxy_set_header X-Auth-Email $email;
|
||||
proxy_pass http://rdpgw;
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Trust Boundary**: RDPGW trusts headers set by the proxy. Ensure the proxy cannot be bypassed.
|
||||
- **Header Validation**: Configure proxy to strip/override user headers from client requests.
|
||||
- **Network Security**: Deploy RDPGW in private network accessible only via the proxy.
|
||||
- **TLS**: Enable TLS between proxy and RDPGW in production environments.
|
||||
|
||||
## Validation
|
||||
|
||||
Test header authentication:
|
||||
```bash
|
||||
curl -H "X-Forwarded-User: testuser@domain.com" \
|
||||
https://your-proxy/connect
|
||||
```
|
||||
156
docs/kerberos-authentication.md
Normal file
156
docs/kerberos-authentication.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Kerberos Authentication
|
||||
|
||||

|
||||
|
||||
RDPGW supports Kerberos authentication via SPNEGO for seamless integration with Active Directory and other Kerberos environments.
|
||||
|
||||
## Important Notes
|
||||
|
||||
**⚠️ DNS Requirements**: Kerberos is heavily reliant on DNS (forward and reverse). Ensure your DNS is properly configured.
|
||||
|
||||
**⚠️ Error Messages**: Kerberos errors are not always descriptive. This documentation provides configuration guidance, but detailed Kerberos troubleshooting is beyond scope.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Valid Kerberos environment (KDC/Active Directory)
|
||||
- Proper DNS configuration (forward and reverse lookups)
|
||||
- Service principal for the gateway
|
||||
- Keytab file with appropriate permissions
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. Create Service Principal
|
||||
|
||||
Create a service principal for the gateway in your Kerberos realm:
|
||||
|
||||
```bash
|
||||
# Active Directory
|
||||
setspn -A HTTP/rdpgw.example.com@YOUR.REALM service-account
|
||||
|
||||
# MIT Kerberos
|
||||
kadmin.local -q "addprinc -randkey HTTP/rdpgw.example.com@YOUR.REALM"
|
||||
```
|
||||
|
||||
### 2. Generate Keytab
|
||||
|
||||
Use `ktutil` or similar tool to create a keytab file:
|
||||
|
||||
```bash
|
||||
ktutil
|
||||
addent -password -p HTTP/rdpgw.example.com@YOUR.REALM -k 1 -e aes256-cts-hmac-sha1-96
|
||||
wkt rdpgw.keytab
|
||||
quit
|
||||
```
|
||||
|
||||
Place the keytab file in a secure location and ensure it's only readable by the gateway user:
|
||||
|
||||
```bash
|
||||
sudo mv rdpgw.keytab /etc/keytabs/
|
||||
sudo chown rdpgw:rdpgw /etc/keytabs/rdpgw.keytab
|
||||
sudo chmod 600 /etc/keytabs/rdpgw.keytab
|
||||
```
|
||||
|
||||
### 3. Configure krb5.conf
|
||||
|
||||
Ensure `/etc/krb5.conf` is properly configured:
|
||||
|
||||
```ini
|
||||
[libdefaults]
|
||||
default_realm = YOUR.REALM
|
||||
dns_lookup_realm = true
|
||||
dns_lookup_kdc = true
|
||||
|
||||
[realms]
|
||||
YOUR.REALM = {
|
||||
kdc = kdc.your.realm:88
|
||||
admin_server = kdc.your.realm:749
|
||||
}
|
||||
|
||||
[domain_realm]
|
||||
.your.realm = YOUR.REALM
|
||||
your.realm = YOUR.REALM
|
||||
```
|
||||
|
||||
### 4. Gateway Configuration
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- kerberos
|
||||
Kerberos:
|
||||
Keytab: /etc/keytabs/rdpgw.keytab
|
||||
Krb5conf: /etc/krb5.conf
|
||||
Caps:
|
||||
TokenAuth: false
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. Client connects to gateway with Kerberos ticket
|
||||
2. Gateway validates ticket using keytab
|
||||
3. Client connects directly without RDP file download
|
||||
4. Gateway proxies TGT requests to KDC as needed
|
||||
|
||||
## KDC Proxy Support
|
||||
|
||||
RDPGW includes KDC proxy functionality for environments where clients cannot directly reach the KDC:
|
||||
|
||||
- Endpoint: `https://your-gateway/KdcProxy`
|
||||
- Supports MS-KKDCP protocol
|
||||
- Automatically configured when Kerberos authentication is enabled
|
||||
|
||||
## Client Configuration
|
||||
|
||||
### Windows Clients
|
||||
|
||||
Configure Windows clients to use the gateway's FQDN and ensure:
|
||||
- Client can resolve gateway hostname
|
||||
- Client time is synchronized with KDC
|
||||
- Client has valid TGT
|
||||
|
||||
### Linux Clients
|
||||
|
||||
Ensure `krb5.conf` is configured and client has valid ticket:
|
||||
|
||||
```bash
|
||||
kinit username@YOUR.REALM
|
||||
klist # Verify ticket
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Clock Skew**: Ensure all systems have synchronized time
|
||||
2. **DNS Issues**: Verify forward/reverse DNS resolution
|
||||
3. **Principal Names**: Ensure service principal matches gateway FQDN
|
||||
4. **Keytab Permissions**: Verify keytab file permissions and ownership
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Test keytab
|
||||
kinit -k -t /etc/keytabs/rdpgw.keytab HTTP/rdpgw.example.com@YOUR.REALM
|
||||
|
||||
# Verify DNS
|
||||
nslookup rdpgw.example.com
|
||||
nslookup <gateway-ip>
|
||||
|
||||
# Check time sync
|
||||
ntpdate -q ntp.your.realm
|
||||
```
|
||||
|
||||
### Log Analysis
|
||||
|
||||
Enable verbose logging in RDPGW and check for:
|
||||
- Keytab loading errors
|
||||
- Principal validation failures
|
||||
- KDC communication issues
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Protect keytab files with appropriate permissions (600)
|
||||
- Regularly rotate service account passwords
|
||||
- Monitor for unusual authentication patterns
|
||||
- Ensure encrypted communication (aes256-cts-hmac-sha1-96)
|
||||
- Use specific service accounts, not user accounts
|
||||
268
docs/ntlm-authentication.md
Normal file
268
docs/ntlm-authentication.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# NTLM Authentication
|
||||
|
||||
RDPGW supports NTLM authentication for simple setup with Windows clients, particularly useful for small deployments with a limited number of users.
|
||||
|
||||
## Advantages
|
||||
|
||||
- **Easy Setup**: Simple configuration without external dependencies
|
||||
- **Windows Client Support**: Works with default Windows client `mstsc`
|
||||
- **No External Services**: Self-contained authentication mechanism
|
||||
- **Quick Deployment**: Ideal for small teams or testing environments
|
||||
|
||||
## Security Warning
|
||||
|
||||
**⚠️ Plain Text Storage**: Passwords are currently stored in plain text to support the NTLM authentication protocol. Keep configuration files secure and avoid reusing passwords for other applications.
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. Gateway Configuration
|
||||
|
||||
Configure RDPGW to use NTLM authentication:
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- ntlm
|
||||
Caps:
|
||||
TokenAuth: false
|
||||
```
|
||||
|
||||
### 2. Authentication Helper Configuration
|
||||
|
||||
Create configuration file for `rdpgw-auth` with user credentials:
|
||||
|
||||
```yaml
|
||||
# /etc/rdpgw-auth.yaml
|
||||
Users:
|
||||
- Username: "alice"
|
||||
Password: "secure_password_1"
|
||||
- Username: "bob"
|
||||
Password: "secure_password_2"
|
||||
- Username: "admin"
|
||||
Password: "admin_secure_password"
|
||||
```
|
||||
|
||||
### 3. Start Authentication Helper
|
||||
|
||||
Run the `rdpgw-auth` helper with NTLM configuration:
|
||||
|
||||
```bash
|
||||
./rdpgw-auth -c /etc/rdpgw-auth.yaml -s /tmp/rdpgw-auth.sock
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. Client initiates NTLM handshake with gateway
|
||||
2. Gateway forwards NTLM messages to `rdpgw-auth`
|
||||
3. Helper validates credentials against configured user database
|
||||
4. Client connects directly on successful authentication
|
||||
|
||||
## User Management
|
||||
|
||||
### Adding Users
|
||||
|
||||
Edit the configuration file and restart the helper:
|
||||
|
||||
```yaml
|
||||
Users:
|
||||
- Username: "newuser"
|
||||
Password: "new_secure_password"
|
||||
- Username: "existing_user"
|
||||
Password: "existing_password"
|
||||
```
|
||||
|
||||
### Password Rotation
|
||||
|
||||
1. Update passwords in configuration file
|
||||
2. Restart `rdpgw-auth` helper
|
||||
3. Notify users of password changes
|
||||
|
||||
### User Removal
|
||||
|
||||
Remove user entries from configuration and restart helper.
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Systemd Service
|
||||
|
||||
Create `/etc/systemd/system/rdpgw-auth.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=RDPGW NTLM Authentication Helper
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=rdpgw
|
||||
ExecStart=/usr/local/bin/rdpgw-auth -c /etc/rdpgw-auth.yaml -s /tmp/rdpgw-auth.sock
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
rdpgw-auth:
|
||||
image: rdpgw-auth
|
||||
volumes:
|
||||
- ./rdpgw-auth.yaml:/etc/rdpgw-auth.yaml:ro
|
||||
- auth-socket:/tmp
|
||||
restart: always
|
||||
|
||||
rdpgw:
|
||||
image: rdpgw
|
||||
volumes:
|
||||
- auth-socket:/tmp
|
||||
depends_on:
|
||||
- rdpgw-auth
|
||||
|
||||
volumes:
|
||||
auth-socket:
|
||||
```
|
||||
|
||||
### Kubernetes Deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: rdpgw-auth-config
|
||||
data:
|
||||
rdpgw-auth.yaml: |
|
||||
Users:
|
||||
- Username: "user1"
|
||||
Password: "password1"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rdpgw-auth
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: rdpgw-auth
|
||||
image: rdpgw-auth
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/rdpgw-auth.yaml
|
||||
subPath: rdpgw-auth.yaml
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: rdpgw-auth-config
|
||||
```
|
||||
|
||||
## Client Configuration
|
||||
|
||||
### Windows (mstsc)
|
||||
|
||||
NTLM authentication works seamlessly with the default Windows Remote Desktop client:
|
||||
|
||||
1. Configure gateway address in RDP settings
|
||||
2. Save gateway credentials when prompted
|
||||
3. Connect using domain credentials or local accounts
|
||||
|
||||
### Alternative Clients
|
||||
|
||||
NTLM is widely supported across RDP clients:
|
||||
|
||||
- **mRemoteNG** (Windows)
|
||||
- **Royal TS/TSX** (Windows/macOS)
|
||||
- **Remmina** (Linux)
|
||||
- **FreeRDP** (Cross-platform)
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### File Permissions
|
||||
|
||||
Secure the configuration file:
|
||||
|
||||
```bash
|
||||
sudo chown rdpgw:rdpgw /etc/rdpgw-auth.yaml
|
||||
sudo chmod 600 /etc/rdpgw-auth.yaml
|
||||
```
|
||||
|
||||
### Password Policy
|
||||
|
||||
- Use strong, unique passwords for each user
|
||||
- Implement regular password rotation
|
||||
- Avoid reusing passwords from other systems
|
||||
- Consider minimum password length requirements
|
||||
|
||||
### Network Security
|
||||
|
||||
- Deploy gateway behind TLS termination
|
||||
- Use private networks when possible
|
||||
- Implement network-level access controls
|
||||
- Monitor authentication logs for suspicious activity
|
||||
|
||||
### Access Control
|
||||
|
||||
- Limit user accounts to necessary personnel only
|
||||
- Regularly audit user list and remove inactive accounts
|
||||
- Use principle of least privilege
|
||||
- Consider time-based access restrictions
|
||||
|
||||
## Migration Path
|
||||
|
||||
For production environments, consider migrating to more secure authentication methods:
|
||||
|
||||
### To OpenID Connect
|
||||
- Better password security (hashed storage)
|
||||
- MFA support
|
||||
- Centralized user management
|
||||
- SSO integration
|
||||
|
||||
### To Kerberos
|
||||
- No password storage in gateway
|
||||
- Enterprise authentication integration
|
||||
- Stronger cryptographic security
|
||||
- Seamless Windows domain integration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Authentication Failed**: Verify username/password in configuration
|
||||
2. **Helper Not Running**: Check if `rdpgw-auth` process is active
|
||||
3. **Socket Errors**: Verify socket path and permissions
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check helper process
|
||||
ps aux | grep rdpgw-auth
|
||||
|
||||
# Verify configuration
|
||||
cat /etc/rdpgw-auth.yaml
|
||||
|
||||
# Test socket connectivity
|
||||
ls -la /tmp/rdpgw-auth.sock
|
||||
|
||||
# Monitor authentication logs
|
||||
journalctl -u rdpgw-auth -f
|
||||
```
|
||||
|
||||
### Log Analysis
|
||||
|
||||
Enable debug logging in `rdpgw-auth` for detailed NTLM protocol analysis:
|
||||
|
||||
```bash
|
||||
./rdpgw-auth -c /etc/rdpgw-auth.yaml -s /tmp/rdpgw-auth.sock -v
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements for NTLM authentication:
|
||||
|
||||
- **Database Backend**: Support for SQLite/PostgreSQL user storage
|
||||
- **Password Hashing**: Secure password storage options
|
||||
- **Group Support**: Role-based access control
|
||||
- **Audit Logging**: Enhanced security monitoring
|
||||
75
docs/openid-authentication.md
Normal file
75
docs/openid-authentication.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# OpenID Connect Authentication
|
||||
|
||||

|
||||
|
||||
RDPGW supports OpenID Connect authentication for integration with identity providers like Keycloak, Okta, Google, Azure, Apple, or Facebook.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use OpenID Connect, ensure you have properly configured your OpenID Connect provider with a client ID and secret. The client ID and secret authenticate the gateway to the OpenID Connect provider. The provider authenticates the user and provides the gateway with a token, which generates a PAA token for RDP host connections.
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- openid
|
||||
OpenId:
|
||||
ProviderUrl: https://<provider_url>
|
||||
ClientId: <your_client_id>
|
||||
ClientSecret: <your_client_secret>
|
||||
Caps:
|
||||
TokenAuth: true
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. User navigates to `https://your-gateway/connect`
|
||||
2. Gateway redirects to OpenID Connect provider for authentication
|
||||
3. User authenticates with the provider (supports MFA)
|
||||
4. Provider redirects back to gateway with authentication token
|
||||
5. Gateway validates token and generates RDP file with temporary credentials
|
||||
6. User downloads RDP file and connects using remote desktop client
|
||||
|
||||
## Multi-Factor Authentication (MFA)
|
||||
|
||||
RDPGW provides multi-factor authentication out of the box with OpenID Connect integration. Configure MFA in your identity provider to enhance security.
|
||||
|
||||
## Provider Examples
|
||||
|
||||
### Keycloak
|
||||
```yaml
|
||||
OpenId:
|
||||
ProviderUrl: https://keycloak.example.com/auth/realms/your-realm
|
||||
ClientId: rdpgw
|
||||
ClientSecret: your-keycloak-secret
|
||||
```
|
||||
|
||||
### Azure AD
|
||||
```yaml
|
||||
OpenId:
|
||||
ProviderUrl: https://login.microsoftonline.com/{tenant-id}/v2.0
|
||||
ClientId: your-azure-app-id
|
||||
ClientSecret: your-azure-secret
|
||||
```
|
||||
|
||||
### Google
|
||||
```yaml
|
||||
OpenId:
|
||||
ProviderUrl: https://accounts.google.com
|
||||
ClientId: your-google-client-id.googleusercontent.com
|
||||
ClientSecret: your-google-secret
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Always use HTTPS for production deployments
|
||||
- Store client secrets securely and rotate them regularly
|
||||
- Configure appropriate scopes and claims in your provider
|
||||
- Enable MFA in your identity provider for enhanced security
|
||||
- Set appropriate session timeouts in both gateway and provider
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Ensure `ProviderUrl` is accessible from the gateway
|
||||
- Verify redirect URI is configured in your provider (usually `https://your-gateway/callback`)
|
||||
- Check that required scopes (openid, profile, email) are configured
|
||||
- Validate that the provider's certificate is trusted by the gateway
|
||||
242
docs/pam-authentication.md
Normal file
242
docs/pam-authentication.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# PAM/Local Authentication
|
||||
|
||||

|
||||
|
||||
RDPGW supports PAM (Pluggable Authentication Modules) for authentication against local accounts, LDAP, Active Directory, and other PAM-supported systems.
|
||||
|
||||
## Important Notes
|
||||
|
||||
**⚠️ Client Limitation**: The default Windows client `mstsc` does not support basic authentication. Use alternative clients or switch to OpenID Connect, Kerberos, or NTLM authentication.
|
||||
|
||||
**⚠️ Container Considerations**: Using PAM for passwd authentication within containers is not recommended. Use OpenID Connect or Kerberos instead. For LDAP/AD authentication, PAM works well in containers.
|
||||
|
||||
## Architecture
|
||||
|
||||
PAM authentication uses a privilege separation model with the `rdpgw-auth` helper program:
|
||||
|
||||
- `rdpgw` - Main gateway (runs as unprivileged user)
|
||||
- `rdpgw-auth` - Authentication helper (runs as root or setuid)
|
||||
- Communication via Unix socket
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. PAM Service Configuration
|
||||
|
||||
Create `/etc/pam.d/rdpgw` for the authentication service:
|
||||
|
||||
**Local passwd authentication:**
|
||||
```plaintext
|
||||
auth required pam_unix.so
|
||||
account required pam_unix.so
|
||||
```
|
||||
|
||||
**LDAP authentication:**
|
||||
```plaintext
|
||||
auth required pam_ldap.so
|
||||
account required pam_ldap.so
|
||||
```
|
||||
|
||||
**Active Directory (via Winbind):**
|
||||
```plaintext
|
||||
auth sufficient pam_winbind.so
|
||||
account sufficient pam_winbind.so
|
||||
```
|
||||
|
||||
### 2. Gateway Configuration
|
||||
|
||||
```yaml
|
||||
Server:
|
||||
Authentication:
|
||||
- local
|
||||
AuthSocket: /tmp/rdpgw-auth.sock
|
||||
BasicAuthTimeout: 5 # seconds
|
||||
Caps:
|
||||
TokenAuth: false
|
||||
```
|
||||
|
||||
### 3. Start Authentication Helper
|
||||
|
||||
Run the `rdpgw-auth` helper program:
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
./rdpgw-auth -n rdpgw -s /tmp/rdpgw-auth.sock
|
||||
|
||||
# With custom PAM service name
|
||||
./rdpgw-auth -n custom-service -s /tmp/rdpgw-auth.sock
|
||||
|
||||
# Run as systemd service
|
||||
systemctl start rdpgw-auth
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. Client connects to gateway with username/password
|
||||
2. Gateway forwards credentials to `rdpgw-auth` via socket
|
||||
3. `rdpgw-auth` validates credentials using PAM
|
||||
4. Gateway generates session tokens on successful authentication
|
||||
5. Client connects directly using authenticated session
|
||||
|
||||
## PAM Module Examples
|
||||
|
||||
### LDAP Integration
|
||||
|
||||
Install and configure LDAP PAM module:
|
||||
|
||||
```bash
|
||||
# Install LDAP PAM module
|
||||
sudo apt-get install libpam-ldap
|
||||
|
||||
# Configure /etc/pam_ldap.conf
|
||||
host ldap.example.com
|
||||
base dc=example,dc=com
|
||||
binddn cn=readonly,dc=example,dc=com
|
||||
bindpw secret
|
||||
```
|
||||
|
||||
### Active Directory Integration
|
||||
|
||||
Configure Winbind PAM module:
|
||||
|
||||
```bash
|
||||
# Install Winbind
|
||||
sudo apt-get install winbind libpam-winbind
|
||||
|
||||
# Configure /etc/samba/smb.conf
|
||||
[global]
|
||||
security = ads
|
||||
realm = EXAMPLE.COM
|
||||
workgroup = EXAMPLE
|
||||
```
|
||||
|
||||
### Two-Factor Authentication
|
||||
|
||||
Integrate with TOTP/HOTP using pam_oath:
|
||||
|
||||
```plaintext
|
||||
auth required pam_oath.so usersfile=/etc/users.oath
|
||||
auth required pam_unix.so
|
||||
account required pam_unix.so
|
||||
```
|
||||
|
||||
## Container Deployment
|
||||
|
||||
### Option 1: External Helper
|
||||
|
||||
Run `rdpgw-auth` on the host and mount socket:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
rdpgw:
|
||||
image: rdpgw
|
||||
volumes:
|
||||
- /tmp/rdpgw-auth.sock:/tmp/rdpgw-auth.sock
|
||||
```
|
||||
|
||||
### Option 2: Privileged Container
|
||||
|
||||
Mount PAM configuration and user databases:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
rdpgw:
|
||||
image: rdpgw
|
||||
privileged: true
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/shadow:/etc/shadow:ro
|
||||
- /etc/pam.d:/etc/pam.d:ro
|
||||
```
|
||||
|
||||
## Systemd Service
|
||||
|
||||
Create `/etc/systemd/system/rdpgw-auth.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=RDPGW Authentication Helper
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/local/bin/rdpgw-auth -n rdpgw -s /tmp/rdpgw-auth.sock
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable and start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable rdpgw-auth
|
||||
sudo systemctl start rdpgw-auth
|
||||
```
|
||||
|
||||
## Compatible Clients
|
||||
|
||||
Since `mstsc` doesn't support basic authentication, use these alternatives:
|
||||
|
||||
### Windows
|
||||
- **Remote Desktop Connection Manager** (RDCMan)
|
||||
- **mRemoteNG**
|
||||
- **Royal TS/TSX**
|
||||
|
||||
### Linux
|
||||
- **Remmina**
|
||||
- **FreeRDP** (with basic auth support)
|
||||
- **KRDC**
|
||||
|
||||
### macOS
|
||||
- **Microsoft Remote Desktop** (from App Store)
|
||||
- **Royal TSX**
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Run `rdpgw-auth` with minimal privileges
|
||||
- Secure the Unix socket with appropriate permissions
|
||||
- Use strong PAM configurations (account lockout, password complexity)
|
||||
- Enable logging for authentication events
|
||||
- Consider rate limiting for brute force protection
|
||||
- Use encrypted connections (TLS) for the gateway
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Socket Permission Denied**: Check socket permissions and ownership
|
||||
2. **PAM Authentication Failed**: Verify PAM configuration and user credentials
|
||||
3. **Helper Not Running**: Ensure `rdpgw-auth` is running and accessible
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Test PAM configuration
|
||||
pamtester rdpgw username authenticate
|
||||
|
||||
# Check socket
|
||||
ls -la /tmp/rdpgw-auth.sock
|
||||
|
||||
# Verify helper process
|
||||
ps aux | grep rdpgw-auth
|
||||
|
||||
# Test authentication manually
|
||||
echo "username:password" | nc -U /tmp/rdpgw-auth.sock
|
||||
```
|
||||
|
||||
### Log Analysis
|
||||
|
||||
Enable PAM logging in `/etc/rsyslog.conf`:
|
||||
|
||||
```plaintext
|
||||
auth,authpriv.* /var/log/auth.log
|
||||
```
|
||||
|
||||
Monitor authentication attempts:
|
||||
|
||||
```bash
|
||||
tail -f /var/log/auth.log | grep rdpgw
|
||||
```
|
||||
Reference in New Issue
Block a user