Lab
udev Rule Builder
Learn udev by building real /etc/udev/rules.d/ rules in the browser: device access, persistent symlinks, NIC renaming, hotplug triggers, and USB lockdown, with inline explanations and validation.
udev Rule Builder
Build Linux udev rules for five of the most common jobs, with an educational
frame around every choice. Everything runs in your browser, no data leaves the
page. Output is a real .rules file plus the exact
commands to install and test it.
What is udev?
udev is the Linux kernel's device manager. When the kernel discovers a new
device, it emits a uevent; systemd-udevd reads rules from /etc/udev/rules.d/ and uses them
to name devices, set permissions, create symlinks, and trigger actions.
What you'll learn
-
Match (
==) vs assign (=) - Attribute walking with
udevadm info - Why rule file names start with numbers
- Common pitfalls:
RUN+=, symlinks, NIC renames - How to dry-run with
udevadm test
When not to use udev
-
Mounting filesystems, use
systemd.mount - Long-running daemons, use a systemd unit with
BindsTo= - USB lockdown, use
usbguardfor policy -
Per-app device ACLs, use
uaccess+ logind
How a udev rule is structured
A udev rule is a single line of comma-separated key/value pairs. Every pair is either a match (filter this rule to certain devices) or an assignment (do something to a matching device). The operator tells udev which one you mean:
-
==must equal -
!=must not equal
-
=replace the value -
+=append to a list -
:=set and make final (cannot be overridden)
Most single-line rules look like this:
SUBSYSTEM=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="6089", MODE="0660", GROUP="plugdev", TAG+="uaccess"
The first four clauses use ==, they're filters.
The last three use = or +=, they act on whatever devices matched. Getting these operators wrong is the
single most common mistake: MODE=="0660" silently does
nothing.
SUBSYSTEM / ATTRS / KERNEL, what the match keys actually check
A device in the kernel lives at a path like
/sys/bus/usb/devices/1-1. That directory has
attributes (files you can cat) and
parents (the USB device has a parent hub, which has
a parent controller). Most match keys just read one of those:
SUBSYSTEM
The device's subsystem (usb, tty, block, net, input, hidraw).
KERNEL
The kernel-assigned base name (sda, ttyUSB0, eth0). Glob OK.
ATTR{name}
An attribute on the device itself.
ATTRS{name}
Same, but walks up the parent chain until it finds a match. USB idVendor lives
on the parent.
ACTION add, remove, change, bind, unbind. ENV{name} Read or set a udev environment variable. How to discover what to match on
# Plug the device in, then walk the sysfs tree:
udevadm info --attribute-walk --name=/dev/ttyUSB0
# Every "looking at parent device" block corresponds to one level where
# ATTRS{...} will search. Pick keys unique enough to match only your device. Rule file naming: why '60-...' and not 'my-rule'
udev reads files from /etc/udev/rules.d/ in
lexical order. Filenames start with a two-digit
priority for exactly this reason, earlier files run first, and later files can
override. Typical ranges used by distributions:
00–09 Very early, reserved for systemd internals. 10–49 Device identification, naming. 50–69
General-purpose. Most user rules live here.
70–89
Permissions, ACLs. NIC renames belong here.
90–99
Last-chance overrides. Useful for local policy.
99-local.rules
Common convention for "always runs last" site rules.
Distro rules live in /usr/lib/udev/rules.d/. A
file in /etc/udev/rules.d/ with the same name
shadows the distro one. That's how you override behavior without modifying
system files.
Pitfalls that bite everyone at least once
- RUN+= kills anything that takes >5 seconds.
udev's
RUN+=fires synchronously and aggressively SIGKILLs long children. If you need a daemon, wrap it:RUN+="/usr/bin/systemd-run --no-block --unit=foo ...". The builder does this for you by default. - RUN+= runs with no PATH and no controlling terminal. Always use absolute paths; don't assume a login shell.
- SYMLINK+= is relative to /dev.
Writing
SYMLINK+="/dev/foo"creates a symlink at/dev//dev/foo. Drop the leading slash. - NAME= is only for network interfaces.
For any other device, use
SYMLINK+=. UsingNAME=on non-net devices has been deprecated since udev 176 (ca. 2012). - Static symlinks collide with second instances.
If two identical devices show up, the second
SYMLINK+=is skipped. Use$attr{serial}or%kto make the name unique. - udev rules do not reload automatically.
After editing files:
udevadm control --reload, thenudevadm trigger. For quick sanity:udevadm test $(udevadm info -q path -n /dev/...). - Matching too broadly is a security hole.
A rule with
SUBSYSTEM=="usb", MODE="0666"and nothing else makes every USB device world-writable. Always pair withATTRS{idVendor}or similar.
Substitutions you can use in SYMLINK, RUN, NAME, ENV values
%k Kernel name (e.g. sda1, ttyUSB0). %n Kernel number (e.g. "1" from sda1). %b Sysfs bus path. $attr{name}
Value of a sysfs attribute (e.g. $attr{serial}).
$env{name} Value of a udev env var. %% Literal %. Each preset is a worked example, change the fields to see how the output and warnings respond.
Worked example: giving plugdev access to a HackRF SDR
Walking through the mechanics end-to-end. Load the HackRF preset above to see the same rule in the builder.
Step 1, Identify the device
$ lsusb
Bus 003 Device 005: ID 1d50:6089 OpenMoko, Inc. HackRF One 1d50 is the vendor ID,
6089 the product ID. Those are what we match on.
Step 2, Confirm which sysfs attributes are actually visible
$ udevadm info --attribute-walk --name=/dev/bus/usb/003/005 | head -30
looking at device '/devices/pci0000:00/.../3-1':
KERNEL=="3-1"
SUBSYSTEM=="usb"
DRIVER=="usb"
ATTR{idVendor}=="1d50"
ATTR{idProduct}=="6089"
ATTR{serial}=="0000000000000000a06463dc2xxxxxxx"
ATTR{manufacturer}=="Great Scott Gadgets"
Note the difference: the device itself has ATTR
(singular), and when you write a rule against the usb_device node directly,
you'd use ATTR{idVendor}. But most rules match against child devices (the tty, the hidraw, the block device), and those child devices need ATTRS (plural, "walk parents") to reach the idVendor on the parent USB device.
Step 3, Write the rule
# HackRF One: grant plugdev group access
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="6089", MODE="0660", GROUP="plugdev", TAG+="uaccess"
The first four clauses filter the rule down to this specific device on insert. The
last three do the work: relax permissions to group read/write, assign the group to
plugdev, and add uaccess so the logged-in desktop user gets
an ACL too.
Step 4, Install and reload
sudo install -m 0644 60-hackrf.rules /etc/udev/rules.d/60-hackrf.rules
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=usb Step 5, Verify
# Replug the HackRF, then:
$ ls -l /dev/bus/usb/003/005
crw-rw----+ 1 root plugdev 189, 260 /dev/bus/usb/003/005
# ^^^^^^^ ← GROUP applied
# ^^^^^^^^^^+ ← the + means uaccess ACL is attached
$ getfacl /dev/bus/usb/003/005
# owner: root
# group: plugdev
user::rw-
user:steven:rw- ← uaccess gave the current user a direct ACL
group::rw-
mask::rw-
other::---
If permissions didn't change, dry-run the rule against the device path with
udevadm test. The output shows every rule that
evaluated and which clauses matched, it's the fastest way to diagnose a silent
failure.
Key concepts, in short
uevent → udevd → rules
The kernel emits a netlink uevent when device state changes.
systemd-udevd reads it, walks every rules file in
lexical order, and applies match/assign logic. Nothing in this pipeline is user-space-optional:
rules fire on boot, on suspend/resume, and on every replug.
uaccess vs explicit GROUP
TAG+="uaccess" tells logind to grant the logged-in
local user an ACL on the device, so only the person sitting at the machine can use
it. A static GROUP= gives permanent access to everyone in that group.
Use uaccess for desktop peripherals, GROUP= for servers.
ATTR vs ATTRS (why the S matters)
ATTR reads the attribute on the
matching device only. ATTRS walks up the
parent chain. A USB serial adapter appears as a tty device;
idVendor lives on its grandparent USB node. Use
ATTRS{idVendor} so the walk finds it.
Why udev rules beat /etc/fstab for removable devices
/etc/fstab assumes a device already exists. udev runs when the device
first appears, which is when you need to name it, set permissions, or fire an automount
unit. Pair a udev rule with a systemd .mount
unit tagged BindsTo=dev-disk-by-uuid-... for robust automount on plug-in.
Reference: SUBSYSTEM values you'll actually use
usb Raw USB devices. tty Serial adapters, modems. block Disks, partitions, loopback. net Network interfaces. input Keyboards, mice, joysticks. hidraw HID devices (YubiKeys, ...) sound ALSA sound cards. video4linux Webcams, capture cards. tpm TPM chips. Reference: common GROUP names
dialout Serial ports (ttyS*, ttyUSB*). plugdev
Hotpluggable USB devices (Debian/Ubuntu convention).
disk Raw block devices. video Webcams, GPUs on some distros. audio Sound devices. input
/dev/input/*, grants low-level keyboard/mouse reading.
Confirm the group exists on your distro (getent group dialout). If
not, either create it or use a group you control.
Reference: diagnostic commands
udevadm info --query=property --name=/dev/... All udev properties for a live device.
udevadm info --attribute-walk --name=... Device plus every parent, with all attributes.
udevadm test $(udevadm info -q path -n /dev/...) Simulate rule evaluation. Shows which rules matched, which didn't, and why.
udevadm monitor --property --udev Watch events live. Plug and unplug; see what the kernel emits.
udevadm control --reload Reload rules from disk. Required after any edit.
udevadm trigger --action=add Replay synthetic add events so new rules apply without replugging.
Security model
Everything here runs in your browser. Nothing you type, IDs, MACs, commands, comments, is sent to any server or persisted between reloads. The rules are assembled in TypeScript on the client and handed to you as a downloadable file.
Generated rules are not audited for your specific threat model. Always review the
output before installing, especially for MODE=,
GROUP=, and RUN+= values.