diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ba33eb6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,16 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Support for multiple encryption mechanisms: TLS and Kerberos (if ldap3 provides it)
+
+## [0.1.0] - 2023-06-05
+
+Initial commit
diff --git a/README.md b/README.md
index 5f8f988..00dd396 100644
--- a/README.md
+++ b/README.md
@@ -10,13 +10,12 @@ Your Active Directory domain must be able to use group Managed Service Account w
* AD schema updated to Windows Server 2012 ([Getting Started with Group Managed Service Accounts](https://learn.microsoft.com/en-us/windows-server/security/group-managed-service-accounts/getting-started-with-group-managed-service-accounts))
* KDS Root Key deployed ([Create the Key Distribution Services KDS Root Key](https://learn.microsoft.com/en-us/windows-server/security/group-managed-service-accounts/create-the-key-distribution-services-kds-root-key))
-In addition, `gmsad` requires a working LDAPS interface on domain controllers with a valid TLS certificate.
-
# Documentation
-- [Getting started with gmsad](doc/getting_started.md)
+- [Getting Started with gmsad](doc/getting_started.md)
- [Why was this tool created ?](doc/genesis.md)
- [How does a gMSA work ?](doc/gmsa.md)
+- [Encryption Mechanisms](doc/encryption.md)
- [Talk at SSTIC 2023 (in french)](https://www.sstic.org/2023/presentation/gmsad/)
# Contributing
diff --git a/doc/encryption.md b/doc/encryption.md
new file mode 100644
index 0000000..857b0d7
--- /dev/null
+++ b/doc/encryption.md
@@ -0,0 +1,11 @@
+# Encryption Mechanisms
+
+To retrieve the gMSA password, i.e. query `msDS-ManagedPassword`, the LDAP connection must provide confidentiality. This can be provided using:
+* GSSAPI privacy (`kerberos`)
+* TLS (`tls`)
+
+`gmsad` relies on `ldap3` to implement these mechanisms :
+- `tls` is supported out of the box.
+- `kerberos` is not officially supported by ldap3, but there is a pull request that implements it: https://github.com/cannatag/ldap3/pull/1042.
+
+By default, gmsad will try to use `kerberos` and fallback to `tls` if it fails (for example if your version of ldap3 does not support Kerberos encryption).
diff --git a/doc/gmsa.md b/doc/gmsa.md
index 749ccbc..77d755a 100644
--- a/doc/gmsa.md
+++ b/doc/gmsa.md
@@ -20,8 +20,9 @@ We did not find any documentation explaining exactly what are those time offsets

To retrieve the password, i.e. query `msDS-ManagedPassword`, the LDAP connection needs to provide confidentiality. It can be provided using:
-* GSSAPI Privacy. This is not supported by `ldap3`, therefore it is not supported by `gmsad`.
-* TLS, supported by `gmsad`.
+* GSSAPI Privacy (`kerberos`)
+* TLS (`tls`)
+
The list of account allowed to access the gMSA password is stored in the `msDS-groupMSAMembership`. This attribute contains a Windows security descriptor. Here is an example:
diff --git a/gmsad.conf.sample b/gmsad.conf.sample
index d4cdab6..a361757 100644
--- a/gmsad.conf.sample
+++ b/gmsad.conf.sample
@@ -40,7 +40,7 @@ check_interval = 60
# # Optional: Specify the salt used to calculate Kerberos keys.
# # This should not be used unless =yes did not work and/or
# # you know what you are doing.
-# # gMSA_salt = CANTINE.LOCALhostsemoule.cantine.local
+# gMSA_salt = CANTINE.LOCALhostsemoule.cantine.local
#
# # Optional: Specify how the salt, used to calculate Kerberos keys, should be
# # calculated.
@@ -50,7 +50,7 @@ check_interval = 60
# # Windows.
# # If this option is set to "yes", the salt will be calculated using this heuristic:
# # host.
-# # gMSA_salt_from_heuristic = yes
+# gMSA_salt_from_heuristic = yes
#
# # Principal of the computer account used to retrieve
# # gMSA secret.
@@ -65,16 +65,22 @@ check_interval = 60
# # Its hostname is retrieved using DNS (SRV record named _ldap._tcp.pdc._msdcs.)
# # Warning: For best redundancy, it is advised to keep this option UNSET.
# # Warning: gmsad uses Kerberos to authenticate to the LDAP Server.
-# # host = dc.cantine.local
+# host = dc.cantine.local
+#
+# # Optional: Specify the encryption algorithms used to encrypt ldap messages.
+# # This field contains an ordered list of mechanisms, separated by commas.
+# # By default, gmsad will attempt to use Kerberos session encryption (if supported by
+# # ldap3), and fall back to TLS if something goes wrong.
+# encryption_mechs = kerberos,tls
#
# # Optional: Specify the CA certificate to use to validate LDAP server
# # certificate. By default, system installed certificates are used.
-# # tls_ca_certs_file = /etc/cantine.local.crt
+# tls_ca_certs_file = /etc/cantine.local.crt
#
# # Optional: Specify valid DNS names used for the TLS LDAP server
# # name validation (comma-separated). See option to define which
# # LDAP server is used.
-# # tls_valid_names = dc.cantine.local,toto.cantine.local
+# tls_valid_names = dc.cantine.local,toto.cantine.local
#
# # Optional: Command to execute when SPN keys are updated.
# on_spn_rotate_cmd = sudo systemctl reload apache2
@@ -82,4 +88,4 @@ check_interval = 60
# # Optional: Command to execute when UPN keys are updated.
# # This only applies if gMSA_servicePrincipalNames is absent or
# # if gMSA_upn_in_keytab is set.
-# # on_upn_rotate_cmd = echo "Do something"
+# on_upn_rotate_cmd = echo "Do something"
diff --git a/gmsad/ldap.py b/gmsad/ldap.py
index 8a8c645..c823a07 100644
--- a/gmsad/ldap.py
+++ b/gmsad/ldap.py
@@ -1,46 +1,95 @@
import configparser
import logging
import ssl
-from typing import List, Any
+from typing import List, Any, Tuple
+import traceback
import ldap3
from gmsad.utils import get_dc
+def setup_kerberos_connection(config: configparser.SectionProxy,
+ host: str) -> Tuple[ldap3.Server, ldap3.Connection]:
+ try:
+ from ldap3 import ENCRYPT
+ except ImportError:
+ raise Exception("ldap3 version does not support Kerberos encryption")
+
+ server = ldap3.Server(host, get_info=ldap3.ALL)
+ connection = ldap3.Connection(
+ server,
+ user=config["principal"],
+ authentication=ldap3.SASL,
+ sasl_mechanism=ldap3.KERBEROS,
+ auto_bind=True,
+ cred_store={'client_keytab': config["keytab"]},
+ session_security=ldap3.ENCRYPT)
+ return (server, connection)
+
+
+def setup_tls_connection(config: configparser.SectionProxy,
+ host: str) -> Tuple[ldap3.Server, ldap3.Connection]:
+ # If is not set, the system wide installed certificates
+ # are used.
+ tls = ldap3.Tls(
+ validate=ssl.CERT_REQUIRED,
+ version=ssl.PROTOCOL_TLSv1_2,
+ ca_certs_file=config.get("tls_ca_certs_file", fallback=None),
+ valid_names=config.getlist("tls_valid_names", fallback=None),
+ )
+
+ server = ldap3.Server(host, get_info=ldap3.ALL, tls=tls)
+ connection = ldap3.Connection(
+ server,
+ user=config["principal"],
+ authentication=ldap3.SASL,
+ sasl_mechanism=ldap3.KERBEROS,
+ auto_bind=True,
+ cred_store={'client_keytab': config["keytab"]})
+ connection.start_tls()
+ return (server, connection)
+
+
+ENCRYPTION_MECHS = {
+ 'kerberos': setup_kerberos_connection,
+ 'tls': setup_tls_connection,
+}
+
+
class LDAPConnection:
server: ldap3.Server
connection: ldap3.Connection
def __init__(self, config: configparser.SectionProxy) -> None:
self.config = config
- # GSSAPI privacy is not supported by ldap3, so TLS is mandatory
- # If is not set, the system wide installed certificates
- # are used.
- tls = ldap3.Tls(
- validate=ssl.CERT_REQUIRED,
- version=ssl.PROTOCOL_TLSv1_2,
- ca_certs_file=self.config.get("tls_ca_certs_file", fallback=None),
- valid_names=self.config.getlist("tls_valid_names", fallback=None),
- )
- if "host" in self.config:
- host = self.config["host"]
- else:
- host = get_dc(self.config['gMSA_domain'])
+ host = self.get_host()
logging.debug("LDAP Server host to contact is %s", host)
- self.server = ldap3.Server(host, get_info=ldap3.ALL, tls=tls)
- self.connection = ldap3.Connection(
- self.server,
- user=self.config["principal"],
- authentication=ldap3.SASL,
- sasl_mechanism=ldap3.KERBEROS,
- auto_bind=True,
- cred_store={'client_keytab': self.config["keytab"]})
- self.connection.start_tls()
+ succeed = False
+ for mech in config.get("encryption_mechs", fallback="kerberos,tls").lower().split(','):
+ if not mech in ENCRYPTION_MECHS:
+ raise ValueError(f"Unknown encryption mechanism '{mech}'")
+ try:
+ logging.debug("Setup a connection with '%s' encryption mechanism", mech)
+ self.server, self.connection = ENCRYPTION_MECHS[mech](config, host)
+ succeed = True
+ break
+ except Exception as e:
+ logging.warning("Failed to setup '%s' encryption mechanism: %s", mech, e)
+ logging.debug(traceback.format_exc())
+
+ if not succeed:
+ raise Exception("Could not setup a connection using specified mechanisms")
logging.debug("Authenticated as %s", self.connection.extend.standard.who_am_i())
+ def get_host(self) -> str:
+ if "host" in self.config:
+ return self.config["host"]
+ else:
+ return get_dc(self.config['gMSA_domain'])
+
def get_gmsa_attributes(self, attributes: List[str]) -> Any:
"""
Retrieve the given list of the gMSA account.