Skip to content

RANGER-5690: UnixAuth service lacks rate limiting on authentication a…#1080

Open
vyommani wants to merge 1 commit into
apache:masterfrom
vyommani:RANGER-5690
Open

RANGER-5690: UnixAuth service lacks rate limiting on authentication a…#1080
vyommani wants to merge 1 commit into
apache:masterfrom
vyommani:RANGER-5690

Conversation

@vyommani

@vyommani vyommani commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

UnixAuthenticationService accepted unlimited authentication attempts with no rate limiting, delay, or lockout. Any network client that could reach the UnixAuth listener could guess OS passwords at native crypt() speed, with a username-enumeration side channel from distinct failure responses.

This PR adds:

  1. Per-source-IP rate limiting and lockoutLoginAttemptTracker tracks failed attempts per source IP within a sliding window (default: 5 failures / 60s window / 30s lockout, all configurable via ranger-ugsync-default.xml). Once the threshold is hit, further attempts are rejected before the OS credential validator is invoked. Covers wrong password, validator errors, malformed/empty requests, and connection timeouts.

  2. Closed username-enumeration side channel — Network clients now receive a single generic FAILED: Authentication failed. regardless of whether the password was wrong or the user does not exist. Full detail remains in server-side logs with source IP.

  3. Optional TLS client-certificate enforcement — Opt-in ranger.usersync.unixauth.require.client.auth (default false) requires clients to present a certificate trusted by the configured truststore.

  4. Socket read timeout — Connections that send no data within a configurable window (default 10s) are closed and counted as a failed attempt.

New / modified files

File Change
LoginAttemptTracker.java New per-IP sliding-window tracker
PasswordValidator.java Generic responses, lockout check, failure/success recording
UnixAuthenticationService.java Tracker init, socket timeout, optional client-auth
ranger-ugsync-default.xml New config keys for rate limit / lockout / timeout
LoginAttemptTrackerTest.java Unit tests for tracker
TestPasswordValidator.java Extended for lockout and generic failure paths

Config keys (defaults)

Property Default
ranger.usersync.unixauth.max.failed.attempts 5
ranger.usersync.unixauth.attempt.window.ms 60000
ranger.usersync.unixauth.lockout.duration.ms 30000
ranger.usersync.unixauth.socket.timeout.ms 10000
ranger.usersync.unixauth.require.client.auth false

How was this patch tested?

Unit tests (automated)

cd unixauthservice && mvn clean test
cd ../unixauthclient && mvn test
Module Result
unixauthservice 25/25 pass
unixauthclient 32/32 pass

LoginAttemptTrackerTest (6 tests): threshold below lockout, lockout at threshold, success clears history, window expiry resets count, re-lock after lockout expires, independent per-IP tracking.

TestPasswordValidator (9 tests): generic failure when validator returns distinct message, blocked IP returns FAILED: Too many attempts. Try again later. without invoking validator, failure/success recorded on tracker, null program / exec errors counted as failures.

TestUnixAuthenticationService (10 tests): config loading, LoginAttemptTracker wiring, socket timeout via setSoTimeout, validator thread spawn on accept.


End-to-end testing (manual, reproducible)

E2E exercises the real UnixAuthenticationService.startService() listener (TCP accept → setSoTimeoutPasswordValidator thread → LoginAttemptTracker). It does not require the full usersync tarball, Docker stack, or native credValidator.uexe — a mock shell validator is used instead.

Prerequisites: JDK 8+, Maven, Python 3, checkout of this PR branch.

git fetch origin pull/1080/head:RANGER-5690
git checkout RANGER-5690
cd unixauthservice
mvn package -DskipTests
mvn dependency:build-classpath -Dmdep.outputFile=target/test-cp.txt

Wire protocol (unchanged)

Client → Server:  LOGIN:<username> <password>\n
Server → Client:  OK: ... | FAILED: Authentication failed. | FAILED: Too many attempts. Try again later.

Step 1 — Mock validator scripts

Save as mock-validator-fail.sh (always fails with a distinct internal message):

#!/bin/bash
read -r line
echo "FAILED: Password did not match."

Save as mock-validator-ok.sh (returns OK for gooduser, fails for others):

#!/bin/bash
read -r line
if echo "$line" | grep -q "gooduser"; then
  echo "OK"
else
  echo "FAILED: Password did not match."
fi
chmod +x mock-validator-fail.sh mock-validator-ok.sh

Step 2 — Start test listener (plain TCP)

The harness below starts UnixAuthenticationService.startService() with ssl=false, threshold 3 (faster demo; production default is 5), and the mock validator. Save as UnixAuthE2eServer.java in unixauthservice/:

import org.apache.ranger.authentication.LoginAttemptTracker;
import org.apache.ranger.authentication.PasswordValidator;
import org.apache.ranger.authentication.UnixAuthenticationService;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class UnixAuthE2eServer {
    private static void set(Object target, String name, Object value) throws Exception {
        Field f = UnixAuthenticationService.class.getDeclaredField(name);
        f.setAccessible(true);
        f.set(target, value);
    }
    public static void main(String[] args) throws Throwable {
        String validator = args.length > 0 ? args[0] : "mock-validator-fail.sh";
        int port = args.length > 1 ? Integer.parseInt(args[1]) : 15151;
        int threshold = args.length > 2 ? Integer.parseInt(args[2]) : 3;

        PasswordValidator.setValidatorProgram(validator);
        PasswordValidator.setAdminUserList(null);
        PasswordValidator.setAdminRoleNames(null);

        UnixAuthenticationService svc = new UnixAuthenticationService();
        set(svc, "sslEnabled", false);
        set(svc, "portNum", port);
        set(svc, "enabledProtocolsList", new ArrayList<>());
        set(svc, "enabledCipherSuiteList", new ArrayList<>());
        set(svc, "keyStorePath", "");
        set(svc, "trustStorePath", "");
        set(svc, "keyStoreType", "JKS");
        set(svc, "trustStoreType", "JKS");
        set(svc, "keyStorePathPassword", "");
        set(svc, "trustStorePathPassword", "");
        set(svc, "requireClientAuth", false);
        set(svc, "socketTimeoutMs", 10_000);
        set(svc, "loginAttemptTracker", new LoginAttemptTracker(threshold, 60_000, 30_000));

        System.out.println("READY port=" + port + " threshold=" + threshold);
        svc.startService();
    }
}

Compile and start (background):

CP="target/classes:$(cat target/test-cp.txt)"
javac -cp "$CP" UnixAuthE2eServer.java
java -cp ".:$CP" UnixAuthE2eServer "$(pwd)/mock-validator-fail.sh" 15151 3 &
sleep 2
grep READY *.log 2>/dev/null || true   # server prints READY to stdout

Step 3 — External TCP client (Python)

Save as unixauth-e2e-client.py:

#!/usr/bin/env python3
import socket
import sys

host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 15151
user = sys.argv[3] if len(sys.argv) > 3 else "baduser"
password = sys.argv[4] if len(sys.argv) > 4 else "wrongpass"
attempts = int(sys.argv[5]) if len(sys.argv) > 5 else 5

for i in range(1, attempts + 1):
    s = socket.create_connection((host, port), timeout=10)
    s.sendall(f"LOGIN: {user} {password}\n".encode())
    resp = s.recv(4096).decode().strip()
    s.close()
    print(f"ATTEMPT_{i}={resp}")

Run lockout test:

python3 unixauth-e2e-client.py 127.0.0.1 15151 baduser wrongpass 5

Expected output:

ATTEMPT_1=FAILED: Authentication failed.
ATTEMPT_2=FAILED: Authentication failed.
ATTEMPT_3=FAILED: Authentication failed.
ATTEMPT_4=FAILED: Too many attempts. Try again later.
ATTEMPT_5=FAILED: Too many attempts. Try again later.

Verified: attempts 1–3 call mock validator but client sees generic message only; attempt 4+ locked out before validator runs.

Step 4 — Enumeration masking + success clears counter

Restart server with mock-validator-ok.sh, then:

python3 unixauth-e2e-client.py 127.0.0.1 15151 nobody wrong 1   # → FAILED: Authentication failed.
python3 unixauth-e2e-client.py 127.0.0.1 15151 gooduser good 1  # → OK
python3 unixauth-e2e-client.py 127.0.0.1 15151 nobody wrong 1   # → FAILED: Authentication failed. (counter reset)

All-in-one runner (optional)

Save as run-unixauth-e2e.sh in unixauthservice/:

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
mvn -q package -DskipTests
mvn -q dependency:build-classpath -Dmdep.outputFile=target/test-cp.txt
CP="target/classes:$(cat target/test-cp.txt)"

cat > mock-validator-fail.sh <<'EOF'
#!/bin/bash
read -r line
echo "FAILED: Password did not match."
EOF
chmod +x mock-validator-fail.sh

# UnixAuthE2eServer.java — paste from Step 2 above, or keep as separate file
javac -cp "$CP" UnixAuthE2eServer.java
java -cp ".:$CP" UnixAuthE2eServer "$(pwd)/mock-validator-fail.sh" 15151 3 &
PID=$!
trap 'kill $PID 2>/dev/null || true' EXIT
sleep 2

python3 unixauth-e2e-client.py 127.0.0.1 15151 baduser wrongpass 5
chmod +x run-unixauth-e2e.sh && ./run-unixauth-e2e.sh

What was NOT covered by the manual E2E harness

Item Notes
Full UnixAuthenticationService -enableUnixAuth daemon Needs JCEKS credential store, SSL keystores, usersync + Admin config
Docker usersync stack Full Ranger compose
Native credValidator.uexe Linux /etc/shadow binary; mock script substitutes
TLS on port 5151 Same auth logic; needs truststore for TLS clients
ranger.usersync.unixauth.require.client.auth=true Needs client CA in truststore

Regression scope

  • Existing UnixAuth wire protocol — unchanged (LOGIN: prefix, OK: / FAILED: responses)
  • Default config — backward compatible (rate limits use sensible defaults; client-auth opt-in)

@ramackri

Copy link
Copy Markdown
Contributor

LGTM

@ramackri
ramackri self-requested a review July 17, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants