Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Kerberos encryption mechanism #3

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions doc/encryption.md
Original file line number Diff line number Diff line change
@@ -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).
5 changes: 3 additions & 2 deletions doc/gmsa.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ We did not find any documentation explaining exactly what are those time offsets
<p align="center"><img src="./update-mecanism.png" style="background-color:white" width="100%"/></p>

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:

Expand Down
18 changes: 12 additions & 6 deletions gmsad.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ check_interval = 60
# # Optional: Specify the salt used to calculate Kerberos keys.
# # This should not be used unless <gMSA_salt_from_heuristic>=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.
Expand All @@ -50,7 +50,7 @@ check_interval = 60
# # Windows.
# # If this option is set to "yes", the salt will be calculated using this heuristic:
# # <gMSA_domain to upper case>host<gMSA_sAMAccountName>.<gMSA_domain to lower case>
# # gMSA_salt_from_heuristic = yes
# gMSA_salt_from_heuristic = yes
#
# # Principal of the computer account used to retrieve
# # gMSA secret.
Expand All @@ -65,21 +65,27 @@ check_interval = 60
# # Its hostname is retrieved using DNS (SRV record named _ldap._tcp.pdc._msdcs.<gMSA_domain>)
# # 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 <host> 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
#
# # 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"
95 changes: 72 additions & 23 deletions gmsad/ldap.py
Original file line number Diff line number Diff line change
@@ -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 <ca_certs_file> 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 <ca_certs_file> 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 <attributes> list of the gMSA account.
Expand Down