YubiKeys are excellent multi-factor authentication (MFA) devices that can enhance your online security while simplifying your daily workflows on Linux.
In this article, we’ll walk through the process of configuring a YubiKey for secure authentication including:
Setting up passwordless sudo or enabling (2FA) for elevated privileges
Setting up 2FA on your Desktop Environment’s login
Setting up 2FA on your system’s TTY login
Setting up passwordless graphical prompts for elevated privileges
Setup
Prerequisites
First, ensure you have the libpam-u2f package (or its equivalent for your Linux distribution) installed. On Debian-based systems, use the following command:
sudo apt-get install libpam-u2f
U2F (Universal 2nd Factor) is an open standard for hardware MFA keys, and integration with Linux is made possible through Yubico’s pam-u2f module.
Adding Your YubiKey
To link your YubiKey with your system, follow these steps:
Connect your YubiKey: Insert the device into your computer.
Create the configuration directory: If it doesn’t already exist, create the directory ~/.config/Yubico:
mkdir-p ~/.config/Yubico
Register your YubiKey: Add the key to the list of accepted devices by running:
pamu2fcfg > ~/.config/Yubico/u2f_keys
If you’ve set a PIN for your YubiKey, you may be prompted to enter it.
Add additional keys (optional): If you have other YubiKeys, you can add them as follows:
pamu2fcfg -n>> ~/.config/Yubico/u2f_keys
Ensure there are no extra newlines between entries in the ~/.config/Yubico/u2f_keys file.
Before you start!
Before you start re-configuring things, it’s worth opening another terminal that is running as root. This way if you
do make any mistakes, you can still use that root terminal to back-out any changes that haven’t gone to plan.
Open a new terminal, and issue the following:
sudo-i
Now leave that terminal running in the background.
Configuring sudo
After setting up your key(s), you can configure sudo to use them for authentication.
Enabling Passwordless sudo
To make sudo passwordless:
Edit your /etc/sudoers file: Add a line like this:
%wheel ALL = (ALL) NOPASSWD: ALL
Ensure your user is part of the wheel group.
Modify /etc/pam.d/sudo: Add the following line before@include common-auth:
auth sufficient pam_u2f.so cue [cue_prompt=Tap your key]
This configuration makes YubiKey authentication sufficient for sudo, bypassing the need for a password.
Enabling 2FA for sudo
To enable 2FA, where both your password and YubiKey are required:
Edit /etc/pam.d/sudo: Add the following line after@include common-auth:
auth required pam_u2f.so cue [cue_prompt=Tap your key]
This ensures the usual password authentication is followed by YubiKey verification.
Edit /etc/pam.d/kde: Add the pam_u2f.so reference:
#%PAM-1.0
auth include system-local-login
auth required pam_u2f.so cue [cue_prompt=Tap your key]
account include system-local-login
password include system-local-login
session include system-local-login
When you change virtual TTY and go to login, we can also require a 2FA token at this point.
Edit /etc/pam.d/login: Add the pam_u2f.so reference:
#%PAM-1.0
auth requisite pam_nologin.so
auth include system-local-login
auth required pam_u2f.so cue [cue_prompt=Tap your key]
account include system-local-login
session include system-local-login
password include system-local-login
Configuring Passwordless polkit
The graphical prompts that you see throughout your desktop environment session are controlled using polkit.
Like me, you may need to install the polkit dependencies if you’re using KDE:
sudo apt install policykit-1 polkit-kde-agent-1
Much like the passwordless configuration for sudo above, we can control polkit in the same way.
Edit /etc/pam.d/polkit-1: Add the pam_u2f.so reference:
#%PAM-1.0
auth sufficient pam_u2f.so cue [cue_prompt=Tap your key]
auth required pam_env.so
auth required pam_deny.so
auth include system-auth
account include system-auth
password include system-auth
session include system-auth
Troubleshooting
Always keep in mind that you have that terminal sat in the background. That terminal can get you out of all sorts of trouble
so that you can rewind any changes that you’ve made that might have broken authentication on your system.
Enable Debugging
If something isn’t working, add debug to the auth line in /etc/pam.d/sudo to enable detailed logging during authentication:
auth sufficient pam_u2f.so debug
The additional logs can help identify configuration issues.
Conclusion
Adding a YubiKey to your Linux authentication setup enhances security and can simplify your workflow by reducing the need to frequently enter passwords. Whether you choose passwordless authentication or 2FA, YubiKeys are a valuable tool for improving your overall security posture.
Setting up email notifications is a critical task in homelab management. Services like Proxmox and OPNsense benefit
greatly from reliable email alerts for updates, backups, or critical events. Configuring Postfix to send emails through
Gmail provides a straightforward and secure solution. This guide will walk you through the steps to set up Postfix on a
Linux system to relay emails using Gmail’s SMTP server.
Prerequisites
Before you begin, ensure the following:
A Linux system with Postfix installed.
A Gmail account with an app password enabled (explained below).
Basic terminal access and permissions to edit configuration files.
Why Use an App Password?
Google enforces stricter security measures for less secure apps. You’ll need to generate an app password
specifically for Postfix:
Log in to your Google account.
Go to Manage Your Google Account > Security.
Under Signing in to Google, enable 2-Step Verification if not already enabled.
Once 2-Step Verification is active, return to the Security page and find App Passwords.
Create a new app password for “Mail” or “Other” and note it down for later.
Step 1: Install Postfix
If Postfix is not already installed, install it using your distribution’s package manager. For example:
sudo apt update
sudo apt install postfix -y
During installation, choose “Internet Site” when prompted, and set the system mail name (e.g., yourdomain.com).
Step 2: Configure Postfix for Gmail SMTP
Edit the Postfix configuration file to use Gmail as the relay host. Open /etc/postfix/main.cf in your preferred text
editor and fill out the following:
When working with concurrency in Rust, channels are a powerful tool for communication between threads or tasks. Two
prominent channel implementations in Rust are std::sync::mpsc from the standard library and tokio::sync::mpsc from
the tokio async runtime. While they share similarities, their use cases and performance characteristics differ
significantly. In this post, we’ll dive into the differences, use cases, and implementation details of these two
channels.
What Are Channels?
Channels are abstractions that enable communication between different parts of a program, typically in a
producer-consumer model. A channel consists of:
Sender: Used to send messages.
Receiver: Used to receive messages.
Rust’s channels enforce type safety, ensuring the data passed through them matches the specified type.
std::sync::mpsc
The std::sync::mpsc module provides a multi-producer, single-consumer (MPSC) channel implementation. It’s part of the
Rust standard library and is suitable for communication between threads in synchronous (blocking) environments.
Key Features
Multi-producer: Multiple threads can hold Sender clones and send messages to the same Receiver.
Single-consumer: Only one Receiver is allowed for the channel.
Blocking Receiver: Calls to recv block until a message is available.
Thread-safe: Designed for use in multi-threaded environments.
Use it when you don’t need the overhead of an async runtime.
Suitable for relatively simple communication patterns.
tokio::sync::mpsc
The tokio::sync::mpsc module provides an async multi-producer, single-consumer channel implementation. It’s part of
the Tokio async runtime, designed specifically for asynchronous programs.
Key Features
Asynchronous API: Works seamlessly with async/await.
Multi-producer: Similar to std::sync::mpsc, it supports multiple producers.
Single-consumer: Only one Receiver can receive messages.
Buffered or Unbuffered: Supports both bounded (buffered) and unbounded channels.
Non-blocking Receiver: The recv method is async and does not block.
Usage Example
In order to use this module (and run the sample below), you’ll need to add tokio as a dependency and enable the
appropriate features:
Best for asynchronous programs that utilize the Tokio runtime.
Useful when integrating with other async components like tokio::task or async-std.
Key Differences
Feature
std::sync::mpsc
tokio::sync::mpsc
Environment
Synchronous
Asynchronous
Blocking Behavior
Blocking recv
Non-blocking recv
Buffering
Bounded
Bounded or unbounded
Runtime Dependency
None
Tokio runtime required
Performance Considerations
std::sync::mpsc: Ideal for low-latency communication in synchronous environments.
tokio::sync::mpsc: Better suited for high-throughput async environments where tasks yield instead of blocking.
Conclusion
Both std::sync::mpsc and tokio::sync::mpsc serve important roles in Rust’s ecosystem. The choice between them
depends on your application’s requirements:
Use std::sync::mpsc for synchronous, multi-threaded scenarios.
Use tokio::sync::mpsc for asynchronous programs leveraging the Tokio runtime.
Concurrency is a cornerstone of modern software development, and the actor pattern is a well-established model for
handling concurrent computations. Rust, with its focus on safety, performance, and concurrency, provides an excellent
platform for implementing the actor model. In this article, we’ll explore what the actor pattern is, how it works in
Rust, and dive into some popular libraries that implement it.
What is the Actor Pattern?
The actor pattern revolves around the concept of “actors,” which are independent, lightweight entities that communicate
exclusively through message passing. Each actor encapsulates state and behavior, processing messages asynchronously and
maintaining its own isolated state. This model eliminates the need for shared state, reducing the complexity and risks
associated with multithreaded programming.
Why Use the Actor Pattern?
Isolation: Each actor manages its own state, ensuring safety.
Message Passing: Communication happens via asynchronous messages, avoiding direct interactions or locks.
Fault Tolerance : Actor hierarchies can implement supervision strategies, enabling automatic recovery from failures.
Libraries
As a basic example for comparison, we’ll create an actor that handles one message “Ping”.
Actix
Actix is the most popular and mature actor framework in Rust. Built on top of tokio, it offers
high-performance async I/O along with a robust actor-based architecture.
Features:
Lightweight actors with asynchronous message handling.
Built-in supervision for error recovery.
Excellent integration with web development (actix-web).
Example:
Here’s how to create a simple actor that responds to messages with Actix:
Any rust type can be an actor, it only needs to implement the Actor trait
We’ve defined MyActor for this
To be able to handle a specific message the actor has to provide a Handler<M> implementation
The Ping message is defined and handled by MyActor’s handle function
The actor is now started
A Ping message is sent, and the response is waited on
Riker
Inspired by Akka (Scala’s popular actor framework), Riker is another actor-based
framework in Rust. While less active than Actix, Riker focuses on distributed systems and fault tolerance.
Features:
Actor supervision strategies.
Distributed messaging.
Strong typing for messages.
Example:
This example is taken from the Riker Github repository:
riker="0.4.2"
usestd::time::Duration;useriker::actors::*;#[derive(Default)]structMyActor;// implement the Actor traitimplActorforMyActor{typeMsg=String;fnrecv(&mutself,_ctx:&Context<String>,msg:String,_sender:Sender){ifmsg=="Ping"{println!("Pong!");}else{println!("Received: {}",msg);}}}// start the system and create an actorfnmain(){letsys=ActorSystem::new().unwrap();letmy_actor=sys.actor_of::<MyActor>("my-actor").unwrap();my_actor.tell("Ping".to_string(),None);std::thread::sleep(Duration::from_millis(500));}
Breakdown
MyActor is implemented from an Actor trait
Messages are handled by the recv function
An actor system is started with ActorSystem::new()
We need to wait at the end for the message to be processed
Xactor
xactor is a more modern and ergonomic actor framework, simplifying async/await
integration compared to Actix. xactor is based on async-std.
Example:
This example was taken from xactor’s Github README.
xactor="0.7.11"
usexactor::*;#[message(result="String")]structPing;structMyActor;implActorforMyActor{}#[async_trait::async_trait]implHandler<Ping>forMyActor{asyncfnhandle(&mutself,_ctx:&mutContext<Self>,_:Ping)->String{"Pong".to_string()}}#[xactor::main]asyncfnmain()->Result<()>{// Start actor and get its addressletaddr=MyActor.start().await?;letres=addr.call(Ping).await?;println!("{}",res);Ok(())}
Breakdown
Defined is a MyActor actor trait, and a Ping message
The handle function is implemented for MyActor
Using this framework, async and await allows for the result to be waited on
Advantages of the Actor Pattern in Rust
Rust’s concurrency features and the actor model complement each other well:
Memory Safety: The actor model eliminates data races, and Rust’s borrow checker enforces safe state access.
Scalability: Asynchronous message passing allows scaling systems efficiently.
Fault Tolerance: Supervision hierarchies help manage errors and recover gracefully.
When to Use the Actor Pattern
The actor pattern is a good fit for:
Distributed Systems: Where isolated units of computation need to communicate across nodes.
Concurrent Systems: That require fine-grained message handling without shared state.
Web Applications: With complex stateful backends (e.g., using Actix-Web).
Alternatives to the Actor Pattern
While powerful, the actor model isn’t always necessary. Rust offers other concurrency paradigms:
Channels: Using std::sync::mpsc or tokio::sync::mpsc for message passing.
Shared-State Concurrency: Leveraging Arc<Mutex<T>> to manage shared state.
Futures and Tasks: Directly working with Rust’s async ecosystem.
Conclusion
The actor pattern is alive and well in Rust, with libraries like Actix, Riker, and xactor making it accessible to
developers. Whether you’re building distributed systems, scalable web applications, or concurrent computation engines,
the actor model can simplify your design while leveraging Rust’s safety and performance guarantees.
Daemons — long-running background processes — are the backbone of many server applications and system utilities. In
this tutorial, we’ll explore how to create a robust daemon using Rust, incorporating advanced concepts like double
forking, setsid, signal handling, working directory management, file masks, and standard file descriptor redirection.
If you’re familiar with my earlier posts on building CLI tools
and daemon development in C, this article builds on those concepts,
showing how Rust can achieve similar low-level control while leveraging its safety and modern tooling.
What Is a Daemon?
A daemon is a background process that runs independently of user interaction. It often starts at system boot and
remains running to perform specific tasks, such as handling requests, monitoring resources, or providing services.
Key Features of a Daemon
Independence from a terminal: It should not terminate if the terminal session closes.
Clean shutdown: Handle signals gracefully for resource cleanup.
File handling: Operate with specific file permissions and manage standard descriptors.
Rust, with its safety guarantees and powerful ecosystem, is an excellent choice for implementing these processes.
The first step in daemonizing a process is separating it from the terminal and creating a new session. This involves
double forking and calling setsid.
usenix::sys::stat::{umask,Mode};usenix::sys::signal::{signal,SigHandler,Signal};usestd::fs::File;usestd::os::unix::io::AsRawFd;usestd::env;usenix::unistd::{ForkResult,fork};pubunsafefndaemonize()->Result<(),Box<dynstd::error::Error>>{// First forkmatchfork()?{ForkResult::Parent{..}=>std::process::exit(0),ForkResult::Child=>{}}// Create a new sessionnix::unistd::setsid()?;// Ignore SIGHUPunsafe{signal(Signal::SIGHUP,SigHandler::SigIgn)?;}// Second forkmatchfork()?{ForkResult::Parent{..}=>std::process::exit(0),ForkResult::Child=>{}}// Set working directory to rootenv::set_current_dir("/")?;// Set file maskumask(Mode::empty());// Close and reopen standard file descriptorsclose_standard_fds();Ok(())}fnclose_standard_fds(){// Close STDIN, STDOUT, STDERRforfdin0..3{nix::unistd::close(fd).ok();}// Reopen file descriptors to /dev/nullletdev_null=File::open("/dev/null").unwrap();nix::unistd::dup2(dev_null.as_raw_fd(),0).unwrap();// STDINnix::unistd::dup2(dev_null.as_raw_fd(),1).unwrap();// STDOUTnix::unistd::dup2(dev_null.as_raw_fd(),2).unwrap();// STDERR}
Notice the usage of unsafe. Because we are reaching out to some older system calls here, we need to bypass some of
the safety that rust provides but putting this code into these unsafe blocks.
Whenever using unsafe in Rust:
Justify its Use: Ensure it is necessary, such as for interacting with low-level system calls.
Minimize its Scope: Encapsulate unsafe operations in a well-tested function to isolate potential risks.
Document Clearly: Explain why unsafe is needed and how the function remains safe in practice.
Handling Signals
Daemons need to handle signals for proper shutdown and cleanup. We’ll use the signal-hook crate for managing signals.
usesignal_hook::iterator::Signals;usestd::thread;pubfnsetup_signal_handlers()->Result<(),Box<dynstd::error::Error>>{// Capture termination and interrupt signalsletmutsignals=Signals::new(&[signal_hook::consts::SIGTERM,signal_hook::consts::SIGINT])?;thread::spawn(move||{forsiginsignals.forever(){matchsig{signal_hook::consts::SIGTERM|signal_hook::consts::SIGINT=>{log::info!("Received termination signal. Shutting down...");std::process::exit(0);}_=>{}}}});Ok(())}
Managing the Environment
A daemon should start in a safe, predictable state.
Working Directory
Change the working directory to a known location, typically the root directory (/).
env::set_current_dir("/")?;
File Mask
Set the umask to 0 to ensure the daemon creates files with the desired permissions.
// Set file maskumask(Mode::empty());
Putting It All Together
Integrate the daemonization process with signal handling and environment setup in main.rs:
moddaemon;modsignals;fnmain()->Result<(),Box<dynstd::error::Error>>{// Initialize loggingenv_logger::init();log::info!("Starting daemonization process...");// Daemonize the processunsafe{daemon::daemonize()?;}// Set up signal handlingsignals::setup_signal_handlers()?;// Main looploop{log::info!("Daemon is running...");std::thread::sleep(std::time::Duration::from_secs(5));}}
Because we marked the daemonize function as unsafe, we must wrap it in unsafe to use it here.
Advanced Features
Signal Handlers for Additional Signals
Add handlers for non-critical signals like SIGCHLD, SIGTTOU, or SIGTTIN.
With the foundational concepts and Rust’s ecosystem, you can build robust daemons that integrate seamlessly with the
operating system. The combination of double forking, signal handling, and proper environment management ensures your
daemon behaves predictably and safely.
A full example of this project is up on my github.