Let me set the stage. You’ve just installed RHEL7 on a workstation and applied a minimal base configuration but leave the SELinux policy untouched. You hand it off to the user and they immediately come back unable to log in. They say they’re able to “log in” but then they get kicked back to the display manager. Classic case of oddjob-mkhomedir failing.

So we check it out in the journal and find:

SELinux is preventing /usr/libexec/oddjob/mkhomedir from write access on the directory /home

Odd, right? You would think that would be the only thing it’s allowed to access. Let’s check out /home and see what’s up.

ls -ldZ /home
drwxr-xr-x. root root system_u:object_r:default_t:s0

So far no big deal, we’ll just clean up these contexts and everything will be just fine.

restorecon -R -v /home
...
ls -ldZ /home
drwxr-xr-x. root root system_u:object_r:default_t:s0

Now we’re in trouble. It’s crazy, but could there be an override?

semanage fcontext --list | home
/home/(.*/)?\.snapshots(/.*)?                      all files          system_u:object_r:snapperd_data_t:s0
/opt/NX/home(/.*)?                                 all files          system_u:object_r:nx_server_var_lib_t:s0
/usr/NX/home(/.*)?                                 all files          system_u:object_r:nx_server_var_lib_t:s0
/home/\.snapshots(/.*)?                            all files          system_u:object_r:snapperd_data_t:s0
/opt/NX/home/nx/\.ssh(/.*)?                        all files          system_u:object_r:nx_server_home_ssh_t:s0
/usr/NX/home/nx/\.ssh(/.*)?                        all files          system_u:object_r:nx_server_home_ssh_t:s0
/var/lib/nxserver/home/.ssh(/.*)?                  all files          system_u:object_r:nx_server_home_ssh_t:s0
/var/lib/containers/home(/.*)?                     all files          system_u:object_r:openshift_var_lib_t:s0
/var/lib/nxserver/home/\.xauth.*                   regular file       system_u:object_r:xauth_home_t:s0
/var/lib/nxserver/home/\.Xauthority.*              regular file       system_u:object_r:xauth_home_t:s0
/var/home = /home
/var/lib/xguest/home = /home

I’m pretty much out of ideas. Is there a way to ask SELinux if it thinks that default_t is the correct type for this file? After searching around the answer was yes.

# Available in libselinux-utils.
matchpathcon /home
/home   system_u:object_r:default_t:s0

So SELinux thinks that the correct type for this directory should be default_t. This has to be a bug. But I need some actual evidence that the policy is wrong or corrupted.

rpm -ql selinux-policy-targeted | grep home
/etc/selinux/targeted/active/homedir_template
/etc/selinux/targeted/contexts/files/file_contexts.homedirs
/etc/selinux/targeted/contexts/files/file_contexts.homedirs.bin

So I just try opening that .homedirs file.

less /etc/selinux/targeted/contexts/files/file_contexts.homedirs
#
#
# User-specific file contexts, generated via libsemanage
# use semanage command to manage system users to change the file_context
#
#


#
# Home Context for user user_u
#

/var/home/[^/]+/.+  unconfined_u:object_r:user_home_t:s0
/var/home/[^/]+/.maildir(/.*)?      unconfined_u:object_r:mail_home_rw_t:s0
/var/home/[^/]+/.*/plugins/nppdf\.so.*      --      unconfined_u:object_r:textrel_shlib_t:s0
/var/home/[^/]+/((www)|(web)|(public_html))(/.+)?   unconfined_u:object_r:httpd_user_content_t:s0
/var/home/[^/]+/((www)|(web)|(public_html))/cgi-bin(/.+)?   unconfined_u:object_r:httpd_user_script_exec_t:s0
/var/home/[^/]+/((www)|(web)|(public_html))(/.*)?/\.htaccess        --      unconfined_u:object_r:httpd_user_htaccess_t:s0
/var/home/[^/]+/((www)|(web)|(public_html))(/.*)?/logs(/.*)?        unconfined_u:object_r:httpd_user_ra_content_t:s0
/var/home/[^/]+/a?quota\.(user|group)       --      unconfined_u:object_r:quota_db_t:s0
...

/var/home? I mean that’s where we put our local users but SELinux doesn’t seem like it would know anything about that. Also, this file is generated? By what? Some more searching reveals that it’s /sbin/genhomedircon.

ls -l /sbin/genhomedircon
lrwxrwxrwx. 1 root root 8 Aug  2 06:52 "/sbin/genhomedircon" -> "semodule"*

Interesting, so semodule is one of those two-faced binaries that does different things depending on what you call it. Realistically, it has to be getting /var/home from some config file that we added, let’s just see what files it opens and go from there.

strace -e open /sbin/genhomedircon
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libsepol.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libsemanage.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libaudit.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libbz2.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libustr-1.0.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libcap-ng.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/etc/selinux/config", O_RDONLY)   = 3
open("/etc/selinux/semanage.conf", O_RDONLY) = 3
open("/etc/selinux/targeted/semanage.trans.LOCK", O_RDONLY) = 3
open("/etc/selinux/targeted/active/modules/100/abrt/cil", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/abrt/cil.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/abrt/hll", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/abrt/hll.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/abrt/lang_ext", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/abrt/lang_ext.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/accountsd/cil", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/accountsd/cil.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/accountsd/hll", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/accountsd/hll.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/accountsd/lang_ext", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/accountsd/lang_ext.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/acct/cil", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/acct/cil.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/acct/hll", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/acct/hll.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
open("/etc/selinux/targeted/active/modules/100/acct/lang_ext", O_RDONLY) = 4
open("/etc/selinux/targeted/tmp/modules/100/acct/lang_ext.tmp", O_WRONLY|O_CREAT|O_TRUNC, 0100644) = 5
...

Okay, it’s probably not anything we changed in /etc/selinux so let’s ignore those.

strace -e open /sbin/genhomedircon 2>&1 | grep -vF '/etc/selinux'
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libsepol.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libsemanage.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libaudit.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libbz2.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libustr-1.0.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libcap-ng.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/proc/meminfo", O_RDONLY|O_CLOEXEC) = 5
open("/etc/default/useradd", O_RDONLY)  = 5
open("/etc/libuser.conf", O_RDONLY)     = 5
open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 5
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5
open("/lib64/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 5
open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 5
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=19468, si_uid=0, si_status=0, si_utime=11, si_stime=0} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=19469, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=19470, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

One of these things is definitely not like the others: /etc/default/useradd. If you’re not familiar, this file is just a small bash-like file that’s sourced by useradd to set some default options.

On the surface this has nothing to do with SELinux. But apparently genhomedircon is using the HOME variable as the base directory for that .homedirs policy. Let’s change it and see what happens.

sed -i 's:/var/home:/home:' /etc/default/useradd
genhomedircon
restorecon -Rv /home
...
ls -ldZ /home
drwxr-xr-x. root root system_u:object_r:home_root_t:s0   /home

Boom!

I have to admit that this was a fun rabbit hole to go down, but it’s a pretty good example of magic leading to unexpected behavior. Admins are typically told that the most maintainable way to add additional home directory locations is by defining an equivalence to /home – which breaks if you make the mistake of setting that as the default for new users.