Skip to content

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

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

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

Conversation

@vyommani

@vyommani vyommani commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Extends PR #1080 per-IP rate limiting with account-level fan-out protection for distributed brute-force attempts against the UnixAuth listener.

This PR adds on top of #1080:

  1. Account fan-out delayLoginAttemptTracker tracks distinct source IPs failing against the same username. When distinct-IP count exceeds a threshold (default 3), a progressive delay is applied (200ms × over-threshold, capped at 2000ms). Single-IP retries (legitimate admin typos) are never delayed.

  2. Account-aware failure/success recordingPasswordValidator passes (sourceIp, username) to the tracker on all failure paths and clears both on success.

  3. New config keys in ranger-ugsync-default.xml:

Property Default
ranger.usersync.unixauth.account.fanout.enabled true
ranger.usersync.unixauth.account.distinct.ip.threshold 3
ranger.usersync.unixauth.account.window.ms 60000
ranger.usersync.unixauth.account.base.delay.ms 200
ranger.usersync.unixauth.account.max.delay.ms 2000

Also inherits from #1080: per-IP lockout, generic failure responses, socket timeout, optional TLS client-auth.


How was this patch tested?

Unit tests (automated)

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

LoginAttemptTrackerTest (18 tests): per-IP lockout (6 from #1080) + account fan-out (12 new): single-IP never delayed, distinct-IP threshold, linear scaling, cap, success/window reset, disabled mode, concurrency, null-account safety.

TestPasswordValidator (9 tests): blocked IP, failure/success recording with account, generic responses.

TestUnixAuthenticationService (10 tests): config loading, tracker wiring, socket timeout, validator thread spawn.


E2E exercises the real UnixAuthenticationService.startService() listener with production-default fan-out settings. Does not require Docker, usersync tarball, or native credValidator.uexe — mock shell validators substitute.

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

git fetch origin pull/1106/head:RANGER-5690-1
git checkout RANGER-5690-1
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:

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

Save as mock-validator-ok.sh:

#!/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)

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);
    }
    private static int arg(String[] args, int idx, int def) {
        return args.length > idx ? Integer.parseInt(args[idx]) : def;
    }
    private static long argL(String[] args, int idx, long def) {
        return args.length > idx ? Long.parseLong(args[idx]) : def;
    }
    private static boolean argB(String[] args, int idx, boolean def) {
        return args.length > idx ? Boolean.parseBoolean(args[idx]) : def;
    }
    public static void main(String[] args) throws Throwable {
        String validator = args.length > 0 ? args[0] : "mock-validator-fail.sh";
        int port = arg(args, 1, 15151);
        int maxFailedAttempts = arg(args, 2, 5);
        long attemptWindowMs = argL(args, 3, 60_000);
        long lockoutDurationMs = argL(args, 4, 30_000);
        int socketTimeoutMs = arg(args, 5, 10_000);
        boolean accountFanoutEnabled = argB(args, 6, true);
        int accountDistinctIpThreshold = arg(args, 7, 3);
        long accountWindowMs = argL(args, 8, 60_000);
        long accountBaseDelayMs = argL(args, 9, 200);
        long accountMaxDelayMs = argL(args, 10, 2000);

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

        LoginAttemptTracker tracker = new LoginAttemptTracker(
                maxFailedAttempts, attemptWindowMs, lockoutDurationMs,
                accountFanoutEnabled, accountDistinctIpThreshold,
                accountWindowMs, accountBaseDelayMs, accountMaxDelayMs);

        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", socketTimeoutMs);
        set(svc, "loginAttemptTracker", tracker);

        System.out.println("READY port=" + port
                + " maxFailedAttempts=" + maxFailedAttempts
                + " accountFanoutEnabled=" + accountFanoutEnabled
                + " accountDistinctIpThreshold=" + accountDistinctIpThreshold);
        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

Step 3 — External TCP client (Python)

Save as unixauth-e2e-client.py:

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

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
bind_ip = sys.argv[6] if len(sys.argv) > 6 else None

for i in range(1, attempts + 1):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(10)
    if bind_ip:
        s.bind((bind_ip, 0))
    s.connect((host, port))
    start = time.monotonic()
    s.sendall(f"LOGIN: {user} {password}\n".encode())
    resp = s.recv(4096).decode().strip()
    elapsed_ms = int((time.monotonic() - start) * 1000)
    s.close()
    print(f"ATTEMPT_{i} src={bind_ip or 'default'} elapsed_ms={elapsed_ms} response={resp}")

Run lockout test:

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

Expected output:

ATTEMPT_1 src=default elapsed_ms=... response=FAILED: Authentication failed.
ATTEMPT_2 src=default elapsed_ms=... response=FAILED: Authentication failed.
ATTEMPT_3 src=default elapsed_ms=... response=FAILED: Authentication failed.
ATTEMPT_4 src=default elapsed_ms=0 response=FAILED: Too many attempts. Try again later.
ATTEMPT_5 src=default elapsed_ms=0 response=FAILED: Too many attempts. Try again later.

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)

Step 5 — Single-IP retries never trigger account fan-out delay

Restart with high IP threshold (maxFailedAttempts=100) so lockout does not interfere:

java -cp ".:$CP" UnixAuthE2eServer "$(pwd)/mock-validator-fail.sh" 15152 100 &
python3 unixauth-e2e-client.py 127.0.0.1 15152 alice wrong 5

Verified: all 5 attempts complete in ~30–50ms each — no progressive delay from account fan-out.

Step 6 — Account fan-out from distinct source IPs (optional)

Requires loopback aliases (127.0.0.2127.0.0.5). On macOS:

sudo ifconfig lo0 alias 127.0.0.2
# repeat for .3, .4, .5
python3 unixauth-e2e-client.py 127.0.0.1 15151 victim wrong 1 127.0.0.2
python3 unixauth-e2e-client.py 127.0.0.1 15151 victim wrong 1 127.0.0.3
python3 unixauth-e2e-client.py 127.0.0.1 15151 victim wrong 1 127.0.0.4
python3 unixauth-e2e-client.py 127.0.0.1 15151 victim wrong 1 127.0.0.5  # 4th distinct IP → delay ≥200ms

If loopback aliases are unavailable, account fan-out is covered by LoginAttemptTrackerTest unit tests.

All-in-one runner (optional)

Save as run-unixauth-e2e.sh in unixauthservice/ and run chmod +x run-unixauth-e2e.sh && ./run-unixauth-e2e.sh. See testing comment for full script.


Reviewer verification (ramackri)

Scenario Result
Unit tests (unixauthservice + unixauthclient) ✅ 37/37 + 32/32
Per-IP lockout (threshold=3)
Enumeration masking + success reset
Single-IP no fan-out delay

@vperiasamy vperiasamy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@rameeshm
rameeshm self-requested a review July 23, 2026 19:53
@ramackri
ramackri self-requested a review July 24, 2026 03:19
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.

3 participants