Why and How to Run Test Stations Without Root Access
Running a production test station as root (Linux) or Administrator (Windows) is the fastest way to get hardware access working, and a shortcut you pay for later in security exposure and operational fragility. The highest-value habit is simple: don't log the station in as an admin. This guide covers that cheap win first, then how far you can take least privilege on Linux and Windows without breaking hardware access or boot-time startup.
The Shortcut and What It Costs
When the priority is to deploy fast, running the whole station as root is often the shortest path: a device throws a Permission denied, and elevating unblocks it right away. Over time it hardens into one of these:
| Shortcut you see on the floor | What it saves | What it quietly costs |
|---|---|---|
| Auto-login as root, no password | Operator opens the session instantly | Anyone with physical access owns the machine |
| Root password on a sticky note | No one waits to unlock | Same as no password, plus a paper trail to your secret |
sudo calls inside the runtime code | Script "just works" | Every run holds full-system power it never needs |
NOPASSWD in sudoers for the runner | No prompts during a shift | Any bug or misuse escalates silently |
Sometimes it is a shortcut under deadline; sometimes a vendor tool genuinely demands root and there's no clean alternative that day. Either way, the cost is the same and it compounds across every station, every audit, and every machine that leaves the building for repair. The fix is not "never elevate." It is: elevate to install, never to run. Root is fine for setup (installing software, drivers, device rules), not for the identity that executes tests shift after shift.
The Cheap Win: Don't Log In as Admin
Before any of the advanced setup below, one change removes most of the risk for almost no effort: the operator logs in as a standard, non-admin user, and the station runs in kiosk mode. This alone is the 80/20.
| Risk | A standard account + kiosk removes it? |
|---|---|
| Operator "borrows" the station to browse, copy files, install a tool | Yes, no admin rights, and the kiosk hides the desktop |
| Operator changes a system setting they shouldn't | Yes, they can't, by design |
| A bug in a parsed file corrupts the whole OS | Mostly, the process can't write outside the user's reach |
| Stolen/repaired machine leaks every stored secret | Reduced, the account's reach bounds what's exposed |
A standard account protects the operator from the machine as much as the machine from the operator: they should physically not be able to do what they aren't meant to. Kiosk mode is covered separately; the point here is that the account it runs under should not be an admin. Everything below is how to keep it that way while still reaching hardware and starting on boot.
The Security Cost (Why This Matters)
Least privilege is error containment: the software equivalent of fusing a circuit instead of trusting nothing will ever short. A test runner parses files it did not write (firmware images, calibration data, serial responses, config from a share), so any bug runs with the identity of the process.
| Risk | As a low-privilege account | As root / admin |
|---|---|---|
| Blast radius | A bad parse corrupts one working directory | The same bug can overwrite the OS or another line's data |
| Credential exposure | Process reads only its own API key | Process can read every secret on the machine |
| Supply chain / privilege escalation | A compromised dependency is contained | A single bad package owns the box, no escalation needed |
| Lost or repaired machine | Data at rest is bounded by the account's reach | Full disk and every stored credential are exposed |
| Audit | Logs attribute actions to a station identity | Logs say root did X for everything, unattributable |
The escalation risk is concrete enough that good tooling refuses to enable it: the TofuPilot CLI will not install a root boot-service whose binary lives in a user-writable directory, because a non-root user who could overwrite that binary would gain root at the next boot. The audit line is now a business consequence too. Standing local admin on floor workstations is increasingly a CMMC / NIST 800-171 audit failure, a real barrier for anyone in aerospace, defense, or high-end EMS supply chains. And the blast-radius line is what security teams call lateral movement: an attacker who lands on one over-privileged station moves across the whole plant before anyone notices.
"But the Station Is Offline"
Root-on-benches is often defended with "the station has no internet, so who cares." That misreads what offline means on a factory floor. The station is air-gapped from the internet, but it usually sits on a secured local network, reachable from other machines and often able to reach the MES and the production server where centralized production data lives.
An IT team only accepts the painful drawback of an offline station when the machines and data on that local network are sensitive enough to justify it. In other words, "offline" is a signal that security matters more here, not less. A station running as root on that network is a soft entry point to everything else on it: exactly the lateral movement the isolation was meant to prevent. Removable media compounds it: an offline station still takes firmware images on USB sticks and still leaves the building for repair (the Stuxnet worm reached isolated industrial systems precisely by USB).
The Principle: Elevate to Install, Never to Run
| Task | Needs elevation? | Who runs it |
|---|---|---|
| Install software, drivers, device rules | Yes, once | Admin, at setup |
| Read/write a serial or USB device at runtime | No | Low-privilege account via group / rule |
| Execute tests, upload results | No | The station's non-admin account |
| Flash firmware via raw I/O | Sometimes | Grant the one capability, not full root |
The rest of this guide is how to build that on Linux and Windows.
Linux: Least-Privilege Setup
Step 1: Run Under a Low-Privilege Account
The account the station runs under should be a normal user with no admin rights: not root, not a member of sudo/wheel. On most benches this is the operator account the kiosk already uses.
# A normal low-privilege account for the station. NOT in sudo/wheel/adm.
# The operator can log in and run the kiosk UI, but can't touch the system.
sudo useradd --create-home operator
id operator # confirm: not a member of sudo, wheel, or admStep 2: Grant Hardware Access With Groups, Not Root
The reason people reach for root is device permissions. The correct fix is group membership: add the operator account to the group that already owns the device.
# Serial ports (/dev/ttyUSB*, /dev/ttyACM*) are group-owned by 'dialout'
sudo usermod -aG dialout operator
# Hot-plugged USB instruments are usually group-owned by 'plugdev'
sudo usermod -aG plugdev operator
# Confirm which group owns your device (4th column)
ls -l /dev/ttyUSB0
# crw-rw---- 1 root dialout 188, 0 ... /dev/ttyUSB0
# ^^^^^^^ group that grants accessFor a device not owned by a standard group, write a udev rule (udev is the Linux subsystem that assigns ownership and permissions to devices as they appear):
# Match a specific instrument by USB vendor/product ID and hand it to
# the 'dialout' group (which the operator is in). No root at runtime.
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="dialout", MODE="0660"sudo udevadm control --reload-rules && sudo udevadm triggerFor a binary that needs one specific kernel privilege (e.g. raw I/O to flash firmware), grant that one capability instead of root. Capabilities split root's powers into independent units:
# Give only raw-I/O to this one binary, nothing else root can do
sudo setcap cap_sys_rawio+ep /usr/local/bin/flash-tool
getcap /usr/local/bin/flash-toolStep 3: Launch on Boot Without Root
This is where root usually sneaks back in. When you enable Launch on boot, the TofuPilot CLI installs the right startup service automatically, so you don't write a unit file by hand:
| CLI runs as | Service it installs | Kiosk |
|---|---|---|
operator (non-admin) | systemd user service (~/.config/systemd/user/) | Yes, on the local display |
root | systemd system service (/etc/systemd/system/) | No, headless, driven from the dashboard |
The trap that pushes people to root: if you enable launch-on-boot over SSH as the operator (a shell with no login session), systemctl --user has no session bus and fails:
Failed to connect to bus: No medium found
systemctl --user: Failed to connect to user scope busThe fix is not to switch to root. It is to let the operator's user services run without an interactive login. That capability is called lingering in systemd (like a machine that keeps its background routine running even when no one is at the HMI):
# Operator's systemd services now start at boot with nobody logged in
sudo loginctl enable-linger operator
loginctl show-user operator --property=Linger # -> Linger=yesWith lingering on, the non-root station starts at every boot, keeps its kiosk display, and never needs root for startup. Credentials follow the same account: run tofupilot login --token <setup-token> as the operator, not sudo.
Step 4: Verify Least Privilege
# The station process should show 'operator', not root
ps -o user,cmd -C tofupilot
# Run the check AS operator (sudo -u switches identity, it does not elevate)
sudo -u operator cat /dev/ttyUSB0 # opens via the dialout group, not "Permission denied"Windows: Least-Privilege Setup
Same principle: the operator account is a standard user, not Administrator. Windows makes most of this easy because standard users already have the access a test runner needs.
| Need | Least-privilege grant | Avoid |
|---|---|---|
| Read/write a COM port | Standard users can open COM ports by default; no admin needed | Running as admin "to be safe" |
| USB instrument via vendor driver | Install the driver once as admin; runtime access is per-device | Admin at runtime |
| Write logs / results | Grant the account write access to one folder (e.g. C:\tofupilot) | Writing under C:\Program Files |
| Launch on boot | TofuPilot registers a per-user Run entry, fires at the operator's logon, no admin | A machine-wide service running as SYSTEM/admin |
# Local standard user (NOT added to Administrators)
$pw = Read-Host -AsSecureString "Password for the operator account"
New-LocalUser -Name "operator" -Password $pw -PasswordNeverExpires `
-Description "Test station operator"
# Deliberately NOT: Add-LocalGroupMember -Group "Administrators"# Give the operator account its own writable working directory
New-Item -ItemType Directory -Path "C:\tofupilot" -Force
icacls "C:\tofupilot" /grant "operator:(OI)(CI)M"On Windows the station runs as a per-user logon process under the standard operator account, so it starts when the operator logs in, with no admin service and no SYSTEM privileges. Store the API key in the operator's user-scoped environment or a file under C:\tofupilot with permissions limited to that account, never machine-wide.
When Root Is the Pragmatic Choice
Best practice sometimes conflicts with productivity, and the person who knows the constraints, the developer at the bench during a production rush, is the one who should decide. At TofuPilot we don't block that decision: the CLI runs under root or admin at runtime if you choose to, because removing deployment friction during a rush matters more than enforcing rigidity from the outside. Running the CLI as root even installs the correct system service for you.
If you do run as root, do it deliberately and shrink the risk:
| If you run as root | Reduce the risk by |
|---|---|
| A vendor tool or raw I/O genuinely needs it | Scope it: setcap on that one binary instead of root everywhere |
| Hardware access is blocking a deadline | Ship the root shortcut, then schedule the udev/group fix as follow-up |
| Headless bench, launch on boot | Keep the binary in /usr/local/bin owned by root (the CLI refuses a user-writable one) |
| Physical machine can be stolen or sent for repair | Encrypt the disk; keep the API key scoped and revocable |
| Operators share the station | Pair root runtime with kiosk lockdown so they can't wander off the test UI |
The point is not purity. It is knowing what the shortcut costs so the choice is informed, and moving to least privilege once the fire is out.
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
systemctl --user: Failed to connect to user scope bus | Enabling launch-on-boot from a shell with no login session (SSH) | sudo loginctl enable-linger operator, don't switch to root |
| Station doesn't start at boot, no error | User service installed but lingering is off | Enable lingering (above); confirm with loginctl show-user operator |
Permission denied on /dev/ttyUSB0 after dropping root | Operator not in the device's group | usermod -aG dialout operator, then log out/in or reboot |
| Device works for root but not the operator | udev rule not applied, or wrong vendor/product ID | Re-check IDs with lsusb, reload with udevadm control --reload-rules && udevadm trigger |
| CLI refuses to install a root system service | Binary sits in a user-writable path (a root-at-boot escalation vector) | Move it to /usr/local/bin owned by root, then re-run |
| Windows runner can't write results after dropping admin | Writing to a protected path | Point output at the operator's own folder (C:\tofupilot) |