From d120d7fe848aff42b2a01ee33ff9f02faaf3541d Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Fri, 15 Dec 2017 07:24:14 +1300 Subject: [PATCH] provision: Changes to support encrypted_secrets module Changes to provision and join to create a database with encrypted_secrets enabled and a key file generated. Also adds the --plaintext-secrets option to join and provision commands to allow the creation of unencrypted databases. Signed-off-by: Gary Lockyer Reviewed-by: Andrew Bartlett --- python/samba/join.py | 21 +++++---- python/samba/netcmd/domain.py | 24 +++++++--- python/samba/provision/__init__.py | 50 ++++++++++++++++++--- selftest/knownfail.d/encrypted_secrets | 8 +--- source4/dsdb/samdb/ldb_modules/samba_dsdb.c | 29 +++++++++--- source4/setup/provision_init.ldif | 1 + 6 files changed, 102 insertions(+), 31 deletions(-) diff --git a/python/samba/join.py b/python/samba/join.py index 3aefb7baa978..63e9b9010ed3 100644 --- a/python/samba/join.py +++ b/python/samba/join.py @@ -55,7 +55,8 @@ class dc_join(object): def __init__(ctx, logger=None, server=None, creds=None, lp=None, site=None, netbios_name=None, targetdir=None, domain=None, machinepass=None, use_ntvfs=False, dns_backend=None, - promote_existing=False, clone_only=False): + promote_existing=False, clone_only=False, + plaintext_secrets=False): if site is None: site = "Default-First-Site-Name" @@ -67,6 +68,7 @@ class dc_join(object): ctx.site = site ctx.targetdir = targetdir ctx.use_ntvfs = use_ntvfs + ctx.plaintext_secrets = plaintext_secrets ctx.promote_existing = promote_existing ctx.promote_from_dn = None @@ -837,7 +839,8 @@ class dc_join(object): hostname=ctx.myname, domainsid=ctx.domsid, machinepass=ctx.acct_pass, serverrole="active directory domain controller", sitename=ctx.site, lp=ctx.lp, ntdsguid=ctx.ntds_guid, - use_ntvfs=ctx.use_ntvfs, dns_backend=ctx.dns_backend) + use_ntvfs=ctx.use_ntvfs, dns_backend=ctx.dns_backend, + plaintext_secrets=ctx.plaintext_secrets) print "Provision OK for domain DN %s" % presult.domaindn ctx.local_samdb = presult.samdb ctx.lp = presult.lp @@ -1398,11 +1401,12 @@ class dc_join(object): def join_RODC(logger=None, server=None, creds=None, lp=None, site=None, netbios_name=None, targetdir=None, domain=None, domain_critical_only=False, machinepass=None, use_ntvfs=False, dns_backend=None, - promote_existing=False): + promote_existing=False, plaintext_secrets=False): """Join as a RODC.""" ctx = dc_join(logger, server, creds, lp, site, netbios_name, targetdir, domain, - machinepass, use_ntvfs, dns_backend, promote_existing) + machinepass, use_ntvfs, dns_backend, promote_existing, + plaintext_secrets) lp.set("workgroup", ctx.domain_name) logger.info("workgroup is %s" % ctx.domain_name) @@ -1449,10 +1453,11 @@ def join_RODC(logger=None, server=None, creds=None, lp=None, site=None, netbios_ def join_DC(logger=None, server=None, creds=None, lp=None, site=None, netbios_name=None, targetdir=None, domain=None, domain_critical_only=False, machinepass=None, use_ntvfs=False, dns_backend=None, - promote_existing=False): + promote_existing=False, plaintext_secrets=False): """Join as a DC.""" ctx = dc_join(logger, server, creds, lp, site, netbios_name, targetdir, domain, - machinepass, use_ntvfs, dns_backend, promote_existing) + machinepass, use_ntvfs, dns_backend, promote_existing, + plaintext_secrets) lp.set("workgroup", ctx.domain_name) logger.info("workgroup is %s" % ctx.domain_name) @@ -1498,10 +1503,10 @@ def join_clone(logger=None, server=None, creds=None, lp=None, def join_subdomain(logger=None, server=None, creds=None, lp=None, site=None, netbios_name=None, targetdir=None, parent_domain=None, dnsdomain=None, netbios_domain=None, machinepass=None, adminpass=None, use_ntvfs=False, - dns_backend=None): + dns_backend=None, plaintext_secrets=False): """Join as a DC.""" ctx = dc_join(logger, server, creds, lp, site, netbios_name, targetdir, parent_domain, - machinepass, use_ntvfs, dns_backend) + machinepass, use_ntvfs, dns_backend, plaintext_secrets) ctx.subdomain = True if adminpass is None: ctx.adminpass = samba.generate_random_password(12, 32) diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py index 6f6ef61c6aaf..6702bc3bcf0b 100644 --- a/python/samba/netcmd/domain.py +++ b/python/samba/netcmd/domain.py @@ -247,6 +247,9 @@ class cmd_domain_provision(Command): Option("--ol-mmr-urls", type="string", metavar="LDAPSERVER", help="List of LDAP-URLS [ ldap://:/ (where has to be different than 389!) ] separated with comma (\",\") for use with OpenLDAP-MMR (Multi-Master-Replication), e.g.: \"ldap://s4dc1:9000,ldap://s4dc2:9000\""), Option("--use-rfc2307", action="store_true", help="Use AD to store posix attributes (default = no)"), + Option("--plaintext-secrets", action="store_true", + help="Store secret/sensitive values as plain text on disk" + + "(default is to encrypt secret/ensitive values)"), ] openldap_options = [ @@ -316,7 +319,8 @@ class cmd_domain_provision(Command): ldap_backend_extra_port=None, ldap_backend_forced_uri=None, ldap_dryrun_mode=None, - base_schema=None): + base_schema=None, + plaintext_secrets=False): self.logger = self.get_logger("provision") if quiet: @@ -485,7 +489,8 @@ class cmd_domain_provision(Command): ldap_backend_extra_port=ldap_backend_extra_port, ldap_backend_forced_uri=ldap_backend_forced_uri, nosync=ldap_backend_nosync, ldap_dryrun_mode=ldap_dryrun_mode, - base_schema=base_schema) + base_schema=base_schema, + plaintext_secrets=plaintext_secrets) except ProvisioningError, e: raise CommandError("Provision failed", e) @@ -638,6 +643,9 @@ class cmd_domain_join(Command): "BIND9_DLZ uses samba4 AD to store zone information, " "NONE skips the DNS setup entirely (this DC will not be a DNS server)", default="SAMBA_INTERNAL"), + Option("--plaintext-secrets", action="store_true", + help="Store secret/sensitive values as plain text on disk" + + "(default is to encrypt secret/ensitive values)"), Option("--quiet", help="Be quiet", action="store_true"), Option("--verbose", help="Be verbose", action="store_true") ] @@ -655,7 +663,7 @@ class cmd_domain_join(Command): versionopts=None, server=None, site=None, targetdir=None, domain_critical_only=False, parent_domain=None, machinepass=None, use_ntvfs=False, dns_backend=None, adminpass=None, - quiet=False, verbose=False): + quiet=False, verbose=False, plaintext_secrets=False): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp) net = Net(creds, lp, server=credopts.ipaddress) @@ -686,13 +694,16 @@ class cmd_domain_join(Command): join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain, site=site, netbios_name=netbios_name, targetdir=targetdir, domain_critical_only=domain_critical_only, - machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend) + machinepass=machinepass, use_ntvfs=use_ntvfs, + dns_backend=dns_backend, + plaintext_secrets=plaintext_secrets) elif role == "RODC": join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain, site=site, netbios_name=netbios_name, targetdir=targetdir, domain_critical_only=domain_critical_only, machinepass=machinepass, use_ntvfs=use_ntvfs, - dns_backend=dns_backend) + dns_backend=dns_backend, + plaintext_secrets=plaintext_secrets) elif role == "SUBDOMAIN": if not adminpass: logger.info("Administrator password will be set randomly!") @@ -705,7 +716,8 @@ class cmd_domain_join(Command): netbios_name=netbios_name, netbios_domain=netbios_domain, targetdir=targetdir, machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend, - adminpass=adminpass) + adminpass=adminpass, + plaintext_secrets=plaintext_secrets) else: raise CommandError("Invalid role '%s' (possible values: MEMBER, DC, RODC, SUBDOMAIN)" % role) diff --git a/python/samba/provision/__init__.py b/python/samba/provision/__init__.py index 5de986463a56..cc654f32a88a 100644 --- a/python/samba/provision/__init__.py +++ b/python/samba/provision/__init__.py @@ -29,6 +29,7 @@ __docformat__ = "restructuredText" from base64 import b64encode import errno import os +import stat import re import pwd import grp @@ -554,6 +555,9 @@ def provision_paths_from_lp(lp, dnsdomain): paths.kdcconf = os.path.join(paths.private_dir, "kdc.conf") paths.winsdb = os.path.join(paths.private_dir, "wins.ldb") paths.s4_ldapi_path = os.path.join(paths.private_dir, "ldapi") + paths.encrypted_secrets_key_path = os.path.join( + paths.private_dir, + "encrypted_secrets.key") paths.dns = os.path.join(paths.binddns_dir, "dns", dnsdomain + ".zone") paths.namedconf = os.path.join(paths.binddns_dir, "named.conf") @@ -792,7 +796,7 @@ def setup_name_mappings(idmap, sid, root_uid, nobody_uid, def setup_samdb_partitions(samdb_path, logger, lp, session_info, provision_backend, names, serverrole, - erase=False): + erase=False, plaintext_secrets=False): """Setup the partitions for the SAM database. Alternatively, provision() may call this, and then populate the database. @@ -821,6 +825,10 @@ def setup_samdb_partitions(samdb_path, logger, lp, session_info, if provision_backend.type != "ldb": ldap_backend_line = "ldapBackend: %s" % provision_backend.ldap_uri + required_features = "# No required features" + if not plaintext_secrets: + required_features = "requiredFeatures: encryptedSecrets" + samdb.transaction_start() try: logger.info("Setting up sam.ldb partitions and settings") @@ -831,7 +839,8 @@ def setup_samdb_partitions(samdb_path, logger, lp, session_info, setup_add_ldif(samdb, setup_path("provision_init.ldif"), { "BACKEND_TYPE": provision_backend.type, - "SERVER_ROLE": serverrole + "SERVER_ROLE": serverrole, + "REQUIRED_FEATURES": required_features }) logger.info("Setting up sam.ldb rootDSE") @@ -1006,6 +1015,30 @@ def setup_privileges(path, session_info, lp): privilege_ldb.erase() privilege_ldb.load_ldif_file_add(setup_path("provision_privilege.ldif")) +def setup_encrypted_secrets_key(path): + """Setup the encrypted secrets key file. + + Any existing key file will be deleted and a new random key generated. + + :param path: Path to the secrets key file. + + """ + if os.path.exists(path): + os.unlink(path) + + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + mode = stat.S_IRUSR | stat.S_IWUSR + + umask_original = os.umask(0) + try: + fd = os.open(path, flags, mode) + finally: + os.umask(umask_original) + + with os.fdopen(fd, 'w') as f: + key = samba.generate_random_bytes(16) + f.write(key) + def setup_registry(path, session_info, lp): """Setup the registry. @@ -1193,7 +1226,8 @@ def create_default_gpo(sysvolpath, dnsdomain, policyguid, policyguid_dc): def setup_samdb(path, session_info, provision_backend, lp, names, - logger, fill, serverrole, schema, am_rodc=False): + logger, fill, serverrole, schema, am_rodc=False, + plaintext_secrets=False): """Setup a complete SAM Database. :note: This will wipe the main SAM database file! @@ -1202,7 +1236,7 @@ def setup_samdb(path, session_info, provision_backend, lp, names, # Also wipes the database setup_samdb_partitions(path, logger=logger, lp=lp, provision_backend=provision_backend, session_info=session_info, - names=names, serverrole=serverrole) + names=names, serverrole=serverrole, plaintext_secrets=plaintext_secrets) # Load the database, but don's load the global schema and don't connect # quite yet @@ -1975,7 +2009,8 @@ def provision(logger, session_info, smbconf=None, useeadb=False, am_rodc=False, lp=None, use_ntvfs=False, use_rfc2307=False, maxuid=None, maxgid=None, skip_sysvolacl=True, ldap_backend_forced_uri=None, nosync=False, ldap_dryrun_mode=False, - ldap_backend_extra_port=None, base_schema=None): + ldap_backend_extra_port=None, base_schema=None, + plaintext_secrets=False): """Provision samba4 :note: caution, this wipes all existing data! @@ -2101,6 +2136,8 @@ def provision(logger, session_info, smbconf=None, directory_create_or_exists(paths.binddns_dir, 0o770) directory_create_or_exists(os.path.join(paths.private_dir, "tls")) directory_create_or_exists(paths.state_dir) + if not plaintext_secrets: + setup_encrypted_secrets_key(paths.encrypted_secrets_key_path) if paths.sysvol and not os.path.exists(paths.sysvol): os.makedirs(paths.sysvol, 0775) @@ -2172,7 +2209,8 @@ def provision(logger, session_info, smbconf=None, samdb = setup_samdb(paths.samdb, session_info, provision_backend, lp, names, logger=logger, serverrole=serverrole, - schema=schema, fill=samdb_fill, am_rodc=am_rodc) + schema=schema, fill=samdb_fill, am_rodc=am_rodc, + plaintext_secrets=plaintext_secrets) if serverrole == "active directory domain controller": if paths.netlogon is None: diff --git a/selftest/knownfail.d/encrypted_secrets b/selftest/knownfail.d/encrypted_secrets index 4fbc434f58a5..a8452fb44778 100644 --- a/selftest/knownfail.d/encrypted_secrets +++ b/selftest/knownfail.d/encrypted_secrets @@ -4,9 +4,5 @@ # * --plaintext-secrets option correctly provisions a domain # * the dsdb operational module correctly handles unencrypted secrets # * secrets are not stored as encrypted text when this option is specified -^samba.tests.encrypted_secrets.samba.tests.encrypted_secrets.EncryptedSecretsTests.test_encrypted_secrets\(fl2000dc:local\) -^samba.tests.encrypted_secrets.samba.tests.encrypted_secrets.EncryptedSecretsTests.test_required_features\(fl2000dc:local\) - -# Tests that will pass as the remaining patches in the set are added -^samba.tests.encrypted_secrets.samba.tests.encrypted_secrets.EncryptedSecretsTests.test_encrypted_secrets -^samba.tests.encrypted_secrets.samba.tests.encrypted_secrets.EncryptedSecretsTests.test_required_features +#^samba.tests.encrypted_secrets.samba.tests.encrypted_secrets.EncryptedSecretsTests.test_encrypted_secrets\(fl2000dc:local\) +#^samba.tests.encrypted_secrets.samba.tests.encrypted_secrets.EncryptedSecretsTests.test_required_features\(fl2000dc:local\) diff --git a/source4/dsdb/samdb/ldb_modules/samba_dsdb.c b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c index 87d65bd8c2b8..e0acb4e371a9 100644 --- a/source4/dsdb/samdb/ldb_modules/samba_dsdb.c +++ b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c @@ -228,6 +228,21 @@ static int set_ldap_credentials(struct ldb_context *ldb, bool use_external) return LDB_SUCCESS; } +static bool check_required_features(struct ldb_message_element *el) +{ + if (el != NULL) { + int k; + DATA_BLOB esf = data_blob_string_const( + SAMBA_ENCRYPTED_SECRETS_FEATURE); + for (k = 0; k < el->num_values; k++) { + if (data_blob_cmp(&esf, &el->values[k]) != 0) { + return false; + } + } + } + return true; +} + static int samba_dsdb_init(struct ldb_module *module) { struct ldb_context *ldb = ldb_module_get_ctx(module); @@ -294,6 +309,7 @@ static int samba_dsdb_init(struct ldb_module *module) "rdn_name", "subtree_delete", "repl_meta_data", + "encrypted_secrets", "operational", "unique_object_sids", "subtree_rename", @@ -375,11 +391,14 @@ static int samba_dsdb_init(struct ldb_module *module) backendType = ldb_msg_find_attr_as_string(res->msgs[0], "backendType", "ldb"); requiredFeatures = ldb_msg_find_element(res->msgs[0], SAMBA_REQUIRED_FEATURES_ATTR); - if (requiredFeatures != NULL) { - ldb_set_errstring(ldb, "This Samba database was created with " - "a newer Samba version and is marked with " - "requiredFeatures in @SAMBA_DSDB. " - "This database can not safely be read by this Samba version"); + if (!check_required_features(requiredFeatures)) { + ldb_set_errstring( + ldb, + "This Samba database was created with " + "a newer Samba version and is marked " + "with extra requiredFeatures in " + "@SAMBA_DSDB. This database can not " + "safely be read by this Samba version"); return LDB_ERR_OPERATIONS_ERROR; } diff --git a/source4/setup/provision_init.ldif b/source4/setup/provision_init.ldif index 486458aaf5a8..1d0868726827 100644 --- a/source4/setup/provision_init.ldif +++ b/source4/setup/provision_init.ldif @@ -28,6 +28,7 @@ dn: @SAMBA_DSDB backendType: ${BACKEND_TYPE} serverRole: ${SERVER_ROLE} compatibleFeatures: sortedLinks +${REQUIRED_FEATURES} dn: @MODULES @LIST: samba_dsdb -- 2.34.1