Hello! This is a guide on how to implement LUKS encrypted home/user directories as well as encrypted swap (FBE, file based encryption) on Fedora Linux Asahi Remix 42 with KDE Plasma (tested it only there). I implemented this on my machine and it seems to work reliably and since encryption/FBE is important for many users (even a dealbraker if not present) and there is no solution out of the box I decided to share how I did it. There can’t be too many resources on this. I tried system-homed/homectl first but this didn’t work at all with LUKS so I did this myself.
A word of warning: this is experimental and you are responsible for any data loss or computer problems when following this guide.
Brief description of what this does
- When a user successfully logs in (KDE Login or TTY doesn’t matter) an encrypted luks-container gets opened with the same password the user entered and mounted in /home/<user>.
- When a user logs out and the device is not busy (e.g. through linger or other terminals) the device is unmounted und the luks container closed. If the device stays busy for a little while after logout it gets unmounted when it’s not busy anymore (a cronjob takes care of this).
- When any user logs in successfully the first time after boot an encrypted luks container with the swapfile gets decrypted with the same password the user entered, mounted and used as swap. This container stays open even if the user logs out and any following logins skip this step.
- Optional: you can easily adapt this to also open other containers with the login password like on encrypted usb drives to give you some extra storage. This might come in handy with devices with only 256G storage and half of it reserved for apple’s bsd.
On logout a script just leaves a cookie in /tmp with the username and the mentioned cronjob then takes care of the cryptsetup-close/unmount. The reason we use a cronjob and not a script on PAM session end is that in KDE plasma the devices in /dev/mapper are still busy a few moments even after the user is logged out and another is logged in. Also the cronjob takes care of edgecases (linger on, other tty with logged in user…).
Administrative overhead
This is not a 100% integrated solution but the administrative overhead once installed is minimal. I highly recommend installing/using zulucrypt (sudo dnf install zulucrypt), a graphical interface for managing luks and other containertypes. When a new user is created, you also have to create a luks container and place it in /home/.encrypted/ and the luks container must have the user password added in its keychain. Also the luks-container for the swapfile needs to have the user-password as a key added. Of course if a user changes their login password, the user’s luks container needs to have the changed password in its keychain. And you need to copy a system-oneshot-service for new users. On systems with only one or two users there should be no issues.
For this setup we create a user along with it. We assume the new username is bob. After you did this once, administrative tasks are also described in point 13 Administration.
0. Precautions
Make sure you have/keep a fallback-user with superuser privileges that doesn’t use a LUKS encrypted homedirectory just in case anything goes wrong (obviously don’t keep sensitive files in their homedirectory). Also set SELinux policy to permissive during this setup (with “sudo setenforce 0” - will be reset to 1 after reboot. After this setup we can setenforce 1) to rule out SELinux if there are problems. Also: this method stores the user password a short moment in a salted and base64-encoded form between confirming login and the “cryptsetup open”-service (1-2 seconds) in a file in /tmp. After opening the LUKS container the tempfile is shredded with 10 iterations and removed. If you don’t like this you can set up a tempfs (directory exists only in RAM) in /etc/fstab and temp-store it there instead. Also use very easy passwords while implementing because if you use 30 character passwords with 100 bits of entropy you will go crazy during testing / troubleshooting. Also I’m using vi so if you don’t know how to use vi use nano, emacs or any other texteditor you like.
1. Set up the new user
The easiest way is to use the GUI tools (system settings → users in KDE Plasma). You can’t use the user you are currently working with. You can add the new user and remove the current unencrypted one if you’re finished if you like (but make sure you have one user with admin privileges and no encrypted home just to be safe). Also make sure you log in with this new user at least once to prevent any initialization-jobs while decrypting. The mountpoint (/home/bob) doesn’t have to be empty for mounting.
2. Setup the directory for the containers
sudo mkdir /home/.encrypted
sudo chmod 755 /home/.encrypted
3. Create the LUKS container with ext4 filesystem
There is a handy GUI tool called Zulucrypt for creating LUKS (or Veracrypt) containers. You can use that if you don’t like the CLI very much (sudo dnf install zulucrypt). Choosing LUKS2 (besides location /home/.encrypted/bob.luks, size, ext4 and password, which should be bobs password) and using the defaults should be enough.
If you prefer the CLI:
The default cipher for luks2 is aes-xts-plain64 and keysize is 512 bits, you can change/pass them if you like. Change the “30G” to the preferred target size of the container (remember that the password must match bobs password).
sudo fallocate -l 30G /home/.encrypted/bob.luks
dd if=/dev/zero of=/home/.encrypted/bob.luks bs=1M status=progress
cryptsetup luksFormat /home/.encrypted/bob.luks --type luks2
3.5 Prefill the container
sudo cryptsetup open /home/.encrypted/bob.luks bob_crypt
If you didn’t use Zulucrypt, create the filesystem with volume label (edit yourDesiredVolumeLabel)
mkfs.ext4 -L yourDesiredVolumeLabel /dev/mapper/bob_crypt
mkdir -p /mnt/secure
sudo mount /dev/mapper/bob_crypt /mnt/secure
Copy the contents of /home/bob/* to the container:
sudo rsync -av /home/bob/ /mnt/secure/
Touch a testfile so you can quickly see if the decrypt/mount works later (ls /home/bob should will it):
sudo touch /mnt/secure/LUKS_CONTAINER_FILE
Rights:
sudo chown -R bob:bob /mnt/secure/*
Closing:
sudo umount /mnt/secure
sudo cryptsetup close bob_crypt
4. LUKS-containerize swap
Turn off swap:
sudo swapoff -a
Edit /etc/fstab and uncomment or delete the swapfile:
sudo vi /etc/fstab
#/var/swap/swapfile swap swap sw 0 0
sudo systemctl daemon-reload
Create a mountpoint:
sudo mkdir -p /var/swap/mount
Create an encrypted luks container in /var/swap/swap.luks that is big enough to contain /var/swap/swapfile (you should know how to do this by now - zulucrypt). Make sure to give it a little extra space because there is also the LUKS header (for a 8G swapfile, a 8.5G container schould be ok).
Open and mount the container
sudo cryptsetup open /var/swap/swap.luks swap_crypt
sudo mount /dev/mapper/swap_crypt /var/swap/mount
Move the swapfile to the container (change file permission like chmod 755 if necessary)
sudo cp /var/swap/swapfile /var/swap/mount/
sudo rm /var/swap/swapfile
sudo chmod 600 /var/swap/mount/swapfile
Unmount/Close the container
sudo unmount /dev/mapper/swap_crypt
sudo cryptsetup close /dev/mapper/swap_crypt
File permissions
sudo chmod 644 /var/swap/swap.luks
5. Add the scripts for cryptsetup open (started by systemd afterwards, replace vi with nano if you like)
sudo touch /var/lib/systemd/cryptopen.sh
sudo chmod 755 /var/lib/systemd/cryptopen.sh
Add/Edit /var/lib/systemd/cryptopen.sh
#!/bin/bash
USER_PAM=$(cat /tmp/tmp.usr)
MOUNT_PATH="/home/.encrypted/$USER_PAM.luks"
MOUNT_LABEL="${USER_PAM}_crypt"
PARAM=$(cat /tmp/tmp.837 | xargs cat)
PARAM="${PARAM:0:-3}"
echo -n "$PARAM" | base64 --decode | /usr/sbin/cryptsetup open --type luks $MOUNT_PATH $MOUNT_LABEL
sudo touch /var/lib/systemd/cryptopen_swap.sh
sudo chmod 755 /var/lib/systemd/cryptopen_swap.sh
Add/Edit sudo vi /var/lib/systemd/cryptopen_swap.sh
#!/bin/bash
MOUNT_PATH="/var/swap/swap.luks"
MOUNT_LABEL="swap_crypt"
PARAM=$(cat /tmp/tmp.837 | xargs cat)
PARAM="${PARAM:0:-3}"
echo -n "$PARAM" | base64 --decode | /usr/sbin/cryptsetup open --type luks $MOUNT_PATH $MOUNT_LABEL
sudo /usr/bin/mount /dev/mapper/$MOUNT_LABEL /var/swap/mount/
sudo /usr/bin/swapon /var/swap/mount/swapfile
6. Add the systemd script for bob and swap
Theoretically a systemd script should not be necessary because the PAM script could just call cryptsetup. However in my case this caused a racecondition and it didn’t work reliably (/dev/mapper/bob_crypt was only there during auth process then gone). The “RemainAfterExit=yes” directive of the service seems to be important here. The namingconvention “cryptsetup-USER.service” is important.
sudo vi /etc/systemd/system/cryptsetup-bob.service
[Unit]
Description=Open LUKS volume for user bob
[Service]
PrivateTmp=no
Type=oneshot
RemainAfterExit=yes
ExecStart=/var/lib/systemd/cryptopen.sh
ExecStop=/usr/sbin/cryptsetup close bob_crypt
StandardInput=null
StandardOutput=journal
StandardError=journal
sudo vi /etc/systemd/system/cryptsetup-swap.service
[Unit]
Description=Open LUKS volume for user swap
[Service]
PrivateTmp=no
Type=oneshot
RemainAfterExit=yes
ExecStart=/var/lib/systemd/cryptopen_swap.sh
ExecStop=/usr/sbin/cryptsetup close swap_crypt
StandardInput=null
StandardOutput=journal
StandardError=journal
7. Create the PAM scripts (replace vi with nano if you prefer that).
In pam_crypt_open.sh I marked the location where you can implement additional cryptsetup-opens/mounts with the same user password (like auto-mount usb drives and such).
sudo vi /usr/local/sbin/pam_crypt_open.sh
#!/bin/bash
#set -euo pipefail
# No PAM_USER, exit
if [[ -z "${PAM_USER:-}" ]]; then
exit 0
fi
if [ "$PAM_TYPE" != "auth" ]; then
exit 0
fi
MAPPER="/dev/mapper/${PAM_USER}_crypt"
LUKSPATH="/home/.encrypted/${PAM_USER}.luks"
# encrypted device exists, nothing to do
if [[ -e "$MAPPER" ]]; then
exit 0
fi
if [[ ! -e "$LUKSPATH" ]]; then
# no LUKS container, probably no home-encryption on purpose?
exit 0
fi
TEMPFILE=$(mktemp)
PARAM=$(cat /dev/stdin | base64)
PARAM="${PARAM}z9c"
sleep 1
echo "$PARAM" > $TEMPFILE
echo "$TEMPFILE" > /tmp/tmp.837
echo "$PAM_USER" > /tmp/tmp.usr
chmod 755 $TEMPFILE
chmod 755 /tmp/tmp/.837
chmod 755 /tmp/tmp/.usr
# main command
sudo -n /usr/bin/systemctl start cryptsetup-${PAM_USER}.service
# mount encrypted swap if swap not active
swap_info=$(swapon --show --noheadings)
if [[ ! -n $swap_info ]]; then
sudo -n /usr/bin/systemctl start cryptsetup-swap.service
fi
#######################################
# Add additional mounts with the same #
# password here if you like #
#######################################
sleep 1
shred -n 10 $TEMPFILE
rm $TEMPFILE
rm /tmp/tmp.837
rm /tmp/tmp.usr
# luks container open, now mounting
set -euo pipefail
CRYPT_NAME="${PAM_USER}_crypt"
MAPPER="/dev/mapper/$CRYPT_NAME"
MOUNTPOINT="/home/$PAM_USER"
# No PAM_USER, exit
if [[ -z "${PAM_USER:-}" ]]; then
exit 0
fi
# No Device -> aborting
if [[ ! -e "$MAPPER" ]]; then
exit 0
fi
# Mount if not already mounted
if ! mountpoint -q "$MOUNTPOINT"; then
mount -t ext4 "$MAPPER" "$MOUNTPOINT"
fi
exit 0
sudo vi /usr/local/sbin/pam_crypt_close.sh
#!/usr/bin/env bash
# no pam user -> exit
if [[ -z "${PAM_USER:-}" ]]; then
exit 0
fi
# not close session -> exit
if [ "$PAM_TYPE" != "close_session" ]; then
exit 0
fi
if [ "$PAM_USER" = "root" ]; then
exit 0
fi
# check user exist
if ! id "$PAM_USER" &>/dev/null; then
exit 1
fi
# UID of user
uid=$(id -u "$PAM_USER")
if (( uid < 1000 )); then
# user is system user (not a person)
exit 0
else
echo "$PAM_TYPE" >> /tmp/logoutcookie_${PAM_USER}
fi
exit 0
8. Update authselect / PAM process
IMPORTANT: be very careful not to change the other values! Only add the “optional” declared lines. Wrong configuration of this file can cause you not to be able to log in!
Check which profile id is active:
sudo authselect current
Create a new one (replace current with profile id):
sudo authselect create-profile local-with-encrypted-home --base-on=<current>
sudo authselect select local-with-encrypted-home # (maybe custom/local-with-encrypted-home)
Edit:
sudo vi /etc/authselect/custom/local-with-encrypted-home/system-auth
In the auth-stack add an “optional” new entry for pam_crypt_open after the first required-block:
auth required pam_env.so
auth required pam_faildelay.so delay=2000000
auth required pam_faillock.so preauth silent {include if "with-faillock"}
auth optional pam_exec.so expose_authtok debug quiet /usr/local/sbin/pam_crypt_open.sh
...
It is important that it is BEFORE the sufficient pam_unix.so entry.
Also, add a session entry for pam_crypt_close at the very bottom of the session-stack/file:
...
session required pam_unix.so
session optional pam_gnome_keyring.so only_if=login auto_start {include if "with-pam-gnome-keyring"}
session optional pam_exec.so /usr/local/sbin/pam_crypt_close.sh
Now do the same thing for /etc/authselect/custom/local-with-encrypted-home/password-auth
Apply the changes:
sudo authselect apply-changes
9. Logoutcookie-Cleanup-Script
On log out a user leave an empty file like: logoutcookie_bob in /tmp. A cronjob then picks it up, unmounts and closes the cryptdevice and deletes the cookie. The script the cronjob uses is here:
sudo vi /usr/local/sbin/check-logout-cookie.sh
# ------------------------------------------------------------
# /usr/local/sbin/check-logout-cookie.sh
# ------------------------------------------------------------
#!/bin/bash
# ------------------------------------------------------------
# Cron‑Job (Fedora/Asahi) – check for logout-cookies in /tmp
# tries to unmount the corresponding LUKS-Device and stop
# the cryptsetup-systemd-service in unount successful.
# Removes cookie regardless of unmount-success because always-
# failing unmounts are nasty but you can adapt this if you
# think otherwise.
# ------------------------------------------------------------
set -euo pipefail
IFS=$'\n\t' # better field-separator
TMPDIR="/tmp"
MAX_TRIES=5
SLEEP_BETWEEN=3 # seconds to sleep between unmounts
log() {
# simple syslog-logging (optional)
logger -t check-logout-cookies "$*"
}
# ----------------------------------------------------------------
# 1. Find all files that match the pattern
# ----------------------------------------------------------------
shopt -s nullglob
for cookie_path in "$TMPDIR"/logoutcookie_*; do
# ------------------------------------------------------------
# 2. extract USER
# ------------------------------------------------------------
cookie_file="${cookie_path##*/}" # logoutcookie_user123
user="${cookie_file#logoutcookie_}" # everything past ookie_
# sanity‑check: USER must not be empty
if [[ -z "$user" ]]; then
log "WARN: unable to extract USER from $cookie_path - skipping"
continue
fi
# ------------------------------------------------------------
# 3. Check for existence of LUKS device
# ------------------------------------------------------------
mapper_dev="/dev/mapper/${user}_crypt"
if [[ ! -e "$mapper_dev" ]]; then
log "INFO: $mapper_dev does not exists, nothing to do for $user"
# remove Cookie anyway
rm -f "$cookie_path"
continue
fi
# ------------------------------------------------------------
# 4. trying to unmount (max. 5x, 3s repetitions)
# ------------------------------------------------------------
unmount_success=0
for ((i=1; i<=MAX_TRIES; i++)); do
if sudo umount "$mapper_dev" 2>/dev/null; then
unmount_success=1
log "INFO: $mapper_dev successfully mounted (try $i) for $user"
break
else
log "WARN: umount $mapper_dev failed (try $i) for $user"
((i < MAX_TRIES)) && sleep "$SLEEP_BETWEEN"
fi
done
# ------------------------------------------------------------
# 5. Stop service (only at success, no fatal error)
# ------------------------------------------------------------
service_name="cryptsetup-${user}.service"
if [[ $unmount_success -eq 1 ]]; then
if sudo systemctl stop "$service_name" 2>/dev/null; then
log "INFO: systemd‑Service $service_name stopped for $user"
else
log "WARN: unable to stop systemd‑Service $service_name for user $user"
fi
fi
# ------------------------------------------------------------
# 6. remove cookie file regardless of the outcome
# ------------------------------------------------------------
rm -f "$cookie_path"
log "INFO: Cookie $cookie_path deleted (User $user)"
done
10. Crontab
I used root to do that. I think it’s convenient but you can use a different user if you like.
sudo crontab -e
Add that line (this runs every minute, you can adjust it if you want)
*/1 * * * * /usr/local/sbin/check-logout-cookie.sh >>/var/log/check-logout-cookies.debug.log 2>&1
11. Sudo rules
I must admit I have been a little bit lazy and implemented those NOPASSWD rules for all users which isn’t really a good practice. However I don’t see any threat be allowing users to enable swapon with one specific swap file or running their specific cryptsetup-systemd-files. If you want to limit this do specific users go ahead and adapt it otherwise use this as it is:
sudo vi /etc/sudoers
## Allow all users to start the cryptsetup-user-services
%users ALL=(root) NOPASSWD: /usr/bin/systemctl start cryptsetup-*.service
%users ALL=(root) NOPASSWD: /usr/bin/systemctl stop cryptsetup-*.service
%users ALL=(root) NOPASSWD: /usr/bin/umount /dev/mapper/*_crypt
%users ALL=(root) NOPASSWD: /usr/bin/mount /dev/mapper/swap_crypt /var/swap/mount/
%users ALL=(root) NOPASSWD: /usr/bin/swapon /var/swap/mount/swapfile
12. Update SELinux & Testing
This part is a little bit tricky because you need to run it and make if fail by triggering SELinux avs deny policies several times.
We alrady set SELinux enforcement to permissive (setenforce 0) temporarily to set this up. However the goal is to keep SELinux enforcement to enforced. Keep in mind that SELinux AVC might still deny a process even in permissive state. I’m no expert on SELinux but this works without changing seemingly unrelated policies.
First we set enforcement to enforced:
sudo setenforce 1
Next we try to log in with bob. You can either do this by using the KDE GUI (session, switch user) or a TTY Console (Ctrl+Alt+Fx if you’re familiar with that). After login, we check if the file LUKS_CONTAINER_FILE is present in /home/bob:
ls /home/bob/LUKS_CONTAINER_FILE
If you get a not found message, it didn’t work. Don’t worry, it’s expected.
Check the AVC message for SELinux denies:
sudo ausearch -m avc -ts recent
If there are messages, let audit2allow compute the minimum required privileges:
ausearch -m avc -ts recent | audit2allow -M recent-login-deny
And update your SELinux config:
sudo semodule -i recent-login-deny.pp
Sometimes ausearch doesn’t list all the denied requests. In that case, use journalctl to find out the denied requests. One way is to call:
sudo journalctl | grep "avc:" | grep "denied" > denied_requests.txt
Then you can compile new rules specifically:
“sudo audit2allow -M denied-luks -i denied_requests.txt”
and load them:
sudo semodule -i denied-luks.pp
You can also find denied requests in /var/log/audit/audit.log. You can copy any of those AVC denied lines, put them in a textfile and use them as input for audit2allow. The mechanism is always the same.
Sometimes the deny information can be found by calling “sudo journalctl”, typing GG (uppercase) to go to the bottom, then ?setroubleshoot <enter>. In that case journalctl provides you with the commands you need to run.
The process of logging in, seeing no LUKS_CONTAINER_FILE in the homedirectory and checking/updating AVC needs to be repeated several times until you log in with bob and LUKS_CONTAINER_FILE is in /home/bob.
13. Administration
After a few login-attempts / reboots and SELinux policy upgrades it should be working now.
If you want to add a new user this way:
- Add the user via KDE GUI (system preferences - users) or via CLI
- Let the user log in once, then log out
- Create an encrypted LUKS container in /home/.encrypted/name.luks and add the same password or ask the user to enter the same password in the luks containerfile. Copy the user’s home directory to the container.
- copy /etc/systemd/system/cryptsetup-bob.service to /etc/systemd/system/cryptsetup-newname.service, open it and edit the mount label in ExecStop command (_crypt)
- Add the key to the luks container /var/swap/swap.luks
If a user wants to change the password:
- Have them add the new password to the keychain of their luks container (zulucrypt is your friend) in /home/.encrypted/user.luks and in /var/swap/swap.luks
- Have them change their password in the system preferences
- Remove old key from the containers if you like
Remove a user:
- Delete their luks container and remove their key from /var/swap/swap.luks alongside their deletion in system preferences.
That’s it, I hope this is useful to somebody. FBE is not quite as good as full disk encryption but it’s way better than no encryption at all and it keeps your home-directories save in case someone gets their hands on your device without your approval. Of course FDE and FBE at the same time is perfect because FBE also keeps files of other users encrypted when the device is in use.
Happy hacking!