Skip to content

Commit

Permalink
Moved NTLM to rdpgw-auth to avoid accessing passwords in main program
Browse files Browse the repository at this point in the history
NTLM tests added
NTLM licensing info added
Avoid logging NTLM messages as it may contain sensitive information
Renamed database authentication to NTLM as requested by bolkedebruin (see PR #109)
  • Loading branch information
m7913d committed Apr 21, 2024
1 parent 8714a87 commit 2f4e5ed
Show file tree
Hide file tree
Showing 15 changed files with 817 additions and 284 deletions.
47 changes: 25 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ If local authentication is used the client will need to provide a username and p
against PAM. This requires, to ensure privilege separation, that ```rdpgw-auth``` is also running and a
valid PAM configuration is provided per typical configuration.

If database authentication is used, the allowed user credentials for the gateway should be configured in the
configuration file.
If NTLM authentication is used, the allowed user credentials for the gateway should be configured in the
configuration file of `rdpgw-auth`.

Finally, RDP hosts that the client wants to connect to are verified against what was provided by / allowed by
the server. Next to that the client's ip address needs to match the one it obtained the gateway token with if
Expand All @@ -61,7 +61,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 a database.
mechanisms such as OpenID Connect, Kerberos, PAM or NTLM.

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)
Expand All @@ -75,7 +75,7 @@ if you want.

### Mixing authentication mechanisms

It is technically possible to mix authentication mechanisms. Currently, you can mix local with Kerberos or database. If you enable
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
Expand Down Expand Up @@ -149,7 +149,7 @@ but it also supports LDAP authentication or even Active Directory if you have th
`rdpgw-auth` that is used to authenticate the user. This program needs to be run as root or setuid.

__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 database authentication.
switch to OpenID Connect, Kerberos or 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
Expand Down Expand Up @@ -183,30 +183,35 @@ Make sure to run both the gateway and `rdpgw-auth`. The gateway will connect to

The client can then connect to the gateway directly by using a remote desktop client.

### Database (Basic Auth or NTLM)
### NTLM

The gateway can also support authentication using a local database.
Currently, only the configuration file is supported as a database.
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.

Database 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, database authentication supports the default windows client ``mstsc``.
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 database authentication make sure to set the following variables in the configuration file.
To enable NTLM authentication make sure to set the following variables in the configuration file.

Configuration file for `rdpgw`:
```yaml
Server:
Authentication:
- database
Users:
- {Username: "username1", Password: "secure_password"} # Modify this password!
- 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.

Expand Down Expand Up @@ -239,17 +244,17 @@ TLS termination.
```yaml
# web server configuration.
Server:
# can be set to openid, kerberos, local and database. If openid is used rdpgw expects
# can be set to openid, kerberos, local and ntlm. If openid is used rdpgw expects
# a configured openid provider, make sure to set caps.tokenauth to true. If local
# rdpgw connects to rdpgw-auth over a socket to verify users and password. Note:
# rdpgw-auth needs to be run as root or setuid in order to work. If kerberos is
# used a keytab and krb5conf need to be supplied. local can be stacked with
# kerberos or database authentication, so that the clients selects what it wants.
# kerberos or ntlm authentication, so that the clients selects what it wants.
Authentication:
# - kerberos
# - local
- openid
# - database
# - ntlm
# The socket to connect to if using local auth. Ensure rdpgw auth is configured to
# use the same socket.
# AuthSocket: /tmp/rdpgw-auth.sock
Expand Down Expand Up @@ -298,9 +303,6 @@ OpenId:
# Keytab: /etc/keytabs/rdpgw.keytab
# Krb5conf: /etc/krb5.conf
# enabled / disabled capabilities
# Users:
# - {Username: "username1", Password: "secure_password"}
# - {Username: "username2", Password: "secure_password2"}
Caps:
SmartCardAuth: false
# required for openid connect
Expand Down Expand Up @@ -404,7 +406,7 @@ In this way you can integrate, for example, it with [pam-jwt](https://github.com
The several clients that Microsoft provides come with their own caveats.
The most important one is that the default client on Windows ``mstsc`` does
not support basic authentication. This means you need to use either OpenID Connect,
Kerberos or database authentication.
Kerberos or ntlm authentication.

In addition to that, ``mstsc``, when configuring a gateway directly in the client requires
you to either:
Expand All @@ -430,4 +432,5 @@ flexibility.
## TODO
* Improve Web Interface


# Acknowledgements
* This product includes software developed by the Thomson Reuters Global Resources. ([go-ntlm](https://github.com/m7913d/go-ntlm) - BSD-4 License)
43 changes: 39 additions & 4 deletions cmd/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"errors"
"fmt"
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
"github.com/bolkedebruin/rdpgw/cmd/auth/database"
"github.com/bolkedebruin/rdpgw/cmd/auth/ntlm"
"github.com/bolkedebruin/rdpgw/shared/auth"
"github.com/msteinert/pam/v2"
"github.com/thought-machine/go-flags"
Expand All @@ -21,16 +24,24 @@ const (
var opts struct {
ServiceName string `short:"n" long:"name" default:"rdpgw" description:"the PAM service name to use"`
SocketAddr string `short:"s" long:"socket" default:"/tmp/rdpgw-auth.sock" description:"the location of the socket"`
ConfigFile string `short:"c" long:"conf" default:"rdpgw-auth.yaml" description:"users config file for NTLM (yaml)"`
}

type AuthServiceImpl struct {
auth.UnimplementedAuthenticateServer

serviceName string
ntlm *ntlm.NTLMAuth
}

var conf config.Configuration
var _ auth.AuthenticateServer = (*AuthServiceImpl)(nil)

func NewAuthService(serviceName string) auth.AuthenticateServer {
s := &AuthServiceImpl{serviceName: serviceName}
func NewAuthService(serviceName string, database database.Database) auth.AuthenticateServer {
s := &AuthServiceImpl{
serviceName: serviceName,
ntlm: ntlm.NewNTLMAuth(database),
}
return s
}

Expand Down Expand Up @@ -77,12 +88,35 @@ func (s *AuthServiceImpl) Authenticate(ctx context.Context, message *auth.UserPa
return r, nil
}

func (s *AuthServiceImpl) NTLM(ctx context.Context, message *auth.NtlmRequest) (*auth.NtlmResponse, error) {
r, err := s.ntlm.Authenticate(message)

if err != nil {
log.Printf("[%s] NTLM failed: %s", message.Session, err)
} else if r.Authenticated {
log.Printf("[%s] User: %s authenticated using NTLM", message.Session, r.Username)
} else if r.NtlmMessage != "" {
log.Printf("[%s] Sending NTLM challenge", message.Session)
}

return r, err
}

func main() {
_, err := flags.Parse(&opts)
if err != nil {
panic(err)
var fErr *flags.Error
if errors.As(err, &fErr) {
if fErr.Type == flags.ErrHelp {
fmt.Printf("Acknowledgements:\n")
fmt.Printf(" - This product includes software developed by the Thomson Reuters Global Resources. (go-ntlm - https://github.com/m7913d/go-ntlm - BSD-4 License)\n")
}
}
return
}

conf = config.Load(opts.ConfigFile)

log.Printf("Starting auth server on %s", opts.SocketAddr)
cleanup := func() {
if _, err := os.Stat(opts.SocketAddr); err == nil {
Expand All @@ -100,7 +134,8 @@ func main() {
log.Fatal(err)
}
server := grpc.NewServer()
service := NewAuthService(opts.ServiceName)
db := database.NewConfig(conf.Users)
service := NewAuthService(opts.ServiceName, db)
auth.RegisterAuthenticateServer(server, service)
server.Serve(listener)
}
42 changes: 42 additions & 0 deletions cmd/auth/config/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package config

import (
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"log"
"os"
)

type Configuration struct {
Users []UserConfig `koanf:"users"`
}

type UserConfig struct {
Username string `koanf:"username"`
Password string `koanf:"password"`
}

var Conf Configuration

func Load(configFile string) Configuration {

var k = koanf.New(".")

k.Load(confmap.Provider(map[string]interface{}{}, "."), nil)

if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("Config file %s not found, skipping config file", configFile)
} else {
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
log.Fatalf("Error loading config from file: %v", err)
}
}

koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
k.UnmarshalWithConf("Users", &Conf.Users, koanfTag)

return Conf

}
4 changes: 2 additions & 2 deletions cmd/rdpgw/database/config.go → cmd/auth/database/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package database

import (
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/config"
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
)

type Config struct {
Expand All @@ -22,4 +22,4 @@ func NewConfig(users []config.UserConfig) *Config {

func (c *Config) GetPassword (username string) string {
return c.users[username].Password
}
}
43 changes: 43 additions & 0 deletions cmd/auth/database/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package database

import (
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
"testing"
)

func createTestDatabase () (Database) {
var users = []config.UserConfig{}

user1 := config.UserConfig{}
user1.Username = "my_username"
user1.Password = "my_password"
users = append(users, user1)

user2 := config.UserConfig{}
user2.Username = "my_username2"
user2.Password = "my_password2"
users = append(users, user2)

config := NewConfig(users)

return config
}

func TestDatabaseConfigValidUsername(t *testing.T) {
database := createTestDatabase()

if database.GetPassword("my_username") != "my_password" {
t.Fatalf("Wrong password returned")
}
if database.GetPassword("my_username2") != "my_password2" {
t.Fatalf("Wrong password returned")
}
}

func TestDatabaseInvalidUsername(t *testing.T) {
database := createTestDatabase()

if database.GetPassword("my_invalid_username") != "" {
t.Fatalf("Non empty password returned for invalid username")
}
}
File renamed without changes.
Loading

0 comments on commit 2f4e5ed

Please sign in to comment.