diff --git a/README.md b/README.md index 3d1350a..22c19bf 100644 --- a/README.md +++ b/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](docs/images/flow-openid.svg) +### 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:// - ClientId: - ClientSecret: -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 -![Kerberos](docs/images/flow-kerberos.svg) -__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](docs/images/flow-pam.svg) +### 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 diff --git a/cmd/rdpgw/config/configuration.go b/cmd/rdpgw/config/configuration.go index 2bbab3c..b289072 100644 --- a/cmd/rdpgw/config/configuration.go +++ b/cmd/rdpgw/config/configuration.go @@ -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 { diff --git a/cmd/rdpgw/config/configuration_test.go b/cmd/rdpgw/config/configuration_test.go new file mode 100644 index 0000000..cb204a7 --- /dev/null +++ b/cmd/rdpgw/config/configuration_test.go @@ -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") + } + }) + } +} \ No newline at end of file diff --git a/cmd/rdpgw/main.go b/cmd/rdpgw/main.go index 73e0d44..307e3c8 100644 --- a/cmd/rdpgw/main.go +++ b/cmd/rdpgw/main.go @@ -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) } } diff --git a/cmd/rdpgw/web/header.go b/cmd/rdpgw/web/header.go new file mode 100644 index 0000000..38fef20 --- /dev/null +++ b/cmd/rdpgw/web/header.go @@ -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) + }) +} \ No newline at end of file diff --git a/cmd/rdpgw/web/header_test.go b/cmd/rdpgw/web/header_test.go new file mode 100644 index 0000000..266edb7 --- /dev/null +++ b/cmd/rdpgw/web/header_test.go @@ -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) + } +} \ No newline at end of file diff --git a/docs/header-authentication.md b/docs/header-authentication.md new file mode 100644 index 0000000..65c8a22 --- /dev/null +++ b/docs/header-authentication.md @@ -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 +``` diff --git a/docs/kerberos-authentication.md b/docs/kerberos-authentication.md new file mode 100644 index 0000000..d2ae391 --- /dev/null +++ b/docs/kerberos-authentication.md @@ -0,0 +1,156 @@ +# Kerberos Authentication + +![Kerberos](images/flow-kerberos.svg) + +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 + +# 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 diff --git a/docs/ntlm-authentication.md b/docs/ntlm-authentication.md new file mode 100644 index 0000000..b44c993 --- /dev/null +++ b/docs/ntlm-authentication.md @@ -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 diff --git a/docs/openid-authentication.md b/docs/openid-authentication.md new file mode 100644 index 0000000..e1c9b54 --- /dev/null +++ b/docs/openid-authentication.md @@ -0,0 +1,75 @@ +# OpenID Connect Authentication + +![OpenID Connect](images/flow-openid.svg) + +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:// + ClientId: + ClientSecret: +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 diff --git a/docs/pam-authentication.md b/docs/pam-authentication.md new file mode 100644 index 0000000..b75c3ba --- /dev/null +++ b/docs/pam-authentication.md @@ -0,0 +1,242 @@ +# PAM/Local Authentication + +![PAM](images/flow-pam.svg) + +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 +```