From 6f7220415389426bee517eb7d2d6b90c04a092e0 Mon Sep 17 00:00:00 2001 From: Vincent Ruello <5345986+vruello@users.noreply.github.com> Date: Fri, 28 Jul 2023 09:55:19 +0200 Subject: [PATCH] Support for Kerberos encryption mechanism --- CHANGELOG.md | 16 ++++++++ README.md | 5 +-- doc/encryption.md | 11 ++++++ doc/gmsa.md | 5 ++- gmsad.conf.sample | 18 ++++++--- gmsad/ldap.py | 95 +++++++++++++++++++++++++++++++++++------------ 6 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 doc/encryption.md 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.