This document outlines security best practices for deploying and operating rouser. Following these guidelines ensures the daemon operates securely in your environment.
When installing system-wide at /etc/rouser/config.toml, restrict permissions:
sudo chown root:root /etc/rouser/config.toml
sudo chmod 0600 /etc/rouser/config.tomlRationale: Prevents unprivileged users from modifying thresholds to keep the system awake (denial of service via configuration tampering).
User-level config files at ~/.config/rouser/config.toml inherit standard home directory permissions. No special handling needed since they are already user-owned.
Note: The configuration file does not store sensitive data (passwords, API keys), so the primary concern is preventing unauthorized modification rather than unauthorized reading.
rouser uses org.freedesktop.login1.Manager.Inhibit on the system D-Bus to prevent sleep. This requires access to the login1 service, which is typically available to any logged-in user via polkit policies.
Security considerations:
- An unprivileged user can inhibit sleep but cannot shut down or reboot without additional polkit rules (the
shutdownlock type still requires elevated privileges on most systems) - The default inhibitor configuration (
what = "shutdown:idle",mode = "block") only blocks idle and shutdown — it does not prevent a privileged user from forcing a shutdown
Some desktop environments (notably KDE Plasma's Powerdevil) ignore D-Bus inhibitors from unprivileged users. To grant inhibition permission:
Create /etc/polkit-1/rules.d/50-rouser.rules:
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.login1.inhibit" &&
subject.user == "your_username") {
return polkit.Result.YES;
}
});Only grant access to users or groups that truly need it. See Introduction for more context.
- Inhibit only what's necessary: Configure the
whatparameter to inhibit only needed operations (e.g., just"idle"instead of"shutdown:idle") - Use
blockmode sparingly: Theblockmode completely blocks sleep, which may conflict with other power management policies - Monitor inhibition state: Use
systemd-inhibit --listto check what's currently inhibiting sleep
When running as a systemd user service, you can add security hardening directives:
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=fullSecurity features:
NoNewPrivileges=true: Prevents privilege escalation after startupPrivateTmp=true: Isolates/tmpfor the serviceProtectSystem=full: Makes the filesystem hierarchy read-only (except for explicitly writable paths)
Prevent resource exhaustion via systemd limits:
[Service]
MemoryMax=128M
CPUQuota=10%Best practices:
- Set memory limits to prevent out-of-memory conditions (rouser typically uses 2–5 MB, so 128 MB is very generous)
- Limit CPU usage to prevent excessive resource consumption
Regularly audit dependencies for vulnerabilities:
# Install cargo-audit
cargo install cargo-audit
# Audit dependencies
cargo auditContinuous integration: Add cargo audit to your CI pipeline and fail builds if critical/high severity vulnerabilities are found.
- Pin dependency versions in
Cargo.toml - Check RustSec advisories regularly for known CVEs
- Update promptly: apply security patches within 48 hours for critical/high vulnerabilities
- Always run tests after updating dependencies
# Audit and update all dependencies
cargo audit
cargo update
cargo testrouser is designed to run as a systemd user service under the unprivileged invoking user's account — no root access required. When running in system-wide mode (under root), apply additional hardening:
- Use
NoNewPrivileges=true - Restrict filesystem access with
ReadOnlyPaths=andReadWritePaths= - Limit capabilities with
CapabilityBoundingSet=
All configuration values undergo validation at startup:
- Numeric bounds: Thresholds validated against allowed ranges (0.0–100.0 for percentages)
- String values: Validated against enumerated sets (e.g., valid log level and inhibition mode values)
- Time durations: Parsed via
humantimewith reasonable range checks
The daemon fails fast on invalid configuration rather than operating with potentially dangerous defaults.
rouser does not log sensitive data (passwords, API keys). Configuration values like thresholds and intervals are safe to log at debug level. However, if you extend rouser with custom logging:
- Never log passwords, tokens, or credentials
- Avoid logging full file contents of config files that may contain secrets
- Use structured logging via the
tracingcrate to control what fields are emitted
- Service uptime: Detect unexpected restarts or failures (
systemctl --user status rouser) - Inhibition state: Unexpected sleep inhibition (check with
systemd-inhibit --list) - Resource usage: Abnormal memory or CPU usage (
ps -p $(pgrep rouser) -o pid,pcpu,pmem,rss)
- Stop the service:
systemctl --user stop rouser.service - Check logs:
journalctl --user -u rouser -n 50 --no-pager - Validate config:
rouser --validate-config - Test in dry-run mode:
rouser --dry-run -l debug - Restart:
systemctl --user start rouser.service
GitHub Actions workflows run in privileged environments that can exfiltrate secrets or inject malicious code into build artifacts. All workflow jobs follow the principle of least privilege: each job declares only the permissions it needs, scoped to the minimum required scope.
jobs:
lint-test:
runs-on: ubuntu-latest
permissions:
contents: read # Only read access — cannot push code or create releases
steps: ...
release-upload:
runs-on: ubuntu-latest
permissions:
contents: write # Write only for the job that uploads to GitHub Releases
packages: write # Required for publishing container/package registries
steps: ...Never declare permissions at the workflow level with broad scopes. This grants every job (including those that never need it) elevated access. If a downstream dependency in any job is compromised, all permissions are exposed. Always use job-level permissions: blocks.
Every PR to main runs zizmor — a GitHub Actions security linter that checks for:
- dangerous-triggers: Workflows triggered by untrusted events (PR forks, issue labels) without safeguards
- cache-poisoning: Malicious artifacts injected via cache keys derived from user input
- unpinned-uses: Actions referenced by mutable tags (e.g.,
@v4) instead of immutable SHAs - template-injection: User-controlled data interpolated into workflow steps
- excessive-permissions: Jobs with more permissions than required
Add to your CI:
zizmor-security-scan:
runs-on: ubuntu-latest
permissions: { contents: read }
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Install zizmor
uses: taiki-e/install-action@7769b73c2ec98c38dfcf2e18c83cfd4880c038c1 # v2
with: { tool: zizmor }
- run: zizmor .All uses: references are pinned to immutable commit SHAs. The following table documents every action reference used across workflow files:
| Action | Pinned SHA | Version | Notes |
|---|---|---|---|
| actions/checkout | @34e114876b0b11c390a56381ad16ebd13914f8d5 |
v4.3.1 | All steps include persist-credentials: false to prevent credential exposure via git config |
| dtolnay/rust-toolchain | @29eef336d9b2848a0b548edc03f92a220660cdb8 |
stable (branch HEAD) | Immutable branch reference; CI installs cross-compilation targets at this SHA. Branch can move — verify against stable tag before updating. |
| Swatinem/rust-cache | @cf9339e04bb8069e37c71ff4a0e6a970f35d6f7d |
v2.7.0 | Cache keys use static strings or matrix values only — no user input in key derivation |
| taiki-e/install-action (v2) | @7769b73c2ec98c38dfcf2e18c83cfd4880c038c1 |
v2 | Used for zizmor, cargo-audit, and cross toolchain installation |
| taiki-e/install-action (git-cliff) | @v2 |
v2 | Renovate manages SHA digest updates via helpers:pinGitHubActionDigestsToSemver; tool specified explicitly as tool: git-cliff |
| actions/upload-artifact | @ea165f8d65b6e75b540449e92b4886f43607fa02 |
v4.6.2 | Release artifact uploads with scoped write permissions per job |
| actions/download-artifact | @d3f86a106a0bac45b974a628896c90dbdf5c8093 |
v4.3.0 | Downloads CI artifacts for packaging jobs; patterns are deterministic |
When adding new action references, always look up the current commit SHA via GitHub API before committing workflow changes:
# Look up tag → full 40-char SHA mapping (use jq -r '.object.sha' for full hash)
curl -sf "https://api.github.com/repos/OWNER/REPO/git/ref/tags/TAG" \
| jq -r '.object.sha'Document any new pinning in this table and add a comment on the uses: line matching the format above.
Both GitHub Actions dependencies and Rust crate dependencies are updated automatically via scheduled dependency bots:
- Renovate (
.github/renovate.json) — Maintains SHA pinning for alluses:references in workflow files via thehelpers:pinGitHubActionDigestsToSemverpreset. Opens PRs when pinned SHAs need updating to match latest version tags, preserving version comments and including release notes for review. - Dependabot (
.github/dependabot.yml) — MonitorsCargo.tomland GitHub Actions references on a weekly schedule. Opens PRs for Rust dependency bumps (actions/checkout@v4→@v4.x.y).
Together they cover all update surfaces: Renovate handles digest maintenance for already-pinned workflow actions; Dependabot handles version bump PRs for both Rust crates and action tags.
All release artifacts are distributed as tarballs with embedded systemd service files and config. The PKGBUILD for Arch Linux computes SHA256 checksums from the actual GitHub release assets during CI generation — never using 'SKIP'. This ensures package integrity verification at install time, preventing supply chain attacks where release binaries could be replaced.
| Anti-pattern | Risk | Fix |
|---|---|---|
permissions: contents: write at workflow level |
Every job gets full repo access | Use per-job scoped permissions |
actions/checkout@v4 without pinning |
Vulnerable to tag hijacking | Pin to full 40-char commit SHA (see table above) — Renovate can automate this via helpers:pinGitHubActionDigestsToSemver preset |
${{ github.event.release.tag_name }} interpolated into shell steps |
Template injection via crafted release names | Use $GITHUB_REF env var with prefix stripping instead |
Using curl ... | bash in CI scripts |
MITM during download + arbitrary code execution | Download to file, verify checksums, then execute |
The project uses cargo-audit (via the cargoaudit crate's advisory database) in CI to scan for known CVEs:
cargo install cargo-audit # Once per environment
cargo audit # Run before every commit / PRCritical and high severity findings fail the build. See Dependency Management above for runtime dependency policies.