Add header authentication

This commit is contained in:
Bolke de Bruin
2025-09-18 22:35:40 +02:00
parent fee421beba
commit 75a7ca62a9
11 changed files with 1392 additions and 127 deletions

139
README.md
View File

@@ -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://<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
![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

View File

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

View 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")
}
})
}
}

View File

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

View 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)
}
}

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

View File

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

View File

@@ -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://<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
View File

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