pam_mount and SELinux

On all my computers, my personal data is encrypted. It is mostly a piece-of-mind thing as I do not want the average burglar to go through my files. For my laptop, full disk encryption (FDE) is not really a problem: I am the only one using it. Therefore I can enter the passphrase at boot and be done with it. Fedora (and Ubuntu) offer a nice checkbox to fully encrypt the drives at installation time.

For a shared computer, this poses a significant problem. If I perform FDE, nobody but me can boot up the machine. So there must be a way to encrypt just my files. Years ago I have used eCryptFS for this. It works quite nice and is an installation option in Ubuntu. Your /home directory is then encrypted per-file. This means that no giant image file is sitting around which needs to be shrunk down occasionally, like FileVault on Mac OS X 10.5. The downsides are that this limits the length of file names and does not seem to leverage the AES-NI instruction set. While using a HDD, this cannot be noticed. But with an SSD, the CPU will suddenly become the bottleneck. Therefore I had to do something else.

Fedora has solved the per-user file encryption with an encrypted image file. I have not tried that yet, but it seems to circumvent the above issues. The downside is that the image file has to resized occasionally, I believe. The option that I have settled for is an extra partition which is encrypted with LUKS. This probably does not differ much from the approach that I know from the older Fedora installers.

LUKS

LUKS are helpers and utilities that make dm-crypt more easy to use. First you should create a partition somewhere, encrypt it with LUKS (cryptsetup ...) and then create a filesystem (mkfs.ext4 or mkfs.btrfs) on it.

Important

The passphrase on the LUKS volume has to be exactly the same as your login password. Otherwise the automatic mounting (which is set up in the text below) will not work.

pam_mount

pam_mount is a nice utility/library that allows you to mount certain volumes when a user logs in. I would have my own partition encrypted with LUKS that pam_mount will mount into /home/mu for me. The setup has to be done by hand.

Defaults on Fedora, Arch Linux and Ubuntu

On Ubuntu I managed to do this rather easily. I am not sure what exactly I did but a line added in /etc/security/pam_mount.conf.xml was needed for the volume and the mount point. On Fedora there was no pam_mount.so entry in any of the files in /etc/pam.d. Therefore I had to add them. The documentation you find on the web is for various distributions and various versions of pam. This is bad as you now really have to understand what’s going on.

I am not opposed of understanding things, quite the opposite. But I am against the need of understanding everything. This is why I use Fedora or Ubuntu instead of Arch Linux. I am happy to use a system and not understand why it works in certain aspects as long as it does its job.

Anyway, there are a lot of files in /etc/pam.d:

-rw-r--r--. 1 root root  272 17. Jun 2015  atd
-rw-r--r--. 1 root root  192 18. Nov 12:28 chfn
-rw-r--r--. 1 root root  192 18. Nov 12:28 chsh
-rw-r--r--. 1 root root  232 12. Aug 17:40 config-util
-rw-r--r--. 1 root root  293 13. Jul 13:37 crond
-r--r--r--. 1 root root  146  2. Dez 16:45 cups
-rw-r--r--. 1 root root   71  2. Nov 18:39 exim
lrwxrwxrwx. 1 root root   19 25. Okt 18:18 fingerprint-auth -> fingerprint-auth-ac
-rw-r--r--. 1 root root  702 25. Okt 18:18 fingerprint-auth-ac
-rw-r--r--. 1 root root  127 17. Jun 2015  hddtemp
-rw-r--r--. 1 root root  159 20. Mai 2015  i3lock
-rw-r--r--. 1 root root  134  8. Jul 2008  kcheckpass
-r--r-xr-x. 1 root root  798 28. Dez 17:13 kde*
-rw-r--r--. 1 root root  134  8. Jul 2008  kscreensaver
-rw-r--r--. 1 root root   70 17. Dez 03:17 ksu
-rw-r--r--. 1 root root  963 10. Sep 19:45 lightdm
-rw-r--r--. 1 root root  698 10. Sep 19:45 lightdm-autologin
-rw-r--r--. 1 root root  409  8. Apr 2014  lightdm-greeter
-rw-r--r--. 1 root root   97 22. Okt 22:03 liveinst
-rw-r--r--. 1 root root  715 18. Nov 12:28 login
-rw-r--r--. 1 root root  564 25. Nov 00:12 mock
-rw-r--r--. 1 root root  154 12. Aug 17:40 other
-rw-r--r--. 1 root root  188 18. Jun 2015  passwd
lrwxrwxrwx. 1 root root   16 25. Okt 18:18 password-auth -> password-auth-ac
-rw-r--r--. 1 root root  974 25. Okt 18:18 password-auth-ac
-rw-r--r--. 1 root root  510 18. Dez 21:44 pluto
-rw-r--r--. 1 root root  155 14. Jul 23:27 polkit-1
lrwxrwxrwx. 1 root root   12 25. Okt 18:18 postlogin -> postlogin-ac
-rw-r--r--. 1 root root  326 25. Okt 18:18 postlogin-ac
-rw-r--r--. 1 root root  144 18. Jun 2015  ppp
-rw-r--r--. 1 root root  640 18. Nov 12:28 remote
-rw-r--r--. 1 root root  143 18. Nov 12:28 runuser
-rw-r--r--. 1 root root  138 18. Nov 12:28 runuser-l
-rw-r--r--. 1 root root  835 16. Nov 18:45 sddm
-rw-r--r--. 1 root root  549 16. Nov 18:45 sddm-autologin
-rw-r--r--. 1 root root  397  4. Nov 21:41 sddm-greeter
-rw-r--r--. 1 root root  145 19. Jun 2015  setup
lrwxrwxrwx. 1 root root   17 25. Okt 18:18 smartcard-auth -> smartcard-auth-ac
-rw-r--r--. 1 root root  752 25. Okt 18:18 smartcard-auth-ac
lrwxrwxrwx. 1 root root   25  2. Dez 15:09 smtp -> /etc/alternatives/mta-pam
-rw-r--r--. 1 root root   76 12. Okt 12:25 smtp.postfix
-rw-r--r--. 1 root root  904 18. Dez 15:20 sshd
-rw-r--r--. 1 root root  540 18. Nov 12:28 su
-rw-r--r--. 1 root root  238  5. Nov 10:47 sudo
-rw-r--r--. 1 root root  178  5. Nov 10:47 sudo-i
-rw-r--r--. 1 root root  137 18. Nov 12:28 su-l
lrwxrwxrwx. 1 root root   14 25. Okt 18:18 system-auth -> system-auth-ac
-rw-r--r--. 1 root root 1015 25. Okt 18:18 system-auth-ac
-rw-r--r--. 1 root root  129 14. Dez 12:36 systemd-user
-rw-r--r--. 1 root root   84 14. Dez 10:28 vlock
-rw-r--r--. 1 root root  276  2. Okt 00:29 vmtoolsd
-rw-r--r--. 1 root root  163 16. Nov 11:10 xserver

So where should the lines go? I looked into the working Ubuntu configuration on another computer and found that the files which contain pam_mount.so are not even present in Fedora. The Arch Linux wiki also has a page about pam_mount but that did not help that much either.

Simon uses Arch Linux and has one computer configured with pam_mount. I called him and we looked into the files a bit more closely. Most of them contain similar looking lines that load some .so file in one way or another. There are also some include directives used to obtain the contents of other files. In the end we drowned in dependencies.

As the file syntax is rather easy I quickly wrote a Python program that parses the files and outputs a dependency graph to use with GraphViz. The idea is simple: Whenever the word include or substack are used, the module loaded is an included file. If the line starts with @include the other file is included.

pam_tree.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright © 2016 Martin Ueding <dev@martin-ueding.de>
# Licensed under The MIT License

import argparse
import os.path


def main():
    options = _parse_args()

    includes = {}

    files = options.input
    for file_ in files:
        basename = os.path.basename(file_)
        includes[basename] = []
        with open(file_) as f:
            for line in f:
                words = line.split()
                if len(words) >= 3 and words[1] in ['include', 'substack']:
                    includes[basename].append(words[2])

                if len(words) == 2 and words[0] == '@include':
                    includes[basename].append(words[1])

    print('digraph {')
    print('rankdir=LR')

    for file_, include in includes.items():
        for inc in set(include):
            print('"{}" -> "{}"'.format(inc, file_))
    print('}')


def _parse_args():
    '''
    Parses the command line arguments.

    :return: Namespace with arguments.
    :rtype: Namespace
    '''
    parser = argparse.ArgumentParser(description='')
    parser.add_argument('input', nargs='+')
    options = parser.parse_args()

    return options


if __name__ == '__main__':
    main()

This can be used with the following in a shell:

python3 pam_tree.py /etc/pam.d/* | dot -T pdf -o pam-tree.pdf

On my Fedora machine the dependencies look like the following:

../../_images/pam-friese.svg

So which file should all this go into? A common suggestion has been the system-auth file. The problem is that on Fedora this is automatically generated by a program called authconfig. Therefore one should not touch that. One tutorial suggests to do that, however. Another Arch BBS post discourages the use of the system-auth file but that is on Arch Linux.

How do the files look like on Arch Linux?

../../_images/pam-arch.svg

This is much simpler! And here you can more clearly see why the Arch system-auth is too high in the chain, every other service will get those settings as well. You only want to display manager, ssh remote and local login to get those settings. The systemd-user part should not be affected as this apparently leads to errors unmounting the encrypted drive after logout.

On Arch Linux the choices would be to add the needed lines into system-remote-login and system-local-login. Fedora does not have these files. Therefore I decided to manually put the lines into sshd and lightdm (my display manager).

How does that look on Ubuntu? Well, it’s complicated:

../../_images/pam-welsh.svg

To be honest, I do not understand why this is so much more complicated. Perhaps the Ubuntu has lots of more options prepared already? The pam_mount.so was already included everywhere as optional which means that once I installed it the system was already done. Perhaps Arch Linux only includes the configurations which are strictly needed and Fedora has a couple more for convenience but not all them.

Just out of curiosity I also wanted to see how the .so modules behave in their dependencies, especially which file the pam_mount.so got included in. Therefore I extended the generation script just a little bit. The changed part is the following:

pam_tree2.py

def main():
    options = _parse_args()

    includes = {}
    loads = {}

    files = options.input
    for file_ in files:
        basename = os.path.basename(file_)
        includes[basename] = []
        loads[basename] = []
        with open(file_) as f:
            for line in f:
                words = line.split()
                if len(words) >= 3 and words[1] in ['include', 'substack']:
                    includes[basename].append(words[2])

                if len(words) >= 3 and words[1] in ['required', 'optional']:
                    loads[basename].append(words[2])

                if len(words) == 2 and words[0] == '@include':
                    includes[basename].append(words[1])

    print('digraph {')
    print('''rankdir=LR   
          overlap = false
          splines = true''')

    for file_, include in includes.items():
        for inc in set(include):
            print('"{}" -> "{}"'.format(inc, file_))

    print('{ rank=same')
    print('"pam_mount.so" [color=red]')
    for file_, load in loads.items():
        for l in set(load):
            print('"{}" [shape=box]'.format(l))
    print('}')

    for file_, load in loads.items():
        for l in set(load):
            print('"{}" -> "{}"'.format(l, file_))
    print('}')

Then for Fedora we get the following. I marked pam_mount.so with a red box. The problem is that it is not connected to anything as this is run on my laptop without configuring pam_mount at all. On the other machine I actually configured it on it would be connected to sshd and lightdm. All the images are SVG, so you can just open them in a new tab and enlarge as you like.

../../_images/pam2-friese-neato.svg

And on Arch Linux it is a little bit simpler. This was also taken from Simon’s laptop which also has full disk encryption instead of pam_mount. On his other laptop it would be connected to system-local-login and system-remote-login.

../../_images/pam2-arch-neato.svg

And Ubuntu it is as complicated as on Fedora. There the pam_mount.so is actually attached. The configuration files that ship with Ubuntu have it already listed.

../../_images/pam2-welsh-neato.svg

Those were made with neato instead of dot as the hierarchy just got too big and intertwined.

SELinux

This has worked fine on Ubuntu 15.10 and then on Fedora 23. With Fedora 24, I was not able to log in any more. In the syslog (see with journalctl) I was able to find messages of this kind:

setroubleshoot[1534]: SELinux is preventing lightdm from getattr access on the file /run/mount/utab. For complete SELinux messages. run sealert -l a6e46e7e-628d-427a-a44d-6258579238fd
python3[1534]: SELinux is preventing lightdm from getattr access on the file /run/mount/utab.

               *****  Plugin catchall (100. confidence) suggests   **************************

               If you believe that lightdm should be allowed getattr access on the utab file by default.
               Then you should report this as a bug.
               You can generate a local policy module to allow this access.
               Do
               allow this access for now by executing:
               # ausearch -c 'lightdm' --raw | audit2allow -M my-lightdm
               # semodule -X 300 -i my-lightdm.pp

Running the ausearch command at the bottom gave me a policy file that I could install. This would have fixed this particular problem. However, I want to deploy this with Ansible on at least two computers and I do not want to clutter my system with bunch of locally installed SELinux policies. Therefore I did the following:

  • Try to login, observe the errors with journalctl -f.
  • Run ausearch ... | audit2allow ... and generate a source policy file .te.
  • Insert this policy using Ansible (see below).
  • Repeat …

After a while I had the following policy extracted:

module lightdm-utab 1.0;

require {
	type unlabeled_t;
	type mount_var_run_t;
        type home_root_t;
	type xdm_t;
	class file { append create getattr read write open };
	class dir { create write setattr };
}

#============= xdm_t ==============
allow xdm_t home_root_t:dir { create setattr };
allow xdm_t home_root_t:file { create write };

allow xdm_t mount_var_run_t:file { read getattr open };

allow xdm_t unlabeled_t:dir write;
allow xdm_t unlabeled_t:file { read append getattr write };

In hindsight it would have been easier to switch lightdm into permissive mode and then just record all needed policy changes in one go. Now it is too late but the result is the same.

This policy needs to be compiled. In a really helpful presentation I have found the command needed to compile it:

make -f /usr/share/selinux/devel/Makefile POLICY.pp

Then you install it like this:

/usr/sbin/semodule --install POLICY.pp

In the slides, there is -u used, but that is deprecated on Fedora 24.

With all this in place, it would still not work. The problem is that creating a directory for a different process and user is something that can cause nasty race conditions. Here it is wanted, so the boolean named polyinstantiation_enabled has to be enabled.

Like with lightdm, some adjustments need to be done for the SSH server as well. The policy is quite similar. Look into the git repository below for all the details.

Deployment with Ansible

There is nothing better than executable documentation; namely having a readable script that will perform all the needed actions. Perhaps easiest would be to check out the pam_mount-crypt role in my playbooks:

GitHub Page

git clone git://github.com/martin-ueding/playbooks.git

The relevant tasks for pam_mount are the following on Fedora:

- name: Add mount of {{ dev }} for mu
  lineinfile:
    dest: /etc/security/pam_mount.conf.xml
    line: '<volume user="mu" fstype="crypt" path="{{ dev }}" mountpoint="/home/mu" options="fsck,noatime" />'
    insertbefore: '</pam_mount>'

- name: Include pam_mount in pam config
  lineinfile:
    dest: /etc/pam.d/login
    line: '@include common-pammount'
    state: absent

- name: auth in sshd
  lineinfile:
    dest: /etc/pam.d/{{ item.file }}
    backup: yes
    state: present
    line: '{{ item.line }}'
    insertafter: '{{ item.after }}'
  with_items:
    - { file: sshd, line: 'auth optional pam_mount.so', after: '#%PAM-1.0' }
    - { file: sshd, line: 'session optional pam_mount.so', after: 'pam_selinux.so close' }
    - { file: lightdm, line: 'auth optional pam_mount.so', after: '#%PAM-1.0' }
    - { file: lightdm, line: 'session optional pam_mount.so', after: 'pam_selinux.so close' }

The variable dev contains the device like /dev/sda3 which contains the LUKS encrypted partition.

The SELinux part is done with this:

- name: Creating selinux custom policy drop folder
  file: path={{ custom_selinux_dir }} state=directory owner=root group=root mode=0750

- name: Copy custom policies
  copy: src="{{ item }}.te" dest="{{ custom_selinux_dir }}/{{ item }}.te"
  with_items: "{{ policies }}"
  register: custom_policies_output

- name: Compile policies
  command: make -f /usr/share/selinux/devel/Makefile "{{ item.item }}.pp"
  args:
    chdir: "{{ custom_selinux_dir }}"
  when: item.changed
  with_items: "{{ custom_policies_output.results }}"

- name: Load custom SELinux policies
  shell: /usr/sbin/semodule --install "{{ custom_selinux_dir }}/{{ item.item }}.pp"
  when: item.changed
  with_items: "{{ custom_policies_output.results }}"

- name: Enable boolean
  seboolean:
    name: polyinstantiation_enabled
    persistent: yes
    state: yes

For this to work you need a couple packages:

  • libselinux-python
  • pam_mount
  • policycoreutils
  • selinux-policy-devel

And the variables used are:

custom_selinux_dir: /etc/selinux/local-policies

policies:
  - lightdm-utab
  - sshd-utab

Using the Ansible role or by performing all the tasks manually, you should be able to get an encrypted partition mounted for you at login.