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
| Mode | Config | How it works | Latency |
|---|---|---|---|
| Daemon | mode = "daemon" (default) | PAM connects via D-Bus, persistent daemon | ~150-600ms |
| D-Bus activation | systemd + D-Bus service | systemd starts daemon on demand | ~700ms+ cold |
| Oneshot | mode = "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
| Crate | Type | Purpose |
|---|---|---|
facelock-core | lib | Config, types, errors, D-Bus interface, traits |
facelock-camera | lib | V4L2 capture, auto-detection, preprocessing |
facelock-face | lib | ONNX inference (SCRFD detection + ArcFace embedding) |
facelock-store | lib | SQLite face embedding storage |
facelock-daemon | lib | Auth/enroll logic, liveness, audit, rate limiting, request handler |
facelock-cli | bin | All CLI commands, daemon runner, direct mode, benchmarks |
pam-facelock | cdylib | PAM module (libc + toml + serde + zbus only) |
facelock-tpm | lib | Optional TPM-bound encryption for embeddings at rest |
facelock-polkit | bin | Polkit authentication agent for face auth |
facelock-test-support | lib | Mock 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
subtlecrate - 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) orv4l-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:
| Setting | Default | Description |
|---|---|---|
device.path | auto-detect | Camera path (prefers IR cameras) |
recognition.threshold | 0.80 | Cosine similarity threshold |
recognition.execution_provider | "cpu" | "cpu", "cuda", "rocm", or "openvino" |
daemon.mode | "daemon" | "daemon" or "oneshot" |
security.require_ir | true | Reject 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.
| Key | Type | Default | Description |
|---|---|---|---|
path | string (optional) | Auto-detect | Camera device path (e.g., /dev/video2). When omitted, Facelock auto-detects the best available camera, preferring IR over RGB. |
max_height | u32 | 480 | Maximum frame height in pixels. Frames taller than this are downscaled to improve processing speed. |
rotation | u16 | 0 | Rotate captured frames. Values: 0, 90, 180, 270. Useful for cameras mounted sideways. |
[recognition]
Face detection and embedding parameters.
| Key | Type | Default | Description |
|---|---|---|---|
threshold | f32 | 0.80 | Cosine 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_secs | u32 | 5 | Maximum seconds to attempt recognition before giving up. Must be > 0. |
detection_confidence | f32 | 0.5 | Minimum confidence for the face detector to report a detection. Lower values detect more faces but increase false positives. |
nms_threshold | f32 | 0.4 | Non-maximum suppression threshold for overlapping detections. |
detector_model | string | "scrfd_2.5g_bnkps.onnx" | ONNX detector model filename. Must exist in daemon.model_dir. |
embedder_model | string | "w600k_r50.onnx" | ONNX embedder model filename. Must exist in daemon.model_dir. |
execution_provider | string | "cpu" | ONNX Runtime execution provider. Values: "cpu", "cuda", "rocm", "openvino". GPU providers require a GPU-enabled ONNX Runtime package installed on the system. |
threads | u32 | 4 | Number of CPU threads for ONNX inference. |
Threshold range guide (ArcFace cosine similarity)
| Range | Description |
|---|---|
| 0.30 -- 0.50 | Very loose -- high false accept rate, not recommended |
| 0.50 -- 0.65 | Loose -- convenient but may accept similar-looking people |
| 0.65 -- 0.80 | Balanced -- good for most setups, low false accept rate |
| 0.80 -- 0.90 | Strict -- 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
| Tier | Detector | Embedder | Total size | Notes |
|---|---|---|---|---|
| Standard | scrfd_2.5g_bnkps.onnx (3MB) | w600k_r50.onnx (166MB) | ~170MB | Fast, good accuracy (default) |
| Balanced | scrfd_2.5g_bnkps.onnx (3MB) | glintr100.onnx (249MB) | ~252MB | ~15-30ms slower, better recognition |
| High accuracy | det_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.
| Key | Type | Default | Description |
|---|---|---|---|
mode | string | "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_dir | string | "/var/lib/facelock/models" | Directory containing ONNX model files. |
idle_timeout_secs | u64 | 0 | Shut down the daemon after this many idle seconds. 0 means never. Useful with D-Bus activation. |
[storage]
| Key | Type | Default | Description |
|---|---|---|---|
db_path | string | "/var/lib/facelock/facelock.db" | SQLite database for face embeddings. File permissions should be 640, owned by root:facelock. |
[security]
| Key | Type | Default | Description |
|---|---|---|---|
disabled | bool | false | Disable face authentication entirely. PAM returns IGNORE, falling through to the next auth method. |
abort_if_ssh | bool | true | Refuse face auth when connected via SSH (no camera available). |
abort_if_lid_closed | bool | true | Refuse face auth when the laptop lid is closed (camera blocked). |
require_ir | bool | true | Require an IR camera for authentication. RGB cameras are trivially spoofed with a printed photo. Only set to false for development/testing. |
require_frame_variance | bool | true | Require multiple frames with different embeddings before accepting. Defends against static photo attacks. |
require_landmark_liveness | bool | false | Require landmark movement between frames to pass liveness check. Detects static images by tracking facial landmark positions across frames. Experimental; off by default. |
suppress_unknown | bool | false | Suppress warnings for unknown users (users with no enrolled face). |
min_auth_frames | u32 | 3 | Minimum number of matching frames required before accepting. Only applies when require_frame_variance is true. |
[security.rate_limit]
| Key | Type | Default | Description |
|---|---|---|---|
max_attempts | u32 | 5 | Maximum auth attempts per user per window. |
window_secs | u64 | 60 | Rate limit window in seconds. |
[notification]
Controls how authentication feedback is delivered.
| Key | Type | Default | Description |
|---|---|---|---|
mode | string | "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_prompt | bool | true | Show prompt when scanning starts ("Identifying face..."). |
notify_on_success | bool | true | Notify on successful face match. |
notify_on_failure | bool | false | Notify on failed face match. |
[snapshots]
Save camera snapshots on auth attempts for debugging or auditing.
| Key | Type | Default | Description |
|---|---|---|---|
mode | string | "off" | "off" -- never save. "all" -- every attempt. "failure" -- failed auth only. "success" -- successful auth only. |
dir | string | "/var/log/facelock/snapshots" | Directory for snapshot JPEG images. |
[encryption]
Controls how face embeddings are encrypted at rest.
| Key | Type | Default | Description |
|---|---|---|---|
method | string | "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_path | string | "/etc/facelock/encryption.key" | Path to AES-256-GCM key file for keyfile method. |
sealed_key_path | string | "/etc/facelock/encryption.key.sealed" | Path to TPM-sealed AES key for tpm method. |
[audit]
Structured audit logging of authentication events.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable structured audit logging to JSONL file. |
path | string | "/var/log/facelock/audit.jsonl" | Path to the audit log file. |
rotate_size_mb | u32 | 10 | Rotate 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".
| Key | Type | Default | Description |
|---|---|---|---|
pcr_binding | bool | false | Bind sealed key to boot state (PCR values). |
pcr_indices | list of u32 | [0, 1, 2, 3, 7] | PCR registers to verify on unseal. |
tcti | string | "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 Vendor | Arch Linux Package | Other Distros |
|---|---|---|
| NVIDIA | onnxruntime-opt-cuda | Install CUDA toolkit + ONNX Runtime with CUDA provider |
| AMD | onnxruntime-opt-rocm | Install ROCm runtime + ONNX Runtime with ROCm provider |
| Intel | onnxruntime-opt-openvino | Install 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
| Provider | Config value | Status |
|---|---|---|
| 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.sodoes not include the requested provider. - Slower than CPU: Ensure the GPU driver is loaded (
nvidia-smifor NVIDIA,rocm-smifor AMD). Small models like SCRFD 2.5G may not benefit from GPU due to transfer overhead. - Daemon crashes on startup: Check
journalctl -u facelock-daemonfor 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
- IR enforcement: Only IR cameras allowed by default (prevents RGB photo attacks)
- Frame variance: Multiple frames must show micro-movement (prevents static photo)
- Rate limiting: 5 attempts per user per 60 seconds
- Model integrity: SHA256 verification at every load
- D-Bus security: System bus policy restricts daemon access
- Audit trail: All auth events logged to syslog
- 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:
- Explicit
--userflag (highest priority) SUDO_USERenvironment variableDOAS_USERenvironment variable- Current user (
$USERorgetpwuid)
Environment Variables
| Variable | Purpose |
|---|---|
FACELOCK_CONFIG | Override config file path |
RUST_LOG | Control 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
C. Dark Frame / IR Texture Validation (Recommended)
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.
C. Rate Limiting (Recommended)
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.
B. Service-Specific Policy (Recommended)
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
A. Capability Dropping (Recommended)
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
| Priority | Mitigation |
|---|---|
| P0 | IR camera enforcement (require_ir) |
| P0 | Frame variance check (anti-photo) |
| P0 | Model SHA256 at load time |
| P0 | D-Bus system bus policy |
| P0 | D-Bus message size limits |
| P0 | PAM audit logging |
| P0 | Database file permissions |
| P1 | IR texture validation |
| P1 | Rate limiting |
| P1 | systemd hardening |
| P1 | Capability dropping |
| P1 | Service-specific PAM policy |
| P2 | Embedding encryption at rest |
| P2 | Memory zeroing on drop |
| P2 | Constant-time similarity comparison |
Troubleshooting
Camera not detected
Symptom: facelock devices shows no cameras, or facelock enroll fails with "no camera found".
Steps:
- Check that your camera is recognized by the kernel:
ls /dev/video* v4l2-ctl --list-devices - 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 - If
/dev/video*exists but Facelock skips it, set the device explicitly:[device] path = "/dev/video0" - 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:
- Check what Facelock detects:
facelock devices - 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.
- If you are certain your camera is IR, check its V4L2 capabilities:
v4l2-ctl -d /dev/video2 --list-formats-ext - 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
- Boot into single-user/recovery mode (GRUB: edit the boot entry, add
singleorinit=/bin/bashto the kernel line). - Remount the filesystem read-write:
mount -o remount,rw / - Restore the PAM backup:
Or remove the Facelock line fromcp /etc/pam.d/sudo.facelock-backup /etc/pam.d/sudo/etc/pam.d/sudo:sed -i '/pam_facelock/d' /etc/pam.d/sudo - 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
sudoonly -- do not add Facelock tologinorsddmuntilsudoworks reliably. - Set
security.disabled = trueas an emergency kill switch (PAM returns IGNORE).
systemd unit not starting
Symptom: systemctl status facelock-daemon.service shows failed or inactive.
Steps:
- Check the journal:
journalctl -u facelock-daemon.service -n 50 --no-pager - Verify the service unit is enabled:
systemctl status facelock-daemon.service systemctl enable --now facelock-daemon.service - Check that the binary exists:
which facelock ls -la /usr/bin/facelock - Check model files exist:
ls -la /var/lib/facelock/models/ - 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:
- Check network connectivity.
- 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" - Verify SHA256 checksums match (Facelock checks these at model load time and rejects tampered files).
- 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
| Target | What it shows |
|---|---|
facelock_camera | Camera detection, format negotiation, frame capture |
facelock_face | Model loading, inference timing, similarity scores |
facelock_daemon | IPC handling, rate limiting, auth flow |
facelock_store | Database operations, embedding storage |
pam_facelock | PAM module decisions (logged to syslog) |
System Contracts
Stable contracts. Do not change without updating this document.
Binaries
| Binary | Crate | Purpose |
|---|---|---|
facelock | facelock-cli | Unified CLI (daemon, auth, enroll, test, setup, etc.) |
pam_facelock.so | pam-facelock | PAM authentication module |
CLI Subcommands
| Command | Purpose |
|---|---|
facelock setup | Download models, create directories |
facelock setup --systemd | Install/enable systemd units |
facelock setup --pam | Install PAM module to /etc/pam.d/ |
facelock enroll | Capture and store a face |
facelock test | Test face recognition |
facelock list | List enrolled face models |
facelock remove <id> | Remove a specific model |
facelock clear | Remove all models for a user |
facelock preview | Live camera preview |
facelock devices | List V4L2 cameras |
facelock status | Check system status |
facelock config | Show/edit configuration |
facelock daemon | Run persistent daemon |
facelock auth --user X | One-shot auth (PAM helper) |
facelock tpm status | TPM status |
facelock bench | Benchmarks |
facelock restart | Restart daemon |
Operating Modes
| Mode | Config | PAM Behavior | CLI Behavior |
|---|---|---|---|
| Daemon | daemon.mode = "daemon" (default) | D-Bus IPC to daemon | Uses daemon if available, falls back to direct |
| Oneshot | daemon.mode = "oneshot" | Spawns facelock auth | Operates 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
| Code | Meaning | PAM Code |
|---|---|---|
| 0 | Face matched | PAM_SUCCESS |
| 1 | No match / timeout / dark | PAM_AUTH_ERR |
| 2 | Error / no enrolled faces | PAM_IGNORE |
Filesystem Paths
| Path | Owner | Mode | Purpose |
|---|---|---|---|
/etc/facelock/config.toml | root:root | 644 | Configuration |
/var/lib/facelock/facelock.db | root:facelock | 640 | Face embeddings |
/var/lib/facelock/models/ | root:root | 755 | ONNX models |
/var/log/facelock/snapshots/ | root:facelock | 750 | Auth snapshots |
/usr/bin/facelock | root:root | 755 | CLI binary |
/lib/security/pam_facelock.so | root:root | 755 | PAM 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
| Section | Key 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:
- Enumerate
/dev/video0through/dev/video63 - Filter to VIDEO_CAPTURE devices
- Prefer IR cameras (name contains "ir"/"infrared", or supports GREY/Y16 format)
- 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
| Outcome | PAM Code |
|---|---|
| Face matched | PAM_SUCCESS (0) |
| No match | PAM_AUTH_ERR (7) |
| Daemon unavailable / error | PAM_IGNORE (25) |
| Timeout | PAM_AUTH_ERR (7) |
PAM module never blocks indefinitely. All operations have timeouts.
Syslog Format
pam_facelock(<service>): <result> for user <username>
Anti-Spoofing
| Defense | Config | Default |
|---|---|---|
| IR camera enforcement | security.require_ir | true |
| Frame variance check | security.require_frame_variance | true |
| Landmark liveness | security.require_landmark_liveness | false |
| Minimum auth frames | security.min_auth_frames | 3 |
| Variance threshold | FRAME_VARIANCE_THRESHOLD | 0.998 |
These defaults must not be weakened without security review.
Models
| Model | File | Size | Default |
|---|---|---|---|
| SCRFD 2.5G | scrfd_2.5g_bnkps.onnx | ~3MB | Yes |
| ArcFace R50 | w600k_r50.onnx | ~166MB | Yes |
| SCRFD 10G | det_10g.onnx | ~16MB | Optional |
| ArcFace R100 | glintr100.onnx | ~249MB | Optional |
Configurable via recognition.detector_model and recognition.embedder_model.
Compatibility
System Requirements
| Component | Requirement |
|---|---|
| OS | Linux (kernel 4.14+ for V4L2) |
| Architecture | x86_64 (ONNX Runtime binaries) |
| Rust | 1.85+ (edition 2024) |
| Camera | V4L2-compatible (USB webcam, built-in IR) |
| PAM | Linux-PAM (pam 1.5+) |
Tested Distributions
| Distribution | Init System | Mode | Status |
|---|---|---|---|
| Arch Linux | systemd | daemon + D-Bus activation | Primary target |
| Arch Linux | systemd | oneshot | Tested |
| Container (Arch) | none | daemon (manual) | CI-tested |
| Container (Arch) | none | oneshot | CI-tested |
Expected to Work (untested)
| Distribution | Init System | Mode |
|---|---|---|
| Fedora 38+ | systemd | daemon + D-Bus activation |
| Ubuntu 22.04+ | systemd | daemon + D-Bus activation |
| Debian 12+ | systemd | daemon + D-Bus activation |
| Any Linux | any / none | oneshot |
| Void Linux | runit | oneshot or manual daemon |
| Alpine Linux | OpenRC | oneshot or manual daemon |
| Gentoo | OpenRC / systemd | oneshot or daemon |
Camera Compatibility
IR Cameras (recommended)
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
| Format | Support | Notes |
|---|---|---|
| MJPG | Full | Most common USB camera format |
| YUYV | Full | Raw format, converted to RGB |
| GREY | Full | IR cameras, replicated to RGB |
| Other | Not supported | Camera negotiates to supported format |
Init System Support
systemd (recommended)
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
| Service | File | Notes |
|---|---|---|
| sudo | /etc/pam.d/sudo | Primary target, safest to test first |
| polkit | /etc/pam.d/polkit-1 | GUI privilege escalation |
Not Recommended
| Service | Reason |
|---|---|
| system-auth | Affects ALL auth -- test sudo first |
| login | Console login -- hard to recover if broken |
| sshd | SSH 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 encryptionpodmanordocker-- 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.
| Provider | Config | Runtime Requirement | Status |
|---|---|---|---|
| CPU | execution_provider = "cpu" | none (default) | Working |
| CUDA (NVIDIA) | execution_provider = "cuda" | CUDA toolkit + GPU-enabled ORT | Config ready, untested |
| ROCm (AMD) | execution_provider = "rocm" | ROCm runtime + GPU-enabled ORT | Config ready, untested |
| OpenVINO (Intel) | execution_provider = "openvino" | OpenVINO runtime + GPU-enabled ORT | Config 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
Tier 4: VM Testing (optional, recommended)
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:
- Open root shell in separate terminal -- keep it open
sudo cp /etc/pam.d/sudo /etc/pam.d/sudo.baksudo facelock setup --pam --service sudo- Test in NEW terminal:
sudo echo test - If broken, revert from root shell:
sudo cp /etc/pam.d/sudo.bak /etc/pam.d/sudo - Never modify
system-authorloginuntil 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
| File | Purpose |
|---|---|
test/Containerfile | Container image (Arch + pamtester) |
test/run-tests.sh | CI script (unit + lint + PAM symbols) |
test/run-container-tests.sh | PAM smoke tests |
test/run-integration-tests.sh | E2E with camera (daemon) |
test/run-oneshot-tests.sh | E2E with camera (oneshot) |
test/pam.d/facelock-test | Test PAM config |
Just Recipes
| Recipe | Description |
|---|---|
just test | Unit tests |
just lint | Clippy |
just check | test + lint + fmt |
just test-pam | Container PAM smoke |
just test-integration | E2E daemon mode |
just test-oneshot | E2E oneshot mode |
just test-shell | Interactive container |
just install | System 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:
| Crate | Type | Purpose |
|---|---|---|
facelock-core | lib | Config, types, errors, D-Bus interface, traits |
facelock-camera | lib | V4L2 capture, auto-detection, preprocessing |
facelock-face | lib | ONNX inference (SCRFD + ArcFace) |
facelock-store | lib | SQLite face embedding storage |
facelock-daemon | lib | Auth/enroll logic, liveness, audit, rate limiting, handler |
facelock-cli | bin | Unified CLI (facelock binary, includes bench subcommand) |
pam-facelock | cdylib | PAM module (libc + toml + serde + zbus only) |
facelock-tpm | lib | Optional TPM-bound encryption for embeddings at rest |
facelock-polkit | bin | Polkit authentication agent for face auth |
facelock-test-support | lib | Mocks 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:
thiserrorfor library error types,anyhowin binaries. ReturnResult<T>over panicking. Neverunwrap()in library code. - Logging:
tracingfor structured logging. Control verbosity viaRUST_LOGenv filter. - Tests:
#[cfg(test)]modules in each source file. - Formatting:
cargo fmt(default rustfmt settings). - Linting:
cargo clippy --workspace -- -D warningsmust 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_irdefaults 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
- Run
just check(or at minimumcargo test --workspace && cargo clippy --workspace -- -D warnings). - Run container tests if your change touches PAM, daemon, or IPC code.
- Keep commits focused. Separate refactoring from behavioral changes.
- Write clear commit messages that explain why, not just what.