finished adding rate limiting

This commit is contained in:
2025-12-03 11:05:32 -05:00
parent f60a81e259
commit f5a5d63eb7
6 changed files with 47 additions and 23 deletions
+2 -2
View File
@@ -13,10 +13,10 @@ RUN mkdir -p /usr/local/etc/php/conf.d
COPY preauth-php.ini /usr/local/etc/php/conf.d/preauth-php.ini
# login sessions are stored here
RUN mkdir -p /tmp/sessions
RUN mkdir -p /tmp/data/sessions
# rate limit monitoring information is stored here
RUN mkdir -p /tmp/monitor
RUN mkdir -p /tmp/data/monitor
# fcgi command for the healthcheck
RUN apk add fcgi
+2 -2
View File
@@ -12,8 +12,8 @@ services:
restart: unless-stopped
user: ${USER_ID}:${GROUP_ID}
volumes:
- preauth-sessions:/tmp/sessions
- preauth:/tmp/data
volumes:
preauth-sessions:
preauth:
+5
View File
@@ -1,5 +1,9 @@
#!/bin/sh
# ensure we have required folders
`mkdir -p /tmp/data/sessions`
`mkdir -p /tmp/data/monitor`
# ensure we have required settings
if [ -z "$PREAUTH_KEY" ]; then
export PREAUTH_KEY=$(cd /preauth && php init-key.php)
@@ -9,5 +13,6 @@ if [ -z "$PREAUTH_TOKEN" ]; then
export PREAUTH_TOKEN=$(cd /preauth && php init-token.php)
fi
# start the process, now that setup is done
exec php-fpm
+12 -11
View File
@@ -1,20 +1,21 @@
# preauth uses "sessions" to store usage statistics by IP address and is used for rate limiting
# login sessions are stored as file in /tmp/sessions, and are unrelated to this configuration
; preauth uses php sessions to store usage statistics by IP address and is used for rate limiting
; login sessions are stored as file in /tmp/data/sessions, and are unrelated to this configuration
# sessions are IP based, cookie not needed nor wanted
; sessions are IP based, cookie not needed nor wanted
session.use_cookies = off
session.use_only_cookies = off
; deprecated in php 8.4
;session.use_only_cookies = off
session.cache_limiter = ''
session.name = preauth
session.save_path = /tmp/monitor
session.save_path = /tmp/data/monitor
# seconds until a session may be pruned
# IE: maximum time range to review for rate limiting and
# maximum time an IP address can be blocked
# 86400 seconds (24 hours) hardcoded upper limit
; seconds until a session may be pruned
; IE: maximum time range to review for rate limiting and
; maximum time an IP address can be blocked
; 86400 seconds aka 24 hours hardcoded upper limit
session.gc_maxlifetime = 86400
# disable random random garbage collection
# our healthcheck runs session_gc()
; disable random random garbage collection
; our healthcheck runs session_gc function
session.gc_probability = 0
+22 -5
View File
@@ -21,7 +21,7 @@ class Auth {
/* cookie name */
private const NAME = '_auth_uuid';
/* directory to store sessions in */
private const BASE = '/tmp/sessions/';
private const BASE = '/tmp/data/sessions/';
/* top-level-domains which are known to have multiple parts */
private const TLD = [
'ai' => ['com','net','off','org'],
@@ -137,12 +137,12 @@ class Auth {
} else {
/* not already logged in, not trying to login, and not on auth page */
/* so send to login screen */
$rt = rawurlencode(
$query = self::RETURN_FIELD . '=' . rawurlencode(
($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'https') . '://' .
($_SERVER['HTTP_X_FORWARDED_HOST'] ?? $this->domain) .
($_SERVER['HTTP_X_FORWARDED_URI'] ?? '/')
);
header("Location: https://$this->preauth.$this->domain/?rt=$rt");
header("Location: https://$this->preauth.$this->domain/?$query");
}
}
@@ -211,13 +211,15 @@ class Auth {
private function rateLimit(): bool {
/* our identifier for this remote-host, replace all special characters with dashes */
/* session_id() only allows ",", "-", and alphanumeric characters */
$limiterId = preg_replace('[^a-zA-Z0-9]', '-', $this->getRemoteHost());
$limiterId = preg_replace('/[^a-zA-Z0-9]/', '-', $this->getRemoteHost());
//error_log("limiterId: {$limiterId}");
session_id($limiterId);
session_start();
/* new remote-ip, start logging */
if ( ! isset($_SESSION['count'], $_SESSION['time'])) {
$this->resetRateLimit(false);
//error_log('limiter new, allow');
return false;
}
@@ -230,14 +232,17 @@ class Auth {
if ($sessionCount >= $rateLimit && time() - $rateBlocked <= $sessionTime) {
/* if over limit, and block current, block them */
//error_log("limiter over limit, block ($sessionCount)");
return true;
} else if ($sessionCount < $rateLimit && time() - $rateTimeout <= $sessionTime) {
/* if under limit, and timeout current, allow them */
//error_log("limiter under limit, allow ($sessionCount)");
return false;
}
/* either over limit and block has expired or */
/* under limit and timeout has expired, so reset them */
//error_log("limiter expired, reset");
$this->resetRateLimit(false);
return false;
}
@@ -265,6 +270,7 @@ class Auth {
$id = substr(preg_replace(self::URL64, '', $this->get[self::ID_FIELD]), 0, 100);
$otp = TOTP::createFromSecret($this->token);
$date = date('Y-m-d H:i:s');
$remoteHost = $this->getRemoteHost();
/* if given token is valid, login, store session file and set the cookie */
if ($otp->now() === $this->get[self::TOKEN_FIELD]) {
@@ -276,7 +282,7 @@ class Auth {
file_put_contents(
self::BASE . $encUUID,
base64_encode($rawIV) .
"\$$id\$$this->expire\$$date\${$this->getRemoteHost()}\$\n"
"\$$id\$$this->expire\$$date\$$remoteHost\$\n"
);
setcookie(
self::NAME,
@@ -293,8 +299,19 @@ class Auth {
return true;
}
/* login attempted and failed, update monitoring for rate limiting */
$sessionCount = (int)$_SESSION['count'];
$rateLimit = (int)getenv('PREAUTH_RATE_LIMIT') ?: 4;
$this->logFailedAttempt();
if (($sessionCount + 1) >= $rateLimit) {
/* +1 to count this failure */
error_log("[$date] rate-limiting trigger for: $remoteHost");
/* just reached the rate limit, block them */
include __DIR__ . '/../400.php';
exit(0);
}
}
return false;
}
+4 -3
View File
@@ -1,4 +1,5 @@
<?php
use Preauth\Auth;
use Preauth\Env;
global $auth;
$env = new Env();
@@ -63,11 +64,11 @@ $env = new Env();
<body>
<h1><?php echo $env->getTitle(); ?></h1>
<form action="/" method="get">
<input type="hidden" name="<?php echo Preauth::RETURN_FIELD; ?>" value="<?php echo $auth->getReturnTo(); ?>">
<input type="hidden" name="<?php echo Auth::RETURN_FIELD; ?>" value="<?php echo $auth->getReturnTo(); ?>">
<div class="right"><label for="id"><?php echo $env->getIdName(); ?>:</label></div>
<div><input type="text" name="<?php echo Preauth::ID_FIELD; ?>" id="id" autocomplete="on" required="required" autofocus="autofocus"></div>
<div><input type="text" name="<?php echo Auth::ID_FIELD; ?>" id="id" autocomplete="on" required="required" autofocus="autofocus"></div>
<div class="right"><label for="token"><?php echo $env->getTokenName(); ?>:</label></div>
<div><input type="text" name="<?php echo Preauth::TOKEN_FIELD; ?>" id="token" autocomplete="off" required="required"></div>
<div><input type="text" name="<?php echo Auth::TOKEN_FIELD; ?>" id="token" autocomplete="off" required="required"></div>
<div class="center"><button type="submit"><?php echo $env->getSubmitName(); ?></button></div>
</form>
</body>