As part of the migration of my MacOS Server to Linux the most tricky service to migrate is Apple’s OpenDirectory service. Although it is based on the open-source OpenLDAP project, Apple did customize things a lot, making it very tricky to move and block a real migration due to these closed-source customizations. Instead of spending a lot of time on trying to figure out how to migrate OpenDirectory, I decided to start with a clean FreeIPA installation and migrate the users to that. Since I have been using MacOS Server’s OpenDirectory handled my authentication for quite some time and ran in to issues before I learned that starting from scratch (and only migrating user information) isn’t that hard and in often the best approach.
As it is already quite long, this post focuses on how to configure FreeIPA on Fedora Core and migrating DNS and users. Integrating MacOS is covered on the FreeIPA WiKi and will be covered in a subsequent post including the integration with Apple’s Profile Server (the main component Apple seems to intend to support in the future). To aid with the configuration / setup I wrote a couple of scripts that are available from my Gitlab instance.
Why FreeIPA on Fedora
FreeIPA is a very interesting and promising project as it does not only provide the needed LDAP and Kerberos services (which MacOS Server also uses) as well as DNS, all managed through either a web-based or a command-line interface. In addition, it also includes a PKI infrastructure to manage internal certificates and can integrate with many systems as it is based on fully open source solutions.
Although I normally use Debian Linux, I decided to use Fedora Server to host FreeIPA for a number of reasons:
- The FreeIPA packages in Debian are
- only available in unstable for quite some time now, which is not a good base for a key production server
- lagging behind a few versions/months (4.6.3 while 4.6.9 is being finalized) and not really moving so it seems not very actively maintained
- FreeIPA is developed on RHEL / Fedora so actively kept up to date
- Fully integrated (no need to port it) and stable for many years already.
Installation of the initial Fedora server is, like with Debian, pretty straightforward. I used the Netinst ISO image to install Fedora 28 Server Edition with FreeIPA in a fresh VM with 4 virtual CPUs, 2GB memory and 8 GB storage. The VM is connected to my management network with a fixed IPv4 and automatic IPv6 address registered in my internal DNS and using my internal NTP server. After installation I ensured all packages were updated to the latest version (and all security patches installed) by running the command:
sudo dnf upgrade
Enable Open VM Tools
Since the server is running in a VM, the
open-vm-tools package must be enabled to allow the hypervisor to interact with it and optimize things. The required package is pre-installed but still needs to be enabled and started with:
sudo systemctl enable vmtoolsd
sudo systemctl start vmtoolsd
Open ports in host firewall
Fedora has a host-based (netfilter) firewall enabled by default that does not allow services in unless specifically enabled. Enable the required ports to allow access from the network to with:
sudo firewall-cmd --add-service=freeipa-ldap \
next make this setting permanent with:
sudo firewall-cmd --runtime-to-permanent
To check with services are enabled in the firewall run (without
--permanent to see what is currently active):
sudo firewall-cmd --list-services --permanent
Initial setup of FreeIPA
For the initial initialization of FreeIPA, one must run the
freeip-install-server command. This is an interactive script that will auto-configure based on minimal user input but also accepts a lot of parameters. For my setup (with some quirks like hostname being different) I used the following parameters:
|(DNS) domain FreeIPA is authoritative for/on, should not be shared with AD!
|Should be main domain in capitals
|specified as actual hostname of the machine was different
|FreeIPA Host addr.
|specified as I use service IP addresses different from host’s main IP address
|Base for x509 structure and LDAP, defaults to REALM
|MyOrg Certificate Authority
|name of the CA, defaults to “Certificate Authority”
|instruct SSHD to lookup SSHFP in DNS
|Create Home Directories upon first login
|Initialize DNS component (required as hostname differs, will be reconfigured)
/etc/resolv.conf DNS servers for recursive lookups
|Reverse DNS Zone
--auto-reverse didn’t work, needed to specify here
Which results in the following
sudo ipa-server-install --mkhomedir \
--domain=mydomain.tld --realm=MYDOMAIN.TLD \
--ca-subject="CN=MyOrg Certificate Authority,O=MyOrg"\
--ssh-trust-dns --setup-dns --auto-forwarders \
With these options, the script will still ask for the Directory admin and IPA admin passwords, print a summary of the settings and ask for confirmation to proceed. Answer yes and after about 8 – 10 minutes the initial setup will be complete, at the end of the process the script will suggest the next steps (of which the 1st has already been done earlier). Create a backup of the CA certificate as recommended and obtain an admin Kerbos ticket with:
Before running any migration, please make sure you done basic setup of FreeIPA and especially have a Password Policy configured for groups that require them as FreeIPA applies policies at the moment users are created / passwords are set.
To migrate DNS zones into FreeIPA there does not seem to be a structured way available / documented. The only approach I found (described here) was to download each zone using a zone-transfer (
AXFR), convert the output to LDAP modifications and directly load these into the FreeIPA LDAP database. Since this approach felt a bit like a hack as it skipped any built-in sanity checks and was not repeatable (which I needed for a gradual migration running old and new in parallel) I didn’t quite like this approach and wrote
freeipa-dns.py, a script providing functionality not available in FreeIPA itself to migrate/synchronize and maintain DNS zones in FreeIPA. With this script I was able to convert each of my DNS zones with commands like:
./freeipa-dns.py -v axfr -T 126.96.36.199 -r -n -f none -t 3600 -D 3600 192.168.1.53 -s 192.168.1.10 domain1.tld domain2.tld domain3.tld
Please refer to the documentation of my FreeIPA support scripts for an explanation of options
freeipa-dns.py accepts. Options used in this case:
|produce verbose output
|perform a zone-transfer (AXFR) to synchronize DNS zones. Please note that the DNS server must allow a Zone Transfer for the domain from the host running the script for this command to work
|Allow zone-transfers from this IP address for the migrated zones (in my case, my externally reachable DNS server in my DMZ)
|Strip domain from hostnames and load simple hostnames
|Disable DNS server existence check (not handy during a migration
|Set FreeIPA forwarding policy to None (as FreeIPA is the DNS master)
|Set zone SOA TTL record to 1 hour
|Set default TTL for record to 1 hour
|IP address of the source DNS server
|Source address to perform
AXFR from (DNS server allows zone transfers from this address)
|domains to migrate / synchronize
As the script is performing a synchronization, it can be run multiple times over time, if needed. It is also possible to (as I did) have FreeIPA running in parallel of the existing DNS server and keep things in sync with this script.
Migrating from split DNS with Bind Views
The general recommendation from the FreeIPA project regarding split horizon DNS / DNS views is to not use them because views make DNS deployment harder to maintain and security benefits are questionable. Although I do not necessarily agree with this, migrating to FreeIPA did require me to change my DNS setup as Bind DNS Views were used to separate internal from external DNS as FreeIPA simply does not support this. The use of views is indeed complex and not necessarily required to separate internal from external DNS, this can also be achieved by not exposing certain subdomains to the internet. As I already had an internal DNS master server and an internet-exposed slave that was the master for the external world this was not really a change to my infrastructure but it did require a restructure of my internal DNS.
To support this migration, the
freeipa-dns.py script supports the commands
copy, which allow migration of hosts / entries from one zone (I migrated my internal zones from
mydomain.internal for this purpose) to another.
For the few hosts that required different addresses internally and externally (e.g. my DNS servers) I created the external entries (e.g.
auth.dns) in the
mydomain.tld zone itself and an overlapping subdomain (
dns.mydomain.tld in this case) with internal addresses. By not transferring these internal zones to the external DNS server I could maintain a similar setup without views in FreeIPA.
Finalizing the migration – Reverse records & Zone Serials
After the migration, I had the
freeipa-dns.py script create the reverse zones as 1) FreeIPA turned out to be not very smart and flexible with this and 2) I don’t want each reverse zone to be at class-C level (but use a roll-up one for smaller subnets) by executing:
./freeipa-dns.py -v reverse-ptr -n -p -c 10. 192.168 192.168.1 2001:0db8:85a3
to initially create the reverse zones in FreeIPA and then:
./freeipa-dns.py -v reverse-ptr -a
to generate populate them based on the zone information in FreeIPA. Once I was satisfied with the migration I reset the migrated zone’s serial number in FreeIPA to an RFC1912 style serial (
YYYYMMDD##) based on current date with:
./freeipa-dns.py -v serial -t domain1.tld domain2.tld domain3.tld
Set DNS source address
While setting up zone transfers to our internet-facing DMZ slave DNS server I noticed that FreeIPA uses the host’s main address when sending DNS NOTIFY messages to its slaves (and slave servers rejecting them). It turned out that bind’s
notify-source settings is ignored by the ldap plugin used, breaking multi-homed setups and in case of IPv6, sending these from the temporary(unpredictable) IPv6 address. The only way I could think of to fix this is to use the built-in firewall to source NAT outgoing DNS packets to enforce they come from the correct source IPv4/IPv6 address. To set this up I created
set-dns-source.sh, a script to setup (or remove) the necessary firewall rules that can be run like this:
sudo IPV4ADDR=192.168.0.100 IPV6ADDR=2001:0db8:85a3::53 ./set-dns-source.sh install
The script will make the rules permanent and effective immediately.
One of the additional reasons for me to migrate to FreeIPA was it’s DNS management. Till now I used a self-written svn-hook script to manage my DNS zone files. This worked well so far, but was not ready for DNSSEC, which FreeIPA supports out of the box. Although the initial setup included DNS, sadly the
ipa-server-install script ran earlier does not include an option to enable DNSSEC, to enable this run:
sudo ipa-dns-install --dnssec-master --force
Please note that if you already have DNSSEC configured, you want to migrate your current DNSSEC zone keys to avoid the pain of a migration. This is not very well documented but seems to be possible, according to this thread on the FreeIPA-Users mailinglist.
To enable DNSSEC for each of my domains I simply followed the FreeIPA DNSSEC Guide, which is very helpful and not very difficult to follow. The tricky part to get right is to add the signing public key to the top level domain, which is something your domain registrar may need to do (had to be done manually for one domain). To test the DNSSEC setup I used Verisign Labs’ DNSSEC Analyzer and DNSViz, both very useful tools to ensure you got things right.
Use Let’s Encrypt certificate for web service
As I did not want to be dependant on importing an internal certificate for users to safely use the FreeIPA web frontend, I decided to use a free Let’s Encrypt certificate for the FreeIPA web front-end. I noticed there were several solutions, e.g. freeipa-letsencrypt and antevens’ implementation, but all of these required an additional scrip setup to be run by cron while with recent versions of EFF’s CertBot this isn’t really necessary. As I wanted the setup to be as simple as possible (i.e. eliminate moving / non-standard parts that need to be maintained across upgrades) I created
freeipa-letsencrypt.sh, a wrapper script for EFF’s CertBot to provide the necessary parameters to set things up. It will use a
DNS-01 challenge to confirm authority over the DNS names (temporarily updating and removing challenges in DNS zones managed by FreeIPA so this required the DNS to be migrated over already) and then replace the certificate user by the FreeIPA front-end. The script will use the FreeIPA host’s principle name and aliases as the primary name and alternative names for the certificate. The script can be used like:
A brief description of what it doest and available options can be found here. In case the hostname or (principle) aliases change, simply re-run the script and it will simply request and reconfigure a new certificate. Please note that running this script will also enable automatic renewals of the certificates
I have been looking for quite some time to look for a way to fully migrate users from Apple’s OpenDirectory to FreeIPA. However, considering the scenarios described in the FreeIPA’s Migration HowTo (especially the Gnome project’s migration) and known difficulties to extract usable passwords from OpenDirectory (this reddit contains good hints), I could only conclude that this was not a useful exercise. Yes, I was able to extract the Kerberos information but putting it back in FreeIPA would be a pain (format of encrypted passwords unclear) and even if this would work, I would have to work-around FreeIPA’s logic to force users to performa a password reset after an import. Since most users didn’t change their password for quite some time, the approach was chosen to simply migrate user and group information, generate new random passwords and instruct users to activate their new account manually with the generated password. Given the number of users to migrate, this was a more sensible approach than spending a lot of time on a perfect migration (of which the outcome was still uncertain).
FreeIPA comes with a migration tool for LDAP (see
ipa migrate-ds) but tha did not need my requirements. As I needed a bit more control on the migration (and again needed something that was repeatable and could synchronize) I wrote
- Creates new users in FreeIPA based on the users in LDAP with a fresh UID and a new password
- Selectively migrates group membership (creating groups where needed)
- Maintains ID View with legacy UID/GID/homedir/shell not to break existing integrations
- Migrates additional OpenLDAP fields: country, homeDirectory, apple-generateduid
- Can limit the migration to specific users or groups or custom LDAP filter
- Installs necessary LDAP schema customizations (and can remove it)
- Can generate users in FreeIPA staging mode
- Is modular and easy to extend for another LDAP structure
- Can be run multiple times to keep users in sync between FreeIPA and LDAP
With this script I migrated the users with a command like:
./users2freeipa.py -v -O -U -c "Legacy LDAP" -g workgroup -x admin -G -p passwords.txt ldap://ldap.mydomain.tld
Please refer to the documentation of my FreeIPA support scripts for an explanation of options
users2freeipa.py accepts. Options used in this case:
|produce verbose output
|migrate from Apple’s MacOS Server OpenDirectory
|Configure necessary LDAP schema customizations
-c "Legacy LDAP"
|Maintain ID Compatibility
Legacy LDAP view to preserve UID/GID/homedir/shell
|migrate/synchronize members of group
|exclude user “admin” from the migration
|migrate membership for all groups
|generate a password for each user and write them to
|LDAP Server to migrate from
As the script is performing a synchronization, it can be run multiple times over time, if needed, or perform the migration in stages (i.e. per group) as I did. Post migration, the next step is to integrate the MacOS Server with FreeIPA, which will be covered in my next post.
This post is part of a series on moving functionality removed as of Fall 2018 from MacOS Server: