Musings of a noob as I migrate from Windows to FreeBSD in my homelab

Getting back at it - I'm in process trying to install BIND on both servers.
  • SR-IOV continues to behave exactly the way I'd want when combined with vnet jails.
    • 11 VFs created on ixl2 (the 1st port of my X710-T2L) and 6 VFs created on ixl3 (the 2nd port). ixl0 & 1 are the onboard ports on my Supermicro X11DPH-i.
      • It's not well-documented, but it is possible to create and use a separate .conf file for each interface that's getting configured with VFs: I have ixl2.conf and ixl3.conf located in /etc/iov/.
      • The syntax to use in /etc/rc.conf is where I had a problem... couldn't figure out how to get both files to load. Eventually I stumbled on a post somewhere (and have since lost the link) that said to simply list the files in a single line with a space separator:
        Code:
        iovctl_files="/etc/iov/ixl2.conf /etc/iov/ixl3.conf"
      • Some extra fun you can have is assigning whatever MAC address you want to VF interfaces.
        • Just specify allow-set-mac : true; at the top of the iov .conf file, and within each VF definition specify the MAC address you want: mac-addr : "XX:XX:XX:XX:XX:XX";
        • According to the IANA, addresses that start in the range 00:00:00 to 00:00:FF are reserved (what for? I don't know).
        • So, I set up my VF MAC addresses using the various jersey numbers my kid wore during his youth hockey career... 00:00:53:24:02:XX. Doesn't seem to cause any problems with my networking gear, so whatever that range is reserved for clearly isn't important (and doesn't exist/conflict).
  • BIND installed on the jail running on the primary server without issue.
  • In order to get jails working on the secondary server (running on consumer-grade hardware), I needed to use a bridge interface. SR-IOV is not an option on the Z270 motherboard, nor does the X540-T2 NIC support it.
    • This machine has been running a pair of bhyve VMs with my old Windows Servers (and very reliably, I might add), so I thought I'd keep things simple and just use one bridge for bhyve and jails. Once the Windows VMs are decommissioned I won't need to do anything except shut them down and archive (or delete) them.
    • Implementation for this is pretty straightforward - no gotchas lurking. The bridge interface that's created in the background when running the vm switch create command doesn't seem like the best choice, since I don't think it'll persist after I get rid of the VMs and destroy the vm switch. It is possible to set up a bridge first and then force the vm switch to use it. Steps I followed:
      • Shut down both Windows VMs
      • vm switch destroy public (I didn't see a need to get creative with the switch name)
      • Set up the bridge in /etc/rc.conf, adding the ix0 interface and assigning the host IP to the bridge instead of the ethernet port.
      • update the host to 14.3-RELEASE-p5 since I'm here...
      • reboot and check ifconfig to see that bridge0 is set up properly. Verify connectivity to the outside world.
      • Re-create the vm-bhyve switch using the existing bridge: vm switch create -t manual -b bridge0 public
        • I gave it the same name (public) so I wouldn't need to modify the VM network configs at all.
      • Start both VMs back up and verify connectivity
    • Worked like a charm, so I moved on to trying to get BIND installed on a jail on the secondary server.
  • My jail.conf on the secondary server has all the appropriate exec.prestart/start/stop/poststop directives for dynamically creating, attaching, detaching, and destroying epairs for the jails when they're started/stopped. Works great. If anyone cares, here's a privacy-redacted version:
    # STARTUP/LOGGING
    exec.prestart = “logger Starting jail ${name}…”;
    exec.prestart += ”/sbin/ifconfig ${epair} create up”;
    exec.prestart += ”/sbin/ifconfig ${epair}a up descr jail:${name}”;
    exec.prestart += ”/sbin/ifconfig ${bridge} addm ${epair}a up”;
    exec.start = “/sbin/ifconfig ${epair}b ${ip} up”;
    exec.start += “/sbin/route add default ${gateway}”;
    exec.start += “/bin/sh /etc/rc”;
    exec.poststart = “logger Started jail ${name}.”;
    exec.prestop = “logger Stopping jail ${name}…”;
    exec.stop = “/bin/sh /etc/rc.shutdown”;
    exec.poststop = “logger Stopped jail ${name}.”;
    exec.poststop += “/sbin/ifconfig ${bridge} delete ${epair}a”;
    exec.poststop += “/sbin/ifconfig ${epair}a destroy”;
    exec.timeout = 90;
    exec.consolelog = “/var/log/jail_console_${name}.log”;

    # PERMISSIONS
    allow.raw_sockets;
    exec.clean;
    mount.devfs;
    devfs_ruleset = “5”;

    # NAME/PATH
    path = “/usr/local/jails/containers/${name}”;
    host.hostname = “${name}.somedomain.com”

    # NETWORKS/INTERFACES
    $ip = “10.0.0.${id}/24”;
    $gateway = “10.0.0.1”;
    $bridge = “bridge0”;
    $epair = “epair${id}”;

    jail10 {
    $id = “10”;
    vnet;
    vnet.interface = “${epair}b”;
    }
    jail11 {
    $id = “11”;
    vnet;
    vnet.interface = “${epair}b”;
    }
    jail12 {
    $id = “12”;
    vnet;
    vnet.interface = “${epair}b”;
    }
    jail13 {
    $id = “13”;
    vnet;
    vnet.interface = “${epair}b”;
    }
  • Last night's hiccup: I really have no idea what I'm doing when it comes to the pf firewall.
    • Unlike on my primary server, where pkg -j jailname install bind920 worked without a hitch, I got some sort of permission denied error when using pkg install against a jail on the secondary machine.
    • The only difference is the network config - I'd wondered before if pf would behave the same on a machine using bridged networking for the jails, and I'm fairly certain the answer is no.
      • This morning I actually went and read the PACKET FILTERING section of the if_bridge(4) manpage (should have done that before), and I think it confirms my suspicions.
      • I don't think the per-jail pf configuration I'm using on the primary server will work... the manpage says that all inbound packets are filtered on the originating interface (ix0 in my case), while outbound packets are filtered on the "appropriate interfaces." My guess is that it means outbound packets are filtered on epairb interfaces...
    • I'll have to toy with this a bit to see what's actually happening and adjust my firewalling strategy. I'm not inclined to simply shut off pf so I can install BIND - I'd rather have the pf setup procedure completely ironed out for a simple service that uses one UDP port and one TCP management port. When I go to set up more involved jails I really don't want to be flailing around wondering why things are behaving poorly.
  • Assuming I'm right and it's just my lack of understanding about how filtering works with bridge interfaces, and assuming I learn enough to move on, I'm fairly confident that I'll get BIND up and running in the next day or two.
    • Simple setup: a single forward lookup zone and a few reverse lookup zones, all of which are private (so I don't need to mess around signing things for DNSSEC). These BIND instances will also act as resolvers - not best practice for networks with large loads, but mine is small and has comparatively light traffic. Should be fine. Filter lists will wait a bit, because...
  • I hadn't planned on it originally, but it looks as though I'll be implementing a DHCP jail. I run a Unifi UDM as my router, and I've been using the built-in DHCP service. Unfortunately Unifi doesn't seem to offer any support for RFC2136 DDNS updates from their DHCP service, which is an (maybe not surprising) unpleasant discovery. Seriously... this stuff has been around for ages. There are some documented attempts I've seen to install nsupdate on a Unifi device, or even add a middleware layer to make the UDM think it's updating a cloud DDNS provider (which it supports). I'm not interested. If Unifi DHCP won't natively interface with a standards-compliant DNS server, I'll just roll my own.
    • So, as soon as I have my static zones set up and working properly in BIND, I'll make the tweaks necessary to support DDNS updates and go ahead with a DHCP jail. I'm thinking Kea, since it's the currently-supported offering from ISC. The old ISC dhcpd 4.4 offering was EOL-ed in 2022... Seems like the FreeBSD handbook should be updated to reflect this. I'm sure it works fine, but EOL means minimal (or no) security fixes, and that's not ok with me.
 
Interesting read... Sometimes I get the impression that the setup is more complicated than it needs to be, esp. with a lot of components that need to be set up by hand. Keeping a system like this up-to-date is gonna be a headache.

But considering that this looks like a private hobbyist project, I'm just gonna sit on the sidelines and enjoy the occasional read.
 
Sometimes I get the impression that the setup is more complicated than it needs to be, esp. with a lot of components that need to be set up by hand.
Likely true, if I'm being completely honest with myself. I think it comes down to 3 factors:
  1. I'm using a random collection of curated (by that I mean, did it fit within budget) hardware that occasionally forces me to look for workarounds to problems I could have avoided if I'd made different selections (e.g. my panic when one of my Seagate HDDs didn't update to 4kn - should have just bought 4kn drives to begin with, but the 512e upgradable drives were cheaper).
  2. Sometimes I'm way off track trying to do stuff without a clue. Most times, in fact. Good thing I know how to read...
  3. iX Systems have had teams of professionals working for years on their Truenas product. What I'm essentially doing is trying to recreate what they've already done, and really there's no good reason besides hobbyist fun. Linux is, imo, a bit of a s***-show these days. While FreeBSD isn't perfect I do appreciate the Unix philosophy more. Since I have no deadlines and no set finish line, puttering along one step at a time suits me just fine. If I needed it today, I'd wipe it all and install Truenas.
Keeping a system like this up-to-date is gonna be a headache.
Likely 15.0-RELEASE will be out before I'm anywhere close to done... we'll see if I feel adventurous when it comes out, but I'm thinking I'll probably hold off a bit. My plan is to update base and/or packages as security errata indicate I should, but not otherwise. The exception would be if I'm suffering a bug that someone's fixed and I want that patch, or if a new feature gets added to a package that I really want. Generally, though, I expect updates to be sparse. Boot environments and snapshots of userland files also have me excited. If I blow it (and I'm sure I will at some point), I'll have better odds of recovering before my wife notices I've bricked her ability to browse the web.
 
I expect updates to be sparse. Boot environments and snapshots of userland files also have me excited. If I blow it (and I'm sure I will at some point), I'll have better odds of recovering before my wife notices I've bricked her ability to browse the web.
Actually this, I'd avoid at all costs.

A setup that I have at home is to have a router with DD-WRT that my family connects to directly for Internet (over wi-fi), and a collection of my own machines that also have a direct connection (both cat5e and wifi) to the router. This allows me to play with networking and servers all I want, even play with IPv6 and HTTP/2, HTTP/3, and the like- and my family is none the wiser, because their own Internet access is not disturbed. There are ways to play with stuff while keeping basic essential functionality undisturbed.

REALLY need to engineer out and isolate points of failure.
 
REALLY need to engineer out and isolate points of failure.
Yup, agree 100%. That's why I'm installing secondary DNS, DHCP failover, secondary LDAP/Kerberos, etc on the junky backup server. At some point in the next year I'll get better hardware for that secondary role and migrate those jails to a more reliable box. Upgrade policy is to only work on one server at a time, verify after performing any updates, and touch the secondary only once the primary is confirmed in a good state. For hobbyist home networks there will always be a single point of failure somewhere.

The best I can do is to push those points to the non-DIY parts of the network. E.g. my Unifi UDM doesn't have a shadow copy waiting in the wings, but I _did_ spring for the service contract on that device. Cost me an extra 20% and it's already paid off - my original UDM had its SSD go bad, which manifested in a slow failure mode: it would run fine for 24 hours or so, then the GUI would die (no ssh access either), and then a few hours after that it would stop routing and switching. A hard power cycle would reset the clock, so the network was limping along and my wife didn't know about it until the warranty replacement showed up on our doorstep. Unifi backup/restore worked perfectly, and I had the dying router replaced in a span of 30 minutes while she was out on an errand. If I hadn't purchased the service contract I'd have had to ship back the old unit before they sent me the new one. Well worth the money, imo, to have the new one in hand before having to send the old router back. Fingers crossed it was a one-time thing... I've never seen a new SSD go bad that quickly before. Then again, I don't know what brand/model of SSD Unifi is using inside their products.
 
I've never seen a new SSD go bad that quickly before. Then again, I don't know what brand/model of SSD Unifi is using inside their products.
Samsung has very good SSDs, I swear by the brand. Expensive, but well worth the money.

Worst brand for SSDs, one I learned to stay away from is Teamgroup. Their SSDs are cheap, but they deteriorate very quickly.
 
Yup, agree 100%. That's why I'm installing secondary DNS, DHCP failover, secondary LDAP/Kerberos, etc on the junky backup server. At some point in the next year I'll get better hardware for that secondary role and migrate those jails to a more reliable box. Upgrade policy is to only work on one server at a time, verify after performing any updates, and touch the secondary only once the primary is confirmed in a good state. For hobbyist home networks there will always be a single point of failure somewhere.
I'd say that's just the wrong way to do it.

Have a secondary router behind your primary one, and all your play boxes behind that secondary router. Your wife gets to simply connect to the primary router for Internet.

Your secondary router can be the box where you host your secondary DNS, DHCP, subnetting, etc. Everything else that you wanna play with - it should be on a subnet behind that secodary router.

Yeah, not an elegant solution, you may need to buy more ethernet cable than you expected - but that does prevent you from bricking your wife's Internet access. The extra expense for more cat5e cable (and running it in weird places) is well worth it, trust me.
 
Isn't that the entire point of virtualization, be it via hypervisor or a container-based concept? If I muck up a jail it shouldn't kill any of the others, nor cause the host to die. Assuming FreeBSD jails are robust in this manner, I'm pretty happy with my trajectory.
 
Isn't that the entire point of virtualization, be it via hypervisor or a container-based concept? If I muck up a jail it shouldn't kill any of the others, nor cause the host to die. Assuming FreeBSD jails are robust in this manner, I'm pretty happy with my trajectory.
No, it's not. First, set up your networking correctly, then muck around with jails and then, based on your results, DNS. Separation of concepts matters. Besides, if you think about it, that's how pros handle things. There's a reason for Best Practices out there.

If you muck up your secondary DNS jail, it should NOT affect the DNS on the primary router. Yeah, there's a bit of redundancy, but that's what it takes to stay out of trouble.
 
No, it's not. First, set up your networking correctly, then muck around with jails. Separation of concepts matters. Besides, if you think about it, that's how pros handle things. There's a reason for Best Practices out there.

If you muck up your secondary DNS jail, it should NOT affect the DNS on the primary router. Yeah, there's a bit of redundancy, but that's what it takes to stay out of trouble.
Most of the jailed services are networking: DNS, DHCP, auth, storage. Where else would I put them except in a jail? I have strong philosophical objections to letting the Unifi UDM do most of the heavy lifting. Sure, it can do more, but thats too many eggs in one (non-redundant) basket.

The whole point of this exercise is to set up networking "correctly" on FreeBSD. Sure, a few services at the end are fluff (eg jellyfin), but most of this is just getting core net services daemons running in containerized environments to keep things simple (and therefore less likely to fail).

I appreciate your level of caution, but I think we differ philosophically about where the line needs to be drawn. Although it's been 2+ decades, I did run a small ISP back in the late 90s... I still have a pretty good idea about how the basic network should be architected and how services should be deployed. Maybe it's dated knowledge, but I'm not flying blind when it comes to network design.
 
Most of the jailed services are networking: DNS, DHCP, auth, storage. Where else would I put them except in a jail? I have strong philosophical objections to letting the Unifi UDM do most of the heavy lifting. Sure, it can do more, but thats too many eggs in one (non-redundant) basket.
In all honesty, I also have strong objections to letting my ISP handle DNS and whatnot at home.

That's why I made sure to get a dumb modem from my ISP (Comcast), and to use my own router (which I flashed with DD-WRT). That dumb modem (Arris S33) was actually difficult to find, but that effort is paying off, I have the flexibility to play without disrupting Internet service at my place.

And man, $300 USD is too much for a wifi router... I use an Asus AC 1900 that I got for less than half that, and it's a workhorse with DD-WRT, and I'm the one in control of DNS entries on it. Comcast did have those all-in-one gateways that they tried to rope me into, I said no, and found a compatible dumb modem instead.
 
Minor distraction (I have the attention span of a cat, in case you hadn't noticed): I decided I need a client machine in the house so I can test services as I deploy them without needing to sit in the basement or "cheat" by SSH-ing from a machine that isn't part of the new deployment.
  • I grabbed my daughter's old laptop, an Asus Vivobook she used during high school, and wiped Windows away.
  • I installed 15.0-BETA5, which as of this morning is now upgraded to 15.0-RC1
  • Mostly it works - the iwl wifi driver is a bit picky (doesn't like the access points at work at ALL, but works fine at home). Sound is kind of scratchy from the speakers (I'll debug that later).
  • The laptop is an upgrade over the POS Clevo I've been lugging with me to conferences (that Clevo is very long in the tooth). I think I'll keep the Asus for that purpose.
    • If I want to take this machine with me on the road, I need it to run Matlab for some light work - mostly re-plotting figures for last-minute edits to my slide decks. Matlab doesn't provide a build for FreeBSD, and I'm not overly eager to find something else, although I'm aware of a couple of free alternatives (I get a Matlab license for free thru work).
    • My first thought was to try Linux emulation by installing Ubuntu in a jail and running Matlab from there. I used the writeup describing how to run Chrome from a Linux jail as an example.
    • I was able to get Chrome to run on my desktop from the jail, but Matlab flat-out refused to work after installation. It seemed to be spinning its wheels loading something and never launched a GUI. Since Matlab isn't on the list of applications that Linuxulator supports, I'm not all that surprised. It was worth a shot...
  • I figured if Linux emulation didn't work I'd go ahead and try a VM.
    • I installed Ubuntu 24.04 LTS in Bhyve and enabled p9fs... with the help of this post, I managed to get vm-bhyve to mount my /home/user directory from the FreeBSD host into Ubuntu as the same /home/user directory (yes, UIDs and GIDs match between systems). Getting the /home directory shared between systems enables seamless transition - everything I'm working on can be seen and edited using Matlab, even though Matlab is running in a VM.
    • as a test I set up Chrome in the Linux VM and taught myself enough about X11 forwarding to get it to work over SSH. I have a desktop launcher that fires up Chrome on the desktop without issue.
    • last step: I installed Matlab and it works flawlessly. It's not snappy at all running the GUI over X11 forwarding, but it will suffice for the occasions where I need to touch up a figure. For real computation this is the wrong machine... I'll just connect to resources at work.
Here's a screenshot of the machine I'll be using as my 1st client device on the new home network:

matlab on ubuntu on 15.0-RC1-b.png
 
Service deployment update: DNS and DHCP are up and running.
  • BIND 9.20 installed from packages in a jail on the primary server, with another named instance installed in a jail on the backup machine.
  • Authoritative zones configured on the primary named instance for my lone forward-lookup domain and the 5 reverse-lookup domains that make up my network.
    • LOTS of room to grow... I'm lazy and configure each VLAN with a /24, which means I have hundreds of unused addresses on nearly every VLAN. That's fine, since it really wasn't that fun manually typing in the static address assignments into the DNS zone files.
    • BIND was without doubt the easiest install I'll have - it's evolved since the late 90s, but the fundamentals are the same.
  • Automatic zone transfers from primary to secondary work flawlessly
  • Recursive resolvers are fast... happy to abandon using my ISPs resolvers (I had Windows DNS servers forwarding to my ISP, since WinDNS sucks as a resolver)
  • pf rules locking down both jails were straightforward in the end:
    • I poked and prodded the bridged config for the backup DNS jail, and wasn't able to get pf to work the way I wanted. What I wanted was something similar to how it works on the primary, where SR-IOV makes the VF assigned to the jail look like an entirely separate adapter. Tying myself into knots trying to make it work my way didn't work, and I'm not interested enough to keep going.
      • So, I punted and moved all pf filtering onto the host... none of the jails on the backup server will run pf. I'll just have one big config on the host doing everything. I think this'll work out, so long as I keep to my plan of putting jail-specific rules in groups, it won't be that hard to edit.
  • For DHCP I configured another pair of jails the same way (one using an SR-IOV VF on the primary, and the other using an epair attached to the backup machine bridge) and installed Kea DHCP 3.0.2 from pkg.
    • Commentary: this is a wildly complex package - config options are endless, and I can see why some people might get frustrated. The documentation along with the knowledgebase are fantastic, however, so if you're willing to spend the time reading you should be up and running reasonably quickly.
    • Having said that, it was not so simple to get DHCP working on my network. I followed the guides for a high-availability setup - specifically I configured for hot spare mode. No load balancing or anything like that... just another instance on another box waiting in the wings in case the primary dies.
  • This should be straightforward - the config files are identical, with only one tweak needed to tell each instance which server it is. So, I configured a test pool, verified healthy config and intra-instance communication (heartbeat), and enabled the dhcp4 service. I disabled the DHCP service on the Unifi UDM SE so there wouldn't be a conflict, and then tried to get a lease on my phone.
    • NOTHING - no lease on the phone, and no info in the logs indicating my phone was asking for a lease.
    • Down the rabbit hole I went...
      • My first try was to disable pf. It was likely that I didn't have the filter rules set correctly, so I thought this was a good 1st step.
      • WRONG. Same behavior (or lack of any behavior whatsoever)
      • Seemed like the DHCPDISCOVER broadcasts weren't making it to the DHCP jail...
      • Digging in, I learned that SR-IOV VFs don't see everything that crosses the adapter - traffic is pre-filtered so that the VF only gets traffic bound for its MAC address. There's a fix for this, if you have hardware that supports it... enable promiscuous mode on the VF that needs to see everything. My Intel X710 supports this (or claims to), so I edited the VF config file and re-created all VFs to allow the DHCP jail's VF to see all traffic on the interface.
      • STILL NOTHING. WTF? I can imagine difficulty in getting broadcast traffic through the bridge on the backup machine, but I thought I'd guessed right by enabling promiscuous mode for the VF on the primary.
      • Last-ditch try: both the primary and backup servers have integrated GigE NICs on the motherboards. Those ports were unused, so I thought I'd try assigning a whole dedicated physical interface to each DHCP jail.
        • this is easy with Vnet jails - the only caveat is that on the backup jail I have to re-enable pf, since it won't be running through that host's bridge.
        • Both DHCP jails communicate just fine... pf rules look correct based on how DHCP should work (clients send from UDP port 68, servers listen on UDP port 67)
      • Guess what? NOTHING.
      • As a final try I switched the tri-position toggle for the Unifi UDM DHCP service from "disabled" to "relay". I shouldn't need a relay, since the SSID my phone is connecting to is tied to the same VLAN that has the DHCP servers. No tagging involved... the native VLAN is correct end to end, and broadcasts using .255 should be going from my phone thru the AP thru the POE port on the UDM SE and into every downstream switch port that's using that VLAN natively (or tagged), including the ports that my DHCP servers are attached to.
        • Surprisingly, this wasn't a worthless thing to do. Once I switched on the DHCP relay feature I started seeing log traffic that indicated the default gateway address for that VLAN was attempting to communicate with the DHCP server, from port 67 to port 67. So, relay works, and I just needed to adjust my pf rules to allow this combination of from/to addresses and ports (I assumed relay would use port 68, but I guess not...)
        • Once I had pf rules set to allow the UDM relay thru, viola! Lease obtained.
      • My conclusion after all this: the UDM SE box that sits at the heart of my network is filtering DHCP broadcasts instead of forwarding them through the VLAN. I've combed thru all the settings and can't find one that says "disable/enable broadcasts," but the behavior is clear as day:
        • enable Unifi DHCP and it'll serve up leases
        • disable Unifi DHCP and it won't allow another DHCP server to serve requests by listening for broadcast DHCPDISCOVER packets, since they're just not forwarded.
        • use Unifi DHCP Relay and let the UDM SE translate the broadcast into a unicast communication coming from the UDM gateway IP, and things work out.
      • Whatever... I really thought it'd be simpler to get DHCP working. My whole reason for falling down this hole was that I wanted DDNS updates sent to my name servers from the DHCP server. Unifi's DHCP service won't do this, but ISC has a nice ddns service bundled with Kea. So, I don't really need my DHCP jails to use the dedicated NIC ports, but now that it's configured I'll just leave it that way. I guess if I ever run short of switch ports I can reclaim a couple by moving back to the old VF or bridge design, but I don't really see that happening any time soon. See my signature below.
  • Kea DHCP is configured for 3 of my 5 VLANs at this point (the other 2 don't need it), and sub-zones are configured in DNS for dynamic clients. All is working perfectly, except:
    • some sort of permissions error updating the in-addr.arpa zones with dynamic PTR records. Those zones were pre-configured with some static addresses, but it seems that in doing so I borked the permissions and named won't create a .jnl file for dynamic updates. Dynamic updates to the forward-lookup sub-zones works perfectly... it's just the reverse-lookup zones that aren't getting updates. A bit more poking and I'll get this
  • Last step will be to set up DNS RPZ to provide blacklist/sinkhole type filtering to prevent ad sites, telemetry sites, and malware sites from resolving. Looking at my options now for whose data feed to use...
Edit: I forgot to mention that I made sure the jails could see /dev/bpf* by creating a custom devfs_ruleset, and raw sockets were allowed... there's no evidence either of these helped or hurt. I just never see broadcast DHCP traffic.
 
DHCP is a protocol that only works across a single broadcast domain (LAN segment). If you want it to reach further you need DHCP relay enabled at your routers / gateways. Always.

As for "high availability" the simplest way is often to just split the ip address pool in two, have two DHCP servers, and run one half on each DHCP server. No complicated config, just a different pool range on each DHCP server. Of course, this assumes that everything else is the same.
 
DHCP is a protocol that only works across a single broadcast domain (LAN segment). If you want it to reach further you need DHCP relay enabled at your routers / gateways. Always.

I shouldn't need a relay, since the SSID my phone is connecting to is tied to the same VLAN that has the DHCP servers. No tagging involved... the native VLAN is correct end to end, and broadcasts using .255 should be going from my phone thru the AP thru the POE port on the UDM SE and into every downstream switch port that's using that VLAN natively
 
...I switched the tri-position toggle for the Unifi UDM DHCP service from "disabled" to "relay"...My conclusion after all this: the UDM SE box that sits at the heart of my network is filtering DHCP broadcasts instead of forwarding them through the VLAN.
Sounds like this Unifi box is not a real bridge. I have two OpenWRT access points in bridge mode on my network, and DHCP works happily across them.
 
Sounds like this Unifi box is not a real bridge
That's my conclusion. The AP is a Unifi U6 Mesh, and the UDM is essentially a slick Linux-based router product with an integrated 8 port POE switch, a WAN port, and a pair of SFP+ ports. One of those two devices is the culprit. Thankfully the relay function works, so I won't need to get something different. Just a word of warning to anyone wondering why running your own DHCP server on a Unifi-centric network doesn't work... something isn't right with the way Unifi handles broadcasts.
 

Service deployment update: OpenLDAP + Kerberos is up and running (1 of 4)​

So what does this mean, exactly?
  • OpenLDAP installed in a jail and configured to provide the basic directory services I need (or what I think I need):
  • Kerberos is set up to use LDAP as a backend storage system, and Kerberos principals are directly tied to LDAP users
  • LDAP is set up to forward authentication requests through the SASL layer to Kerberos, so any LDAP search will be authenticated against the principal's Kerberos password - only the LDAP/kerberos admin and service accounts (global schema admin, database admin, simple bind user, and kdc+kadmin services) have password hashes in LDAP. All other accounts (user and service) use the SASL passthru mechanism.
  • While I'm running 14.3-RELEASE, I chose to skip over the built-in Heimdal kdc and instead use MIT Kerberos. MIT is the default selection in 15.0, so I figured I'd future-proof myself by starting with MIT Kerberos now.
  • I had initially planned on putting OpenLDAP and Kerberos in separate jails, but after seeing how tightly integrated they can be when you use an LDAP backend for Kerberos I changed my mind. I put everything in a single jail, which allows me to more closely follow the (few) online examples I found about how to get this integration working.
surfrock66's self-hosted blog post offered a great jump-off point, however the differences between running OpenLDAP+Kerberos+SASL on Ubuntu vs FreeBSD offer plenty of pain points. In other words, it took me a while to figure this out. Rather than offer my usual stream of conciousness post about things I did, the problems I created for myself, and how I solved them, I'm going to break this post into two major sections (4 total posts). First will be a "clean" prescription for how to replicate what I did (feel free to copy, but no promises it'll work for you). This took 3 posts, leaving a mere 25000 characters in the final post for me to detail my mistakes. I promise not to add a 5th post. I figure this will be more helpful for people looking to get something up and running than trying to parse out the good from the bad like you'd have to do from my usual posts. The second section will describe in some detail all the mistakes I made and problems I encountered setting this up. Feel free to ignore it - it's full of plenty of brain farts and "doh" moments. I burned it all down and started from scratch multiple times (I won’t admit to a number)… getting access permissions and security set up is like hiking on a narrow mountain ridge while wearing stilts. One typo or command issued out of sequence can see you locked out with puzzling errors that are difficult for a newb to interpret. My advice if you decide to try this in your setup: go slowly, measure twice and cut once, and be prepared to start over.

"Clean" setup of OpenLDAP + MIT Kerberos on FreebBSD 14.3-RELEASE (jailed)​

This setup is very similar to what surfrock66 posted in his blog - in fact, I stole from him as much as I could and he deserves a ton of credit for slogging through some of the tougher issues and providing the basic formula. So, thank you surfrock66! I encourage anyone looking to replicate this to also read through surfrock66’s initial LDAP blog post and subsequent LDAP+Kerberos post – they’re both very helpful. Note that this setup assumes a local certificate authority already installed somewhere on your network. If you don’t have a CA you’ll need to adjust accordingly. I relied heavily on this helpful post from JamieLinux to set up my CAs – my root CA jail is protected on an encrypted ZFS dataset with the encryption key on a thumb drive stored in a lockbox. The root CA spends almost all of its time shut down, so it can’t be compromised. All the work is actually done by my intermediate CA, which hopefully explains some of the file paths you’ll see below.
  1. From the jailhost, install OpenLDAP, SASL, and OpenSSL:

    # pkg -j ldapjail install openldap26-server cyrus-sasl openssl
  2. Grab a prompt over on the intermdiate CA and create a certificate for the LDAP jail:
    # cd /root/ca
    # openssl genrsa -out intermediate/private/ldapjail.mydomain.com.key.pem 2048
    # openssl req -config intermediate/openssl.cnf -key intermediate/private/ldapjail.mydomain.com.key.pem -new -sha256 -out intermediate/csr/ldapjail.mydomain.com.csr.pem
    # openssl ca -config intermediate/openssl.cnf -extensions server_cert -days 375 -notext -md sha256 -in intermediate/csr/ldapjail.mydomain.com.csr.pem -out intermediate/certs/ldapjail.mydomain.com.cert.pem
  3. Verify the CA chain of trust:
    # openssl verify -CAfile intermediate/certs/ca-chain.cert.pem intermediate/certs/ldapjail.mydomain.com.cert.pem
    ldapjail.mydomain.com.cert.pem: OK
  4. Copy the root and intermediate CA public certs into the the ldapjail at /usr/share/certs/trusted/, copy ldapjail.mydomain.com.cert.pem into the ldapjail at /usr/local/etc/openldap/, and copy the private key into the ldapjail at /usr/local/etc/openldap/private/
  5. From the jailhost, log in to the LDAP jail: # jexec -l ldapjail login -f root
  6. Set up some permissions and get the certificates hashed into the system:
    # chown root:ldap /usr/local/etc/openldap/ldapjail.mydomain.com.cert.pem && chmod 644 /usr/local/etc/openldap/ldapjail.mydomain.com.cert.pem
    # chown root:ldap /usr/local/etc/openldap/private && chmod 710 /usr/local/etc/openldap/private
    # chown root:ldap /usr/local/etc/openldap/private/ldapjail.mydomain.com.key.pem && chmod 640 /usr/local/etc/openldap/private/ldapjail.mydomain.com.key.pem
    # certctl rehash
    # openssl rehash /usr/local/etc/openssl/
  7. Perform initial configuration of slapd.ldif:
    • The FreeBSD handbook chapter 32.5 describes how to set up OpenLDAP, but it lacks sufficient detail. It doesn’t describe how to set up ACLs that map the UID/GID information supplied when LDAP is accessed over Unix sockets with ldapi:/// and SASL. This mapping is necessary to allow the root user to be mapped to the global config admin in OpenLDAP – this is highly convenient in that it allows login-free access to the LDAP instance for setting up the schema and getting things ready for adding a database. The sample slapd.ldif that ships with the package install of OpenLDAP also omits this detail.
    • The Ubuntu OpenLDAP package has a much more full-featured slapd.ldif template to work from, so I swiped their template .ldif file and edited it to create the following starting point.
    • All SHA-2 password hashes were created in advance on the command line using:
      # slappasswd -o module-load=pw-sha2 -h '{SSHA512}' -s 'your-password-here'
    • Prepare the folder for physical storage of the LDAP database. If it doesn’t exist, mkdir /var/db/openldap-data
      # chown ldap:ldap /var/db/openldap-data && chmod 700 /var/db/openldap-data
    • Here’s the slapd.ldif file I created to get started. Edit yours as appropriate (e.g. certificate names, password hashes, etc):
    /usr/local/etc/openldap/slapd.ldif
    Code:
    # Global config
    dn: cn=config
    objectClass: olcGlobal
    cn: config
    olcArgsFile: /var/run/openldap/slapd.args
    olcPidFile: /var/run/openldap/slapd.pid
    olcTLSCertificateFile: /usr/local/etc/openldap/ldapjail.mydomain.com.cert.pem
    olcTLSCertificateKeyFile: /usr/local/etc/openldap/private/ldapjail.mydomain.com.key.pem
    olcTLSCACertificateFile: /usr/share/certs/trusted/intermediateca.mydomain.com.cert.pem
    olcTLSProtocolMin: 3.1
    olcTLSVerifyClient: never
    olcLogLevel: none
    # The tool-threads parameter sets the actual amount of cpu's that are used for indexing
    olcToolThreads: 1
    
    # Frontend settings – straight out of the Ubuntu template
    # The key addition is the ACL entry that allows the system root to connect with elevated
    #   privilages via the frontend – handbook and pkg config example don’t show how to do
    #   this. SASL converts the root uid+gid into a DN with a specific format when, for
    #   example, ldapadd is used from the local system prompt with the -Y EXTERNAL flag.
    dn: olcDatabase={-1}frontend,cn=config
    objectClass: olcDatabaseConfig
    objectClass: olcFrontendConfig
    olcDatabase: {-1}frontend
    # The maximum number of entries that is returned for a search operation
    olcSizeLimit: 500
    # Allow unlimited access to local connection from the local root user
    olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
    # Allow unauthenticated read access for schema and base DN autodiscovery
    olcAccess: {1}to dn.exact="" by * read
    olcAccess: {2}to dn.base="cn=Subschema" by * read
    
    # Config db settings – this is the global LDAP config database and doesn’t contain any
    #   objects we care about during day-to-day use. It does contain schema, attribute
    #   definitions, etc, so we need to allow admin access to the system root user. We don’t
    #   need an olcRootPW here because the system root is the only account that can access
    #   this (default deny policy only superceded by the ACL below)
    dn: olcDatabase=config,cn=config
    objectClass: olcDatabaseConfig
    olcDatabase: config
    # Allow unlimited access to local connection from the local root user
    olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
    olcRootDN: cn=admin,cn=config
    
    # Load schemas
    dn: cn=schema,cn=config
    objectClass: olcSchemaConfig
    cn: schema
    
    # The sample that ships with pkg doesn’t include anything but core. There are useful
    #   attribute definitions in the other schema, so we include those as well.
    include: file:///usr/local/etc/openldap/schema/core.ldif
    include: file:///usr/local/etc/openldap/schema/cosine.ldif
    include: file:///usr/local/etc/openldap/schema/nis.ldif
    include: file:///usr/local/etc/openldap/schema/inetorgperson.ldif
    
    # Load modules. We’ve added the pw-sha2 so we can use SHA-2 for hashing passwords
    dn: cn=module{0},cn=config
    objectClass: olcModuleList
    cn: module{0}
    olcModulePath: /usr/local/libexec/openldap
    olcModuleLoad: back_mdb.la
    olcModuleLoad: pw-sha2.la
    
    # The database definitions:
    # olcDbMaxSize sets a max 1 GiB database for compatibility with 32-bit systems.
    #     Seems more than big enough for homelab needs
    # olcDbCheckpoint forces a checkpoint periodically in case of system failure and to speed shutdown.
    # olcLastMod saves the time that the entry gets modified
    # olcSuffix is the root of the directory tree
    # olcDbDirectory is the physical storage location for the database. This folder should have ownership
    #    ldap:ldap with mode 700. It MUST exist prior to running slapd.
    # olcRootDN names the DATABASE administrator, NOT the GLOBAL admin – the database
    #   admin will not be able to change global config. Use the host root account authenticated via SASL
    #   for all config database changes.
    # olcRootPW has the password hash for this acount. Password is hashed using:
    #     slappasswd -o module-load=pw-sha2 -h '{SSHA512}' -s 'your-password-here'
    #      copy the result into this file, eg olcRootPW: {SSHA512}++Redacted==
    dn: olcDatabase=mdb,cn=config
    objectClass: olcDatabaseConfig
    objectClass: olcMdbConfig
    olcDatabase: mdb
    olcDbMaxSize: 1073741824
    olcDbCheckpoint: 512 30
    olcLastMod: TRUE
    olcSuffix: dc=mydomain,dc=com
    olcDbDirectory: /var/db/openldap-data
    # Database superuser credentials
    olcRootDN: cn=admin,dc=mydomain,dc=com
    olcRootPW: {SSHA512}++Redacted==
    # Indexing options for database #1
    olcDbIndex: objectClass eq
    olcDbIndex: cn,uid eq
    olcDbIndex: uidNumber,gidNumber eq
    olcDbIndex: member,memberUid eq
    # ACLs for accessing the database will be added later. cn=admin already has access by default
 

Service deployment update: OpenLDAP + Kerberos is up and running (2 of 4)​

OK, the jail is prepped and ready, so it’s time to get LDAP up and running. Run the following two commands to create the initial LDAP database:
# mkdir /usr/local/etc/openldap/slapd.d
# /usr/local/sbin/slapadd -n0 -F /usr/local/etc/openldap/slapd.d/ -l /usr/local/etc/openldap/slapd.ldif
Edit /etc/rc.conf to enable LDAP:
/etc/rc.conf
Code:
slapd_enable="YES"
slapd_flags='-h "ldapi://%2fvar%2frun%2fopenldap%2fldapi/ ldap://0.0.0.0/ ldaps://0.0.0.0/"'
slapd_sockets="/var/run/openldap/ldapi"
slapd_cn_config="YES"
And now start LDAP:
# service slapd start
Run a test query to confirm the initial database is online:
# ldapsearch -x -b '' -s base '(objectclass=*)' namingContexts
This spits back a screenful of stuff, and buried in the middle is namingContexts: dc=mydomain,dc=com, which tells me the initial slapd.ldif created the database I’d intended.
Next up is to make changes to the environment to enable the custom user and group schema, as well as set things up for integration of MIT Kerberos.
  1. Most of this will be accomplished using a series of .ldif files and the ldapadd or ldapmodify commands. Create a folder to hold all the .ldif files:
    # mkdir /usr/local/etc/openldap/custom
  2. Scroll through the rest of this section and copy the files into /usr/local/etc/openldap/custom/. Use your favorite text editor to make appropriate modifications for your setup. Once you have everything ready… double-check each file for correctness. Then check them all again.
  3. Move into the custom directory so you don’t have to type a bunch of bothersome paths when referencing filenames:
    # cd /usr/local/etc/openldap/custom
  4. Now apply the .ldif files to modify the environment. Some of these commands will inherit the system root user’s UID/GID as passed through the SASL layer and mapped via ACL to the global config user. These commands (with -Y EXTERNAL) don’t require you to authenticate with a password. Commands that create/modify entities in the base directory (dc=mydomain,dc=com) are run using the database admin’s credentials, so you’ll need that password. Hopefully you wrote it down when you created the hash for it before finalizing slapd.ldif. In order, do the following:
    1. Install memberOf so we can associate domainUsers with domainGroups:
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 01.config.InstallMemberOf.ldif
      Contents of 01.config.InstallMemberOf.ldif:
      Code:
      dn: cn=module{0},cn=config
      changetype: modify
      add: olcModuleLoad
      olcModuleLoad: memberof.la
    2. Modify the schema to add the sshPublicKey attribute:
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 02.schema.attribute.sshPublicKey.ldif
      Contents of 02.schema.attribute.sshPublicKey.ldif:
      Code:
      dn: cn=sshPublicKey,cn=schema,cn=config
      objectClass: olcSchemaConfig
      cn: attribute.sshPublicKey
      #
      # LDAP Public Key Patch schema for use with openssh-ldappubkey
      #                              useful with PKA-LDAP also
      #
      # Adjusted: Dennis Leeuw <dleeuw@made-it.com>
      #           Making the uid a MUST, but the sshPublicKey a MAY
      #           so we can add the objectClass and later add the key
      #
      # Author: Eric AUGE <eau@phear.org>
      # 
      # Based on the proposal of : Mark Ruijter
      #
      # octetString SYNTAX
      olcAttributeTypes: ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' 
        DESC 'MANDATORY: OpenSSH Public key'
        EQUALITY octetStringMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
      # printableString SYNTAX yes|no
      olcObjectClasses: ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
        DESC 'MANDATORY: OpenSSH LPK objectclass'
        MUST uid
        MAY sshPublicKey
        )
    3. Modify the schema to create a sudo role we can apply later:
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 03.schema.sudo.ldif
      Contents of 03.schema.sudo.ldif:
      Code:
      dn: cn=sudo,cn=schema,cn=config
      objectClass: olcSchemaConfig
      cn: sudo
      olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.1 NAME 'sudoUser' DESC 'User(s) who may  run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
      olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.2 NAME 'sudoHost' DESC 'Host(s) who may run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
      olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.3 NAME 'sudoCommand' DESC 'Command(s) to be executed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
      olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.4 NAME 'sudoRunAs' DESC 'User(s) impersonated by sudo (deprecated)' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
      olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.5 NAME 'sudoOption' DESC 'Options(s) followed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
      olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.6 NAME 'sudoRunAsUser' DESC 'User(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
      olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.7 NAME 'sudoRunAsGroup' DESC 'Group(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
      olcObjectClasses: ( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL DESC 'Sudoer Entries' MUST ( cn ) MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ description ) )
    4. Modify the schema to create a domainAccount type to use as the base for all accounts. If you registered for your own OID, replace surfrock66’s (1.3.6.1.4.1.57470.2.2.1) with your own 5-digit OID:
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 04.schema.objectClass.domainAccount.ldif
      Contents of 04.schema.objectClass.domainAccount.ldif:
      Code:
      dn: cn=objectClass.domainAccount,cn=schema,cn=config
      objectClass: olcSchemaConfig
      cn: objectClass.domainAccount
      olcObjectClasses: ( 1.3.6.1.4.1.57470.2.2.1 NAME 'domainAccount'
        DESC 'A user/account/person in the organization'
        SUP top STRUCTURAL
        MUST ( cn $ name $ sn $ uid )
        MAY ( 
        audio $   businessCategory $ carLicense $ departmentNumber $ 
        description $ destinationIndicator $ displayName $ 
        employeeNumber $ employeeType $ facsimileTelephoneNumber $ 
        gecos $ gidNumber $ givenName $ homeDirectory $ homePhone $ 
        homePostalAddress $ initials $ internationaliSDNNumber $ 
        jpegPhoto $ l $ labeledURI $ loginShell $ 
        mail $ manager $ mobile $ o $ ou $ pager $ photo $ 
        physicalDeliveryOfficeName $ postalAddress $ postalCode $ 
        postOfficeBox $ preferredDeliveryMethod $ 
        preferredLanguage $ registeredAddress $ roomNumber $ 
        secretary $ seeAlso $ shadowExpire $ shadowInactive $ 
        shadowLastChange $ shadowMax $ shadowMin $ shadowWarning $ 
        sshPublicKey $ st $ street $ telephoneNumber $ 
        teletexTerminalIdentifier $ telexNumber $ title $ 
        uidNumber $ userCertificate $ userPassword $ userPKCS12 $ 
        userSMIMECertificate $ x121Address $ x500uniqueIdentifier ) )
    5. Modify the schema to create a domainGroup type to use as the default type for groups. Again, change the OID if you have your own.
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 05.schema.objectClass.domainGroup.ldif
      Contents of 05.schema.objectClass.domainGroup.ldif:
      Code:
      dn: cn=objectClass.domainGroup,cn=schema,cn=config
      objectClass: olcSchemaConfig
      cn: objectClass.domainGroup
      olcObjectClasses: ( 1.3.6.1.4.1.57470.2.2.2 NAME 'domainGroup'
        DESC 'A group of names in the organization'
        SUP top STRUCTURAL
        MUST ( cn )
        MAY ( 
        businessCategory $ description $ gidNumber $ member $
        o $ ou $ owner $ seeAlso ) )
    6. Modify memberOf to support domainGroups and enforce referential integrity:
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 06.config.EnableMemberOf.ldif
      Contents of 06.config.EnableMemberOf.ldif:
      Code:
      dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
      objectClass: olcOverlayConfig
      objectClass: olcMemberOf
      olcOverlay: memberof
      olcMemberOfRefint: TRUE
      olcMemberOfGroupOC: domainGroup
    7. Create the top-level DN for the directory, and then create 3 OUs for general use – maybe more will get added later, but for now we need accounts, groups, and hosts. This command requires authentication with the DB admin (NOT the global config admin):
      # ldapadd -x -D 'cn=admin,dc=mydomain,dc=com' -W -H ldapi:/// -f 07.ous.ldif
      Contents of 07.ous.ldif:
      Code:
      dn: dc=mydomain,dc=com
      objectClass: dcObject
      objectClass: organization
      o: MyDomain
      dc: mydomain
      
      dn: ou=accounts,dc=mydomain,dc=com
      objectClass: organizationalUnit
      ou: accounts
      
      dn: ou=groups,dc=mydomain,dc=com
      objectClass: organizationalUnit
      ou: groups
      
      dn: ou=hosts,dc=mydomain,dc=com
      objectClass: organizationalUnit
      ou: hosts
    8. Create the LDAP bind user that will be used for most general queries.
      1. Create a simple password for this user. It’ll be stored unencrypted on all your hosts, so no need to make it too complex. Hash the password with the following:
        # slappasswd -o module-load=pw-sha2 -h '{SSHA512}' -s 'your-password-here'
      2. Use your favorite text editor to copy the password hash into 08.user.ldapbinduser.ldif.
      3. Use ldapadd to add the user to the directory:
        # ldapadd -x -D 'cn=admin,dc=mydomain,dc=com' -W -H ldapi:/// -f 08.user.ldapbinduser.ldif
      Contents of 08.user.ldapbinduser.ldif:
      Code:
      version: 1
      
      dn: uid=ldapbinduser,ou=accounts,dc=mydomain,dc=com
      cn: ldapbinduser
      description: LDAP Bind User
      displayname: ldapbinduser
      gecos: ldapbinduser
      gidnumber: 10001
      homedirectory: /nonexistent
      loginshell: No Login
      name: ldapbinduser
      objectclass: domainAccount
      objectclass: top
      ou: ou=accounts,dc=mydomain,dc=com
      sn: ldapbinduser
      uid: ldapbinduser
      uidnumber: 10001
      userPassword: {SSHA512}++Redacted==
      Run a query to check that everything has worked to this point:​
      # ldapsearch -x -D ‘cn=admin,dc=mydomain,dc=com’ -W -H ldapi:/// -b "dc=mydomain,dc=com" "(uid=ldapbinduser)"
      # extended LDIF
      #
      # LDAPv3
      # base <dc=mydomain,dc=com> with scope subtree
      # filter: (uid=ldapbinduser)
      # requesting: ALL
      #

      # ldapbinduser, accounts, mydomain.com
      dn: uid=ldapbinduser,ou=accounts,dc=mydomain,dc=com
      cn: ldapbinduser
      description: LDAP Bind User
      displayName: ldapbinduser
      gecos: ldapbinduser
      gidNumber: 10001
      homeDirectory: /nonexistent
      loginShell: No Login
      name: ldapbinduser
      objectClass: domainAccount
      objectClass: top
      ou: ou=accounts,dc=mydomain,dc=com
      sn: ldapbinduser
      uid: ldapbinduser
      uidNumber: 10001

      # search result
      search: 2
      result: 0 Success

      # numResponses: 2
      # numEntries: 1
    9. Create the Kerberos kdc-service account:
      1. Create a strong password for this user. Once everything is set up you won’t need to type this password anywhere, but you should archive a copy of the password in case you should ever need it. Hash the password with the following:
        # slappasswd -o module-load=pw-sha2 -h '{SSHA512}' -s 'your-password-here'
      2. Use your favorite text editor to copy the password hash into 09.user.kdc-service.ldif.
      3. Use ldapadd to add the user to the directory:
        # ldapadd -x -D 'cn=admin,dc=mydomain,dc=com' -W -H ldapi:/// -f 09.user.kdc-service.ldif
      Contents of 09.user.kdc-service.ldif:
      Code:
      version: 1
      
      dn: uid=kdc-service,ou=accounts,dc=mydomain,dc=com
      objectClass: domainAccount
      objectClass: simpleSecurityObject
      objectClass: top
      cn: kdc-service
      name: kdc-service
      sn: kdc-service
      uid: kdc-service
      userPassword: {SSHA512}++Redacted==
      description: Account used for the Kerberos KDC
    10. Create the Kerberos kadmin-service account:
      1. Create a strong password for this user. Once everything is set up you won’t need to type this password anywhere, but you should archive a copy of the password in case you should ever need it. Hash the password with the following:
        # slappasswd -o module-load=pw-sha2 -h '{SSHA512}' -s 'your-password-here'
      2. Use your favorite text editor to copy the password hash into 10.user.kadmin-service.ldif.
      3. Use ldapadd to add the user to the directory:
        # ldapadd -x -D 'cn=admin,dc=mydomain,dc=com' -W -H ldapi:/// -f 10.user.kadmin-service.ldif
      Contents of 10.user.kadmin-service.ldif:
      Code:
      version: 1
      
      dn: uid=kadmin-service,ou=accounts,dc=mydomain,dc=com
      objectClass: domainAccount
      objectClass: simpleSecurityObject
      objectClass: top
      cn: kadmin-service
      name: kadmin-service
      sn: kadmin-service
      uid: kadmin-service
      userPassword: {SSHA512}++Redacted==
      description: Account used for the Kerberos Admin server
    11. Create the first real user account. This user will be part of the sudo group network-wide, and the DN is baked into other ldif files.
      1. Create a weak and easy-to-remember password for this user. Once Kerberos is integrated this user will authenticate using passthru authentication to the KDC- in other words, the password you choose now will get deleted once the KDC is integrated, so what you choose now really doesn’t matter. Hash the password with the following:
        # slappasswd -o module-load=pw-sha2 -h '{SSHA512}' -s 'your-password-here'
      2. Use your favorite text editor to copy the password hash into 11.user.firstuser.ldif.
      3. Use ldapadd to add the user to the directory:
        # ldapadd -x -D 'cn=admin,dc=mydomain,dc=com' -W -H ldapi:/// -f 11.user.firstuser.ldif
      Contents of 11.user.firstuser.ldif:
      Code:
      version: 1
      
      dn: uid=firstuser,ou=accounts,dc=mydomain,dc=com
      cn: Full Name
      displayname: firstuser
      gecos: firstuser
      gidnumber: 10002
      givenname: First Name
      homedirectory: /home/firstuser
      initials:  Redacted
      l: City or Town
      loginshell: /bin/sh
      mail:  Redacted
      mobile:  Redacted
      name: First Name
      o: Organization, eg MyDomain
      objectclass: domainAccount
      objectclass: top
      ou: ou=accounts,dc=mydomain,dc=com
      postalcode:  Redacted
      preferredlanguage: English
      sn: Last Name
      st: state or province
      street: number and street
      telephonenumber:  Redacted
      uid: firstuser
      uidnumber: 10002
      userPassword: ++Redacted=={SSHA512}
    12. The database was configured without any access rules, which means only cn=admin can perform operations against it. Add some rules that make sense. Note that some DNs are baked in to the rules, so edit this file so that the names match exactly with what’s been implemented.
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 12.access.permissions.ldif
      Contents of 12.access.permissions.ldif:
      Code:
      dn: olcDatabase={1}mdb,cn=config
      changetype: modify
      replace: olcAccess
      olcAccess: {0}to *
        by dn="cn=admin,dc=mydomain,dc=com" manage
        by dn="uid=firstuser,ou=accounts,dc=mydomain,dc=com" manage
        by dn="uid=ldapbinduser,ou=accounts,dc=mydomain,dc=com" read
        by dn="uid=kdc-service,ou=accounts,dc=mydomain,dc=com" read
        by dn="uid=kadmin-service,ou=accounts,dc=mydomain,dc=com" write
        by * break
      -
      add: olcAccess
      olcAccess: {1}to dn.children="ou=accounts,dc=mydomain,dc=com" attrs=userPassword,shadowExpire,shadowInactive,shadowLastChange,shadowMax,shadowMin,shadowWarning
        by self write
        by anonymous auth
      -
      add: olcAccess
      olcAccess: {2}to dn.subtree="dc=mydomain,dc=com"
        by self read
      Verify that the rules look correct by running a query:​
      # ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config '(olcAccess=*)' olcAccess olcSuffix
      dn: olcDatabase={-1}frontend,cn=config
      olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external
      ,cn=auth manage by * break
      olcAccess: {1}to dn.exact="" by * read
      olcAccess: {2}to dn.base="cn=Subschema" by * read

      dn: olcDatabase={0}config,cn=config
      olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break

      dn: olcDatabase={1}mdb,cn=config
      olcSuffix: dc=mydomain,dc=com
      olcAccess: {0}to * by dn="cn=admin,dc=mydomain,dc=com" manage by dn="uid=firstuser,ou=accounts,dc=mydomain,dc=com" manage by dn="uid=ldapbinduser,ou=accounts,dc=mydomain,dc=com" read by dn="uid=kdc-service,ou=accounts,dc=dc=mydomain,dc=com" read by dn="uid=kadmin-service,ou=accounts,dc=mydomain,dc=com" write by * break
      olcAccess: {1}to dn.children="ou=accounts,dc=mydomain,dc=com" attrs=userPassword,shadowExpire,shadowInactive,shadowLastChange,shadowMax,shadowMin,
      shadowWarning by self write by anonymous auth
      olcAccess: {2}to dn.subtree="dc=mydomain,dc=com" by self read
    13. Force-enable TLS
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 13.config.forceTLS.ldif
      Contents of 13.config.forceTLS.ldif:
      Code:
      dn: olcDatabase={0}config,cn=config
      changetype: modify
      add: olcSecurity
      olcSecurity: ssf=71
      -
      
      dn: olcDatabase={1}mdb,cn=config
      changetype: modify
      add: olcSecurity
      olcSecurity: ssf=71
    14. Restart LDAP to get TLS up and running:
      # service slapd restart
    15. (Optional) Assign a password for the global config account. This would be useful if, for example, you plan on authenticating as the config user from a remote machine. I skipped this step, since it’s easy enough for me to get access to the jail using jexec and then use the already-configured root user mapping to act as cn=config.
      1. Create a strong password for this user and archive a copy of the password in case you should ever need it. Hash the password with the following:
        # slappasswd -o module-load=pw-sha2 -h '{SSHA512}' -s 'your-password-here'
      2. Use your favorite text editor to copy the password hash into 14.config.setConfigAdminPass.ldif.
      3. Use ldapadd to add the user to the directory:
        # ldapadd -Y EXTERNAL -H ldapi:/// -f 14.config.setConfigAdminPass.ldif
      Contents of 14.config.setConfigAdminPass.ldif:
      Code:
      dn: cn=config
      changetype: modify
      
      dn: olcDatabase={0}config,cn=config
      changetype: modify
      add: olcRootPW
      olcRootPW: {SSHA512}++Redacted==
    16. Add the Kerberos schema for OpenLDAP. I’ve included a copy here, but it’s just cut and pasted from the schema found in the MIT Kerberos source at /path/to/krb5/source/src/plugins/kdb/ldap/libkdb_ldap/kerberos.openldap.ldif.
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 15.kerberos.openldap.ldif
      The contents of the Kerberos schema add nothing to the readability of this post, so I’m skipping this boring bit and including it as a file attachment, instead.​
    17. Configure SASL regexps so that the SASL backend DNs (different format) are correctly mapped to LDAP DNs:
      # ldapadd -Y EXTERNAL -H ldapi:/// -f 16.config.SASLConfig.ldif
      Contents of 16.config.SASLConfig.ldif:
      Code:
      dn: cn=config
      changetype: modify
      add: olcSaslHost
      olcSaslHost: ldapjail.mydomain.com
      -
      add: olcSaslRealm
      olcSaslRealm: MYDOMAIN.COM
      -
      add: olcAuthzRegexp
      olcAuthzRegexp: {0}"uid=([^/]*),cn=mydomain.com,cn=GSSAPI,cn=auth" "uid=$1,ou=accounts,dc=mydomain,dc=com"
      -
      add: olcAuthzRegexp
      olcAuthzRegexp: {1}"uid=host/([^/]*).mydomain.com,cn=mydomain.com,cn=GSSAPI,cn=auth" "uid=$1,ou=hosts,dc=mydomain,dc=com"
      -
      add: olcAuthzRegexp
      olcAuthzRegexp: {2}"uid=ldap/config,cn=mydomain.com,cn=GSSAPI,cn=auth" "cn=config,dc=mydomain,dc=com"
    18. Create a SUDOers OU:
      # ldapadd -x -D 'cn=admin,dc=mydomain,dc=com' -W -H ldapi:/// -f 17.ou.sudoers.ldif
      Contents of 17.ou.sudoers.ldif:
      Code:
      dn: ou=SUDOers,dc=mydomain,dc=com
      objectclass: organizationalunit
      ou: SUDOers
      description: Users who can use Sudo
    19. Define SUDOers OU defaults:
      # ldapadd -x -D 'cn=admin,dc=mydomain,dc=com' -W -H ldapi:/// -f 18.sudorole.default.ldif
      Contents of 18.sudorole.default.ldif:
      Code:
      dn: cn=defaults,ou=SUDOers,dc=mydomain,dc=com
      objectClass: top
      objectClass: sudoRole
      cn: defaults
      description: Default Sudo Options
      sudoOption: !visiblepw
      sudoOption: always_set_home
      sudoOption: match_group_by_gid
      sudoOption: always_query_group_plugin
      sudoOption: env_reset
      sudoOption: env_keep =  "COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS"
      sudoOption: env_keep += "MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE"
      sudoOption: env_keep += "LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES"
      sudoOption: env_keep += "LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE"
      sudoOption: env_keep += "LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY"
      sudoOption: env_keep+=SSH_AUTH_SOCK
      sudoOption: secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
    20. Add the sudo role under the SUDOers OU:
      # ldapadd -x -D 'cn=config,dc=mydomain,dc=com' -W -H ldapi:/// -f 19.sudorole.sudo.ldif
      Contents of 19.sudorole.sudo.ldif:
      Code:
      dn: cn=sudo,ou=SUDOers,dc=mydomain,dc=com
      objectClass: top
      objectClass: sudoRole
      cn: sudo
      sudoHost: ALL
      sudoRunAsUser: ALL
      sudoCommand: ALL
    21. Add the firstuser account to the sudo group:
      # ldapadd -x -D 'cn=config,dc=mydomain,dc=com' -W -H ldapi:/// -f 20.sudouser.firstuser.ldif
      Contents of 20.sudouser.firstuser.ldif:
      Code:
      dn: cn=sudo,ou=SUDOers,dc=mydomain,dc=com
      changetype: modify
      add: sudoUser
      sudoUser: firstuser
    22. Set up indexing for common queries run against the Kerberos container
      # ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f 21.kerberos.indexing.ldif
      Contents of 21.kerberos.indexing.ldif:
      Code:
      dn: olcDatabase={1}mdb,cn=config
      add: olcDbIndex
      olcDbIndex: krbPrincipalName eq,pres,sub
    23. Add ACLs for Kerberos services and principals:
      # ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f 22.kerberos.ACLs.ldif
      Contents of 22.kerberos.ACLs.ldif:
      Code:
      dn: olcDatabase={1}mdb,cn=config
      add: olcAccess
      olcAccess: {3}to attrs=krbPrincipalKey
        by anonymous auth
        by dn.exact="uid=kdc-service,ou=accounts,dc=mydomain,dc=com" read
        by dn.exact="uid=kadmin-service,ou=accounts,dc=mydomain,dc=com" write
        by self write
        by * none
      -
      add: olcAccess
      olcAccess: {4}to dn.subtree="cn=krbContainer,dc=mydomain,dc=com"
        by dn.exact="uid=kdc-service,ou=accounts,dc=mydomain,dc=com" read
        by dn.exact="uid=kadmin-service,ou=accounts,dc=mydomain,dc=com" write
        by * none
 

Attachments

Service deployment update: OpenLDAP + Kerberos is up and running (3 of 4)​

The LDAP side is done. Now we need to get the remaining packages installed on the jail. Exit out of the jail, and on the jailhost install the required software:
# pkg -j ldapjail install krb5-ldap cyrus-sasl-gssapi cyrus-sasl-saslauthd
Now head back into the jail to continue configuring:
# jexec -l ldapjail login -f root
There are several files we want to have pre-configured before attempting to start Kerberos: both the kdc and kadmin services need their config files set up, and there’s a bit of LDAP and SASL configuration, too. Oh, and can’t forget the DNS entries… Diving in:
  1. Use your favorite text editor to create /etc/krb5.conf. I prefer vim. You’ll notice duplicate entries for kdc under the MYDOMAIN.COM realm – that’s because I plan on having a backup LDAP+KDC jail on a separate machine (soon), so I’m just holding its place in line from the start:
    # vim /etc/krb5.conf
    Contents of /etc/krb5.conf:
    Code:
    [libdefaults]
        dns_lookup_kdc = true
        default_realm = MYDOMAIN.COM
        forwardable = true
    [logging]
        kdc = SYSLOG:DEBUG
        admin_server = SYSLOG:DEBUG
        default = SYSLOG:DEBUG
        kdc = FILE:/var/log/krb5kdc/kdc.log
        admin_server = FILE:/var/log/krb5kdc/kadmin.log
        default = FILE:/var/log/krb5kdc/krb5lib.log
    [realms]
        MYDOMAIN.COM = {
            kdc = ldapjail.mydomain.com
            kdc = ldapjail2.mydomain.com
            admin_server = ldapjail.mydomain.com
            default_domain = mydomain.com
            database_module = openldap_ldapconf
        }
    [domain_realm]
        .mydomain.com = MYDOMAIN.COM
        mydomain.com = MYDOMAIN.COM
    [dbdefaults]
        ldap_kerberos_container_dn = cn=krbContainer,dc=mydomain,dc=com
    [dbmodules]
        openldap_ldapconf = {
            db_library = kldap
            disable_last_success = true
            disable_lockout = true
            acl_file = /usr/local/var/krb5kdc/kadm5.acl
            key_stash_file = /usr/local/var/krb5kdc/stash
            ldap_kdc_dn = "uid=kdc-service,ou=accounts,dc=mydomain,dc=com"
            ldap_kadmin_dn = "uid=kadmin-service,ou=accounts,dc=mydomain,dc=com"
            ldap_service_password_file = /usr/local/var/krb5kdc/service.keyfile
            ldap_servers = ldapi:///
            ldap_conns_per_server = 5
        }
  2. Also needed is /usr/local/var/krb5kdc/kdc.conf.
    1. If it doesn’t already exist, create the folder:
      # mkdir /usr/local/var/krb5kdc
    2. Now create the kdc.conf file:
      # vim /usr/local/var/krb5kdc/kdc.conf
    3. Contents of /usr/local/var/krb5kdc/kdc.conf[:
      Code:
      [kdcdefaults]    kdc_listen = 88    kdc_tcp_listen = 88[realms]    MYDOMAIN.COM = {        kadmind_port = 749        max_life = 12h 0m 0s        max_renewable_life = 7d 0h 0m 0s        database_module = openldap_ldapconf    }[logging]    kdc = FILE:/var/log/krb5kdc/kdc.log    admin_server = FILE:/var/log/krb5kdc/kadmin.log    default = FILE:/var/log/krb5kdc/krb5lib.log[dbdefaults]    ldap_kerberos_container_dn = cn=krbContainer,dc=mydomain,dc=com[dbmodules]    openldap_ldapconf = {        db_library = kldap        disable_last_success = true        ldap_kdc_dn = "uid=kdc-service,ou=accounts,dc=mydomain,dc=com"        ldap_kadmind_dn = "uid=kadmin-service,ou=accounts,dc=mydomain,dc=com"        ldap_service_password_file = /usr/local/var/krb5kdc/service.keyfile        ldap_servers = ldapi:///        ldap_conns_per_server = 5    }
  3. Create the directory for the logfiles:
    # mkdir /var/log/krb5kdc
  4. Forgot to do this earlier – set up logging for LDAP. Create a file /etc/syslog.d/slapd.conf with contents: *.* /var/log/slapd.log. Restart syslog afterwards: service syslogd restart
  5. Set up access control for the kadmin service in /usr/local/var/krb5kdc/kadm5.acl:
    # vim /usr/local/var/krb5kdc/kadm5.acl
    Contents of /usr/local/var/krb5kdc/kadm5.acl:
    Code:
    */admin@MYDOMAIN.COM    *
    firstuser@MYDOMAIN.COM    *
  6. Configure the local LDAP environment to use the appropriate SASL mechanism/realm/base. This config goes in /usr/local/etc/openldap/ldap.conf:
    # vim /usr/local/etc/openldap/ldap.conf
    Contents of /usr/local/etc/openldap/ldap.conf:
    Code:
    TLS_CACERTDIR    /usr/share/certs/trusted/
    SASL_MECH        GSSAPI
    SASL_REALM    MYDOMAIN.COM
    BASE    dc=mydomain,dc=com
  7. Further configure SASL to tell it how to interact with LDAP. This goes in the (confusingly named!) file /usr/local/lib/sasl2/slapd.conf:
    # vim /usr/local/lib/sasl2/slapd.conf
    Contents of /usr/local/lib/sasl2/slapd.conf:
    Code:
    mech_list: GSSAPI PLAIN
    keytab: /etc/krb5.keytab
    pwcheck_method: saslauthd
    saslauthd_path: /var/run/saslauthd/mux
  8. Configure some ownership/permissions:
  9. Set up DNS SRV records on the primary BIND server. Don’t forget to increment the zone file serial number! Run rndc reload to load the updated zone when finished. Remember to reload the secondary if you have one...
    # vim /path/to/db.mydomain
    Additions to db.mydomain:
    Code:
    ;
    ; Add Kerberos SRV records
    ;
    _kerberos._udp        IN    SRV    01 00 88    ldapjail.mydomain.com.
    _kerberos._udp        IN    SRV    02 00 88    ldapjail2.mydomain.com.
    _kerberos._tcp        IN    SRV    01 00 88    ldapjail.mydomain.com.
    _kerberos._tcp        IN    SRV    02 00 88    ldapjail2.mydomain.com.
    _kpasswd._udp        IN    SRV    01 00 464    ldapjail.mydomain.com.
    _kerberos-adm._tcp    IN    SRV    01 00 749    ldapjail.mydomain.com.
    _kerberos        IN    TXT    MYDOMAIN.COM
    
    ;
    ; LDAP SRV records
    ;
    _ldap._tcp        IN    SRV    01 00 389    ldapjail.mydomain.com.
    _ldap._tcp        IN    SRV    02 00 389    ldapjail2.mydomain.com.
    _ldaps._tcp        IN    SRV    01 00 636    ldapjail.mydomain.com.
    _ldaps._tcp        IN    SRV    02 00 636    ldapjail2.mydomain.com.
  10. Enable the kdc, kadmind, and saslauthd services in /etc/rc.conf. The _program directives tell the system to ignore the built-in Heimdal Kerberos and instead look elsewhere… point at the MIT binaries (only required for FreeBSD 14.X and earlier):
    # vim /etc/rc.conf
    Additions to /etc/rc.conf:
    Code:
    kdc_program="/usr/local/sbin/krb5kdc"
    kadmind_program="/usr/local/sbin/kadmind"
    kdc_flags=""
    kdc_enable="YES"
    kadmind_enable="YES"
    
    saslauthd_enable="YES"
    saslauthd_flags="-a kerberos5 -c -m /var/run/saslauthd"
OK, now we’re ready to start working with the KDC. Let’s get it configured to use an LDAP backend and establish the principals, appropriate file permissions (ldap needs access to the keytab), and initialize the ticket cache:
  1. Use kdb5_ldap_util to create the KDC container object in the LDAP databse: you’ll be prompted for a KDC database master password. DONT LOSE IT.
    # kdb5_ldap_util -D cn=admin,dc=mydomain,dc=com create -subtrees dc=mydomain,dc=com -r MYDOMAIN.COM -sscope SUB -s -H ldapi:///
  2. Now create a password stash for binding to the LDAP server. Run it once for kdc-service and once for kadmin-service. Also stash the KDC master key:
    # kdb5_ldap_util -D cn=admin,dc=mydomain,dc=com stashsrvpw -f /usr/local/var/krb5kdc/service.keyfile uid=kdc-service,ou=accounts,dc=mydomain,dc=com
    # kdb5_ldap_util -D cn=admin,dc=mydomain,dc=com stashsrvpw -f /usr/local/var/krb5kdc/service.keyfile uid=kadmin-service,ou=accounts,dc=mydomain,dc=com
    # kdb5_util stash -f /usr/local/var/krb5kdc/stash
  3. Start the kdc and kadmind services:
    # service kdc start
    # service kadmind start
  4. Let’s do some work in kadmin. First, take control of kadmin/admin@MYDOMAIN.COM and set the password to be the same as the LDAP database admin password:
    # kadmin.local
    > cpw kadmin/admin
  5. Still in kadmin, add ldap and host principals for the jail. Permanently store keys for these in /etc/krb5.keytab:
    > addprinc -randkey [noparse]ldap/ldapjail.mydomain.com@MYDOMAIN.COM[/noparse]
    > ktadd -k /etc/krb5.keytab [noparse]ldap/ldapjail.mydomain.com@MYDOMAIN.COM[/noparse]
    > addprinc -randkey [noparse]host/ldapjail.mydomain.com@MYDOMAIN.COM[/noparse]
    > ktadd -k /etc/krb5.keytab [noparse]host/ldapjail.mydomain.com@MYDOMAIN.COM[/noparse]
    > exit
  6. Set ownership/permissions on /etc/krb5.keytab so the ldap user can make use of it
    # chown root:ldap /etc/krb5.keytab
    # chmod 660 /etc/krb5.keytab
  7. Also set ownership on /var/run/saslauthd/, which is by default created with mail servers in mind. The default ownership is root:mail, which doesn’t make sense here. The ldap user needs to be able to write to the Unix socket here.
    # chown root:ldap /var/run/saslauthd
  8. Restart Kerberos and LDAP, and start saslauthd
    # service slapd restart
    # service kdc restart
    # service kadmind restart
    # service saslauthd start
  9. CRITICAL: At this point you must run kinit to initialize the ticket cache. I’m fuzzy about why, but if you don’t do this exactly now, in exactly this way, things just won’t work out. Among my several (many) attempts, only on my last did I finally get this in the right order, in the correct part of the setup sequence, and it was smooth sailing after that.
    # kinit -k -t /etc/krb5.keytab
Allright, the KDC should now be ready to go – time to alter the firstuser account to use passthru authentication. Run a test first: query the firstuser entry using the LDAP credentials that were put in place when setting up the database:
# ldapsearch -x -D 'uid=firstuser,ou=accounts,dc=mydomain,dc=com' -W -b 'dc=mydomain,dc=com' -H ldapi:/// '(uid=firstuser)'
The entire firstuser entry is dumped to stdout, ending with the base64-encoded password hash and some statistics about the query​
Add the Kerberos principal associate with the firstuser LDAP entry:
# kadmin.local
> addprinc -x dn=uid=firstuser,ou=accounts,dc=mydomain,dc=com firstuser
Type the Kerberos password you’d like associated with this account, stronger and more permanent than the temporary weak password that’s hashed in the LDAP entry​
> exit
Run the previous query again. Remember to use the weak LDAP password to authenticate. Passthru hasn’t been enabled yet.
# ldapsearch -x -D 'uid=firstuser,ou=accounts,dc=mydomain,dc=com' -W -b 'dc=mydomain,dc=com' -H ldapi:/// '(uid=firstuser)'
The entire firstuser entry is dumped to stdout, but you’ll notice this time that there are some new attributes near the end of the record: krbPrincipalName, krbPrincipalKey, some krbExtraData lines, etc.​
Nearly there: the last step is to enable passthru authentication. Create a password change .ldif file to do this:
/usr/local/etc/openldap/custom/23.firstuser.passwordChange.ldif
Code:
dn: uid=firstuser,ou=accounts,dc=mydomain,dc=com
changetype: modify
replace: userPassword
userPassword: {SASL}firstuser@MYDOMAIN.COM
The {SASL} block tells OpenLDAP to run any authentication requests for this user through the SASL layer, which has been configured to use the GSSAPI method to query the KDC. Apply the password change with the following (authenticating as the database admin):
# cd /usr/local/etc/openldap/custom/
# ldapmodify -x -D ‘cn=admin,dc=mydomain,dc=com’ -W -H ldapi:/// -f 23.firstuser.passwordChange.ldif
And run the same query a 3rd time: try using firstuser’s old LDAP password first:
# ldapsearch -x -D 'uid=firstuser,ou=accounts,dc=mydomain,dc=com' -W -b 'dc=mydomain,dc=com' -H ldapi:/// '(uid=firstuser)'[/INDENT][/INDENT][/INDENT]
[INDENT][INDENT][INDENT][indent=2][cmd]ldap_bind: Invalid credentials (49)
As expected, since we changed the password the old weak one doesn’t work. Try it again with the new Kerberos password that was just added. If everything has been set up correctly, the result will be the expected dump of the entire firstuser record to stdout.
Take a look at # klist: there should be two tickets listed. The first is the ticket granting ticket from the KDC, called krbtgt, and the second is a ticket for ldap/ldapjail.mydomain.com@MYDOMAIN.COM. There’s nothing there that looks like a firstuser ticket – the LDAP to SASL GSSAPI to Kerberos communication is happening behind the scenes, which is pretty slick. Try running the same query from a remote system, and you’ll find it still works - the only password in the system for the firstuser account is in Kerberos, and that works to authenticate the query:
remotehost# ldapsearch -x -D 'uid=firstuser,ou=accounts,dc=mydomain,dc=com' -W[/INDENT][/INDENT][/INDENT]
[INDENT][INDENT][INDENT]-H ldaps://ldapjail.mydomain.com -b "dc=mydomain,dc=com" "(uid=firstuser)"
And that’s it! Time to set up a workstation… I’m not going to include details for this, since I have a couple of different scenarios in mind already that will require different configs. I once again refer you to the excellent blog surfrock66 posted – he’s got the basics for onboarding hosts detailed. The above setup is almost all his… I just kept pounding on it until I figured out exactly how to translate to FreeBSD.

Part 4 of this post will come in a day or two, once I have a chance to put my notes in some sort of order. Right now that’s a mess I don’t want to tackle...

Feel free to PM me if you spot a typo… not all the text was copy/paste (I anonymized quite a bit), so if you spot something let me know and I’ll fix it.
 
Back
Top