Introduction

Facelock is a modern face authentication system for Linux PAM. It provides Windows Hello-style facial authentication with IR anti-spoofing, configurable as a persistent daemon or daemonless one-shot. All inference runs locally on your hardware -- no cloud services, no network requests, no telemetry. Your biometric data never leaves your machine.

Quick Start

cargo build --workspace
FACELOCK_CONFIG=dev/config.toml cargo run --bin facelock -- setup    # download models
FACELOCK_CONFIG=dev/config.toml cargo run --bin facelock -- enroll   # capture face
FACELOCK_CONFIG=dev/config.toml cargo run --bin facelock -- test     # verify recognition

No daemon needed -- the CLI auto-falls back to direct mode when no daemon is running.

Operating Modes

ModeConfigHow it worksLatency
Daemonmode = "daemon" (default)PAM connects via D-Bus, persistent daemon~150-600ms
D-Bus activationsystemd + D-Bus servicesystemd starts daemon on demand~700ms+ cold
Oneshotmode = "oneshot"PAM spawns facelock auth subprocess~700ms+

Daemon latency depends on camera state: ~600ms with a cold camera, ~150-180ms on back-to-back auths when the camera is already warm.

The CLI works in all modes -- it connects to the daemon if available, otherwise operates directly.

Architecture

facelock (unified binary)
├── facelock setup          Download models, install systemd/PAM
├── facelock enroll         Capture and store a face
├── facelock test           Test recognition
├── facelock list           List enrolled models
├── facelock preview        Live camera preview
├── facelock daemon         Run persistent daemon
├── facelock auth           One-shot auth (PAM helper)
├── facelock devices        List cameras
├── facelock tpm status     TPM status
└── facelock bench          Benchmarks

pam_facelock.so (PAM module)
├── daemon mode → D-Bus IPC to daemon
├── polkit agent → facelock-polkit
└── oneshot mode → fork/exec facelock auth

Crates

CrateTypePurpose
facelock-corelibConfig, types, errors, D-Bus interface, traits
facelock-cameralibV4L2 capture, auto-detection, preprocessing
facelock-facelibONNX inference (SCRFD detection + ArcFace embedding)
facelock-storelibSQLite face embedding storage
facelock-daemonlibAuth/enroll logic, liveness, audit, rate limiting, request handler
facelock-clibinAll CLI commands, daemon runner, direct mode, benchmarks
pam-facelockcdylibPAM module (libc + toml + serde + zbus only)
facelock-tpmlibOptional TPM-bound encryption for embeddings at rest
facelock-polkitbinPolkit authentication agent for face auth
facelock-test-supportlibMock camera/engine for testing

Face Recognition Pipeline

Camera Frame → SCRFD Detection → 5-point landmarks
  → Affine Alignment → 112x112 face crop
  → ArcFace Embedding → 512-dim L2-normalized vector
  → Cosine Similarity vs stored embeddings → MATCH / NO MATCH

Configuration

All keys are optional. Camera is auto-detected if device.path is omitted. See the Configuration chapter for full reference.

[device]
# path = "/dev/video2"     # auto-detected if omitted (prefers IR)

[recognition]
# threshold = 0.80         # cosine similarity threshold

[daemon]
# mode = "daemon"          # "daemon" or "oneshot"

[security]
# require_ir = true        # refuse auth on RGB cameras
# require_frame_variance = true  # reject photo attacks

Installation

See Quick Start for full instructions.

Privacy & Security

Privacy: Facelock is 100% local. Face detection and recognition run entirely on your hardware via ONNX Runtime. No images, embeddings, or metadata are ever sent to any external server. There is no telemetry, no analytics, no phone-home behavior. Models are downloaded once during setup -- after that, Facelock never touches the network.

Security:

  • IR camera enforcement on by default (anti-spoofing)
  • Frame variance checks reject static photo attacks
  • Constant-time embedding comparison via subtle crate
  • AES-256-GCM encryption at rest with optional TPM-sealed keys
  • Model SHA256 verification at every load
  • D-Bus system bus policy
  • PAM audit logging to syslog
  • Rate limiting (5 attempts/user/60s)
  • systemd service hardening

See Security for the full threat model.

License

Dual-licensed under MIT or Apache 2.0, at your option.

The ONNX face models used by Facelock are licensed separately under the InsightFace non-commercial research license.

Quick Start

Prerequisites

  • Rust 1.85+ (rustup update)
  • just task runner
  • Linux with V4L2 support
  • System dependencies: libv4l-dev libpam0g-dev clang (Debian/Ubuntu) or v4l-utils pam clang (Arch)
  • A webcam (IR recommended for production; RGB works for development)

Development Setup

1. Build

just build

2. Download Models and Enroll

sudo facelock setup     # interactive wizard (camera, models, encryption)
sudo facelock enroll    # capture your face (look at camera)
sudo facelock test      # verify recognition works

No daemon needed -- the CLI auto-falls back to direct mode when no daemon is running.

3. Explore

sudo facelock devices            # list cameras
sudo facelock list               # see enrolled models
sudo facelock preview --text-only  # live detection output
sudo facelock status             # check system status
sudo facelock bench warm-auth    # measure auth latency

4. Run Tests

just check                # unit tests + clippy + fmt
just test-pam             # container PAM smoke tests (no camera)
just test-integration     # end-to-end with camera (daemon mode)
just test-oneshot         # end-to-end with camera (no daemon)
just test-shell           # interactive container shell

System Installation

A broken PAM module can lock you out. Keep a root shell open until you've verified face auth works. See the Testing chapter for details.

Install

just install              # build release + install everything
sudo facelock setup       # download models
sudo facelock enroll      # register your face

This installs the binary, PAM module, systemd service, D-Bus policy, and adds face auth to /etc/pam.d/sudo.

Verify

Open a new terminal and run:

sudo echo "face auth works"

You should see "Identifying face..." and authenticate by looking at the camera.

GPU Acceleration (Optional)

GPU support is runtime-only -- no special build flags needed. The setup wizard (facelock setup) offers CPU or CUDA selection and warns if dependencies are missing.

For manual configuration, install a GPU-enabled ONNX Runtime package:

sudo pacman -S onnxruntime-opt-cuda      # NVIDIA
sudo pacman -S onnxruntime-opt-rocm      # AMD
sudo pacman -S onnxruntime-opt-openvino  # Intel

Set execution_provider in /etc/facelock/config.toml to "cuda", "rocm", or "openvino". CPU is the default.

Uninstall

just uninstall

Config and data are preserved in /etc/facelock and /var/lib/facelock. To remove everything:

sudo rm -rf /etc/facelock /var/lib/facelock /var/log/facelock

Configuration

Config file: /etc/facelock/config.toml (installed) or config/facelock.toml (source).

Key settings:

SettingDefaultDescription
device.pathauto-detectCamera path (prefers IR cameras)
recognition.threshold0.80Cosine similarity threshold
recognition.execution_provider"cpu""cpu", "cuda", "rocm", or "openvino"
daemon.mode"daemon""daemon" or "oneshot"
security.require_irtrueReject RGB-only cameras

Full reference: config/facelock.toml (all keys documented with comments).

Configuration Reference

Facelock reads its configuration from /etc/facelock/config.toml. Override the path with the FACELOCK_CONFIG environment variable.

All settings are optional. Facelock auto-detects the camera and uses sensible defaults. The annotated config file at config/facelock.toml in the repository serves as the canonical example.

[device]

Camera settings.

KeyTypeDefaultDescription
pathstring (optional)Auto-detectCamera device path (e.g., /dev/video2). When omitted, Facelock auto-detects the best available camera, preferring IR over RGB.
max_heightu32480Maximum frame height in pixels. Frames taller than this are downscaled to improve processing speed.
rotationu160Rotate captured frames. Values: 0, 90, 180, 270. Useful for cameras mounted sideways.

[recognition]

Face detection and embedding parameters.

KeyTypeDefaultDescription
thresholdf320.80Cosine similarity threshold for accepting a face match. Must be between 0.0 and 1.0. Higher values are stricter. See the range guide below.
timeout_secsu325Maximum seconds to attempt recognition before giving up. Must be > 0.
detection_confidencef320.5Minimum confidence for the face detector to report a detection. Lower values detect more faces but increase false positives.
nms_thresholdf320.4Non-maximum suppression threshold for overlapping detections.
detector_modelstring"scrfd_2.5g_bnkps.onnx"ONNX detector model filename. Must exist in daemon.model_dir.
embedder_modelstring"w600k_r50.onnx"ONNX embedder model filename. Must exist in daemon.model_dir.
execution_providerstring"cpu"ONNX Runtime execution provider. Values: "cpu", "cuda", "rocm", "openvino". GPU providers require a GPU-enabled ONNX Runtime package installed on the system.
threadsu324Number of CPU threads for ONNX inference.

Threshold range guide (ArcFace cosine similarity)

RangeDescription
0.30 -- 0.50Very loose -- high false accept rate, not recommended
0.50 -- 0.65Loose -- convenient but may accept similar-looking people
0.65 -- 0.80Balanced -- good for most setups, low false accept rate
0.80 -- 0.90Strict -- rarely accepts wrong person, may reject on bad angles
0.90+Very strict -- may require near-ideal lighting and pose

Run facelock test to see your similarity scores, then set the threshold below your typical match score with some margin.

Model tiers

TierDetectorEmbedderTotal sizeNotes
Standardscrfd_2.5g_bnkps.onnx (3MB)w600k_r50.onnx (166MB)~170MBFast, good accuracy (default)
Balancedscrfd_2.5g_bnkps.onnx (3MB)glintr100.onnx (249MB)~252MB~15-30ms slower, better recognition
High accuracydet_10g.onnx (17MB)glintr100.onnx (249MB)~266MB~40-50ms slower, best accuracy

Run facelock setup to select a model tier interactively and download the required models.

[daemon]

Controls how the PAM module reaches the face engine.

KeyTypeDefaultDescription
modestring"daemon""daemon" connects to a persistent daemon via D-Bus system bus (~150-600ms depending on camera state). "oneshot" spawns facelock auth per PAM call (slower, ~700ms+, no background process).
model_dirstring"/var/lib/facelock/models"Directory containing ONNX model files.
idle_timeout_secsu640Shut down the daemon after this many idle seconds. 0 means never. Useful with D-Bus activation.

[storage]

KeyTypeDefaultDescription
db_pathstring"/var/lib/facelock/facelock.db"SQLite database for face embeddings. File permissions should be 640, owned by root:facelock.

[security]

KeyTypeDefaultDescription
disabledboolfalseDisable face authentication entirely. PAM returns IGNORE, falling through to the next auth method.
abort_if_sshbooltrueRefuse face auth when connected via SSH (no camera available).
abort_if_lid_closedbooltrueRefuse face auth when the laptop lid is closed (camera blocked).
require_irbooltrueRequire an IR camera for authentication. RGB cameras are trivially spoofed with a printed photo. Only set to false for development/testing.
require_frame_variancebooltrueRequire multiple frames with different embeddings before accepting. Defends against static photo attacks.
require_landmark_livenessboolfalseRequire landmark movement between frames to pass liveness check. Detects static images by tracking facial landmark positions across frames. Experimental; off by default.
suppress_unknownboolfalseSuppress warnings for unknown users (users with no enrolled face).
min_auth_framesu323Minimum number of matching frames required before accepting. Only applies when require_frame_variance is true.

[security.rate_limit]

KeyTypeDefaultDescription
max_attemptsu325Maximum auth attempts per user per window.
window_secsu6460Rate limit window in seconds.

[notification]

Controls how authentication feedback is delivered.

KeyTypeDefaultDescription
modestring"terminal"Notification mode. "off" -- no notifications. "terminal" -- PAM text prompts only. "desktop" -- desktop popups only (via D-Bus/notify-send). "both" -- terminal and desktop.
notify_promptbooltrueShow prompt when scanning starts ("Identifying face...").
notify_on_successbooltrueNotify on successful face match.
notify_on_failureboolfalseNotify on failed face match.

[snapshots]

Save camera snapshots on auth attempts for debugging or auditing.

KeyTypeDefaultDescription
modestring"off""off" -- never save. "all" -- every attempt. "failure" -- failed auth only. "success" -- successful auth only.
dirstring"/var/log/facelock/snapshots"Directory for snapshot JPEG images.

[encryption]

Controls how face embeddings are encrypted at rest.

KeyTypeDefaultDescription
methodstring"none""none" -- no encryption. "keyfile" -- AES-256-GCM with a plaintext key file. "tpm" -- AES-256-GCM with a TPM-sealed key (recommended if TPM available).
key_pathstring"/etc/facelock/encryption.key"Path to AES-256-GCM key file for keyfile method.
sealed_key_pathstring"/etc/facelock/encryption.key.sealed"Path to TPM-sealed AES key for tpm method.

[audit]

Structured audit logging of authentication events.

KeyTypeDefaultDescription
enabledboolfalseEnable structured audit logging to JSONL file.
pathstring"/var/log/facelock/audit.jsonl"Path to the audit log file.
rotate_size_mbu3210Rotate the log file when it exceeds this size (in MB).

[tpm]

TPM 2.0 settings for sealing the AES encryption key. These settings apply when encryption.method = "tpm".

KeyTypeDefaultDescription
pcr_bindingboolfalseBind sealed key to boot state (PCR values).
pcr_indiceslist of u32[0, 1, 2, 3, 7]PCR registers to verify on unseal.
tctistring"device:/dev/tpmrm0"TPM Communication Interface.

GPU Acceleration

GPU support in Facelock is runtime-only -- no special build flags or recompilation needed. Install a GPU-enabled ONNX Runtime package for your hardware and set execution_provider in the configuration.

Setup

1. Install a GPU-enabled ONNX Runtime

GPU VendorArch Linux PackageOther Distros
NVIDIAonnxruntime-opt-cudaInstall CUDA toolkit + ONNX Runtime with CUDA provider
AMDonnxruntime-opt-rocmInstall ROCm runtime + ONNX Runtime with ROCm provider
Intelonnxruntime-opt-openvinoInstall OpenVINO runtime + ONNX Runtime with OpenVINO provider

On Arch Linux:

sudo pacman -S onnxruntime-opt-cuda      # NVIDIA
sudo pacman -S onnxruntime-opt-rocm      # AMD
sudo pacman -S onnxruntime-opt-openvino  # Intel

2. Set the execution provider

Edit /etc/facelock/config.toml:

[recognition]
execution_provider = "cuda"    # or "rocm" or "openvino"

3. Restart the daemon

facelock restart

4. Verify

facelock bench warm-auth

Compare latency with execution_provider = "cpu" to confirm GPU acceleration is active.

How it works

Facelock uses the ort crate with the load-dynamic feature. At startup, it loads libonnxruntime.so from the system library path. If a GPU-enabled ONNX Runtime is installed, it provides CUDA/ROCm/OpenVINO execution providers automatically. The execution_provider config selects which provider to register.

If the requested provider is not available (e.g., CUDA requested but only CPU ORT installed), Facelock falls back to CPU with a warning.

Supported providers

ProviderConfig valueStatus
CPU"cpu"Default, tested
CUDA (NVIDIA)"cuda"Config ready, requires GPU-enabled ORT
ROCm (AMD)"rocm"Config ready, requires GPU-enabled ORT
OpenVINO (Intel)"openvino"Config ready, requires GPU-enabled ORT

systemd note

The systemd service has MemoryDenyWriteExecute=yes commented out because GPU inference runtimes (CUDA, TensorRT) use JIT compilation which requires writable+executable memory pages. If you are using CPU-only, you can re-enable this directive for additional hardening.

Troubleshooting

  • "Failed to load execution provider": The GPU-enabled ONNX Runtime package is not installed or libonnxruntime.so does not include the requested provider.
  • Slower than CPU: Ensure the GPU driver is loaded (nvidia-smi for NVIDIA, rocm-smi for AMD). Small models like SCRFD 2.5G may not benefit from GPU due to transfer overhead.
  • Daemon crashes on startup: Check journalctl -u facelock-daemon for ORT initialization errors. GPU memory allocation failures are the most common cause.

Architecture

Overview

Facelock is a face authentication system for Linux PAM. It detects faces via SCRFD, extracts embeddings via ArcFace, and matches against stored models using cosine similarity. All processing is local -- no network calls, no cloud services, no telemetry.

System Diagram

┌──────────────────┐     ┌──────────────────────────────────────┐
│  sudo / login    │     │  facelock CLI                          │
│  (PAM stack)     │     │  (enroll, test, list, preview, ...)  │
└────────┬─────────┘     └───────────────┬──────────────────────┘
         │                               │
         │                               │ direct mode (fallback)
         │                               │ or IPC to daemon
    ┌────▼────────────┐                  │
    │  pam_facelock.so  │──────────────────┤
    │  (~2MB cdylib) │                  │
    │                 │                  │
    │  daemon mode:   │                  │
    │  → D-Bus IPC    │          ┌───────▼──────────────┐
    │                 │          │  facelock daemon        │
    │  oneshot mode:  │          │  (persistent process) │
    │  → facelock auth  │          │                       │
    └─────────────────┘          │  ┌─────────────────┐  │
                                 │  │ V4L2 Camera     │  │
                                 │  │ (auto-detected) │  │
                                 │  └────────┬────────┘  │
                                 │           │           │
                                 │  ┌────────▼────────┐  │
                                 │  │ SCRFD Detection  │  │
                                 │  │ → Alignment     │  │
                                 │  │ → ArcFace Embed │  │
                                 │  └────────┬────────┘  │
                                 │           │           │
                                 │  ┌────────▼────────┐  │
                                 │  │ SQLite Store    │  │
                                 │  │ (embeddings)    │  │
                                 │  └─────────────────┘  │
                                 └───────────────────────┘

Crate Dependencies

facelock-core (config, types, IPC, traits)
    ├── facelock-camera (V4L2, auto-detect, preprocessing)
    ├── facelock-face (ONNX: SCRFD + ArcFace)
    ├── facelock-store (SQLite)
    ├── facelock-tpm (optional TPM encryption)
    └── facelock-test-support (mocks, dev-only)

facelock-daemon (auth/enroll logic, liveness, audit, rate limiter, handler)
    └── depends on: core, camera, face, store, tpm

facelock-cli (unified binary)
    └── depends on: core, camera, face, store, daemon, tpm

facelock-polkit (polkit agent)
    └── depends on: core

pam-facelock (PAM module)
    └── depends on: libc, toml, serde, zbus ONLY (no facelock crates)

Mermaid Diagrams

The diagrams below render in GitHub, mdBook, and any Mermaid-capable viewer. They cover the same information as the ASCII diagrams above but add external integrations and the authentication data flow.

Crate Dependency Graph

graph TD
    subgraph Workspace Crates
        core[facelock-core<br/><i>config, types, errors,<br/>D-Bus interface, traits</i>]
        camera[facelock-camera<br/><i>V4L2 capture, preprocessing</i>]
        face[facelock-face<br/><i>ONNX: SCRFD + ArcFace</i>]
        store[facelock-store<br/><i>SQLite embeddings</i>]
        tpm[facelock-tpm<br/><i>TPM / AES-256-GCM</i>]
        test[facelock-test-support<br/><i>mocks, fixtures</i>]
        daemon[facelock-daemon<br/><i>auth, enroll, rate limit,<br/>liveness, audit</i>]
        cli[facelock-cli<br/><i>unified binary</i>]
        polkit[facelock-polkit<br/><i>Polkit auth agent</i>]
        pam[pam-facelock<br/><i>PAM module cdylib</i>]
    end

    camera --> core
    face --> core
    store --> core
    tpm --> core
    test -.-> core
    polkit --> core

    daemon --> core
    daemon --> camera
    daemon --> face
    daemon --> store
    daemon --> tpm

    cli --> core
    cli --> camera
    cli --> face
    cli --> store
    cli --> daemon
    cli --> tpm

    pam -. "no facelock crates<br/>(libc, toml, serde, zbus)" .-> pam

    style pam fill:#f9f,stroke:#333
    style core fill:#bbf,stroke:#333
    style daemon fill:#bfb,stroke:#333
    style cli fill:#bfb,stroke:#333

System Data Flow and IPC

flowchart LR
    subgraph Clients
        login[sudo / login<br/><i>PAM stack</i>]
        cliclient[facelock CLI]
    end

    subgraph IPC
        dbus[[D-Bus<br/>system bus]]
    end

    subgraph Daemon["facelock daemon"]
        direction TB
        cam[facelock-camera<br/><i>V4L2 capture +<br/>preprocessing</i>]
        det[facelock-face<br/><i>SCRFD detection +<br/>alignment</i>]
        emb[facelock-face<br/><i>ArcFace embedding</i>]
        st[facelock-store<br/><i>load stored<br/>embeddings</i>]
        match[facelock-core<br/><i>constant-time<br/>cosine match</i>]

        cam --> det --> emb --> st --> match
    end

    subgraph External Systems
        v4l2[(V4L2<br/>camera)]
        onnx[(ONNX<br/>Runtime)]
        sqlite[(SQLite)]
        tpmd[(TPM)]
        sysd[systemd]
        syslog[syslog]
        polkitd[polkit]
    end

    login -->|pam_facelock.so| dbus
    cliclient -->|zbus client| dbus
    dbus --> Daemon

    cam ---|capture| v4l2
    det ---|inference| onnx
    emb ---|inference| onnx
    st ---|query| sqlite
    Daemon ---|optional key sealing| tpmd
    Daemon ---|service activation| sysd
    Daemon ---|audit logging| syslog

    polkitagent[facelock-polkit] --> polkitd
    polkitagent --> dbus

    style dbus fill:#ff9,stroke:#333
    style Daemon fill:#eef,stroke:#339
    style match fill:#bfb,stroke:#333

Face Recognition Pipeline

Detection (SCRFD)

  • Input: grayscale frame after CLAHE enhancement
  • Output: bounding boxes + 5-point landmarks (eyes, nose, mouth corners)
  • Confidence threshold: recognition.detection_confidence (default 0.5)
  • NMS threshold: recognition.nms_threshold (default 0.4)

Alignment

  • Affine transform from 5 landmarks to canonical positions
  • Output: 112x112 aligned face crop
  • Uses Umeyama similarity transform

Embedding (ArcFace)

  • Input: 112x112 RGB face crop
  • Output: 512-dimensional L2-normalized float32 vector
  • Cosine similarity = dot product (since L2-normalized)

Matching

  • Compare live embedding against all stored embeddings for the user
  • Accept if best similarity >= recognition.threshold (default 0.80)
  • Frame variance check: multiple frames must show different embeddings (anti-photo)

Auth Flow

1. Pre-checks (disabled? SSH? lid closed? has models? rate limit? IR?)
2. Load user embeddings from store
3. Capture loop (until deadline):
   a. Capture frame
   b. Skip if dark
   c. Detect faces
   d. For each face: compute best_match against stored embeddings
   e. Track matched frames for variance check
   f. If variance passes (or disabled): return match
4. If timeout: return no_match

Operating Modes

Daemon Mode

The daemon (facelock daemon) runs persistently, holding ONNX models and camera resources in memory. The PAM module and CLI connect via D-Bus system bus. Benefits:

  • ~600ms typical auth latency (~150ms with warm camera)
  • Camera stays warm between back-to-back requests
  • Single point of resource management

Oneshot Mode

The PAM module spawns facelock auth --user X for each auth attempt. The process loads models, opens camera, runs one auth cycle, and exits. Benefits:

  • No background process
  • No systemd dependency
  • Works on any Linux system

Direct CLI Mode

The CLI silently detects whether the daemon via D-Bus is available. If yes, uses IPC. If no, operates directly (opens camera, loads models inline). The user doesn't need to know which mode is active.

Security Layers

  1. IR enforcement: Only IR cameras allowed by default (prevents RGB photo attacks)
  2. Frame variance: Multiple frames must show micro-movement (prevents static photo)
  3. Rate limiting: 5 attempts per user per 60 seconds
  4. Model integrity: SHA256 verification at every load
  5. D-Bus security: System bus policy restricts daemon access
  6. Audit trail: All auth events logged to syslog
  7. Process hardening: systemd service runs with ProtectSystem=strict, NoNewPrivileges, etc.

CLI Reference

All commands are subcommands of the facelock binary.

facelock setup

Interactive setup wizard. Walks through camera selection, model quality, inference device (CPU/CUDA), model downloads, encryption, enrollment, and PAM configuration. Can also be run with flags for individual setup tasks.

facelock setup                          # interactive wizard
facelock setup --systemd                # install systemd units
facelock setup --systemd --disable      # disable systemd units
facelock setup --pam                    # install to /etc/pam.d/sudo
facelock setup --pam --service login    # install to specific service
facelock setup --pam --remove           # remove PAM line
facelock setup --pam --service sshd -y  # skip confirmation for sensitive services

facelock enroll

Capture and store a face model.

facelock enroll                         # current user, auto-label
facelock enroll --user alice            # specific user
facelock enroll --label "office"        # specific label

Captures 3-10 frames over ~15 seconds. Requires exactly one face per frame. Re-enrolling with the same label replaces the previous model.

facelock test

Test face recognition against enrolled models.

facelock test                           # current user
facelock test --user alice              # specific user

Reports match similarity and latency.

facelock list

List enrolled face models.

facelock list                           # current user
facelock list --user alice              # specific user
facelock list --json                    # JSON output

facelock remove

Remove a specific face model by ID.

facelock remove 3                       # remove model #3
facelock remove 3 --user alice          # for specific user
facelock remove 3 --yes                 # skip confirmation

facelock clear

Remove all face models for a user.

facelock clear                          # current user
facelock clear --user alice --yes       # skip confirmation

facelock preview

Live camera preview with face detection overlay.

facelock preview                        # Wayland graphical window
facelock preview --text-only            # JSON output to stdout
facelock preview --user alice           # match against specific user

Text-only mode outputs one JSON object per frame:

{"frame":1,"fps":15.2,"width":640,"height":480,"recognized":1,"unrecognized":0,"faces":[...]}

facelock devices

List available V4L2 video capture devices.

facelock devices

Shows device path, name, driver, formats, resolutions, and IR status.

facelock status

Check system status -- config, daemon, camera, models.

facelock status

facelock config

Show or edit the configuration file.

facelock config                         # show config path and contents
facelock config --edit                  # open in $EDITOR

facelock daemon

Run the persistent authentication daemon.

facelock daemon                         # use default config
facelock daemon --config /path/to/config.toml

Normally managed by systemd, not run manually.

facelock auth

One-shot authentication. Used by the PAM module in oneshot mode.

facelock auth --user alice              # authenticate
facelock auth --user alice --config /etc/facelock/config.toml

Exit codes: 0 = matched, 1 = no match, 2 = error.

facelock tpm status

Report TPM availability and configuration.

facelock tpm status

facelock bench

Benchmark and calibration tools.

facelock bench cold-auth                # cold start authentication latency
facelock bench warm-auth                # warm authentication latency
facelock bench model-load               # model loading time
facelock bench report                   # full benchmark report

facelock restart

Restart the persistent daemon. On systemd systems, runs systemctl restart facelock-daemon.service. Otherwise, sends a D-Bus shutdown request and the daemon restarts on next use via D-Bus activation.

facelock restart

User Resolution

For commands that accept --user:

  1. Explicit --user flag (highest priority)
  2. SUDO_USER environment variable
  3. DOAS_USER environment variable
  4. Current user ($USER or getpwuid)

Environment Variables

VariablePurpose
FACELOCK_CONFIGOverride config file path
RUST_LOGControl log verbosity (e.g., facelock_daemon=debug)

Security Model

Threat Model

Facelock is a local biometric authentication system. The threat model assumes:

  • Attacker has physical access to the machine (the entire point of face auth is physical-presence scenarios like unlocking a laptop)
  • Attacker may have a photo or video of the enrolled user
  • Attacker does not have root (if they do, game over regardless)
  • Attacker cannot modify files in /etc/facelock/, /var/lib/facelock/, or /lib/security/

Privacy Guarantees

Facelock is designed to keep biometric data under the user's exclusive control:

  • Local-only inference: All face detection and recognition runs on-device via ONNX Runtime. No images, embeddings, or metadata are ever transmitted over the network.
  • No telemetry: Facelock contains zero analytics, tracking, or phone-home code. After the one-time model download during facelock setup, it never contacts any server.
  • No cloud dependencies: Authentication works fully offline. No account registration, no API keys, no external services.
  • Data stays on disk: Face embeddings are stored in a local SQLite database (/var/lib/facelock/facelock.db) with restrictive permissions (640, root:facelock). Optional AES-256-GCM encryption with TPM-sealed keys provides defense in depth.
  • Open source: All code is MIT/Apache-2.0 licensed. No proprietary blobs or obfuscated network calls. Privacy claims are verifiable by reading the source.

Attack Vectors & Mitigations

1. Photo/Video Spoofing (CRITICAL)

Attack: Hold a photo or video of the enrolled user in front of the camera.

Why this matters: This is the #1 attack against face authentication. Without mitigation, anyone with a Facebook photo can unlock the machine.

Mitigations (layered, implement all):

A. IR Camera Enforcement (Required)

security.require_ir config flag, default true:

[security]
require_ir = true  # Refuse to authenticate on RGB-only cameras

Rationale: Phone screens and printed photos do not emit infrared light correctly. An IR camera sees a flat, textureless surface where a real face would have depth and skin texture in IR. This single check eliminates the vast majority of spoofing attacks.

Limitation: IR camera detection by format/name is heuristic. Some cameras report YUYV but are actually IR. The facelock devices command should display whether each camera is detected as IR.

B. Frame Variance Check (Required)

Require minimum variance across consecutive frames during authentication. Real faces have micro-movements causing slight embedding variation. A static photo produces near-identical embeddings (similarity > 0.99).

Config:

[security]
require_frame_variance = true  # Reject static images (photo attack defense)
min_auth_frames = 3            # Minimum frames before accepting match

In IR mode, verify that the face region has expected IR texture characteristics:

  • Real skin has micro-texture visible in IR
  • Photos/screens appear as flat, uniform surfaces in IR
  • Compute standard deviation of pixel intensity within the face bounding box
  • Reject faces with abnormally low texture variance

2. Model Tampering

Attack: Replace ONNX model files with adversarial models that always match (or match specific attackers).

Mitigations:

A. SHA256 Verification at Load Time (Required)

Verify model integrity not just at download, but every time the daemon loads models. Tampered files are rejected before any inference runs.

B. File Permissions on Model Directory (Required)

# Models owned by root, not writable by others
chown -R root:root /var/lib/facelock/models
chmod 755 /var/lib/facelock/models
chmod 644 /var/lib/facelock/models/*.onnx

3. Embedding / Database Security

Attack: Read or modify the SQLite database to extract biometric data or inject fake embeddings.

Mitigations:

A. Database File Permissions (Required)

# Database owned by root, readable only by root and facelock group
chown root:facelock /var/lib/facelock/facelock.db
chmod 640 /var/lib/facelock/facelock.db

B. Embedding Sensitivity Warning

Face embeddings are biometric data. Unlike passwords, they cannot be changed. The database contains irreversible biometric templates -- if compromised, the user's face embeddings cannot be "rotated" like a password.

C. Encryption at Rest (Implemented)

For high-security deployments, embeddings can be encrypted with AES-256-GCM using either a plaintext key file (encryption.method = "keyfile") or a TPM-sealed key (encryption.method = "tpm"). The TPM method seals the AES key at rest; it is unsealed at daemon startup and held in memory. See Configuration for the [encryption] and [tpm] sections.

4. D-Bus IPC Security

Attack: Unauthorized user connects to the daemon via D-Bus to trigger auth, enroll faces, or extract data.

Mitigations:

A. D-Bus System Bus Policy (Required)

The D-Bus system bus policy (/etc/dbus-1/system.d/org.facelock.Daemon.conf) restricts which users and groups can own the bus name and invoke methods. Only root and members of the facelock group are granted access.

B. D-Bus Message Size Limits (Required)

The D-Bus bus daemon enforces message size limits, preventing memory exhaustion attacks.

Throttle authentication attempts: 5 per user per 60 seconds by default. Prevents brute-force and rapid-retry attacks.

5. PAM Module Hardening

A. Audit Logging (Required)

All authentication attempts are logged to syslog with user, service, and outcome:

pam_facelock(sudo): match for user alice
pam_facelock(sudo): no_match for user bob

This creates an audit trail in /var/log/auth.log or journald.

Allow different PAM services to have different security levels:

[security.pam_policy]
allowed_services = ["sudo", "polkit-1"]
denied_services = ["login", "sshd", "su"]

6. Daemon Process Hardening

After initialization, the daemon drops all unnecessary capabilities.

B. systemd Hardening (Required)

The systemd unit includes: ProtectSystem=strict, ProtectHome=yes, NoNewPrivileges=yes, PrivateTmp=yes, and other sandboxing directives.

Security Configuration Reference

[security]
disabled = false
abort_if_ssh = true          # Refuse face auth over SSH
abort_if_lid_closed = true   # Refuse if laptop lid closed
require_ir = true            # CRITICAL: refuse RGB-only cameras (anti-spoof)
require_frame_variance = true # Reject static images (photo defense)
require_landmark_liveness = false # Require landmark movement between frames (off by default)
min_auth_frames = 3          # Minimum frames before accepting (variance check)

[notification]
enabled = true               # Show "Identifying face..." on login screen

[security.pam_policy]
allowed_services = ["sudo", "polkit-1"]
denied_services = ["login", "sshd"]

[security.rate_limit]
max_attempts = 5             # Max auth attempts per user
window_secs = 60             # Rate limit window

Summary: Security Implementation Priority

PriorityMitigation
P0IR camera enforcement (require_ir)
P0Frame variance check (anti-photo)
P0Model SHA256 at load time
P0D-Bus system bus policy
P0D-Bus message size limits
P0PAM audit logging
P0Database file permissions
P1IR texture validation
P1Rate limiting
P1systemd hardening
P1Capability dropping
P1Service-specific PAM policy
P2Embedding encryption at rest
P2Memory zeroing on drop
P2Constant-time similarity comparison

Troubleshooting

Camera not detected

Symptom: facelock devices shows no cameras, or facelock enroll fails with "no camera found".

Steps:

  1. Check that your camera is recognized by the kernel:
    ls /dev/video*
    v4l2-ctl --list-devices
    
  2. Verify your user has access to the video device:
    groups  # should include "video"
    sudo usermod -aG video $USER  # add yourself, then log out and back in
    
  3. If /dev/video* exists but Facelock skips it, set the device explicitly:
    [device]
    path = "/dev/video0"
    
  4. Some cameras expose multiple /dev/video* nodes (capture + metadata). Try each one.

"No IR camera" error when IR camera is available

Symptom: security.require_ir = true (the default) rejects your camera even though it supports IR.

Steps:

  1. Check what Facelock detects:
    facelock devices
    
  2. IR detection looks for keywords like "ir" or "infrared" in the device name and checks for grayscale formats (GREY, Y16). Some cameras do not advertise IR in their name.
  3. If you are certain your camera is IR, check its V4L2 capabilities:
    v4l2-ctl -d /dev/video2 --list-formats-ext
    
  4. As a workaround, you can set security.require_ir = false -- but understand this weakens spoofing resistance. Only do this for testing.

Auth too slow

First-start latency (~700ms -- 2s)

The first authentication after boot (or after the daemon starts) is slow because ONNX models must be loaded into memory. This is normal. Subsequent auths in daemon mode typically take ~600ms (or ~150ms if the camera is still warm from a recent auth).

Consistently slow (~700ms+ every time)

You may be running in oneshot mode. Check your config:

[daemon]
mode = "daemon"  # default; uses persistent daemon

With daemon mode, enable D-Bus activation:

sudo facelock setup --systemd

Inference slow on CPU

Try reducing frame resolution or switching to the smaller model set:

[device]
max_height = 320

[recognition]
detector_model = "scrfd_2.5g_bnkps.onnx"
embedder_model = "w600k_r50.onnx"
threads = 4  # increase if you have more cores

PAM lockout recovery

A broken PAM module can lock you out of your system. Always keep a root shell open when testing PAM changes.

If you are locked out

  1. Boot into single-user/recovery mode (GRUB: edit the boot entry, add single or init=/bin/bash to the kernel line).
  2. Remount the filesystem read-write:
    mount -o remount,rw /
    
  3. Restore the PAM backup:
    cp /etc/pam.d/sudo.facelock-backup /etc/pam.d/sudo
    
    Or remove the Facelock line from /etc/pam.d/sudo:
    sed -i '/pam_facelock/d' /etc/pam.d/sudo
    
  4. Reboot normally.

If you still have a root shell open

# From your root shell:
cp /etc/pam.d/sudo.facelock-backup /etc/pam.d/sudo

Prevention

  • Always test in containers first (just test-pam).
  • Keep a root shell open during PAM testing.
  • Start with sudo only -- do not add Facelock to login or sddm until sudo works reliably.
  • Set security.disabled = true as an emergency kill switch (PAM returns IGNORE).

systemd unit not starting

Symptom: systemctl status facelock-daemon.service shows failed or inactive.

Steps:

  1. Check the journal:
    journalctl -u facelock-daemon.service -n 50 --no-pager
    
  2. Verify the service unit is enabled:
    systemctl status facelock-daemon.service
    systemctl enable --now facelock-daemon.service
    
  3. Check that the binary exists:
    which facelock
    ls -la /usr/bin/facelock
    
  4. Check model files exist:
    ls -la /var/lib/facelock/models/
    
  5. Manual test run (should print errors to stderr):
    sudo /usr/bin/facelock daemon
    

Known issue: ONNX runtime crashes under restrictive systemd sandboxing

The ONNX runtime requires access to /dev/null, /dev/urandom, and /proc/sys. If you have customized the systemd unit with DevicePolicy=closed, ProtectKernelTunables=yes, or ProtectProc=invisible, the daemon may crash before main() with no stderr output. Use the default unit file or add restrictions incrementally, testing each one.

Model download failures

Symptom: facelock setup fails to download models.

Steps:

  1. Check network connectivity.
  2. Try downloading manually:
    curl -L -o /var/lib/facelock/models/scrfd_2.5g_bnkps.onnx \
      "https://github.com/visomaster/visomaster-assets/releases/download/v0.1.0/scrfd_2.5g_bnkps.onnx"
    curl -L -o /var/lib/facelock/models/w600k_r50.onnx \
      "https://github.com/visomaster/visomaster-assets/releases/download/v0.1.0/w600k_r50.onnx"
    
  3. Verify SHA256 checksums match (Facelock checks these at model load time and rejects tampered files).
  4. Ensure the model directory exists and has correct permissions:
    sudo mkdir -p /var/lib/facelock/models
    sudo chown root:root /var/lib/facelock/models
    sudo chmod 755 /var/lib/facelock/models
    

Permission issues

"Permission denied" when running facelock commands

Ensure your user is in the facelock group:

groups  # check current groups
sudo usermod -aG facelock $USER
# Log out and back in for group changes to take effect

Database permission errors

The SQLite database requires specific permissions:

sudo chown root:facelock /var/lib/facelock/facelock.db
sudo chmod 640 /var/lib/facelock/facelock.db
# The directory needs group write for SQLite WAL files:
sudo chmod 770 /var/lib/facelock

PAM module cannot reach daemon

The daemon is activated via D-Bus. Verify:

busctl status org.facelock.Daemon
systemctl status facelock-daemon.service

Debugging with RUST_LOG

Facelock uses the tracing crate with RUST_LOG env-filter syntax.

# Verbose output for all facelock crates:
RUST_LOG=debug facelock test

# Trace a specific crate:
RUST_LOG=facelock_camera=trace facelock devices

# Multiple filters:
RUST_LOG=facelock_daemon=debug,facelock_face=trace facelock daemon

sudo strips environment variables

sudo sanitizes the environment by default. Use env to preserve RUST_LOG:

sudo env RUST_LOG=debug facelock test
sudo env RUST_LOG=facelock_daemon=trace facelock daemon

Useful log targets

TargetWhat it shows
facelock_cameraCamera detection, format negotiation, frame capture
facelock_faceModel loading, inference timing, similarity scores
facelock_daemonIPC handling, rate limiting, auth flow
facelock_storeDatabase operations, embedding storage
pam_facelockPAM module decisions (logged to syslog)

System Contracts

Stable contracts. Do not change without updating this document.

Binaries

BinaryCratePurpose
facelockfacelock-cliUnified CLI (daemon, auth, enroll, test, setup, etc.)
pam_facelock.sopam-facelockPAM authentication module

CLI Subcommands

CommandPurpose
facelock setupDownload models, create directories
facelock setup --systemdInstall/enable systemd units
facelock setup --pamInstall PAM module to /etc/pam.d/
facelock enrollCapture and store a face
facelock testTest face recognition
facelock listList enrolled face models
facelock remove <id>Remove a specific model
facelock clearRemove all models for a user
facelock previewLive camera preview
facelock devicesList V4L2 cameras
facelock statusCheck system status
facelock configShow/edit configuration
facelock daemonRun persistent daemon
facelock auth --user XOne-shot auth (PAM helper)
facelock tpm statusTPM status
facelock benchBenchmarks
facelock restartRestart daemon

Operating Modes

ModeConfigPAM BehaviorCLI Behavior
Daemondaemon.mode = "daemon" (default)D-Bus IPC to daemonUses daemon if available, falls back to direct
Oneshotdaemon.mode = "oneshot"Spawns facelock authOperates directly (no daemon)

The CLI silently falls back to direct mode when the daemon is not available on D-Bus, regardless of config mode.

facelock auth Exit Codes

CodeMeaningPAM Code
0Face matchedPAM_SUCCESS
1No match / timeout / darkPAM_AUTH_ERR
2Error / no enrolled facesPAM_IGNORE

Filesystem Paths

PathOwnerModePurpose
/etc/facelock/config.tomlroot:root644Configuration
/var/lib/facelock/facelock.dbroot:facelock640Face embeddings
/var/lib/facelock/models/root:root755ONNX models
/var/log/facelock/snapshots/root:facelock750Auth snapshots
/usr/bin/facelockroot:root755CLI binary
/lib/security/pam_facelock.soroot:root755PAM module

All paths overridable via config. FACELOCK_CONFIG env var overrides config location.

Config Schema

TOML format. All keys optional -- camera auto-detected, sensible defaults for everything. See Configuration for the full reference.

Sections

SectionKey fields
[device]path (Option), max_height, rotation
[recognition]threshold, timeout_secs, detector_model, embedder_model, threads, execution_provider
[daemon]mode (DaemonMode enum), model_dir, idle_timeout_secs
[storage]db_path
[security]require_ir, require_frame_variance, min_auth_frames, abort_if_ssh, abort_if_lid_closed, rate_limit sub-section
[notification]mode (off/terminal/desktop/both), notify_prompt, notify_on_success, notify_on_failure
[snapshots]mode (off/all/failure/success), dir
[encryption]method (none/keyfile/tpm), key_path, sealed_key_path
[audit]enabled, path, rotate_size_mb
[tpm]pcr_binding, pcr_indices, tcti

Camera Auto-Detection

When device.path is omitted:

  1. Enumerate /dev/video0 through /dev/video63
  2. Filter to VIDEO_CAPTURE devices
  3. Prefer IR cameras (name contains "ir"/"infrared", or supports GREY/Y16 format)
  4. Fall back to first available device

Database Schema

SQLite with WAL mode and foreign keys:

CREATE TABLE face_models (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user TEXT NOT NULL,
    label TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    UNIQUE(user, label)
);

CREATE TABLE face_embeddings (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    model_id INTEGER NOT NULL REFERENCES face_models(id) ON DELETE CASCADE,
    embedding BLOB NOT NULL  -- 512 x f32 = 2048 bytes
);

IPC Protocol

D-Bus system bus (org.facelock.Daemon). Only used in daemon mode. The daemon exposes a D-Bus interface on the system bus, and both the PAM module and CLI connect as D-Bus clients. Access is controlled by D-Bus system bus policy (/etc/dbus-1/system.d/org.facelock.Daemon.conf).

Methods

Authenticate, Enroll, ListModels, RemoveModel, ClearModels, PreviewFrame, PreviewDetectFrame, ListDevices, ReleaseCamera, Ping, Shutdown

Return Types

AuthResult, Enrolled, Models, Removed, Frame, DetectFrame, Devices, Ok, Error

PAM Semantics

OutcomePAM Code
Face matchedPAM_SUCCESS (0)
No matchPAM_AUTH_ERR (7)
Daemon unavailable / errorPAM_IGNORE (25)
TimeoutPAM_AUTH_ERR (7)

PAM module never blocks indefinitely. All operations have timeouts.

Syslog Format

pam_facelock(<service>): <result> for user <username>

Anti-Spoofing

DefenseConfigDefault
IR camera enforcementsecurity.require_irtrue
Frame variance checksecurity.require_frame_variancetrue
Landmark livenesssecurity.require_landmark_livenessfalse
Minimum auth framessecurity.min_auth_frames3
Variance thresholdFRAME_VARIANCE_THRESHOLD0.998

These defaults must not be weakened without security review.

Models

ModelFileSizeDefault
SCRFD 2.5Gscrfd_2.5g_bnkps.onnx~3MBYes
ArcFace R50w600k_r50.onnx~166MBYes
SCRFD 10Gdet_10g.onnx~16MBOptional
ArcFace R100glintr100.onnx~249MBOptional

Configurable via recognition.detector_model and recognition.embedder_model.

Compatibility

System Requirements

ComponentRequirement
OSLinux (kernel 4.14+ for V4L2)
Architecturex86_64 (ONNX Runtime binaries)
Rust1.85+ (edition 2024)
CameraV4L2-compatible (USB webcam, built-in IR)
PAMLinux-PAM (pam 1.5+)

Tested Distributions

DistributionInit SystemModeStatus
Arch Linuxsystemddaemon + D-Bus activationPrimary target
Arch LinuxsystemdoneshotTested
Container (Arch)nonedaemon (manual)CI-tested
Container (Arch)noneoneshotCI-tested

Expected to Work (untested)

DistributionInit SystemMode
Fedora 38+systemddaemon + D-Bus activation
Ubuntu 22.04+systemddaemon + D-Bus activation
Debian 12+systemddaemon + D-Bus activation
Any Linuxany / noneoneshot
Void Linuxrunitoneshot or manual daemon
Alpine LinuxOpenRConeshot or manual daemon
GentooOpenRC / systemdoneshot or daemon

Camera Compatibility

IR cameras provide anti-spoofing protection. Facelock auto-detects IR cameras by:

  • Device name containing "ir" or "infrared"
  • Supporting GREY or Y16 pixel formats

Known working:

  • Logitech BRIO (IR mode)
  • Intel RealSense (IR stream)
  • Most laptops with Windows Hello IR cameras

RGB Cameras (development only)

RGB cameras work with security.require_ir = false but provide no anti-spoofing. Any photo of the enrolled user will authenticate.

Format Support

FormatSupportNotes
MJPGFullMost common USB camera format
YUYVFullRaw format, converted to RGB
GREYFullIR cameras, replicated to RGB
OtherNot supportedCamera negotiates to supported format

Init System Support

Full support via D-Bus activation:

sudo facelock setup --systemd

Features:

  • D-Bus activation (daemon starts on first connection)
  • Idle timeout (daemon stops when idle)
  • Service hardening (ProtectSystem, NoNewPrivileges, etc.)
  • Automatic restart on failure

Non-systemd

Use oneshot mode (no daemon needed):

[daemon]
mode = "oneshot"

Or manage the daemon manually:

facelock daemon &                    # start
kill $(pidof facelock)               # stop

For process supervisors (runit, s6, dinit, OpenRC), create a service that runs facelock daemon. The daemon handles SIGTERM for graceful shutdown.

PAM Stack Compatibility

Facelock works with standard Linux-PAM. The module is installed as:

auth  sufficient  pam_facelock.so

Tested PAM Services

ServiceFileNotes
sudo/etc/pam.d/sudoPrimary target, safest to test first
polkit/etc/pam.d/polkit-1GUI privilege escalation
ServiceReason
system-authAffects ALL auth -- test sudo first
loginConsole login -- hard to recover if broken
sshdSSH has no camera -- always fails

Build Dependencies

Runtime

  • pam (Linux-PAM library)
  • gcc-libs (C runtime)

Build

  • rust + cargo (1.85+)
  • clang (for ONNX Runtime bindings)
  • System headers: libv4l-dev, libxkbcommon-dev, libpam0g-dev (names vary by distro)

Optional

  • tpm2-tss -- TPM2 support for embedding encryption
  • podman or docker -- container testing

ONNX Runtime

Facelock uses the ort crate (Rust bindings for ONNX Runtime). The runtime binary is downloaded at build time via the download-binaries feature.

Execution Providers

GPU support is runtime-only -- no special build flags needed. Install a GPU-enabled ONNX Runtime package and set execution_provider in config.

ProviderConfigRuntime RequirementStatus
CPUexecution_provider = "cpu"none (default)Working
CUDA (NVIDIA)execution_provider = "cuda"CUDA toolkit + GPU-enabled ORTConfig ready, untested
ROCm (AMD)execution_provider = "rocm"ROCm runtime + GPU-enabled ORTConfig ready, untested
OpenVINO (Intel)execution_provider = "openvino"OpenVINO runtime + GPU-enabled ORTConfig ready, untested

CPU is the default and only tested provider.

Testing & Safety Strategy

READ THIS BEFORE implementing anything PAM-related.

The Golden Rule

Never install pam_facelock.so on the host or edit /etc/pam.d/* until validated in container. A broken PAM module can lock you out of sudo, login, and su.

Testing Tiers

Tier 1: Unit Tests (always safe)

cargo test --workspace
cargo clippy --workspace -- -D warnings

Covers: config parsing, format conversion, NMS, cosine similarity, IPC serialization, SQLite CRUD, frame variance logic. No hardware, no root.

Tier 2: Hardware Integration (camera + models)

cargo test --workspace -- --ignored

Requires camera and downloaded ONNX models. Tests capture, model loading, full pipeline.

Tier 3: Container Tests (requires podman)

just test-pam             # PAM smoke tests (no camera needed)
just test-integration     # full E2E with camera (daemon mode)
just test-oneshot         # full E2E with camera (no daemon)
just test-shell           # interactive shell for manual testing

Container tests validate:

  • PAM module loads without crashing
  • Returns PAM_IGNORE when daemon unavailable
  • Handles missing/invalid config
  • Exports correct PAM symbols
  • End-to-end: enroll, list, test, PAM auth, clear
  • Both daemon and oneshot modes

Disposable VM with snapshots. USB camera passthrough for real hardware testing. Verify sudo, su, login scenarios with rollback safety.

Tier 5: Host PAM Installation

Safety checklist:

  1. Open root shell in separate terminal -- keep it open
  2. sudo cp /etc/pam.d/sudo /etc/pam.d/sudo.bak
  3. sudo facelock setup --pam --service sudo
  4. Test in NEW terminal: sudo echo test
  5. If broken, revert from root shell: sudo cp /etc/pam.d/sudo.bak /etc/pam.d/sudo
  6. Never modify system-auth or login until sudo works perfectly

Emergency recovery: boot from USB, mount partition, remove PAM line, reboot.

Development Workflow

Setup

export FACELOCK_CONFIG=dev/config.toml
cargo build --workspace
cargo run --bin facelock -- setup       # download models

No-Daemon Development

All CLI commands work without a daemon -- the CLI falls back to direct mode silently:

facelock enroll
facelock test
facelock list
facelock devices

Daemon Development

facelock daemon &
facelock enroll       # uses daemon (faster for repeated commands)
facelock test
kill %1             # stop daemon

Logging

Control via RUST_LOG environment variable:

RUST_LOG=facelock_daemon=debug facelock daemon    # verbose daemon
RUST_LOG=facelock_cli=debug facelock test         # verbose CLI

Dev Config

dev/config.toml -- temp paths, no root, camera auto-detected:

[device]
max_height = 480

[daemon]
model_dir = "./models"

[storage]
db_path = "/tmp/facelock-dev.db"

[security]
require_ir = true
require_frame_variance = true

CI

GitHub Actions at .github/workflows/ci.yml:

  • Build + test + clippy + fmt check
  • Container PAM smoke tests

Local full CI: bash test/run-tests.sh

Test Files

FilePurpose
test/ContainerfileContainer image (Arch + pamtester)
test/run-tests.shCI script (unit + lint + PAM symbols)
test/run-container-tests.shPAM smoke tests
test/run-integration-tests.shE2E with camera (daemon)
test/run-oneshot-tests.shE2E with camera (oneshot)
test/pam.d/facelock-testTest PAM config

Just Recipes

RecipeDescription
just testUnit tests
just lintClippy
just checktest + lint + fmt
just test-pamContainer PAM smoke
just test-integrationE2E daemon mode
just test-oneshotE2E oneshot mode
just test-shellInteractive container
just installSystem install (root)

Contributing

Prerequisites

  • Rust 1.85+ (rustup update)
  • Linux with V4L2 support
  • A webcam (IR recommended; RGB works for development)
  • Podman (for container tests)

Building

cargo build --workspace

Workspace structure

Facelock is a Cargo workspace with 10 crates:

CrateTypePurpose
facelock-corelibConfig, types, errors, D-Bus interface, traits
facelock-cameralibV4L2 capture, auto-detection, preprocessing
facelock-facelibONNX inference (SCRFD + ArcFace)
facelock-storelibSQLite face embedding storage
facelock-daemonlibAuth/enroll logic, liveness, audit, rate limiting, handler
facelock-clibinUnified CLI (facelock binary, includes bench subcommand)
pam-facelockcdylibPAM module (libc + toml + serde + zbus only)
facelock-tpmlibOptional TPM-bound encryption for embeddings at rest
facelock-polkitbinPolkit authentication agent for face auth
facelock-test-supportlibMocks and fixtures for testing

Version is declared once in the root Cargo.toml and inherited via version.workspace = true. Inter-crate dependencies use relative paths.

Code style

  • Error handling: thiserror for library error types, anyhow in binaries. Return Result<T> over panicking. Never unwrap() in library code.
  • Logging: tracing for structured logging. Control verbosity via RUST_LOG env filter.
  • Tests: #[cfg(test)] modules in each source file.
  • Formatting: cargo fmt (default rustfmt settings).
  • Linting: cargo clippy --workspace -- -D warnings must pass with zero warnings.

Dependency rules

The PAM module (pam-facelock) must stay lightweight: libc, toml, serde, zbus only. No ort, no v4l, no facelock-core. This keeps the shared library small and avoids dragging heavy dependencies into every PAM-using process.

Each crate has a defined dependency boundary. See the Contracts chapter for the full table.

Testing

Tier 1: Unit tests (no hardware)

cargo test --workspace
cargo clippy --workspace -- -D warnings

Run these before every commit. They require no camera or models.

Tier 2: Hardware tests (camera + models)

cargo test --workspace -- --ignored

Requires a connected camera and downloaded models. These tests are marked #[ignore] and skipped by default.

Tier 3: Container tests (requires podman)

just test-pam          # PAM smoke tests (no camera)
just test-integration  # end-to-end with camera (daemon mode)
just test-oneshot      # end-to-end with camera (no daemon)
just test-shell        # interactive container shell for debugging

Container tests validate PAM integration without risking host lockout.

Tier 4: VM testing

Use a disposable VM with snapshots for testing PAM changes against real login flows.

Tier 5: Host PAM testing

Only after tiers 3--4 pass. Always keep a root shell open. Start with sudo only -- never add Facelock to login or display manager PAM until sudo works reliably.

All checks at once

just check  # runs test + clippy + fmt

Security considerations

Read the Security chapter before implementing any auth-related code. Key rules:

  • security.require_ir defaults to true. Never weaken this default.
  • Frame variance checks must remain in the auth path.
  • Model files are SHA256-verified at load time.
  • D-Bus message size limits are enforced by the bus daemon. Never allocate unbounded buffers.
  • D-Bus system bus policy restricts daemon access.
  • The PAM module logs all auth attempts to syslog.
  • Rate limiting is enforced in the daemon (5 attempts/user/60s default).

Contracts

Do not change binary names, paths, config keys, database schema, or auth semantics without updating the Contracts chapter.

Submitting changes

  1. Run just check (or at minimum cargo test --workspace && cargo clippy --workspace -- -D warnings).
  2. Run container tests if your change touches PAM, daemon, or IPC code.
  3. Keep commits focused. Separate refactoring from behavioral changes.
  4. Write clear commit messages that explain why, not just what.