LDAP provider
Introduction
LDAP Provider enables applications that use LDAP for user authentication to take advantage of modern authentication (e.g., OIDC) without rewriting the application.
A deployment pattern known as a facade is typically used to update and modernize LDAP-based applications. When an application uses this pattern, it utilizes an orchestrator acting as an identity-aware proxy (Proxy app) and an orchestrator acting as an LDAP server (LDAP Provider) and sandwiches the application between the orchestrators.
During implementation, the LDAP configuration for the application is updated to point to the orchestrator, acting as the LDAP server instead of the currently configured LDAP server (e.g., Active Directory). This configuration change is the only update required on the application server, no application code changes are needed.
Using this pattern, end-user requests flow through the orchestrator configured as an app proxy, so they can be authenticated using your preferred IDPs, support resilience, and be authorized based on the configured policy.
Once the user is authenticated, the orchestrator uses the configured service extensions to log into the application behind the scenes, so the end-user never sees the native application login page. The application typically uses the credentials received and authenticates them via an LDAP Bind request. When the orchestrator receives this Bind request, it authenticates the user based on the type of Bind request (e.g., Simple, SASL) received and how the orchestrator has been configured.
For instance, the orchestrator acting as an identity-aware proxy can:
- Create a one-time user.
- Pass this one-time user to the application being protected.
- The application will send a Bind request using these credentials.
- The orchestrator acting as an LDAP server authenticates the request using the data stored about the one-time user (e.g., hashed one-time password).
Once authenticated, the application typically sends a Search request to the LDAP Provider asking for the attributes of the authenticated user (e.g., employee ID, email, name). Upon receiving the response, the application finishes performing any required actions to show the user the requested page.
This deployment pattern supports many applications requiring LDAP and can easily be fine-tuned for your environment. Additionally, while we mention that this can be deployed on two separate orchestrators, one acting as an identity-aware proxy and one acting as an LDAP server, these can be the same orchestrator, or groups of orchestrators to support Highly Available (HA) deployments.
Supported Features
LDAP Operations
Bind
The Bind operation is used to authenticate the LDAP connection. Currently, the following authentication methods are supported:
See the RFC for more information.
Simple
Simple authentication consists of sending the LDAP server the fully qualified DN of the client and the client’s clear-text password (RFC 2251 and RFC 2829). Given that the password is not protected and can be easily read from the network, it is recommended that the connection use TLS.
See the RFC for more information.
SASL
The Simple Authentication and Security Layer (SASL) enables LDAP to support pluggable authentication. This allows LDAP clients and servers to negotiate standard or non-standard custom mechanisms for authentication. In addition, SASL mechanisms can support negotiating SASL-layer encryption/integrity verification. When negotiated, this layer can help reduce the risk of compromised connection.
While the LDAP Provider permits SASL binds to be performed on a TLS-protected connection, it does not allow the use of SASL-layer encryption/integrity verification mechanisms on such a connection.
Once a SASL-layer encryption/integrity verification mechanism is used on a connection, the client MUST NOT send an additional bind request. If an additional Bind request is received after the SASL layer is used, an “Unwilling To Perform (53)” response will be returned.
See the RFC for more information.
The following SASL Mechanisms are currently supported:
GSS-SPNEGO
GSS-SPNEGO is a pseudo-security mechanism that enables GSS-API peers to determine in-band whether their credentials support a common set of one or more GSS-API mechanisms. This is useful for applications that share multiple mechanisms with the LDAP Provider. At this time, only NTLM is supported.
See the RFC for more information.
NTLM
NTLM is the NT LAN Manager Authentication protocol that is typically used in Windows environments for authentication between clients and servers, and optionally, can provide session security when requested by the client. Within LDAP, session security is provided via a SASL-layer encryption/integrity verification mechanism.
Supported NTLM Versions:
- v2
See the specification for more information
Unbind
Unbind signals to close the session’s connection.
See the RFC for more information.
Search
The Search operation is used to request a server to return a set of entries matching the search criterion. If the request is for the Root DSE (an empty DN), the LDAP Provider will return the entries based on the attributes requested. The following attributes are supported out of the box, and their values will be based on the Orchestrator configuration:
supportedLDAPVersion
: The value will always be3
(only LDAP v3 is supported).supportedSASLMechanisms
: The values will be based on which SASL Mechanisms are enabled.
RootDSE requests can be made on unauthenticated connections since clients typically use the response to determine which authentication methods are mutually supported.
If the client requires specific Root DSE attributes in the response, additional
values can be set via the rootDSE.attributes
configuration. Any values set here
will be included in Root DSE Search requests.
For other Search requests, the connection MUST be authenticated. When received, the
processing is deferred to the Search
Service Extension to consolidate and filter
data sources and return the results to the client.
See the specification for more information
Extended - StartTLS
StartTLS upgrades the TCP connection to TLS using the provided tls
config. LDAP
clients may leverage StartTLS to secure their connection if the LDAP provider has
been configured with ldap://
vs. ldaps://
.
See the specification for more information
Configuration
enabled
(Required) - Whether or not the LDAP Provider is enabled. It MUST be set to
true
for the LDAP Provider to start.
uri
(Required) - The address that the LDAP Provider listens on. It MUST begin
with either ldap://
or ldaps://
. If using ldaps
then the configuration must
define and use tls
. Default ports will be used if unspecified in uri
: ldap://
defaulting to 389
or ldaps://
defaulting to 636
. Examples below:
ldaps://:636
- The LDAP Provider will listen on all interfaces at port 636 using TLS.ldap://127.0.0.1:389
The LDAP Provider will listen on the loopback interface at port 389 and will not use TLS.ldap://0.0.0.0
The LDAP Provider will listen on all interfaces at the default port 389 and will not use TLS.
readDeadline
(Optional. Defaults to 5m
) - The max period of time that
established connections will stay connected without receiving data. If this time is
elapsed, the connection will be closed.
writeDeadline
(Optional. Defaults to 5m
) - The max period of time that
established connections will stay connected without sending data. If this time is
elapsed, the connection will be closed.
tls
(Optional) - The name of the TLS configuration specified at the root of the
Orchestrator config.
Root DSE
rootDSE.attributes
(Optional) - The custom attributes that should be returned
by the LDAP Provider when a Root DSE Search is received.
Search
search.searchSE
(Optional) - The service extension that is invoked when a
non-Root DSE search is received. While this is optional, for most applications, this
service extension will be required.
Authentication
authentication.allowAnonymous
(Optional. Defaults to false
) - Whether the
LDAP Provider allows anonymous authentication when receiving an LDAP Search.
Simple Authentication
authentication.methods.simple.enabled
(Optional. Defaults to false
) - Whether
the LDAP Provider allows Bind requests using the Simple authentication choice.
authentication.methods.simple.authenticateSE
(Optional. Required when Simple Authentication is enabled) - The required service extension that is invoked when a Bind request is received
using simple authentication.
SASL
authentication.methods.sasl.enabled
(Optional. Defaults to false
) - Whether
the LDAP Provider supports Bind requests using a SASL Mechanism. If disabled, all
SASL mechanisms will be disabled, regardless of whether they are enabled or not.
GSS-SPNEGO
authentication.methods.sasl.mechanisms.gssspnego.enabled
(Optional. Defaults to false
) - Whether the LDAP Provider supports the
GSS-SPNEGO Mechanism. Currently, when enabled, NTLM MUST also be enabled. If
disabled, NTLM will also be disabled.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.enabled
(Optional. Defaults to false
) -
Whether the LDAP Provider supports negotiating NTLM as part of the GSS-SPNEGO mechanism.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.disableRequiring128bitEncryption
(Optional. Defaults to false
) -
Whether the LDAP Provider disables requiring 128-bit encryption for NTLM.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.netbiosDomainName
(Optional) - Sets the NetBIOS domain name used by NTLM.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.netbiosMachineName
(Optional) -
Sets the NetBIOS machine name used by NTLM. This is typically the name of the server
that the LDAP Provider is hosted on.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.dnsDomainName
(Optional) -
Sets the FQDN of the server’s domain.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.dnsForestName
(Optional) -
Sets the FQDN of the server’s forest. The DnsForestName is empty on machines that are
not domain joined.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.dnsMachineName
(Optional) -
Sets the FQDN of the server.
authentication.methods.sasl.mechanisms.gssspnego.ntlm.getHashedCredentialsSE
(Required) -
The service extension that is invoked during the verification of the NTLM
Authenticate message. It is responsible for retrieving the NT and LM hashes for the
provided user.
Configuration Examples
LDAP Provider with TLS
tls:
ldapTLS:
certFile: /etc/maverics/certs/ldap.crt
keyFile: /etc/maverics/certs/ldap.key
maverics:
certFile: /etc/maverics/certs/maverics.crt
keyFile: /etc/maverics/certs/maverics.key
http:
address: :443
tls: maverics
ldapProvider:
enabled: true
uri: ldaps://ldap.example.com:636
tls: ldapTLS
Add support for Simple Authentication
tls:
ldapTLS:
certFile: /etc/maverics/certs/ldap.crt
keyFile: /etc/maverics/certs/ldap.key
maverics:
certFile: /etc/maverics/certs/maverics.crt
keyFile: /etc/maverics/certs/maverics.key
http:
address: :443
tls: maverics
ldapProvider:
enabled: true
uri: ldaps://ldap.example.com:636
tls: ldapTLS
authentication:
methods:
simple:
enabled: true
authenticateSE:
funcName: Authenticate
file: /etc/maverics/extensions/simple.go
Add support for non-Root DSE Search Requests
tls:
ldapTLS:
certFile: /etc/maverics/certs/ldap.crt
keyFile: /etc/maverics/certs/ldap.key
maverics:
certFile: /etc/maverics/certs/maverics.crt
keyFile: /etc/maverics/certs/maverics.key
http:
address: :443
tls: maverics
ldapProvider:
enabled: true
uri: ldaps://ldap.example.com:636
tls: ldapTLS
search:
searchSE:
funcName: Search
file: /etc/maverics/extensions/search.go
Add support for custom attributes to be returned upon a Root DSE request
tls:
ldapTLS:
certFile: /etc/maverics/certs/ldap.crt
keyFile: /etc/maverics/certs/ldap.key
maverics:
certFile: /etc/maverics/certs/maverics.crt
keyFile: /etc/maverics/certs/maverics.key
http:
address: :443
tls: maverics
ldapProvider:
enabled: true
uri: ldaps://ldap.example.com:636
tls: ldapTLS
rootDSE:
attributes:
subschemaSubentry:
- "CN=Aggregate,CN=Schema,CN=Configuration,DC=acme,DC=local"
namingContexts:
- "DC=acme,DC=local"
- "CN=Configuration,DC=acme,DC=local"
- "CN=Schema,CN=Configuration,DC=acme,DC=local"
- "DC=ForestDnsZones,DC=acme,DC=local"
- "DC=DomainDnsZones,DC=acme,DC=local"
defaultNamingContext:
- "DC=acme,DC=local"
schemaNamingContext:
- "CN=Schema,CN=Configuration,DC=acme,DC=local"
configurationNamingContext:
- "CN=Configuration,DC=acme,DC=local"
rootDomainNamingContext:
- "DC=acme,DC=local"
Add support for SASL / GSS-SPNEGO / NTLM Authentication
tls:
ldapTLS:
certFile: /etc/maverics/certs/ldap.crt
keyFile: /etc/maverics/certs/ldap.key
maverics:
certFile: /etc/maverics/certs/maverics.crt
keyFile: /etc/maverics/certs/maverics.key
http:
address: :443
tls: maverics
ldapProvider:
enabled: true
uri: ldaps://ldap.example.com:636
tls: ldapTLS
authentication:
sasl:
enabled: true
mechanisms:
gssspnego:
enabled: true
ntlm:
enabled: true
disableRequiring128bitEncryption: false
netbiosDomainName: ACME
netbiosMachineName: MAVERICS01
dnsDomainName: acme.org
dnsForestName: acme.org
dnsMachineName: MAVERICS01.acme.org
getHashedCredentialsSE:
funcName: NTLMGetHashedCredentials
file: /etc/maverics/extensions/gss_spnego_ntlm.go
Complete configuration
tls:
ldapTLS:
certFile: /etc/maverics/certs/ldap.crt
keyFile: /etc/maverics/certs/ldap.key
maverics:
certFile: /etc/maverics/certs/maverics.crt
keyFile: /etc/maverics/certs/maverics.key
http:
address: :443
tls: maverics
ldapProvider:
enabled: true
uri: ldaps://ldap.example.com:636
readDeadline: 5m
writeDeadline: 5m
tls: ldapTLS
rootDSE:
attributes:
subschemaSubentry:
- "CN=Aggregate,CN=Schema,CN=Configuration,DC=acme,DC=local"
namingContexts:
- "DC=acme,DC=local"
- "CN=Configuration,DC=acme,DC=local"
- "CN=Schema,CN=Configuration,DC=acme,DC=local"
- "DC=ForestDnsZones,DC=acme,DC=local"
- "DC=DomainDnsZones,DC=acme,DC=local"
defaultNamingContext:
- "DC=acme,DC=local"
schemaNamingContext:
- "CN=Schema,CN=Configuration,DC=acme,DC=local"
configurationNamingContext:
- "CN=Configuration,DC=acme,DC=local"
rootDomainNamingContext:
- "DC=acme,DC=local"
search:
searchSE:
funcName: Search
file: /etc/maverics/extensions/search.go
authentication:
allowAnonymous: false
methods:
simple:
enabled: true
authenticateSE:
funcName: Authenticate
file: /etc/maverics/extensions/simple.go
sasl:
enabled: true
mechanisms:
gssspnego:
enabled: true
ntlm:
enabled: true
disableRequiring128bitEncryption: false
netbiosDomainName: ACME
netbiosMachineName: MAVERICS01
dnsDomainName: acme.org
dnsForestName: acme.org
dnsMachineName: MAVERICS01.acme.org
getHashedCredentialsSE:
funcName: NTLMGetHashedCredentials
file: /etc/maverics/extensions/gss_spnego_ntlm.go
Service Extension Examples
/etc/maverics/extensions/search.go
import (
"strings"
"github.com/strata-io/go-ntlm"
"github.com/strata-io/service-extension/orchestrator"
)
// This is an example of an Search Service Extension and shows a simple way to verify
// the LDAP connection between the application and the Orchestrator. It is configured
// to return hard-coded attributes for a test user, regardless of the Search request
// received.
//
// A production version will be dependant on the application and environment; however,
// typically, attributes originate from the end-users Session and would NOT be
// hardcoded. For example, if the policy requires a user to be authenticated
// using Entra ID, then the attributes would likely come from Entra ID, and stored
// on the cache via the Orchestrator servicing as the proxy for the application.
func Search(api orchestrator.Orchestrator, dn string, filter string, reqAttrs []string) (map[string]map[string]interface{}, error) {
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "Search",
"msg", "processing request",
"dn", dn,
"filter", filter,
"attributes", strings.Join(reqAttrs, ","),
)
result := make(map[string]map[string]interface{})
// User attributes would typically originate from the users Session and would NOT
// be hardcoded. For example, if the policy requires a user to be authenticated
// using Entra ID, then the attributes would likely come from Entra ID.
userAttrs := make(map[string]interface{})
userAttrs["Name"] = "Test User"
userAttrs["mail"] = "[email protected]"
userAttrs["givenName"] = "Test"
userAttrs["sn"] = "User"
result["CN=Test,CN=Domain Users,CN=Users,DC=acme,DC=local"] = userAttrs
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "Search",
"msg", "request processed",
"dn", dn,
"filter", filter,
"attributes", strings.Join(reqAttrs, ","),
)
return result, nil
}
/etc/maverics/extensions/simple.go
import (
"strings"
"github.com/strata-io/go-ntlm"
"github.com/strata-io/service-extension/orchestrator"
)
// This is an example Simple Authentication Service Extension that shows a quick way
// to test a Bind request using the Simple Authentication method between the
// application and the Orchestrator. In this example, only a user with the username
// of `bob` and the password of `th3Bu!ld3R` to be authenticated.
//
// A production version of this Service Extension would typically retrieve the
// password hash of the user from either `api.Cache` or `api.Session` and would
// compute the hash of the password received and compare it with the one stored. The
// user is typically a short-lived / one-time user created by an Orchestrator serving
// as the identity-aware proxy, after the end-user has been authenticated by
// the configured IdP.
func Authenticate(api orchestrator.Orchestrator, username string, password string) (bool, error) {
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "Authenticate",
"msg", "authenticating user",
"username", username,
)
if strings.EqualFold(username, "bob") && password == "th3Bu!ld3R" {
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "Authenticate",
"msg", "user authenticated",
"username", username,
)
return true, nil
}
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "Authenticate",
"msg", "user not authenticated",
"username", username,
)
return false, nil
}
/etc/maverics/extensions/gss_spnego_ntlm.go
import (
"fmt"
"github.com/strata-io/go-ntlm"
"github.com/strata-io/service-extension/orchestrator"
)
var (
// THIS IS FOR TESTING PURPOSES ONLY. Do not use this pattern in a production
// deployment.
testUsers = map[string][]byte{
fmt.Sprintf("%s:%s", ntlm.ToUnicode("acme"), ntlm.ToUnicode("user123")): ntlm.ToUnicode("abc123"),
}
)
// This is an example of an NTLM NTLMGetHashedCredentials Service Extension and shows
// a simple way to test users while verifying the LDAP connection between the
// application and the Orchestrator.
//
// A production version of this Service Extension would typically retrieve the
// NT and LM hashes of the user from either `api.Cache` or `api.Session`, which would
// typically be computed by an Orchestrator serving as the application proxy. The
// user is typically a short-lived / one-time user created after the end-user has
// been authenticated and authorized.
func NTLMGetHashedCredentials(
api orchestrator.Orchestrator,
user []byte,
domain []byte,
) (ntHash []byte, lmHash []byte, err error) {
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "NTLMGetHashedCredentials",
"msg", "retrieving NT and LM hashes for user",
"domain", fmt.Sprintf("%q", user[:]),
"user", fmt.Sprintf("%q", user[:]),
)
key := fmt.Sprintf("%s:%s", string(domain), string(user))
pass, ok := testUsers[key]
if !ok {
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "NTLMGetHashedCredentials",
"msg", "invalid user",
"domain", fmt.Sprintf("%q", domain[:]),
"user", fmt.Sprintf("%q", user[:]),
)
return nil, nil, fmt.Errorf("invalid user: %s", key)
}
ntHash = ntlm.NTOWFv2(pass, user, domain)
lmHash = ntlm.LMOWFv2(pass, user, domain)
api.Logger().Debug(
"service", "LDAP Provider",
"extension", "NTLMGetHashedCredentials",
"msg", "NT and LM hashes found for user",
"domain", fmt.Sprintf("%q", domain[:]),
"user", fmt.Sprintf("%q", user[:]),
)
return ntHash, lmHash, err
}
Telemetry
Coming soon.
Glossary
- SASL - Simple Authentication and Security Layer.
- GSS - Generic Security Service API.
- SPNEGO - Simple and Protected GSSAPI Negotiation Mechanism.
- NTLM - New Technology LAN Manager.
- OID - Object Identifier.