Allow non-root processes to use the PAM service#1649
Conversation
ce3a6d4 to
2f665a1
Compare
There was a problem hiding this comment.
Pull request overview
This PR removes the previously root-only “global” permission gating for PAM gRPC calls so that non-root processes (e.g., KDE/Sway lock screens) can interact with the PAM service, and adds a failed-authentication delay to mitigate brute-force attempts.
Changes:
- Remove the gRPC “global permissions” interceptor and related
CheckGlobalAccesshooks for PAM/NSS. - Add per-username failed-auth tracking and impose a delay after repeated authentication failures.
- Update tests and broker mocks to reflect the new authorization behavior and exercise the fail-delay logic.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/testutils/broker.go | Extend broker mock to simulate a denied authentication outcome used by new tests. |
| internal/services/user/user_test.go | Remove test-only interceptor that enforced the deleted global-access check. |
| internal/services/user/permissions.go | Remove no-op CheckGlobalAccess implementation (global access layer removed). |
| internal/services/permissions.go | Remove global unary interceptor that previously enforced PAM/NSS global access checks. |
| internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_not_root/IsAuthenticated | Remove golden output tied to the removed “must be root” behavior. |
| internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_not_root/cache.db | Remove golden DB snapshot tied to the removed “must be root” behavior. |
| internal/services/pam/permissions.go | Remove PAM CheckGlobalAccess root-only check (enables non-root PAM calls). |
| internal/services/pam/pam.go | Implement failed-auth tracking and delayed responses after repeated failures. |
| internal/services/pam/pam_test.go | Remove non-root error cases; add a new test covering the fail-delay threshold behavior. |
| internal/services/pam/export_test.go | Export fail-delay constants to external test package. |
| internal/services/manager.go | Stop wiring the removed global-permissions interceptor into the gRPC server. |
| internal/brokers/manager.go | Track session→username mapping to support per-user failure accounting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
d3d804f to
cd1316f
Compare
96c133e to
8f242f1
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1649 +/- ##
==========================================
+ Coverage 84.96% 87.91% +2.94%
==========================================
Files 25 96 +71
Lines 1942 6965 +5023
Branches 0 111 +111
==========================================
+ Hits 1650 6123 +4473
- Misses 292 786 +494
- Partials 0 56 +56 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
992b0c6 to
688b8cd
Compare
denisonbarbosa
left a comment
There was a problem hiding this comment.
A few things that can be addressed in a final rebase. Everything else looks great!
603d834 to
bef5e1a
Compare
bef5e1a to
d06713e
Compare
|
Reminder: Once merged, ask affected users to try the edge version |
The permission check was introduced in #311 to: 1. "prevent spamming the service with invalid authentication request", and 2. "prevent some NSS (shadow) requests" Regarding 1., our assessment is that it's not feasible to protect against DoS from a local user. For example, all D-Bus services accessible to unprivileged users on the system bus are also prone to DoS. Regarding 2., the shadow requests (GetShadowEntries and GetShadowByName) were dropped in 62ffae0 without replacement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expose UsernameFromSessionID on the broker Manager so callers can look up the username associated with an active session without going through broker internals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Without the root-only restriction on the PAM gRPC socket, any local user can now call IsAuthenticated in a tight loop against another user's cached (offline) OIDC credentials. The OIDC broker hashes those credentials with argon2id, but the parameters yield ~26ms per attempt on modern hardware — far faster than pam_unix's enforced 2s fail delay. Track consecutive authentication failures per username in an in-memory counter. The first authFailDelayThreshold (3) denials are returned immediately, matching the leniency of pam_faillock's deny=3 default and giving users a couple of free retries for typos. From the fourth failure onward, the response is held for authFailDelay (2s) before returning, matching the delay pam_unix imposes via pam_fail_delay(). A successful authentication resets the counter for that user. The counter is keyed by username (not session ID) so it cannot be reset by starting a new session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The three security-policy knobs that govern the PAM brute-force mitigation were hardcoded constants. Operators may need to tune them to satisfy site-specific compliance requirements, so expose them in the authd.yaml config: auth_fail_delay_threshold – failures before a delay is imposed (default 3) auth_fail_delay – length of that delay (default 2s) auth_fail_reset_window – inactivity window before reset (default 15m) authFailMaxTracked is intentionally kept hardcoded: it is a memory- safety bound rather than a security-policy value, and exposing it creates a DoS surface with no meaningful benefit. The values are grouped in pam.Config / pam.DefaultConfig following the same pattern used by users.Config for UID/GID ranges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously recordFailure returned 0 when the tracker was full, meaning the delay was never applied. An attacker could exploit this by flooding the tracker with bogus usernames (a fill attack) to disable brute-force protection for the real target. Return math.MaxInt instead so the delay is always applied regardless of whether the username could be recorded. This means the fill attack degrades to 'everyone gets delayed' rather than 'no one does'. Add TestIsAuthenticated_FailDelayTrackerFull to verify the fail-secure behaviour: fill the tracker to capacity with one username, then confirm the delay is still applied for a new username that cannot be tracked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Non-root PAM consumers can no longer make the follow-up SetBroker RPC after the root-only peer check, which left successful logins unable to remember their selected broker. authd already knows the authenticated user and session broker inside IsAuthenticated, so persisting the default broker there avoids the extra privileged round-trip. Drop the unused SetBroker RPC and simplify the PAM client and test flows so AcctMgmt now consistently returns PAM_IGNORE while broker persistence happens server-side. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The delay was only applied when the broker returned Denied or DeniedMaxTries. Because DeniedMaxTries is only issued after N failed attempts within the same session, an attacker who opens a fresh session for every guess always receives Retry and the delay never fires. Count Retry as a failure so that the per-username counter (shared across sessions) triggers the delay regardless of which response the broker returns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Setting the reset window to 0 was silently broken: time.Since() always returns a non-negative duration, so the stale-entry check was always true, resetting the failure counter on every attempt and defeating brute-force protection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
93e480b to
bbc9164
Compare
The permission check was introduced in #311 to:
Regarding 1., our assessment is that it's not feasible to protect against DoS from a local user. For example, all D-Bus services accessible to unprivileged users on the system bus are also prone to DoS.
Regarding 2., the shadow requests (GetShadowEntries and GetShadowByName) were dropped in 62ffae0 without replacement.
There is the concern that this allows non-root processes to repeatedly call the gRPC API to brute-force the local password. The local password is hashed with argon2id (time=1, memory=64MB, threads=4), which makes each authentication attempt with the local password somewhat expensive. However, on modern hardware, it can still be done in a few dozen milliseconds. So to avoid brute forcing, this PR also adds a fail delay that delays each failed authentication attempt after the first three by 2 seconds.
Closes #856
UDENG-6981