feature: shutdown option in pam_faillock to protect FDE keys from lock-screen brute force

Open
#976 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
28/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Quiet
Tech stack
c, linux

Research direction

This is a design proposal for linux-pam pam_faillock, not a ready first patch: read the issue’s questions for maintainers before coding. The sketch names struct options, deny=N parsing, write_tally() after update_tally(), reboot(RB_POWER_OFF), faillock.conf, faillock.conf.5.xml and pam_faillock.8.xml. Done means maintainers accept the option (vs a separate module), then a PR with tally persisted and sync before poweroff, skipped for root unless even_deny_root, and no behavior change when shutdown=0. There are no comments after months, so wait for maintainer direction.

Written by the indexing model from the issue text.

Description

Feature proposal: shutdown=N option in pam_faillock to protect FDE keys from lock-screen brute force

Background / Threat model

Laptops with full-disk encryption are only secure before the machine is booted. Once the user has logged in (even if the screen is then locked), the FDE master key lives in RAM. Users keep laptops unattended for bio break at school, coffee shops, at work.

This proposal addresses one specific attack vector: password brute-force at the lock screen — either by a human physically at the machine, or by an automated USB HID device (e.g. a rubber-ducky-style device that types password guesses at high speed). In this scenario, the attacker has physical access to a running, locked machine and tries to guess the password through the normal authentication path without leaving permanent marks on the machine that would be noticed by the returning user.

Against this attack, the existing deny=N mechanism locks the account but keeps the machine running — so the FDE key stays in RAM and the threat is not mitigated. Powering the machine off clears DRAM within seconds, destroying the key material before the attacker can extract it by any means.

Out of scope

This proposal explicitly does not address:

  • Thunderbolt / PCIe / FireWire DMA attacks — the attacker never touches the auth path, so no failure counter is incremented. These are properly addressed by IOMMU / Kernel DMA Protection, bolt/boltctl, and firmware-level settings. I intend to file a separate proposal covering the kernel/userspace side of DMA protection.
  • Cold-boot attacks — again, no auth attempt is made; the attacker physically removes RAM. Mitigations (memory encryption, rapid RAM clearing on shutdown) live elsewhere in the stack.
  • Evil-maid boot-media attacks — addressed by Secure Boot and TPM-backed measured boot.

The scope here is narrow on purpose: the one thing pam_faillock can plausibly defend against is password-based brute force, because that's the one thing it sees.

This mitigation is currently only achievable through out-of-tree scripts glued on with pam_exec, pam_faillock, and shell wrappers. That works, but it has a large and unnecessary attack surface (exposed script paths, inherited environment, writable /usr/local, etc.) for what is fundamentally a very small piece of logic.

Why existing options are insufficient

  • deny=N locks the account but keeps the machine running — FDE key stays in RAM, so the threat is not mitigated.
  • pam_exec can invoke poweroff from a script, but:
    • The script path becomes an attack surface (anything that can write to it gains root-exec-on-auth-fail).
    • Environment/FD inheritance needs careful handling to be safe.
    • Users end up writing and maintaining bespoke shell scripts for what should be a one-line config option.

Proposal

Add an optional shutdown=N parameter to pam_faillock (mirroring the existing deny=N syntax):

# /etc/security/faillock.conf
shutdown = 3      # poweroff after 3 consecutive failures
deny = 5          # lock account after 5 (fallback)

Semantics:

  • N = 0 (default): feature disabled. No behavior change vs. current pam_faillock.
  • N > 0: when the recorded consecutive failure count for a user reaches exactly N, the module calls reboot(RB_POWER_OFF) directly.
  • Skipped for root / admin_group unless even_deny_root is set (consistent with deny semantics).
  • Tally is persisted to disk before the shutdown, and sync() is called, so the evidence survives the reboot.
  • If reboot(2) fails (e.g. the PAM service lacks CAP_SYS_BOOT), the failure is logged and the auth stack continues normally. No existing behavior is broken.

Why inside pam_faillock and not a separate module

pam_faillock already:

  • Tracks per-user consecutive failure counts with a sliding window.
  • Has the exact hook point needed (inside write_tally(), right where the deny threshold is currently checked).
  • Owns the tally-file semantics that any N-failures-based action has to integrate with.

Duplicating all of that into a separate module would be considerable redundant code. Adding one small, opt-in code path next to the existing deny logic is the minimal change.

Why a direct syscall rather than exec-a-command

This is deliberate. The motivation for the feature is reducing attack surface compared to the existing pam_exec-based workarounds. reboot(RB_POWER_OFF) from inside the module means:

  • No external binary path that an attacker could swap.
  • No environment variables or file descriptors to sanitize.
  • No fork + exec — simpler to reason about, no zombie handling, no race windows.

A configurable on_deny=<command> option was considered and rejected for exactly these reasons.

Interaction with deny

For the feature to be effective, shutdown <= deny must hold. If shutdown > deny, the account is locked at deny and subsequent failures aren't recorded — so shutdown would never fire. This is a documentable user-misconfiguration concern, similar to how fail_interval can be misused.

The intended usage pattern is:

  • shutdown — small, aggressive threshold — protects the machine (FDE keys in RAM against lock-screen brute force).
  • deny — larger, fallback threshold — protects the account if shutdown can't fire (e.g. running without CAP_SYS_BOOT).

Questions for maintainers

  1. Is this direction acceptable in principle, or would you prefer this live as a separate module (e.g. pam_failshutdown)?
  2. Naming: shutdown=N feels natural alongside deny=N, but is there a preferred convention? (poweroff=N? reboot_on_fail=N?)
  3. Should the behavior be gated behind a compile-time option (--enable-faillock-shutdown or similar) so distros that don't want it can opt out?
  4. Any concerns about the default being 0 / disabled and the zero-behavior-change claim for existing deployments?

Implementation sketch

I have a working patch (~30 lines of code + config parsing + man page updates) ready to go. Happy to submit it as a PR once there's agreement on the design. The patch:

  • Adds unsigned short shutdown to struct options.
  • Adds shutdown=N parsing alongside the existing deny=N parser.
  • Adds the syscall call site inside write_tally() after update_tally() returns success.
  • Updates faillock.conf, faillock.conf.5.xml, and pam_faillock.8.xml.

Thanks for taking the time to review this proposal. This is my first open-source contribution. My GitHub account is new, but I am a long-time developer (20+ years).

Dominant language
C
Stars
807
Forks
368
Avg merge
6d 16h
Merged PRs (30d)
4

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from linux-pam/linux-pam

All issues in linux-pam/linux-pam

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.