LDAP authentication on Home Assistant
Last week I wrote a few sentences about a beautiful script I found, to authenticate against an LDAP server, which could be used e.g. on the Home Assistant, a platform to manage home automation and the like. We deployed a Home Assistant instance at work, to monitor temperatures in various rooms and fringes, and to raise notifications and alarms, should temperatures exceed certain thresholds. All team members should be able to log into the system, using their central login credentials from the LDAP server.
Unforeseen difficulties
The shell script uses either of the command line utilities ldapsearch
(from
the openldap-clients package) or curl
to make a request to the LDAP server,
which requires a valid username and password. Both scripts will return an error
code > 0 if something goes wrong; as usual, the exit code 0 will let us know if
the command worked and thus if the username/password combination was correct.
Further, the LDAP server can be queried for some extra attributes like the
displayName
or others, which can be mapped into the requesting system.
However, there was one issue I hadn't anticipated; neither ldapsearch
nor
curl
compiled with LDAP support was available on the Home Assistant.
There are plenty of ways to deploy Home Assistant. We had a spare Raspberry Pi and decided to use the HassOS distribution that is recommended when installing on a Pi. HassOS (the Home Assistant Operating System) is a minimalistic operating system that deploys the individual modules of Home Assistant as containers. The containers that are deployed are usually built on Alpine images. However, there were two problems:
- Software that I would install in any container would not be persistent but vanish on every re-boot.
- I couldn't even locate, let alone access the correct container that does the authentication.
Trial and error
As proof of concept, I installed an SSH integration that would at least let me communicate with parts of the Home Assistant system via ssh. The ssh container per default also mounts the config and other persistent directories of Home Assistant.
So I downloaded the ldap-auth.sh
script to the persistent config
folder and
started by adding the ldapsearch
tool, with apk add openldap-clients
and
configured ldap-auth.sh
until I was able to authenticate. I updated the Home
Assessment config with an auth_provider section like this:
homeassistant: auth_providers: - type: command_line command: /config/scripts/ldap-auth.sh meta: true - type: homeassistant
Beware! Do include type: homeassistant
in your list of auth providers or you
will lock yourself out of the system if the script does not work correctly (just
like I did).
After reloading the config, login with the command_line
type of course failed,
but I didn't find any logs that would propagate the error message, so I added
some echo
lines in the script manually, to find out that ldapsearch
cannot
be found by the authenticating container.
So I tried my luck with curl
; however I could not make any reasonable request
without the built-in LDAP support.
Build my custom curl
So I figured I basically had three possibilities:
- Using a different distribution of Home Assistant that I maybe would be able to control better
- request the feature of having
openldap-clients
baked into the container images, or build (and maintain) the image myself or - build
curl
for my target container with all the needed functions linked statically into one binary.
I assumed that all containers in the Pi's Home Assistant ecosystem would be the
same architecture, which is Alpine on aarch64
for the ssh container. So I
installed all dependencies I needed on the ssh container, cloned the curl repo
and started configuring, installing missing dependencies on the fly.
./configure --with-openssl --with-ldap --disable-shared
Choosing the ssl library is mandatory; --disable-shared
should prevent the use
of any shared library, so any dependency I had to install that would not be
available on the target machine later.
The built went through and I had an LDAP enabled curl
that I could test my
requests with, so again I tinkered with the ldap-auth.sh
script until it would
succeed.
However, when used from the web interface it would not work, again, this time complaining about missing dependencies, which I thought I had all included.
Checking the compiled binary I found 769.4K, so much bigger than my 199K system curl, so something must have been linked statically. Looking up shared object dependencies revealed what was missing:
[core-ssh ~]$ ldd curl /lib/ld-musl-aarch64.so.1 (0x7f930c0000) libssl.so.1.1 => /lib/libssl.so.1.1 (0x7f92f76000) libcrypto.so.1.1 => /lib/libcrypto.so.1.1 (0x7f92d26000) libldap.so.2 => /lib/libldap.so.2 (0x7f92cc1000) liblber.so.2 => /lib/liblber.so.2 (0x7f92ca3000) libc.musl-aarch64.so.1 => /lib/ld-musl-aarch64.so.1 (0x7f930c0000) libsasl2.so.3 => /lib/libsasl2.so.3 (0x7f92c79000)
While this is still a lot less dependencies than my system installed curl:
=> ldd `which curl` linux-vdso.so.1 (0x00007ffc8fdb6000) libcurl.so.4 => /usr/lib/libcurl.so.4 (0x00007fce55263000) libc.so.6 => /usr/lib/libc.so.6 (0x00007fce55057000) libnghttp2.so.14 => /usr/lib/libnghttp2.so.14 (0x00007fce5502c000) libidn2.so.0 => /usr/lib/libidn2.so.0 (0x00007fce5500a000) libssh2.so.1 => /usr/lib/libssh2.so.1 (0x00007fce54fc9000) libpsl.so.5 => /usr/lib/libpsl.so.5 (0x00007fce54fb6000) libssl.so.1.1 => /usr/lib/libssl.so.1.1 (0x00007fce54f1f000) libcrypto.so.1.1 => /usr/lib/libcrypto.so.1.1 (0x00007fce54c3f000) libgssapi_krb5.so.2 => /usr/lib/libgssapi_krb5.so.2 (0x00007fce54bea000) libzstd.so.1 => /usr/lib/libzstd.so.1 (0x00007fce54b41000) libbrotlidec.so.1 => /usr/lib/libbrotlidec.so.1 (0x00007fce54b33000) libz.so.1 => /usr/lib/libz.so.1 (0x00007fce54b19000) /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fce55380000) libunistring.so.2 => /usr/lib/libunistring.so.2 (0x00007fce5496b000) libkrb5.so.3 => /usr/lib/libkrb5.so.3 (0x00007fce54892000) libk5crypto.so.3 => /usr/lib/libk5crypto.so.3 (0x00007fce54862000) libcom_err.so.2 => /usr/lib/libcom_err.so.2 (0x00007fce5485c000) libkrb5support.so.0 => /usr/lib/libkrb5support.so.0 (0x00007fce5484d000) libkeyutils.so.1 => /usr/lib/libkeyutils.so.1 (0x00007fce54846000) libresolv.so.2 => /usr/lib/libresolv.so.2 (0x00007fce54831000) libbrotlicommon.so.1 => /usr/lib/libbrotlicommon.so.1 (0x00007fce5480e000)
there were still way too many shared libraries involved for my taste.
I even asked in #curl
in the libera net what I could have done wrong or
misunderstood.
14:57:34 schubisu | hi everyone! I'm trying to build a statically linked curl
| and configured with `--with-openssl --with-ldap --disable-shared`.
| However, when I run the binary on another machine it says
| it cannot find the shared libraries libldap and liblber. Did I
| misunderstand static linking?
15:27:25 bagder | static linking is a beast
Well, it was nice to hear that it may not have been entirely my fault :) bagder pointed me to Static curl, a github repository that builds static releases for multiple platforms (YAY), but sadly also with disabled LDAP support (AWWW). Running the build script with LDAP enabled also didn't run through.
An ugly hack to the rescue
Having spent way too much time on this issue, I went ahead with something that
may be an ugly hack, but it's also a "works for me": I had already copied the
statically linked curl in the persistent config
folder already, so I would
just add the missing libraries there as well.
I figured that from the 7 shared dependencies, 4 were available in the standard Alpine image anyway, so I was missing only three files:
- libldap.so.2
- liblber.so.2
- libsasl2.so.3
that I copied from my ssh container into the persistent storage. I adjusted the
ldap-auth.sh
script one last time to add one line:
export LD_LIBRARY_PATH="/config/scripts"
and that did the trick.
I also confirmed that on the fresh system after re-boot, everything is still in place and working beautifully :)