Backport of the FIDO2 authfile fix (PR #7904 by @mdisec, merged to quattro as
23dab9ec) onto the v4-0-1 release branch.
pamu2fcfg wrote to /tmp/fido2 and the registration was then moved into place
with sudo mv. Any other local user can pre-create /tmp/fido2, and rename(2) does
not dereference the final component, so the privileged move installed the
attacker's symlink itself as pam_u2f's global authfile -- a file consulted by
sufficient lines in /etc/pam.d/sudo and /etc/pam.d/polkit-1.
The same move also carried the staged file's ownership into /etc, so on every
install to date /etc/fido2/fido2 is owned by the invoking user at mode 0644.
That needs no attacker, no race and no second account: anything running as that
uid can append its own credential and satisfy the machine's sudo prompt without
knowing the password.
The setup now creates a unique staging file as root beside the final authfile
and pipes pamu2fcfg into it, so root never reopens a caller-owned pathname,
rejects failed or empty enrollment output, publishes with an atomic mv -Tf,
cleans the exact staging file after every failure, refuses non-regular authfile
states, and installs root:root 0644. A migration repairs machines set up by
earlier versions by replacing the inode rather than chowning in place: a process
that already holds a writable descriptor on the legacy user-owned mapping keeps
it, so the repair has to leave that inode behind where PAM no longer reads it.
Clean cherry-pick: all six files are byte-identical to quattro, so merging
v4-0-1 into quattro resolves without a conflict. The migration's timestamp
(1787494718) is older than others already on this branch, which is harmless:
omarchy-migrate marks each migration by filename rather than tracking a
watermark, so this one runs on every machine that has not run this exact file.
test/shell passes: 195 files, including the three this adds -- the setup's
staging and failure paths, the migration's repair and unrepairable states, and
the removal's symlink handling. test/cli passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
149 lines
4.8 KiB
Bash
Executable File
149 lines
4.8 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Set up FIDO2 authentication for sudo and polkit
|
|
# omarchy:requires-sudo=true
|
|
|
|
set -e
|
|
set -o pipefail
|
|
|
|
|
|
check_fido2_hardware() {
|
|
tokens=$(fido2-token -L 2>/dev/null)
|
|
if [[ -z $tokens ]]; then
|
|
echo -e "\e[31m\nNo FIDO2 device detected. Please plug it in (you may need to unlock it as well).\e[0m"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
setup_pam_config() {
|
|
# Configure sudo
|
|
if ! grep -q pam_u2f.so /etc/pam.d/sudo; then
|
|
echo "Configuring sudo for FIDO2 authentication..."
|
|
sudo sed -i '1i auth sufficient pam_u2f.so cue authfile=/etc/fido2/fido2' /etc/pam.d/sudo
|
|
fi
|
|
|
|
# Configure polkit
|
|
if [[ -f /etc/pam.d/polkit-1 ]] && ! grep -q 'pam_u2f.so' /etc/pam.d/polkit-1; then
|
|
echo "Configuring polkit for FIDO2 authentication..."
|
|
sudo sed -i '1i auth sufficient pam_u2f.so cue authfile=/etc/fido2/fido2' /etc/pam.d/polkit-1
|
|
elif [[ ! -f /etc/pam.d/polkit-1 ]]; then
|
|
echo "Creating polkit configuration with FIDO2 authentication..."
|
|
sudo tee /etc/pam.d/polkit-1 >/dev/null <<'EOF'
|
|
auth sufficient pam_u2f.so cue authfile=/etc/fido2/fido2
|
|
auth required pam_unix.so
|
|
|
|
account required pam_unix.so
|
|
password required pam_unix.so
|
|
session required pam_unix.so
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
echo -e "\e[32mSetting up FIDO2 device for authentication.\n\e[0m"
|
|
|
|
# Install required packages
|
|
echo "Installing required packages..."
|
|
omarchy-pkg-add libfido2 pam-u2f
|
|
|
|
if ! check_fido2_hardware; then
|
|
exit 1
|
|
fi
|
|
|
|
# Create the pamu2fcfg file
|
|
authdir=/etc/fido2
|
|
authfile=/etc/fido2/fido2
|
|
|
|
# install -d follows a symlink here and applies the mode and ownership to
|
|
# whatever it points at, so the credential would be staged and published inside
|
|
# the link target and that directory reopened to root:root 755. This is the
|
|
# threat omarchy-remove-security-fido2 already names on its side.
|
|
if [[ -L $authdir || ( -e $authdir && ! -d $authdir ) ]]; then
|
|
echo -e "\e[31m\n$authdir is not a FIDO2 configuration directory.\e[0m"
|
|
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
|
exit 1
|
|
fi
|
|
|
|
# -f follows symlinks, so the already-registered check below reads a symlinked
|
|
# authfile as a registration and leaves it in place, and is false for a
|
|
# directory, so it tries to register over one. Only a regular file is a valid
|
|
# pam_u2f authfile.
|
|
if [[ -L $authfile || ( -e $authfile && ! -f $authfile ) ]]; then
|
|
echo -e "\e[31m\n$authfile is not a FIDO2 registration file.\e[0m"
|
|
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f $authfile ]]; then
|
|
sudo install -d -m 755 -o root -g root "$authdir"
|
|
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
|
|
echo -e "Touch your FIDO2 key when it lights up...\n"
|
|
|
|
# A unique sibling created by root cannot be replaced by another process
|
|
# running as this user. Stream pamu2fcfg into it instead of asking root to
|
|
# reopen a caller-owned path: an observed temporary name could otherwise be
|
|
# replaced with a symlink before the privileged copy. The final rename is
|
|
# atomic, and -T refuses a directory at the destination. Mode 644 keeps the
|
|
# root-owned global authfile readable when pam_u2f uses openasuser; only root
|
|
# can still rewrite it.
|
|
stage=""
|
|
|
|
# mktemp's output is an operand for four privileged commands below, one of
|
|
# them an rm. Take only the name this script asked for rather than whatever
|
|
# came back on stdout.
|
|
safe_stage_path() {
|
|
local candidate=$1
|
|
local prefix="$authfile.new."
|
|
local suffix
|
|
|
|
[[ $candidate == "$prefix"* ]] || return 1
|
|
suffix=${candidate#"$prefix"}
|
|
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
|
}
|
|
|
|
cleanup_stage() {
|
|
local status=$?
|
|
|
|
if safe_stage_path "$stage"; then
|
|
sudo rm -f -- "$stage" || true
|
|
fi
|
|
|
|
return "$status"
|
|
}
|
|
|
|
trap cleanup_stage EXIT
|
|
stage=$(sudo mktemp "$authfile.new.XXXXXX")
|
|
|
|
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
|
|
echo -e "\e[31m\nCould not create a safe staging file beside $authfile.\e[0m"
|
|
exit 1
|
|
fi
|
|
|
|
if pamu2fcfg | sudo tee "$stage" >/dev/null && [[ -s $stage ]]; then
|
|
sudo chmod 644 "$stage"
|
|
sudo mv -Tf "$stage" "$authfile"
|
|
stage=""
|
|
trap - EXIT
|
|
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
|
|
else
|
|
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "FIDO2 device already registered."
|
|
fi
|
|
|
|
# Configure PAM
|
|
setup_pam_config
|
|
|
|
# Test with sudo
|
|
echo -e "\nTesting FIDO2 authentication with sudo..."
|
|
echo -e "Touch your FIDO2 key when prompted.\n"
|
|
|
|
if sudo echo "FIDO2 authentication test successful"; then
|
|
echo -e "\e[32m\nPerfect! FIDO2 authentication is now configured.\e[0m"
|
|
echo "You can use your FIDO2 key for sudo and polkit authentication."
|
|
else
|
|
echo -e "\e[31m\nVerification failed. You may want to check your configuration.\e[0m"
|
|
fi
|