├── .gitignore ├── README.md ├── convert_groups.py ├── convert_hashes.py ├── convert_users.py ├── extensions ├── apple-user-homeurl-contain.ldif └── apple-user-homeurl.ldif ├── extract_hashes.py ├── groups.json.example ├── in └── README.md ├── kerberos2supplementalCredentials.py ├── od2samba4.conf.example ├── out └── README.md └── sync ├── README.md ├── od2samba4-sync.service ├── od2samba4-sync.timer └── sync.sh /.gitignore: -------------------------------------------------------------------------------- 1 | od2samba4.conf 2 | groups.json 3 | in/* 4 | !in/README.md 5 | out/* 6 | !out/README.md 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Directory to Samba4 Migration tools 2 | od2samba4 is a set of tools that simplify migrating users (including passwords) and groups from Apple Open Directory to Samba4 Active Directory Domain Controller. od2samba4 preserves `apple-generateduid`s of users and groups, which will become `objectGUID`s in Samba4. RC4, AES128-CTS-HMAC-SHA1-96 and AES256-CTS-HMAC-SHA1-96 Password hashes are converted to a Samba4-compatible format using [Heimdal](https://www.h5l.org/). After migration and before making the final switch to Samba4, Open Directory and Samba4 can be used simultaneously, while new users and password updates are automatically synchronized. 3 | 4 | ## Architecture 5 | Apart from the `sync.sh` script, od2samba4 does *not* modify data on the Samba4 server. Instead, the python scripts only generate outputs (LDIF files) that have to be manually imported into the LDB database. This way, od2samba4 can modify normally immutable attributes like objectGUIDs and password hashes without you having to worry about accidentally changing entries in the Active Directory. On the downside, this means that after messing up the Samba4 database (e.g. by deleting a user that is still present in Open Directory) there is no way to recover other than re-provisioning Samba4. 6 | 7 | Apart from the sync utilities, od2samba4 does not have to be used on the Samba4 server itself. Output files can just as well be generated on any system and copied over to the Samba4 server. 8 | 9 | ## Usage 10 | od2samba4 has been tested with **Debian 8**. This guide only covers Debian-specific packages and commands, please adapt them to your linux distribution accordingly. 11 | 12 | ### Step 1 - Install Samba4 and Utilities 13 | On the Samba4 server, install Samba4 and some dependencies: 14 | ```bash 15 | apt install samba smbclient winbind krb5-user 16 | ``` 17 | 18 | On the system running od2samba4 (which can be the Samba4 server, but doesn't have to be), install heimdal and python-ldap for python2. Also, immediately disable the heimdal server, which would otherwise interfere with samba. 19 | ```bash 20 | apt install heimdal-clients heimdal-kdc python-ldap 21 | systemctl disable heimdal-kdc 22 | systemctl stop heimdal-kdc 23 | ``` 24 | 25 | ### Step 2 - Samba4 Provisioning and Setup 26 | Follow the [official guide for provisioning a Samba4 Domain Controller](https://wiki.samba.org/index.php/Setup_a_Samba_Active_Directory_Domain_Controller). Make sure to use `--use-rfc2307` when provisioning and configure `/etc/resolv.conf` according to the official guide to make sure the hostname of the Open Directory server can be resolved or manually add an entry to `/etc/hosts`. Enable the Samba4 Active Directory Domain Controller (Debian: should happen automatically, or use `systemctl start samba-ad-dc`). Mind that from now on, the Samba4 server has to be running for internet access (since it acts as the DNS server). 27 | 28 | ### Step 3 - Install Schema Extensions 29 | od2samba4 migrates the Open Directory attribute `apple-user-homeurl` to Samba4. Since that is not a default Active Directory attribute, it has to be manually added by installing schema extensions. 30 | 31 | Make sure to *adapt the DN* (`dn: ` line in `extensions/apple-user-homeurl.ldif`, `extensions/apple-user-homeurl-contain.ldif`) in the schema extension files to your specific domain setup! 32 | 33 | The following commands install the schema extensions from the `extensions` folder. Samba4 must not be running while the schema is modified. 34 | ```bash 35 | systemctl stop samba-ad-dc 36 | ldbmodify -H /var/lib/samba/private/sam.ldb extensions/apple-user-homeurl.ldif --option="dsdb:schema update allowed"=true 37 | ldbmodify -H /var/lib/samba/private/sam.ldb extensions/apple-user-homeurl-contain.ldif --option="dsdb:schema update allowed"=true 38 | systemctl start samba-ad-dc 39 | ``` 40 | 41 | ### Step 4 - Modify Password Settings 42 | od2samba4 will set all `pwdLastSet` fields in the Active Directory to the time `convert_hashes.py` is executed. If you don't want all your passwords to suddenly expire at the same time, disable maximum / minimum password age: 43 | ```bash 44 | samba-tool domain passwordsettings set --min-pwd-age=0 45 | samba-tool domain passwordsettings set --max-pwd-age=0 46 | ``` 47 | 48 | ### Step 5 - Initial Migration of Users, Passwords and Groups 49 | #### `od2samba4.conf` Settings 50 | od2samba4 needs information on how to contact both Open Directory and Samba4 as well as information on where to find and store files. Samba4 server contact information is only used to read the directory (e.g. to find out which users haven't been migrated yet), od2samba4 won't write to the Samba4 directory. Copy the configuration file template using 51 | ```bash 52 | cp od2samba4.conf.example od2samba4.conf 53 | ``` 54 | and enter the following settings: 55 | 56 | * `[files]` section: 57 | * For details on input and output files, see `in/README.md` and `out/README.md` respectively 58 | * `heimdal_path`: Path to `hprop` and `hpropd` executables, which are included in heimdal. Propably `/usr/sbin`. 59 | * `[opendirectory]` section: 60 | * `dc`: Domain component of the OD server 61 | * `url`: Where to reach your OD server via LDAP protocol 62 | * `username`: Username for OD server 63 | * `password`: Password for given username on OD server 64 | * `host`, `sshuser`, `sshpass`: only required for automatic synchronization, see `sync/README.md` 65 | * `[samba4]` section: 66 | * `dc`: Domain component of the Samba4 server 67 | * `url`: Where to reach your Samba4 server via LDAP (or LDAPS) protocol 68 | * `username`: Username for AD server 69 | * `password`: Password for AD server 70 | * `nis_domain`: `msSFU30NisDomain` attribute of users and groups, usually the lowercase domain name 71 | * `upn_domain`: UPN suffix, domain part of userPrincipalName, usually the domain components of the DN in DNS format 72 | 73 | #### `groups.json` Settings 74 | od2samba4 needs to know which groups you want to migrate and how you want to accomplish the migration. The configuration file `groups.json` is used for this purpose. Get started using the sample file: 75 | ```bash 76 | cp groups.json.example groups.json 77 | ``` 78 | 79 | This JSON file must have the following structure 80 | ```json 81 | { 82 | "odname" : { 83 | "target" : "sambaname", 84 | "type" : "migrate" OR "merge" 85 | }, 86 | ... 87 | } 88 | ``` 89 | where 90 | 91 | * `odname` is the CN of the group on the Open Directory Server 92 | * `sambaname` is the CN the group will be given after getting migrated to Samba4 93 | * `type` can either be: 94 | * `migrate`: A new group called `sambaname` will be created in the Samba4 Active Directory; `gidNumber`, `objectGUID` (from `apple-generateduid`) and other attributes will be copied 95 | * `merge`: An existing group called `sambaname` is modified to contain `gidNumber` and other neccessary properties. The predetermined `objectGUID` in Samba4 won't be changed. 96 | 97 | od2samba4 will *only* migrate groups listed in `groups.json`, so make sure to migrate at least the primary groups of your users. 98 | 99 | #### Migrate Groups 100 | `convert_groups.py -a` will generate an LDIF file with all Open Directory groups for Samba4 import. Group migration is only meant to be done once (there is no option to only migrate new groups) and *has to happen before migrating users*. This is because users need to know their primary group's `objectSid`, which is generated during import, in order to determine their `primaryGroupID` value, which establishes **primary** group membership. It is recommended to call `convert_groups.py` with the `-a` (= `--amend-nis-props`) command line flag which makes sure preexisting Samba groups will also be equipped with a NIS Domain, NIS Name and gidNumber matching the group's RID + 1e8. In that case, Samba4 has to be running while executing `convert_groups.py`. 101 | 102 | Groups can then be imported using 103 | ```bash 104 | ldbmodify -H /var/lib/samba/private/sam.ldb --relax 105 | ``` 106 | 107 | Additionally, a script that establishes **secondary** group membership and parent-children relationships between groups (nested groups) is created. This script has to be executed *after* users have been imported! 108 | 109 | #### Migrate Users 110 | `convert_users.py` will generate an LDIF file with all Open Directory users for Samba4 import. You may also choose to only extract users that have not already been migrated ("new users") using `convert_users.py --new`. 111 | 112 | Users can then be imported using 113 | ```bash 114 | ldbadd -H /var/lib/samba/private/sam.ldb --relax 115 | ``` 116 | The `--relax` option makes sure, LDB accepts the LDIF despite it specifying objectGUIDs, which can't normally be written directly. 117 | 118 | #### Migrate Password Hashes 119 | Obtain `mit_dump` and `master_key` files as described in `in/README.md`. Extract hashes using `extract_hashes.py`. This will generate the `hashes` file which contains all hashes assigned to usernames in a JSON format. This file is for internal usage in od2samba4 only. 120 | 121 | Convert hashes to LDIF for Samba4 import using `convert_hashes.py`. This script will also make sure to only include those hashes in the LDIF, whose corresponding users are known by Samba4. The LDIF generated by `convert_hashes.py` also sets `pwdLastSet` to the current system time and enables the user account. 122 | 123 | Import password hashes into Samba4 using 124 | ```bash 125 | ldbmodify -H /var/lib/samba/private/sam.ldb --controls=local_oid:1.3.6.1.4.1.7165.4.3.12:0 126 | ``` 127 | The control (`1.3.6.1.4.1.7165.4.3.12 = DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID`, which was intended to be used for Samba3 import) will make sure, to override a check that prevents ldbmodify to directly change password hashes. 128 | 129 | #### Establish secondary group and group-in-group membership 130 | `convert_groups.py` from step "Migrate Groups" will have generated a script that establishes secondary group membership for users and groups. Primary group membership was already established by `convert_users.py`, by setting the correct `primaryGroupID` and `gidNumber`. By default, the script is called `out/setmembership.sh`. It calls `samba-tool group addmembers`, which adds `member` and `memberUid` attributes to the group: 131 | ```bash 132 | ./out/setmembership.sh 133 | ``` 134 | 135 | This script also takes care of processing nested groups, if both parent and child group are migrated to Samba4. 136 | 137 | ### Step 6 - Simultaneous OD and Samba4 Operation with Automatic Import 138 | If you want to test Samba4 for some time before making the final switch while synchronizing password changes and new users from OD over to the Samba4 server, see `sync/README.md` for information on how to accomplish that. 139 | -------------------------------------------------------------------------------- /convert_groups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # Convert Open Directory groups to LDIF for Samba4 import. 4 | # Generates a script that establishes secondary group membership for all users. 5 | 6 | from __future__ import print_function 7 | from ConfigParser import RawConfigParser 8 | from optparse import OptionParser 9 | import struct 10 | import ldap 11 | import ldif 12 | import stat 13 | import json 14 | import os 15 | 16 | # Parse command line options 17 | parser = OptionParser() 18 | parser.add_option("-a", "--amend-nis-props", action="store_true", default = False, help = "Amend NIS Domain, NIS Name and gidNumber attribute to existing AD system groups") 19 | (cmdline_opts, args) = parser.parse_args() 20 | 21 | # Parse configuration 22 | config = RawConfigParser() 23 | config.read("od2samba4.conf") 24 | 25 | od_password = config.get("opendirectory", "password") 26 | outfile_ldif_name = config.get("files", "groups_ldif") 27 | outfile_script_name = config.get("files", "membership_script") 28 | od_username = config.get("opendirectory", "username") 29 | od_url = config.get("opendirectory", "url") 30 | od_dc = config.get("opendirectory", "dc") 31 | samba4_dc = config.get("samba4", "dc") 32 | samba4_url = config.get("samba4", "url") 33 | samba4_username = config.get("samba4", "username") 34 | samba4_password = config.get("samba4", "password") 35 | nis_domain = config.get("samba4", "nis_domain") 36 | 37 | # Parse JSON that defines what to do with groups (migrate or merge) 38 | groupactions = json.loads(open("groups.json", "r").read()) 39 | 40 | # Group attributes that will be retrieved from OD DC (and then processed) 41 | GROUPATTRIBUTES = [ 42 | "gidNumber", # GID (Group ID) 43 | "cn", # Group name (short version) 44 | "apple-group-realname", # Group name (long, human-readable version), becomes description in samba4 45 | "apple-generateduid", # Becomes objectGUID in samba4 46 | "memberUid", # Will be used to generate secondary membership-establishing script and kept for samba4 import 47 | "apple-group-nestedgroup" # Used to replicate nested group structure on Samba4 AD DC 48 | ] 49 | 50 | # Use certificates only for encryption, not authentication (self-signed) 51 | ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) 52 | 53 | # Connect to Open Directory 54 | print("Connecting to Open Directory server") 55 | od = ldap.initialize(od_url) 56 | od.simple_bind_s("uid=" + od_username + ",cn=users," + od_dc, od_password) 57 | od_results = od.search_s("cn=groups," + od_dc, ldap.SCOPE_SUBTREE, "(objectclass=posixGroup)", GROUPATTRIBUTES) 58 | 59 | # If command line option -a / --amend-nis-props is used, amend existing samba groups with NIS Domain, NIS Name and a gidNumber matching 60 | # the group's RID + 1e8 (= last Block of objectSid = number used for primaryGroupID). Connect to Samba4 server to retrieve a list of existing groups. 61 | samba4_sysgroups = {} 62 | if cmdline_opts.amend_nis_props: 63 | print("Connecting to Samba4 server") 64 | samba = ldap.initialize(samba4_url) 65 | samba.set_option(ldap.OPT_REFERRALS, 0) 66 | samba.start_tls_s() 67 | samba.simple_bind_s("cn=" + samba4_username + ",cn=Users," + samba4_dc, samba4_password) 68 | 69 | samba_results = samba.search_s("cn=Users," + samba4_dc, ldap.SCOPE_SUBTREE, "(objectclass=group)", ["cn", "objectSid"]) 70 | groupslist = [g[1] for g in samba_results] 71 | for sysgroup in groupslist: 72 | samba4_sysgroups[sysgroup["cn"][0]] = struct.unpack(" Has child: " + child["cn"][0]) 123 | print("samba-tool group addmembers \"" + target + "\" \"" 124 | + groupactions[child["cn"][0]]["target"] + "\"", file = outfile_script) 125 | 126 | # Merge group: Change gidNumber and msSFU30* attributes; description, name and 127 | # objectGUID of existing AD group stay the same. 128 | if actiontype == "merge": 129 | print("changetype: modify", file = outfile_ldif) 130 | write_replace(outfile_ldif, "msSFU30Name", target) 131 | write_replace(outfile_ldif, "msSFU30NisDomain", nis_domain) 132 | write_replace(outfile_ldif, "gidNumber", group["gidNumber"][0]) 133 | 134 | # Migrate group: Add new group including all group properties 135 | elif actiontype == "migrate": 136 | print("changetype: add", file = outfile_ldif) 137 | print("cn: " + target, file = outfile_ldif) 138 | print("objectclass: top", file = outfile_ldif) 139 | print("objectclass: group", file = outfile_ldif) 140 | print("gidNumber: " + group["gidNumber"][0], file = outfile_ldif) 141 | print("sAMAccountName: " + target, file = outfile_ldif) 142 | print("msSFU30Name: " + target, file = outfile_ldif) 143 | print("msSFU30NisDomain: " + nis_domain, file = outfile_ldif) 144 | if "apple-group-realname" in group: 145 | print("description: " + group["apple-group-realname"][0], file=outfile_ldif) 146 | 147 | # Use `apple-generateduid` as `objectGUID` when migrating 148 | print("objectGUID: " + group["apple-generateduid"][0], file=outfile_ldif) 149 | else: 150 | print(group["cn"][0] + ": Invalid group action type: " + actiontype) 151 | quit() 152 | 153 | print(file = outfile_ldif) 154 | od_count += 1 155 | 156 | # If -a / --amend-nis-props was specified (otherwise samba4_sysgroups is empty): 157 | # Add gidNumber and NIS properties to all preexisting Samba4 groups, ignore groups that are marked for 158 | # migration or merger in groups.json input file (group_is_manual is set in this case) 159 | sysgroup_count = 0 160 | for sysgroup_cn, sysgroup_rid in samba4_sysgroups.iteritems(): 161 | group_is_manual = False 162 | 163 | for odgroup, groupprops in groupactions.iteritems(): 164 | if groupprops["target"] == sysgroup_cn: 165 | group_is_manual = True 166 | continue 167 | 168 | if not group_is_manual: 169 | print("dn: CN=" + sysgroup_cn + ",CN=Users," + samba4_dc, file = outfile_ldif) 170 | print("changetype: modify", file = outfile_ldif) 171 | write_replace(outfile_ldif, "msSFU30Name", sysgroup_cn) 172 | write_replace(outfile_ldif, "msSFU30NisDomain", nis_domain) 173 | write_replace(outfile_ldif, "gidNumber", str(int(sysgroup_rid + 1e8))) 174 | print(file = outfile_ldif) 175 | sysgroup_count += 1 176 | 177 | outfile_ldif.close() 178 | outfile_script.close() 179 | os.chmod(outfile_script_name, os.stat(outfile_script_name).st_mode | stat.S_IEXEC) 180 | 181 | print("Extracted " + str(od_count) + " groups from Open Directory into " + outfile_ldif_name + ".") 182 | print("Amended " + str(sysgroup_count) + " groups from Samba4 with NIS properties.") 183 | print("Copy this file to the samba4 server and import groups by executing") 184 | print("# ldbmodify -H /var/lib/samba/private/sam.ldb " + outfile_ldif_name + " --relax") 185 | print("Generated " + outfile_script_name + " for establishing group membership.") 186 | print("Copy this script to the samba4 server and apply memberships by executing it.") 187 | 188 | -------------------------------------------------------------------------------- /convert_hashes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # Convert hashes to LDIF for Samba4 import using ldbmodify. 4 | # Go through list of users on the Samba4 domain controller and generate 5 | # LDIF entry for every user. Passwords of users on the Samba4 server that 6 | # are not found in the hashes file will remain unchanged. 7 | 8 | from __future__ import print_function 9 | from ConfigParser import RawConfigParser 10 | import subprocess 11 | import string 12 | import ldap 13 | import json 14 | import math 15 | import time 16 | import sys 17 | import os 18 | 19 | k2sc_path = os.path.dirname(os.path.realpath(__file__)) 20 | 21 | # Parse configuration 22 | config = RawConfigParser() 23 | config.read("od2samba4.conf") 24 | 25 | samba4_dc = config.get("samba4", "dc") 26 | samba4_url = config.get("samba4", "url") 27 | samba4_username = config.get("samba4", "username") 28 | samba4_password = config.get("samba4", "password") 29 | hashes_filename = config.get("files", "hashes") 30 | outfile_filename = config.get("files", "hashes_ldif") 31 | 32 | # Parse username / hash directory from JSON 33 | injson = json.loads(open(hashes_filename, "r").read()) 34 | 35 | # Use certificates only for encryption, not authentication (self-signed) 36 | ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) 37 | 38 | # Get user list from Samba4 server 39 | # samba.search_s returns a key-value dictionary dc:uid. We want uid:dc so 40 | # that we can use the uid to find the corresponding hash in JSON hashes file. 41 | samba = ldap.initialize(samba4_url) 42 | samba.set_option(ldap.OPT_REFERRALS, 0) 43 | samba.start_tls_s() 44 | samba.simple_bind_s("cn=" + samba4_username + ",cn=Users," + samba4_dc, samba4_password) 45 | samba_results = samba.search_s("cn=Users," + samba4_dc, ldap.SCOPE_SUBTREE, "(objectclass=person)", ["uid"]) 46 | userlist = [(u[1]["uid"][0], u[0]) for u in samba_results if "uid" in u[1]] 47 | 48 | # The pwdLastSet time format is an integer that counts the number of 100ns intervals since January 1, 1601 UTC. 49 | # Convert current time from unix epoch to pwdLastSetFormat. 50 | pwdLastSetTime = "{:.0f}".format(math.ceil(time.time() * 10000000) + 116444736000000000) 51 | 52 | # Associate hashes with usernames and generate hash-updating LDIF 53 | # `user` is a tuple (uid, dc) 54 | # We don't use ldif.LDIFWriter here since it sorts LDIF attributes alphabetically. 55 | # Samba, however, won't import the LDIF if "replace: " isn't mentioned 56 | # before the attribute itself. 57 | outfile = open(outfile_filename, "w") 58 | count = 0 59 | 60 | def addModify(dn, key, value, base64=False): 61 | print("dn: " + dn, file=outfile) 62 | print("changetype: modify", file=outfile) 63 | print("replace: " + key, file=outfile) 64 | 65 | # base64 is specified by double colon (::) in LDIF 66 | print(key + (":: " if base64 else ": ") + value, file=outfile) 67 | print(file=outfile) 68 | 69 | for user in userlist: 70 | if not user[0] in injson: 71 | print("No hashes for user " + user[0] + " were found, ignoring.") 72 | else: 73 | userprops = injson[user[0]] 74 | 75 | # Enable or disable account according to HDBFlags in Heimdal dump 76 | # This reads the "invalid" flag of HDBflags, see lib/hdb/hdb.asn1 in Heimdal. 77 | # If this bit is set to "1", the account will stay disabled. 78 | # 79 | # userAccountControl = 512 means UF_NORMAL_ACCOUNT 80 | # userAccountControl = 514 means UF_NORMAL_ACCOUNT and UF_ACCOUNT_DISABLE 81 | # After Samba4 import, userAccountControl defaults to 548 which means the account is disabled and no password is required. 82 | # If the user was enabled in Open Directory, we enable the account only now, so that the system is not vulnerable before password hash migration. 83 | # If the user was disabled in Open Directory (in the kerberos dump), we set the account to disabled, but migrate all hashes. 84 | flags_bin = "{0:032b}".format(int(userprops["flags"])) 85 | account_disabled = (flags_bin[len(flags_bin) - 8] == "1") 86 | addModify(user[1], "userAccountControl", "514" if account_disabled else "512") 87 | 88 | # Add arcfour hash as "unicodePwd" attribute 89 | addModify(user[1], "unicodePwd", userprops["type23"].decode("hex").encode("base64").replace("\n", ""), True) 90 | 91 | # Convert type 1, 3, 17, 18 hashes to supplementalCredentials blob using kerberos2supplementalCredentials.py utility 92 | # If hash types 1 and/or 3 are not provided, create a new "0" hash. This is only to make sure Samba accepts the 93 | # supplementalCredentials blob when importing. supportedEncryptionTypes will be written to msDS-SupportedEncryptionTypes. 94 | supportedEncryptionTypes = 0b00011100 95 | 96 | if not "type1" in userprops: 97 | userprops["type1"] = "0" * 16 98 | else: 99 | supportedEncryptionTypes |= 0b01 100 | 101 | if not "type3" in userprops: 102 | userprops["type3"] = "0" * 16 103 | else: 104 | supportedEncryptionTypes |= 0b10 105 | 106 | if len(set(userprops.keys()) & {"type1", "type3", "type17", "type18"}) != 4: 107 | print("User " + user[0] + ": Not enough hashes for supplementalCredentials, ignoring supplementalCredentials") 108 | else: 109 | k2sc_params = " ".join(["--" + e + " " + userprops[e] for e in set(userprops.keys()) & {"type1", "type3", "type17", "type18"}]) 110 | k2sc_popen = subprocess.Popen([k2sc_path + os.sep + "kerberos2supplementalCredentials.py --base64 " + userprops["salt"] + " " + k2sc_params], 111 | shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 112 | supplementalCredentials = k2sc_popen.stdout.readlines() 113 | if (not all((d in string.ascii_letters or d in string.digits or d in "+/=\n") for d in supplementalCredentials[0])): 114 | sys.exit("kerberos2supplementalCredentials.py error:\n" + "".join(supplementalCredentials)) 115 | addModify(user[1], "supplementalCredentials", supplementalCredentials[0].replace("\n", ""), True) 116 | 117 | # Authentication with arcfour-hmac (23), aes128-cts-hmac-sha1-96 (17) and aes256-cts-hmac-sha1-96 (18) 118 | # will always be enabled. Only enable authentication with des-cbc-md5 (3) and des-cbc-crc (1) if a valid hash 119 | # was found in the kerberos dump. 120 | addModify(user[1], "msDS-SupportedEncryptionTypes", str(supportedEncryptionTypes)) 121 | 122 | # Change pwdLastSet to current time. Technically, any timestamp != 0 would work if password policy is set to no expiry. 123 | # The default value 0, however, will cause samba4 to ask for password renewal (NT_STATUS_PASSWORD_MUST_CHANGE). 124 | # To set at least some meaningful value (since OD doesn't store pwdLastSet), set the current date. 125 | addModify(user[1], "pwdLastSet", pwdLastSetTime) 126 | 127 | count += 1 128 | if count % 50 == 0: 129 | print("Number of converted users: " + str(count)) 130 | 131 | outfile.close() 132 | 133 | print(str(count) + " password hash changes were successfully processed.") 134 | print("Output LDIF was written to " + outfile_filename + ". You can import this into samba4 using:") 135 | print("# ldbmodify " + outfile_filename + " -H /var/lib/samba/private/sam.ldb --controls=local_oid:1.3.6.1.4.1.7165.4.3.12:0") 136 | print("The control 1.3.6.1.4.1.7165.4.3.12 enables editing of the unicodePwd and supplementalCredentials attributes.") 137 | -------------------------------------------------------------------------------- /convert_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # Convert Open Directory user database to LDIF for Samba4 import. 4 | # It is not possible to just upload users to the Samba4 directory, 5 | # since we want to keep objectGUIDs. Setting objectGUIDs of users 6 | # is only allowed during provisioning with `ldbadd --relax` though. 7 | 8 | from ConfigParser import RawConfigParser 9 | from optparse import OptionParser 10 | import xml.etree.ElementTree 11 | import struct 12 | import ldap 13 | import ldif 14 | 15 | # Parse command line options 16 | parser = OptionParser() 17 | parser.add_option("-n", "--new", action="store_true", default = False, help = "Only convert new users (users that are not in the samba4 directory)") 18 | (cmdline_opts, args) = parser.parse_args() 19 | 20 | # Parse configuration 21 | config = RawConfigParser() 22 | config.read("od2samba4.conf") 23 | 24 | od_password = config.get("opendirectory", "password") 25 | outfile_new_name = config.get("files", "newusers_ldif") 26 | outfile_all_name = config.get("files", "users_ldif") 27 | od_username = config.get("opendirectory", "username") 28 | od_url = config.get("opendirectory", "url") 29 | od_dc = config.get("opendirectory", "dc") 30 | samba4_dc = config.get("samba4", "dc") 31 | samba4_url = config.get("samba4", "url") 32 | samba4_username = config.get("samba4", "username") 33 | samba4_password = config.get("samba4", "password") 34 | samba4_upn_realm = config.get("samba4", "upn_realm") 35 | nis_domain = config.get("samba4", "nis_domain") 36 | 37 | outfile_name = (outfile_new_name if cmdline_opts.new else outfile_all_name) 38 | 39 | USERATTRIBUTES = [ 40 | "cn", # Common Name (First + Last Name) 41 | "uid", # Username(s), multiple accounts possible! 42 | "givenName", # First Name 43 | "sn", # Last Name 44 | "apple-user-homeurl", # Home Directory URL (on remote file server) 45 | "homeDirectory", # Home Mountpoint (e.g. /home/jdoe) 46 | "loginShell", # Login Shell, e.g. /bin/bash 47 | "gidNumber", # Primary Group Number 48 | "uidNumber", # UID Number 49 | "mail", # E-Mail address, only first entry will be used 50 | "apple-generateduid", # Becomes objectGUID in samba4 51 | "apple-user-mailattribute" # XML format, forwarding address is extracted 52 | ] 53 | 54 | # Use certificates only for encryption, not authentication (self-signed) 55 | ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) 56 | 57 | # Connect to Open Directory 58 | print("Connecting to Open Directory server") 59 | od = ldap.initialize(od_url) 60 | od.simple_bind_s("uid=" + od_username + ",cn=users," + od_dc, od_password) 61 | 62 | # Connect to Samba4 63 | print("Connecting to Samba4 server") 64 | samba = ldap.initialize(samba4_url) 65 | samba.set_option(ldap.OPT_REFERRALS, 0) 66 | samba.start_tls_s() 67 | samba.simple_bind_s("cn=" + samba4_username + ",cn=Users," + samba4_dc, samba4_password) 68 | 69 | # Retrieve list of users from OD and clean search results: 70 | # - Search result is list of tuples (DN, attributes), extract attributes 71 | # - Remove users that should not be migrated 72 | od_results = od.search_s("cn=users," + od_dc, ldap.SCOPE_SUBTREE, "(objectclass=person)", USERATTRIBUTES) 73 | users_all = [u[1] for u in od_results] 74 | users_od = [] 75 | for user in users_all: 76 | if (user["uid"][0] != "root" and user["uid"][0] != "diradmin" and user["uid"][0] != "_ldap_replicator" 77 | and not user["uid"][0].startswith("vpn_") and not user["uid"][0].startswith("_krb_")): 78 | users_od.append(user) 79 | 80 | print("Retrieved user list with " + str(len(users_od)) + " user entries from Open Directory") 81 | 82 | # Retrieve list of groups from Samba4 - groups have to be migrated before running this script! 83 | # RID (the last 4 bytes in little endian byte format, usually displayed as number after the last "-") 84 | # of group's objectSid determines the primary group of the user. Build a dictionary that matches the 85 | # group's gidNumber to the right RID. Open Directory contains the user's gidNumber attribute, so we 86 | # can find a matching group RID for that. This RID will then be used as the user's primaryGroupID. 87 | # The group's RID is also known as primaryGroupToken, though that attribute doesn't actually exist 88 | # separately in Samba4. 89 | print("Building gidNumber to primaryGroupToken Dictionary for Primary Group Membership") 90 | samba_group_results = samba.search_s("cn=Users," + samba4_dc, ldap.SCOPE_SUBTREE, "(objectclass=group)", ["objectSid", "gidNumber"]) 91 | gid2rid = {} 92 | for group in samba_group_results: 93 | if "gidNumber" in group[1]: 94 | gid2rid[group[1]["gidNumber"][0]] = struct.unpack(") looking for forwarding address 110 | # Returns False if no forwarding Address was found 111 | def extractForwardingAddress(xmlstring): 112 | root = xml.etree.ElementTree.fromstring(xmlstring) 113 | for key, child in enumerate(root): 114 | if child.text == "kAutoForwardValue" and not (root[key + 1].text is None): 115 | return root[key + 1].text.encode("utf-8") 116 | return False 117 | 118 | # Generate LDIF for import into Samba4 via ldbadd 119 | outfile = ldif.LDIFWriter(open(outfile_name, "wb")) 120 | count = 0 121 | for user in users_od: 122 | # Use OD's UID as CN and use OD's CN as displayName 123 | dn = "CN=" + user["uid"][0] + ",CN=Users," + samba4_dc 124 | user["displayName"] = [user["cn"][0]] 125 | user["cn"] = [user["uid"][0]] 126 | user["objectclass"] = ["top", "user", "organizationalPerson", "person", "posixAccount"] 127 | user["sAMAccountName"] = [user["uid"][0]] 128 | user["primaryGroupID"] = [str(gid2rid[user["gidNumber"][0]])] 129 | user["userPrincipalName"] = [user["uid"][0] + "@" + samba4_upn_realm] 130 | user["msSFU30Name"] = [user["uid"][0]] 131 | user["msSFU30NisDomain"] = [nis_domain] 132 | 133 | # Keep `apple-generateduid` from OD, rename to `objectGUID` 134 | user["objectGUID"] = [user["apple-generateduid"][0]] 135 | del user["apple-generateduid"] 136 | 137 | # If "mail" Attribute in OD is specified, use this for "mail" attribute in samba4. 138 | # Otherwise, try to extract forwarding mail address from "apple-user-mailattribute". 139 | if "mail" in user: 140 | user["mail"] = [user["mail"][0]] 141 | elif ("apple-user-mailattribute" in user) and extractForwardingAddress(user["apple-user-mailattribute"][0]): 142 | user["mail"] = [extractForwardingAddress(user["apple-user-mailattribute"][0])] 143 | if "apple-user-mailattribute" in user: 144 | del user["apple-user-mailattribute"] 145 | 146 | # Rename "homeDirectory" to "unixHomeDirectory", but only create attribute if it contains a valid entry (starts with "/") 147 | if user["homeDirectory"][0].startswith("/"): 148 | user["unixHomeDirectory"] = [user["homeDirectory"][0]] 149 | del user["homeDirectory"] 150 | 151 | # Only keep first UID attribute, discard others 152 | user["uid"] = [user["uid"][0]] 153 | 154 | outfile.unparse(dn, user) 155 | count += 1 156 | 157 | print("Extracted " + str(count) + " user account details into " + outfile_name + ".") 158 | print("Copy this file to the samba4 server and import users by executing") 159 | print("# ldbadd -H /var/lib/samba/private/sam.ldb " + outfile_name + " --relax") 160 | -------------------------------------------------------------------------------- /extensions/apple-user-homeurl-contain.ldif: -------------------------------------------------------------------------------- 1 | # add apple-user-homeurl to mayContain property of User class 2 | dn: CN=User,CN=Schema,CN=Configuration,DC=physcip,DC=uni-stuttgart,DC=de 3 | changetype: modify 4 | add: mayContain 5 | mayContain: apple-user-homeurl 6 | -------------------------------------------------------------------------------- /extensions/apple-user-homeurl.ldif: -------------------------------------------------------------------------------- 1 | # apple-user-homeurl attribute definition 2 | dn: CN=apple-user-homeurl,CN=Schema,CN=Configuration,DC=physcip,DC=uni-stuttgart,DC=de 3 | changetype: add 4 | objectClass: top 5 | objectClass: attributeSchema 6 | cn: apple-user-homeurl 7 | description: home directory URL 8 | attributeID: 1.3.6.1.4.1.63.1000.1.1.1.1.6 9 | lDAPDisplayName: apple-user-homeurl 10 | attributeSyntax: 2.5.5.5 11 | isSingleValued: TRUE 12 | oMSyntax: 22 13 | -------------------------------------------------------------------------------- /extract_hashes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # Extract arcfour-hmac-md5 (RC4) hashes from MIT Kerberos, decrypt them 4 | # and convert them to base64 format for unicodePwd attribute in Samba4. 5 | # 6 | # Extract hashes from MIT Kerberos using `kdb5_util dump -b7 dump.mit`. 7 | # Get the kerberos master key: File location is determined by `key_stash_file` 8 | # property in `/var/db/krb5kdc/kdc.conf`. Configure paths (relative or absolute) 9 | # to these files in `[files]` section in `od2samba4.conf`. 10 | # 11 | # This script requires Heimdal (https://www.h5l.org/). Please change 12 | # `heimdal_path` in `od2samba4.conf` to the directory where the executables 13 | # `hprop` and `hpropd` reside. 14 | 15 | from ConfigParser import RawConfigParser 16 | import subprocess 17 | import string 18 | import json 19 | import os 20 | 21 | # Parse configuration 22 | config = RawConfigParser() 23 | config.read("od2samba4.conf") 24 | 25 | mit_dump = config.get("files", "mit_dump") 26 | master_key = config.get("files", "master_key") 27 | hprop = config.get("files", "heimdal_path") + "/hprop" 28 | hpropd = config.get("files", "heimdal_path") + "/hpropd" 29 | outfile_name = config.get("files", "hashes") 30 | 31 | # Convert hashes with heimdal 32 | conv = subprocess.Popen([hprop + " --database=" + mit_dump + " --source=mit-dump --decrypt --master-key=" + master_key + " --stdout | " + hpropd + " -n --print"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 33 | users = conv.stdout.readlines() 34 | 35 | # Parse heimdal output, convert from hex to base64, write output file 36 | count = 0 37 | outjson = {} 38 | for user in users: 39 | attribs = string.split(user, " ") 40 | userprops = {} 41 | 42 | # The following types of hashes will be extracted: 43 | # des-cbc-crc (type 1), des-cbc-md5 (type 3), aes128-cts-hmac-sha1-96 (type 17), aes256-cts-hmac-sha1-96 (type 18), arcfour-hmac-md5 (type 23) 44 | # types 1, 3, 17, 18 will be used for the "supplementalCredentials" attribute, 45 | # type 23 will be used for the "unicodePwd" attribute 46 | # hashlengths stores which hashes to migrate and the length of those hashes in hexadecimal form, 47 | # which will be checked to make sure the hash matches 48 | hashlengths = {"1" : 16, "3" : 16, "17" : 32, "18" : 64, "23" : 32} 49 | 50 | keys = string.split(attribs[1], ":") 51 | for i, etype in enumerate(keys): 52 | if etype in hashlengths: 53 | if len(keys[i + 1]) == hashlengths[etype]: 54 | userprops["type" + etype] = keys[i + 1] 55 | 56 | # Change this if you don't use the NORMAL salt (see kerberos2supplementalCredentials.py for explanation) 57 | principal = string.split(attribs[0], "@") 58 | salt = principal[1] + principal[0] 59 | username = principal[0] 60 | flags = attribs[9] 61 | 62 | if not userprops: 63 | print("No hashes for user " + username + " were not found, ignoring user.") 64 | else: 65 | userprops["salt"] = salt 66 | userprops["flags"] = attribs[9] 67 | 68 | outjson[username] = userprops 69 | count += 1 70 | 71 | outfile = open(outfile_name, "w") 72 | outfile.write(json.dumps(outjson, indent = 4)) 73 | outfile.close() 74 | 75 | print(str(count) + " hashes were succesfully extracted and written to " + outfile_name + ".") 76 | -------------------------------------------------------------------------------- /groups.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "mygroup" : { 3 | "target" : "mygroup", 4 | "type" : "migrate" 5 | }, 6 | "foobar" : { 7 | "target" : "barfoo", 8 | "type" : "migrate" 9 | }, 10 | "admin" : { 11 | "target" : "Domain Admins", 12 | "type" : "merge" 13 | }, 14 | "staff" : { 15 | "target" : "Domain Users", 16 | "type" : "merge" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /in/README.md: -------------------------------------------------------------------------------- 1 | ### Input Files 2 | * `kdc_dump.mit`: MIT Kerberos dump from Open Directory, can be generated using `kdb5_util dump -b7 kdc_dump.mit`. od2samba4 will extract the password hashes from this dump. Required by `extract_hashes.py`. 3 | * `kdc_master_key`: MIT Kerberos Master Key from Open Directory. File location is determined by `key_stash_file` property in `/var/db/krb5kdc/kdc.conf`. Required by `extract_hashes.py`. 4 | -------------------------------------------------------------------------------- /kerberos2supplementalCredentials.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | # This script converts hashes as can be obtained from a MIT Kerberos / Heimdal dump to the supplementalCredentials 4 | # binary blob used in Active Directory / Samba4. 5 | # Using heimdal, you can print out these hashes to the console using: 6 | # hprop --database=dump.mit --source=mit-dump --decrypt --master-key=kdc_master_key --stdout | hpropd -n --print 7 | # 8 | # Usage: 9 | # kerberos2supplementalCredentials.py --salt SALT [--type1 HASH1] [--type3 HASH3] [--type17 HASH17] [--type18 HASH18] [--base64] 10 | # Where hashes 1, 3, 17, 18 correspond to the kerberos enctypes des-cbc-crc, des-cbc-md5, aes128-cts-hmac-sha1-96, aes256-cts-hmac-sha1-96. 11 | # Hashes 1, 3, 17, 18 are optional and can be omitted if you don't want to include that specific hash in your supplementalCredentials blob. 12 | # If --base64 is specified, output will be in base64 format (ready for LDIF import), otherwise binary output is generated. 13 | # 14 | # If you don't have any salt data, you propably want to try using the "normal" kerberos salt, which is defined as the concatenation 15 | # [REALM] + [PRINCIPAL]. So if the domain is "EXAMPLE.ORG" and the principal is "Administrator", the salt would be "EXAMPLE.ORGAdministrator". 16 | # See pr_to_salt.c in MIT Kerberos or krb5_get_pw_salt(...) in salt.c in Heimdal for more information / source. 17 | # The result supplementalCredentials blob will be printed to stdout. 18 | # 19 | # This script does NOT encode the WDigest credentials, since those hashes cannot be obtained from Kerberos. 20 | # This script does NOT support random / different salts for the different hashes. 21 | # 22 | # Note about the supplementalCredentialsPackage's reserved property: 23 | # This property is defined here: https://msdn.microsoft.com/en-us/library/cc245501.aspx and will be ignored, but is set to 1 or 2 according to 24 | # https://msdn.microsoft.com/en-us/library/cc245831.aspx#Appendix_A_22; Samba sets this as 1 for non-"Package" supplementalCredentialsPackages 25 | # and to 2 for the "Package" supplementalCredentialsPackage, see source4/dsdb/samdb/ldb_modules/password_hash.c; that's the behaviour we emulate 26 | # here as well 27 | 28 | import argparse 29 | import string 30 | import binascii 31 | import sys 32 | from samba.ndr import ndr_unpack, ndr_pack 33 | from samba.dcerpc import drsblobs 34 | 35 | # Mind that the order of this list matters! 36 | # The order in the "Primary:Kerberos-Newer-Keys" list must be: type18, type17, type3, type1 37 | # The order in the "Primary:Kerberos" list must be: type3, type1 38 | # Therefore, this list has to be sorted by type numbers in descending order. 39 | hashes = [ 40 | { "arg" : "type18", "name" : "aes256-cts-hmac-sha1-96", "type" : 18, "length" : 32, "ctr3" : False }, 41 | { "arg" : "type17", "name" : "aes128-cts-hmac-sha1-96", "type" : 17, "length" : 16, "ctr3" : False }, 42 | { "arg" : "type3", "name" : "des-cbc-md5", "type" : 3, "length" : 8, "ctr3" : True }, 43 | { "arg" : "type1", "name" : "des-cbc-crc", "type" : 1, "length" : 8, "ctr3" : True } 44 | ] 45 | 46 | # Parse command line arguments 47 | parser = argparse.ArgumentParser() 48 | parser.add_argument("salt", help="Salt string that the hashes were created with") 49 | parser.add_argument("--base64", help="Output supplementalCredentials blob in base64 format", action="store_true") 50 | 51 | for props in hashes: 52 | parser.add_argument("--" + props["arg"], help = "Enctype " + str(props["type"]) + " (" + props["name"] + ") hash in HEX format") 53 | 54 | args = parser.parse_args() 55 | 56 | # Make sure parameters are valid (check lengths, check if strings are hexadecimal) 57 | hash_specified = False 58 | for props in hashes: 59 | if vars(args)[props["arg"]]: 60 | hash_specified = True 61 | assert len(binascii.unhexlify(vars(args)[props["arg"]])) == props["length"] 62 | if (not all(d in string.hexdigits for d in vars(args)[props["arg"]])): 63 | sys.exit("Error: Hashes must be in hexadecimal format") 64 | 65 | if not hash_specified: 66 | sys.exit("Error: At least one hash must be specified. Nothing to do.") 67 | 68 | salt_blob = drsblobs.package_PrimaryKerberosString() 69 | salt_blob.string = args.salt 70 | 71 | # Dictionary containing property name (string) and content (a supplementalCredentialsPackage) 72 | properties = {} 73 | 74 | # Store type 1, 3, 17, 18 hashes in Primary:Kerberos-Newer-Keys 75 | # https://msdn.microsoft.com/en-us/library/cc941808.aspx 76 | # "ctr4" because this is a revision 4 key container 77 | newer_keys_list = [] 78 | for props in hashes: 79 | if vars(args)[props["arg"]]: 80 | key = drsblobs.package_PrimaryKerberosKey4() 81 | key.keytype = props["type"] 82 | key.value = binascii.unhexlify(vars(args)[props["arg"]]) 83 | key.value_len = props["length"] 84 | newer_keys_list.append(key) 85 | 86 | newer_keys_ctr = drsblobs.package_PrimaryKerberosCtr4() 87 | newer_keys_ctr.num_keys = len(newer_keys_list) 88 | newer_keys_ctr.salt = salt_blob 89 | newer_keys_ctr.keys = newer_keys_list 90 | 91 | newer_keys_blob_unpacked = drsblobs.package_PrimaryKerberosBlob() 92 | newer_keys_blob_unpacked.version = 4 93 | newer_keys_blob_unpacked.ctr = newer_keys_ctr 94 | newer_keys_blob = ndr_pack(newer_keys_blob_unpacked) 95 | 96 | newer_keys_package = drsblobs.supplementalCredentialsPackage() 97 | newer_keys_package_hex = binascii.hexlify(newer_keys_blob).upper() 98 | newer_keys_package.data = newer_keys_package_hex 99 | newer_keys_package.data_len = len(newer_keys_package.data) 100 | newer_keys_package.name = "Primary:Kerberos-Newer-Keys" 101 | newer_keys_package.name_len = len(newer_keys_package.name) 102 | newer_keys_package.reserved = 1 # see note about this property above 103 | 104 | properties["Kerberos-Newer-Keys"] = newer_keys_package 105 | 106 | # Store type 1 and 3 hashes in Primary:Kerberos 107 | # https://msdn.microsoft.com/en-us/library/cc245503.aspx 108 | # "ctr3" because this is a revision 3 key container 109 | normal_keys_list = [] 110 | for props in hashes: 111 | if vars(args)[props["arg"]] and props["ctr3"]: 112 | key = drsblobs.package_PrimaryKerberosKey3() 113 | key.keytype = props["type"] 114 | key.value = binascii.unhexlify(vars(args)[props["arg"]]) 115 | key.value_len = props["length"] 116 | normal_keys_list.append(key) 117 | 118 | # It is possible to only specify keys 17, 18 119 | # Then no need to generate the old format entry 120 | if (len(normal_keys_list) > 0): 121 | normal_keys_ctr = drsblobs.package_PrimaryKerberosCtr3() 122 | normal_keys_ctr.num_keys = len(normal_keys_list) 123 | normal_keys_ctr.salt = salt_blob 124 | normal_keys_ctr.keys = normal_keys_list 125 | 126 | normal_keys_blob_unpacked = drsblobs.package_PrimaryKerberosBlob() 127 | normal_keys_blob_unpacked.version = 3 128 | normal_keys_blob_unpacked.ctr = normal_keys_ctr 129 | normal_keys_blob = ndr_pack(normal_keys_blob_unpacked) 130 | 131 | normal_keys_package = drsblobs.supplementalCredentialsPackage() 132 | normal_keys_package_hex = binascii.hexlify(normal_keys_blob).upper() 133 | normal_keys_package.data = normal_keys_package_hex 134 | normal_keys_package.data_len = len(normal_keys_package.data) 135 | normal_keys_package.name = "Primary:Kerberos" 136 | normal_keys_package.name_len = len(normal_keys_package.name) 137 | normal_keys_package.reserved = 1 # see note about this property above 138 | 139 | properties["Kerberos"] = normal_keys_package 140 | 141 | # Build packages property Blob 142 | # https://msdn.microsoft.com/en-us/library/cc245678.aspx 143 | propertynames = [] 144 | propertydata = [] 145 | for name, package in properties.iteritems(): 146 | propertynames.append(name) 147 | propertydata.append(package) 148 | 149 | packages_listblob = "\0".join(propertynames).encode("utf-16le") 150 | 151 | packages_blob = drsblobs.supplementalCredentialsPackage() 152 | packages_blob.name = "Packages" 153 | packages_blob.name_len = len(packages_blob.name) 154 | packages_blob.data = binascii.hexlify(packages_listblob).upper() 155 | packages_blob.data_len = len(packages_blob.data) 156 | packages_blob.reserved = 2 # see note about this property above 157 | 158 | # Build supplementalCredentials blob 159 | # https://msdn.microsoft.com/en-us/library/cc245500.aspx 160 | supcred_sections = [packages_blob] 161 | supcred_sections.extend(propertydata) 162 | 163 | supcred_subblock = drsblobs.supplementalCredentialsSubBlob() 164 | supcred_subblock.packages = supcred_sections 165 | supcred_subblock.num_packages = len(supcred_sections) 166 | supcred_subblock.prefix = drsblobs.SUPPLEMENTAL_CREDENTIALS_PREFIX 167 | supcred_subblock.signature = drsblobs.SUPPLEMENTAL_CREDENTIALS_SIGNATURE 168 | 169 | supcred_blob_unpacked = drsblobs.supplementalCredentialsBlob() 170 | supcred_blob_unpacked.sub = supcred_subblock 171 | supcred_blob = ndr_pack(supcred_blob_unpacked) 172 | 173 | if args.base64: 174 | print(binascii.b2a_base64(supcred_blob)) 175 | else: 176 | print(supcred_blob) 177 | -------------------------------------------------------------------------------- /od2samba4.conf.example: -------------------------------------------------------------------------------- 1 | [files] 2 | mit_dump = in/kdc_dump.mit 3 | master_key = in/kdc_master_key 4 | heimdal_path = /usr/sbin 5 | hashes = out/user_hashes.json 6 | users_ldif = out/addusers.ldif 7 | newusers_ldif = out/newusers.ldif 8 | groups_ldif = out/addgroups.ldif 9 | membership_script = out/setmembership.sh 10 | hashes_ldif = out/sethashes.ldif 11 | 12 | [opendirectory] 13 | dc = dc=mydirectory,dc=example,dc=org 14 | url = ldap://mydirectory 15 | username = adminuser 16 | password = SecretPassword 17 | host = mydirectory 18 | sshuser = root 19 | sshpass = SecretPassword 20 | 21 | [samba4] 22 | dc = dc=example,dc=org 23 | url = ldaps://dc01 24 | username = Administrator 25 | password = SecretPassword 26 | nis_domain = example 27 | upn_realm = example.org 28 | -------------------------------------------------------------------------------- /out/README.md: -------------------------------------------------------------------------------- 1 | ### Output Files 2 | * `user_hashes.json`: All RC4 hashes that were extracted from the MIT Kerberos dump correlated to their UIDs. Created by `extract_hashes.py`. Required by `convert_hashes.py`. 3 | * `addusers.ldif`: LDIF file with all user for import into samba4 AD DC. Can only be imported using `ldbadd` and only once after provisioning, since it force-sets objectGUIDs. Created by `convert_users.py`. 4 | * `newusers.ldif`: LIDF file with new users (since last directory import from OD into samba4) for import into samba4 AD DC. Can only be imported using `ldbadd` and only once. Created by `convert_users.py --new`. 5 | * `sethashes.ldif`: LDIF file that contains all user password hashes for import into samba4 AD DC. Accounts will only be enabled after this LDIF was imported. Can be imported using `ldbmodify` as many times as you wish. Created by `convert_hashes.py`. 6 | * `addgroups.ldif`: LDIF file with all groups for import into samba4 AD DC. Can only be imported using `ldbadd` and only once after provisioning, since it force-sets objectGUIDs. Created by `convert_groups.py`. 7 | * `setmembership.sh`: Shell script for establishing group membership. This will execute `samba-tool group addmembers ` for every known group membership. Can be executed as many times as you wish. Created by `convert_groups.py`. 8 | -------------------------------------------------------------------------------- /sync/README.md: -------------------------------------------------------------------------------- 1 | # Synchronization script with systemd timer 2 | ## Purpose 3 | `sync.sh` is meant to be installed on the Samba4 Domain Controller you are migrating to. It automatically generates and copies the most recent KDC `mit_dump` dump file from the Open Directory server to the Samba4 Domain Controller, extracts password hashes and adds them together with new users to the Samba4 user directory. It will also establish new group memberships, but it won't migrate new groups. Modifications to existing users (except for new passwords) will not be migrated. 4 | 5 | ## Preparation 6 | `sync.sh` requires python-ldap, sshpass and heimdal; on Debian, run the following command to install these packages: 7 | ```bash 8 | apt install heimdal-clients heimdal-kdc python-ldap sshpass 9 | ``` 10 | 11 | `sync.sh` will NOT automatically copy the kerberos master key. Therefore, you need to manually copy the kerberos master key to the destination specified by `master_key` in `od2samba4.conf`. Both Open Directory and Samba4 must be running for `sync.sh` to work. 12 | 13 | Configure `host`, `sshuser` and `sshpass` settings in od2samba4.conf. `sync.sh` will connect to the Open Directory server (at `sshuser@host` using `sshpass` as password) via SSH in order to remotely dump the Kerberos database and copy (`scp`) the database over to the Samba4 server. Alternatively, set up SSH key authentication and modify `sync.sh` accordingly. 14 | 15 | Also, try running `sync.sh` prior to installing service file and timer so that you can detect and correct any configuration issues. 16 | 17 | ## Install systemd timer 18 | Make sure to update the `ExecStart=` path in `od2samba4-sync.service` to point to your `sync.sh` script. Copy both `od2samba4-sync.service` and `od2samba4-sync.timer` to `/etc/systemd/system/` and enable the timer with 19 | ```bash 20 | systemctl enable od2samba4-sync.timer 21 | systemctl start od2samba4-sync.timer 22 | ``` 23 | 24 | The timer should now appear in the list generated by `systemctl list-timers` and `sync.sh` should be executed every 15 minutes (every full hour, *:15, *:30, *:45). `sync.sh` will also be started 1 minute after enabling the timer. Output can be monitored using `journalctl -fu od2samba4-sync`. 25 | -------------------------------------------------------------------------------- /sync/od2samba4-sync.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Sychronize OD database to Samba4 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/root/od2samba4/sync/sync.sh 7 | -------------------------------------------------------------------------------- /sync/od2samba4-sync.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Sychronize OD database to Samba4 every 15 Minutes 3 | 4 | [Timer] 5 | OnBootSec=5min 6 | OnActiveSec=1min 7 | OnCalendar=*:0,15,30,45 8 | 9 | [Install] 10 | WantedBy=timers.target 11 | -------------------------------------------------------------------------------- /sync/sync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Synchronize new users and new group memberships from Open Directory Server to Samba4 Server. 3 | # Overwrite all password hashes on Samba4 server with hashes from Open Directory. 4 | # This script must be executed on the Samba4 server. 5 | 6 | set -e 7 | 8 | CWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | 10 | # Usage: readconfig SECTION KEY 11 | function read_od2s4_config { 12 | python2 << END 13 | from ConfigParser import RawConfigParser 14 | import sys 15 | 16 | config = RawConfigParser() 17 | config.read("$CWD" + "/../od2samba4.conf") 18 | print(config.get("$1", "$2")) 19 | END 20 | } 21 | 22 | SSHHOST=$(read_od2s4_config opendirectory host) 23 | SSHUSER=$(read_od2s4_config opendirectory sshuser) 24 | SSHPASS=$(read_od2s4_config opendirectory sshpass) 25 | MITDUMP=$(read_od2s4_config files mit_dump) 26 | 27 | # Generate MIT KDC password dump on remote Open Directory server and copy file over 28 | echo "Copying MIT Kerberos dump via SSH" 29 | sshpass -p "$SSHPASS" ssh -o StrictHostKeyChecking=no $SSHUSER@$SSHHOST "kdb5_util dump -b7 /tmp/kdc_dump.mit" 30 | sshpass -p "$SSHPASS" scp -o StrictHostKeyChecking=no $SSHUSER@$SSHHOST:/tmp/kdc_dump.mit $CWD/../$MITDUMP 31 | sshpass -p "$SSHPASS" ssh -o StrictHostKeyChecking=no $SSHUSER@$SSHHOST "rm /tmp/kdc_dump.mit" 32 | 33 | # Process KDC dump, generate LDIFs for import 34 | # This will not add newly generated groups, but it will establish group membership for new users 35 | cd $CWD/.. 36 | ./extract_hashes.py 37 | ./convert_hashes.py 38 | ./convert_users.py -n 39 | ./convert_groups.py 40 | 41 | # LDIF import 42 | echo "Importing LDIFs into Samba4 AD DC" 43 | echo "Adding new users" 44 | ldbadd -H /var/lib/samba/private/sam.ldb $CWD/../$(read_od2s4_config files newusers_ldif) --relax 45 | echo "Updating hashes" 46 | ldbmodify $CWD/../$(read_od2s4_config files hashes_ldif) -H /var/lib/samba/private/sam.ldb --controls=local_oid:1.3.6.1.4.1.7165.4.3.12:0 47 | echo "Updating secondary group memberships" 48 | $CWD/../$(read_od2s4_config files membership_script) 49 | 50 | --------------------------------------------------------------------------------