contrib: auth_delay module
The attached patch is a contrib module to set login restrictions on users with
too many authentication failure. The administrator could manage several GUC
parameters to control the login restrictions which are listed below.
- set the wait time when password authentication fails.
- allow the wait time grows when users of the same IP consecutively logon failed.
- set the maximum authentication failure number from the same user. The system
will prevent a user who gets too many authentication failures from entering the
database.
I hope this will be useful to future development.
Thanks.
-----
zhcheng@ceresdata.com
Attachments:
pgsql-v12.8-auth-delay.1.patchapplication/octet-stream; charset=utf-8; name="=?UTF-8?B?cGdzcWwtdjEyLjgtYXV0aC1kZWxheS4xLnBhdGNo?="Download
--- a/contrib/auth_delay/Makefile
+++ b/contrib/auth_delay/Makefile
@@ -3,6 +3,9 @@
MODULES = auth_delay
PGFILEDESC = "auth_delay - delay authentication failure reports"
+EXTENSION = auth_delay
+DATA = auth_delay--1.0.sql
+
ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/auth_delay/auth_delay--1.0.sql b/contrib/auth_delay/auth_delay--1.0.sql
new file mode 100644
--- /dev/null
+++ b/contrib/auth_delay/auth_delay--1.0.sql
@@ -0,0 +1,44 @@
+CREATE OR REPLACE FUNCTION auth_delay_conn(OUT id int, OUT remote_host text, OUT delay double precision)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME', 'auth_delay_conn'
+ LANGUAGE C IMMUTABLE;
+
+CREATE VIEW auth_delay_conn AS SELECT * FROM auth_delay_conn();
+
+create or replace function release_auth_delay_conn_by_id(id int)
+ returns void
+ AS 'MODULE_PATHNAME', 'release_auth_delay_conn_by_id'
+ LANGUAGE C IMMUTABLE;
+
+create or replace function release_auth_delay_conn_by_name(name cstring)
+ returns void
+ AS 'MODULE_PATHNAME', 'release_auth_delay_conn_by_name'
+ LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION auth_delay_user(OUT id int, OUT user_name text, OUT total_attempts int)
+ RETURNS SETOF record
+ AS 'MODULE_PATHNAME', 'auth_delay_user'
+ LANGUAGE C IMMUTABLE;
+
+CREATE VIEW auth_delay_user AS SELECT * FROM auth_delay_user();
+
+create or replace function release_auth_delay_user_by_id(id int)
+ returns void
+ AS 'MODULE_PATHNAME', 'release_auth_delay_user_by_id'
+ LANGUAGE C IMMUTABLE;
+
+create or replace function release_auth_delay_user_by_name(name cstring)
+ returns void
+ AS 'MODULE_PATHNAME', 'release_auth_delay_user_by_name'
+ LANGUAGE C IMMUTABLE;
+
+alter view auth_delay_conn owner to sys;
+alter view auth_delay_user owner to sys;
+revoke execute on function release_auth_delay_conn_by_id(int) from public;
+revoke execute on function release_auth_delay_conn_by_name(cstring) from public;
+revoke execute on function release_auth_delay_user_by_id(int) from public;
+revoke execute on function release_auth_delay_user_by_name(cstring) from public;
+grant execute on function release_auth_delay_conn_by_id(int) to sys;
+grant execute on function release_auth_delay_conn_by_name(cstring) to sys;
+grant execute on function release_auth_delay_user_by_id(int) to sys;
+grant execute on function release_auth_delay_user_by_name(cstring) to sys;
diff --git a/contrib/auth_delay/auth_delay.c b/contrib/auth_delay/auth_delay.c
--- a/contrib/auth_delay/auth_delay.c
+++ b/contrib/auth_delay/auth_delay.c
@@ -2,7 +2,7 @@
*
* auth_delay.c
*
- * Copyright (c) 2010-2019, PostgreSQL Global Development Group
*
* IDENTIFICATION
* contrib/auth_delay/auth_delay.c
@@ -18,35 +18,711 @@
#include "utils/guc.h"
#include "utils/timestamp.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "lib/ilist.h"
+#include "storage/ipc.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "miscadmin.h"
+#include "pg_config_manual.h"
+#include "funcapi.h"
+
+#include "catalog/pg_authid.h"
+#include "utils/syscache.h"
+
+#include <time.h>
+
PG_MODULE_MAGIC;
void _PG_init(void);
+#define MAX_CONN_RECORDS 50
+#define MAX_USER_RECORDS 10
+#define SECONDS_IN_HOUR 3600
+#define MIN(a, b) (((a) < (b)) ? (a) : (b))
+
/* GUC Variables */
static int auth_delay_milliseconds;
+static int auth_delay_max_seconds;
+static double auth_delay_ratio;
+static int auth_delay_max_attempts;
+static int auth_delay_time_range;
+static int auth_delay_time_interval;
/* Original Hook */
static ClientAuthentication_hook_type original_client_auth_hook = NULL;
+typedef struct AuthConnRecord
+{
+ char remote_host[NAMEDATALEN]; /* how to decide the length? */
+ bool used;
+ double sleep_time; /* in milliseconds */
+} AuthConnRecord;
+
+typedef struct AuthUserRecord
+{
+ char user_name[NAMEDATALEN];
+ bool used;
+ time_t last_record_time;
+ int last_record_index;
+ int fail_slots[FLEXIBLE_ARRAY_MEMBER];
+} AuthUserRecord;
+
+typedef enum AuthStatus
+{
+ AUTH_PASS,
+ AUTH_EQUAL,
+ AUTH_FAIL
+} AuthStatus;
+
+static shmem_startup_hook_type shmem_startup_next = NULL;
+static AuthUserRecord *aur_array = NULL;
+static int *count;
+static AuthConnRecord *acr_array = NULL;
+
+//static bool check_denied_day(Port *port);
+
+static AuthStatus check_auth_rights(Port *port);
+static AuthUserRecord * find_user_record(char *user_name, int *free_index);
+static AuthConnRecord * find_conn_record(char *remote_host, int *free_index);
+static void cleanup_expired_slots(AuthUserRecord *aur);
+
+static double record_failed_conn_auth(Port *port);
+static void record_conn_failure(AuthConnRecord *acr);
+static void record_failed_user_auth(Port *port);
+static void record_user_failure(AuthUserRecord *aur);
+static int get_failure_count(AuthUserRecord *aur);
+static time_t truncated_min_and_sec(const time_t clock);
+
+static void cleanup_conn_record(Port *port);
+static void cleanup_user_record(Port *port);
+
+Datum auth_delay_conn(PG_FUNCTION_ARGS);
+Datum release_auth_delay_conn_by_id(PG_FUNCTION_ARGS);
+Datum release_auth_delay_conn_by_name(PG_FUNCTION_ARGS);
+Datum auth_delay_user(PG_FUNCTION_ARGS);
+Datum release_auth_delay_user_by_id(PG_FUNCTION_ARGS);
+Datum release_auth_delay_user_by_name(PG_FUNCTION_ARGS);
+
/*
* Check authentication
*/
static void
auth_delay_checks(Port *port, int status)
{
+// bool denied;
+ AuthStatus auth_status;
+ elog(DEBUG1, "status: %d", status);
+
/*
* Any other plugins which use ClientAuthentication_hook.
*/
if (original_client_auth_hook)
original_client_auth_hook(port, status);
+// denied = check_denied_day(port);
+ auth_status = check_auth_rights(port);
+
/*
* Inject a short delay if authentication failed.
*/
if (status != STATUS_OK)
{
- pg_usleep(1000L * auth_delay_milliseconds);
+ double delay; /* in milliseconds */
+
+ /* ClientAuthentication() is called twice, ignore the first one
+ * TODO: why? */
+ if (status == STATUS_EOF)
+ return;
+
+ record_failed_user_auth(port);
+ delay = record_failed_conn_auth(port);
+ delay = MIN(delay, 1000L * auth_delay_max_seconds);
+ // pg_usleep(1000L * auth_delay_milliseconds);
+ elog(LOG, "Authentication delayed for %f seconds", delay / 1000.0);
+ pg_usleep(1000L * (long) delay);
+
+// if (denied)
+// elog(ERROR, "user \"%s\" is not allowed to login today", port->user_name);
+
+ /* There are two kinks of failure: 1. wrong password 2. current user
+ * is locked. We should not reveal the information that the password
+ * is wrong in the first place. */
+ if (auth_status == AUTH_EQUAL || auth_status == AUTH_FAIL)
+ elog(ERROR, "Too many failed login attempts in the recent");
+ }
+ else if (auth_status == AUTH_FAIL)
+ {
+ /* authentication passed but current user is locked */
+ elog(ERROR, "Too many failed login attempts in the recent");
+ }
+ else
+ {
+ cleanup_conn_record(port);
+ cleanup_user_record(port);
+ }
+ // TODO: success, clean up
+}
+
+/*
+static int
+get_dayofweek()
+{
+ time_t cur_time;
+ struct tm *p;
+
+ time(&cur_time);
+ p = localtime(&cur_time);
+
+ elog(DEBUG1, "day of week: %d", p->tm_wday);
+ return p->tm_wday;
+}
+*/
+
+/*
+ * Check whether a user is denied to login on current day of week.
+ * Return:
+ * true: login is denied
+ * false: login is allowed
+ */
+/*
+static bool
+check_denied_day(Port *port)
+{
+ HeapTuple tuple;
+ Datum datum;
+ bool isnull;
+ char *denied_day;
+ int i;
+ int dayofweek;
+ bool result;
+
+ tuple = SearchSysCache1(AUTHNAME,
+ PointerGetDatum(port->user_name));
+ if (!HeapTupleIsValid(tuple))
+ {
+ elog(ERROR, "Role \"%s\" does not exist.", port->user_name);
}
+
+ datum = SysCacheGetAttr(AUTHNAME, tuple, Anum_pg_authid_roldeniedday, &isnull);
+ if (isnull)
+ {
+ result = false;
+ }
+ else
+ {
+ denied_day = text_to_cstring(DatumGetTextP(datum));
+
+ dayofweek = get_dayofweek();
+
+ result = false;
+ for (i = 0; i < strlen(denied_day); i++)
+ {
+ if (denied_day[i] == ',')
+ continue;
+ if (denied_day[i] == ('0' + dayofweek))
+ result = true;
+ }
+ }
+
+ ReleaseSysCache(tuple);
+
+ return result;
+}
+*/
+
+/*
+ * Get AuthUserRecord pointer according to given index
+ * Since the length of AuthUserRecord is not fixed, we have to calculate
+ * the offset.
+ */
+static AuthUserRecord *
+get_aur(int index)
+{
+ int length;
+
+ Assert(index < MAX_USER_RECORDS);
+ length = sizeof(AuthUserRecord) + sizeof(int) * (auth_delay_time_range /
+ auth_delay_time_interval - 1);
+
+ return (AuthUserRecord *) ((char *)aur_array + length * index);
+}
+
+/*
+ * Check whether current user has the right to login
+ */
+static AuthStatus
+check_auth_rights(Port *port)
+{
+ int free_index;
+ AuthUserRecord *aur = NULL;
+
+ aur = find_user_record(port->user_name, &free_index);
+ if (aur == NULL)
+ return AUTH_PASS;
+
+ cleanup_expired_slots(aur);
+ if (get_failure_count(aur) > auth_delay_max_attempts)
+ return AUTH_FAIL;
+ else if (get_failure_count(aur) == auth_delay_max_attempts)
+ return AUTH_EQUAL;
+ else
+ return AUTH_PASS;
+}
+
+static AuthUserRecord *
+find_user_record(char *user_name, int *free_index)
+{
+ int i;
+
+ *free_index = -1;
+ for (i = 0; i < MAX_USER_RECORDS; i++)
+ {
+ if (!get_aur(i)->used)
+ {
+ if (*free_index == -1)
+ *free_index = i;// record unused element
+ continue;
+ }
+ if (strcmp(get_aur(i)->user_name, user_name) == 0)
+ return get_aur(i);
+ }
+
+ return NULL;
+}
+
+static AuthConnRecord *
+find_conn_record(char *remote_host, int *free_index)
+{
+ int i;
+
+ *free_index = -1;
+ for (i = 0; i < MAX_CONN_RECORDS; i++)
+ {
+ if (!acr_array[i].used)
+ {
+ if (*free_index == -1)
+ *free_index = i;// record unused element
+ continue;
+ }
+ if (strcmp(acr_array[i].remote_host, remote_host) == 0)
+ return &acr_array[i];
+ }
+
+ return NULL;
+}
+
+static void
+cleanup_expired_slots(AuthUserRecord *aur)
+{
+ time_t cur_time;
+ int i;
+ int index;
+ int interval;
+ int num_slots;
+
+ time(&cur_time);
+ cur_time = truncated_min_and_sec(cur_time);
+
+ interval = (cur_time - aur->last_record_time) / SECONDS_IN_HOUR;
+ interval = MIN(interval, auth_delay_time_range) / auth_delay_time_interval;
+ elog(DEBUG1, "interval: %d", interval);
+ num_slots = auth_delay_time_range / auth_delay_time_interval;
+
+ index = aur->last_record_index;// in case interval = 0
+ for (i = 1; i <= interval; i++)
+ {
+ /* These slots are out of time range */
+ index = (aur->last_record_index + i) % num_slots;
+ aur->fail_slots[index] = 0;
+ }
+ aur->last_record_index = index;
+ // aur->last_record_time = cur_time;
+}
+
+static double
+record_failed_conn_auth(Port *port)
+{
+ AuthConnRecord *acr = NULL;
+ int j = -1;
+
+ acr = find_conn_record(port->remote_host, &j);
+
+ if (!acr)
+ {
+ Assert(j != -1);// XXX: no free space, MAX_CONN_RECORDS too small
+ acr = &acr_array[j];
+ strcpy(acr->remote_host, port->remote_host);
+ acr->used = true;
+ elog(DEBUG1, "new connection: %s, index: %d", acr->remote_host, j);
+ }
+
+ record_conn_failure(acr);
+ return acr->sleep_time;
+}
+
+static void
+record_conn_failure(AuthConnRecord *acr)
+{
+ if (acr->sleep_time == 0)
+ acr->sleep_time = (double) auth_delay_milliseconds;
+ else
+ acr->sleep_time *= auth_delay_ratio;
+ elog(DEBUG1, "connection %s will sleep for %f seconds",
+ acr->remote_host, acr->sleep_time / 1000.0);
+}
+
+static void
+record_failed_user_auth(Port *port)
+{
+ AuthUserRecord *aur = NULL;
+ int j = -1;
+
+ (*count)++;
+ elog(DEBUG1, "count: %d", *count);
+
+ aur = find_user_record(port->user_name, &j);
+ elog(DEBUG1, "record_failed_user_auth: %d, %d", get_aur(0)->fail_slots[0],
+ get_aur(0)->fail_slots[1]);
+
+ if (!aur)
+ {
+ Assert(j != -1);// XXX: no free space, MAX_USER_RECORDS too small
+ aur = get_aur(j);
+ strcpy(aur->user_name, port->user_name);
+ aur->used = true;
+ elog(DEBUG1, "new user: %s, index: %d", aur->user_name, j);
+ }
+
+ record_user_failure(aur);
+}
+
+static time_t
+truncated_min_and_sec(const time_t clock)
+{
+ struct tm *p = localtime(&clock);
+
+ p->tm_min = 0;
+ p->tm_sec = 0;
+
+ return mktime(p);
+}
+
+static void
+record_user_failure(AuthUserRecord *aur)
+{
+ int index = -1;
+ time_t cur_time;
+
+ if (aur->last_record_time == 0)
+ {
+ /* New record */
+ aur->fail_slots[0] = 1;
+ aur->last_record_index = 0;
+ }
+ else
+ {
+ index = aur->last_record_index;
+ aur->fail_slots[index]++;
+ }
+
+ time(&cur_time);
+ cur_time = truncated_min_and_sec(cur_time);
+ aur->last_record_time = cur_time;
+ elog(DEBUG1, "user: %s, failure count: %d, index: %d",
+ aur->user_name, get_failure_count(aur), index);
+ elog(DEBUG1, "record_user_failure: %d, %d", get_aur(0)->fail_slots[0],
+ get_aur(0)->fail_slots[1]);
+}
+
+static int
+get_failure_count(AuthUserRecord *aur)
+{
+ int i;
+ int count = 0;
+ for (i = 0; i < auth_delay_time_range / auth_delay_time_interval; i++)
+ count += aur->fail_slots[i];
+
+ elog(DEBUG1, "total count: %d", count);
+ return count;
+}
+
+static void
+cleanup_conn_record(Port *port)
+{
+ int free_index;
+ AuthConnRecord *acr = NULL;
+
+ acr = find_conn_record(port->remote_host, &free_index);
+ if (acr == NULL)
+ return;
+
+ acr->used = false;
+ acr->sleep_time = 0.0;
+}
+
+static void
+cleanup_user_record(Port *port)
+{
+ int free_index;
+ AuthUserRecord *aur = NULL;
+
+ aur = find_user_record(port->user_name, &free_index);
+ if (aur == NULL)
+ return;
+
+ aur->used = false;
+ aur->last_record_time = 0;
+ aur->last_record_index = 0;
+ memset(aur->fail_slots, 0, auth_delay_time_range/auth_delay_time_interval);
+}
+
+Datum
+auth_delay_conn(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ Datum values[3];
+ bool nulls[3];
+
+ elog(DEBUG1, "auth_delay_conn() is called");
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TupleDesc tupdesc;
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+/*
+ * Modified: CreateTemplateTupleDesc(int, bool) -> CreateTemplateTupleDesc(int)
+ */
+ tupdesc = CreateTemplateTupleDesc(3);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "id",
+ INT4OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "remote_host",
+ TEXTOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "delay",
+ FLOAT8OID, -1, 0);
+ funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+
+ funcctx->max_calls = MAX_CONN_RECORDS;
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+
+ while (funcctx->call_cntr < funcctx->max_calls)
+ {
+ HeapTuple tuple;
+ Datum result;
+ AuthConnRecord *acr;
+
+ acr = &acr_array[funcctx->call_cntr];
+ elog(DEBUG1, "cntr: %ld, remote host: %s, used: %d", funcctx->call_cntr, acr->remote_host, acr->used);
+ if (!acr->used)
+ {
+ funcctx->call_cntr++;
+ continue;
+ }
+
+ elog(DEBUG1, "remote host: %s", acr->remote_host);
+ memset(nulls, 0, sizeof(nulls));
+ values[0] = Int32GetDatum(funcctx->call_cntr);
+ values[1] = CStringGetTextDatum(acr->remote_host);
+ values[2] = Float8GetDatum(acr->sleep_time / 1000.0);
+
+ tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+
+ SRF_RETURN_NEXT(funcctx, result);
+ }
+ SRF_RETURN_DONE(funcctx);
+}
+PG_FUNCTION_INFO_V1(auth_delay_conn);
+
+Datum
+release_auth_delay_conn_by_id(PG_FUNCTION_ARGS)
+{
+ int id = PG_GETARG_DATUM(0);
+
+ if (id >= MAX_CONN_RECORDS || !acr_array[id].used)
+ elog(ERROR, "Invalid id");
+ else
+ {
+ acr_array[id].used = false;
+ acr_array[id].sleep_time = 0.0;
+ }
+ PG_RETURN_VOID();
+}
+PG_FUNCTION_INFO_V1(release_auth_delay_conn_by_id);
+
+Datum
+release_auth_delay_conn_by_name(PG_FUNCTION_ARGS)
+{
+ int j;
+ char *remote_host = PG_GETARG_CSTRING(0);
+ AuthConnRecord *acr = find_conn_record(remote_host, &j);
+
+ if (!acr)
+ elog(ERROR, "Invalid remote host");
+ else
+ {
+ acr->used = false;
+ acr->sleep_time = 0.0;
+ }
+
+ PG_RETURN_VOID();
+}
+PG_FUNCTION_INFO_V1(release_auth_delay_conn_by_name);
+
+Datum
+auth_delay_user(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ Datum values[3];
+ bool nulls[3];
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TupleDesc tupdesc;
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+/*
+ * Modified: CreateTemplateTupleDesc(int, bool) -> CreateTemplateTupleDesc(int)
+ */
+ tupdesc = CreateTemplateTupleDesc(3);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "id",
+ INT4OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "user_name",
+ TEXTOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "total_attempts",
+ INT4OID, -1, 0);
+ funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+
+ funcctx->max_calls = MAX_USER_RECORDS;
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+
+ while (funcctx->call_cntr < funcctx->max_calls)
+ {
+ HeapTuple tuple;
+ Datum result;
+ AuthUserRecord *aur;
+
+ aur = get_aur(funcctx->call_cntr);
+ if (!aur->used)
+ {
+ funcctx->call_cntr++;
+ continue;
+ }
+
+ elog(DEBUG1, "fail_slots: %d, %d", aur->fail_slots[0], aur->fail_slots[1]);
+ memset(nulls, 0, sizeof(nulls));
+ values[0] = Int32GetDatum(funcctx->call_cntr);
+ values[1] = CStringGetTextDatum(aur->user_name);
+ values[2] = Int32GetDatum(get_failure_count(aur));
+
+ tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+
+ SRF_RETURN_NEXT(funcctx, result);
+ }
+ SRF_RETURN_DONE(funcctx);
+}
+PG_FUNCTION_INFO_V1(auth_delay_user);
+
+Datum
+release_auth_delay_user_by_id(PG_FUNCTION_ARGS)
+{
+ int id = PG_GETARG_DATUM(0);
+ AuthUserRecord *aur;
+
+ if (id >= MAX_USER_RECORDS || !get_aur(id)->used)
+ elog(ERROR, "Invalid id");
+ else
+ {
+ aur = get_aur(id);
+ aur->used = false;
+ aur->last_record_time = 0;
+ aur->last_record_index = 0;
+ memset(aur->fail_slots, 0, auth_delay_time_range/auth_delay_time_interval);
+ }
+ PG_RETURN_VOID();
+}
+PG_FUNCTION_INFO_V1(release_auth_delay_user_by_id);
+
+Datum
+release_auth_delay_user_by_name(PG_FUNCTION_ARGS)
+{
+ int j;
+ char *user_name = PG_GETARG_CSTRING(0);
+ AuthUserRecord *aur = find_user_record(user_name, &j);
+
+ if (!aur)
+ elog(ERROR, "Invalid user name");
+ else
+ {
+ aur->used = false;
+ aur->last_record_time = 0;
+ aur->last_record_index = 0;
+ memset(aur->fail_slots, 0, auth_delay_time_range/auth_delay_time_interval);
+ }
+ PG_RETURN_VOID();
+}
+PG_FUNCTION_INFO_V1(release_auth_delay_user_by_name);
+
+/*
+ * Set up shared memory
+ */
+static void
+auth_delay_shmem_startup(void)
+{
+ Size required;
+ bool found;
+
+ if (shmem_startup_next)
+ shmem_startup_next();
+
+ count = ShmemInitStruct("Count of failure", sizeof(int), &found);
+ if (found)
+ elog(DEBUG1, "variable count already exists: %d", *count);
+ *count = 0;
+
+ required = (sizeof(AuthUserRecord) + sizeof(int) * (auth_delay_time_range /
+ auth_delay_time_interval - 1)) * MAX_USER_RECORDS;
+ aur_array = ShmemInitStruct("Array of AuthUserRecord", required, &found);
+ if (found)// this should not happen?
+ elog(DEBUG1, "variable aur_array already exists");
+ /* all fileds are set to 0 */
+ memset(aur_array, 0, required);
+
+ required = sizeof(AuthConnRecord) * MAX_CONN_RECORDS;
+ acr_array = ShmemInitStruct("Array of AuthConnRecord", required, &found);
+ if (found)// this should not happen?
+ elog(DEBUG1, "variable acr_array already exists");
+ /* all fileds are set to 0 */
+ memset(acr_array, 0, required);
+}
+
+static bool
+guc_check_time_interval(int *newval, void **extra, GucSource source)
+{
+ if (*newval > auth_delay_time_range)
+ elog(ERROR, "auth_delay.time_interval should not be greater than "
+ "auth_delay.time_range");
+ if (auth_delay_time_range % *newval != 0)
+ elog(ERROR, "auth_delay.time_range should be integer multiple of "
+ "auth_delay.time_interval");
+
+ return true;
}
/*
@@ -55,6 +731,13 @@ auth_delay_checks(Port *port, int status)
void
_PG_init(void)
{
+ Size required;
+
+ if (!process_shared_preload_libraries_in_progress)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("auth_delay must be loaded via shared_preload_libraries")));
+
/* Define custom GUC variables */
DefineCustomIntVariable("auth_delay.milliseconds",
"Milliseconds to delay before reporting authentication failure",
@@ -64,10 +747,64 @@ _PG_init(void)
0, INT_MAX / 1000,
PGC_SIGHUP,
GUC_UNIT_MS,
+ NULL, NULL, NULL);
+ DefineCustomIntVariable("auth_delay.max_seconds",
+ "Maximum seconds to wait if fail to login",
NULL,
+ &auth_delay_max_seconds,
+ 100,
+ 0, INT_MAX,
+ PGC_POSTMASTER,
+ GUC_UNIT_S,
+ NULL, NULL, NULL);
+ DefineCustomRealVariable("auth_delay.ratio",
+ "Incremental ratio of waiting time",
+ NULL,
+ &auth_delay_ratio,
+ 1.0,
+ 1.0, 1000.0,
+ PGC_POSTMASTER,
+ GUC_UNIT_S,
+ NULL, NULL, NULL);
+ DefineCustomIntVariable("auth_delay.max_attempts",
+ "Maximum number of attempts to login of a user",
+ NULL,
+ &auth_delay_max_attempts,
+ 2,
+ 0, 1000,
+ PGC_POSTMASTER,
+ 0,
+ NULL, NULL, NULL);
+ DefineCustomIntVariable("auth_delay.time_range",
+ "Time range during which failure login attempts are counted",
NULL,
- NULL);
+ &auth_delay_time_range,
+ 1,
+ 1, 100,
+ PGC_POSTMASTER,
+ 0,
+ NULL, NULL, NULL);
+ DefineCustomIntVariable("auth_delay.time_interval",
+ "Time interval during which failure login attempts are counted",
+ NULL,
+ &auth_delay_time_interval,
+ 1,
+ 1, 100,
+ PGC_POSTMASTER,
+ 0,
+ guc_check_time_interval,
+ NULL, NULL);
/* Install Hooks */
original_client_auth_hook = ClientAuthentication_hook;
ClientAuthentication_hook = auth_delay_checks;
+
+ /* Set up shared memory */
+ shmem_startup_next = shmem_startup_hook;
+ shmem_startup_hook = auth_delay_shmem_startup;
+
+ required = (sizeof(AuthUserRecord) + sizeof(int) * (auth_delay_time_range /
+ auth_delay_time_interval - 1)) * MAX_USER_RECORDS;
+ required += sizeof(AuthConnRecord) * MAX_CONN_RECORDS;
+ required += sizeof(int);
+ RequestAddinShmemSpace(required);
}
diff --git a/contrib/auth_delay/auth_delay.control b/contrib/auth_delay/auth_delay.control
new file mode 100644
--- /dev/null
+++ b/contrib/auth_delay/auth_delay.control
@@ -0,0 +1,5 @@
+# auth_delay extension
+comment = 'Extented authentication delay for users and connections'
+default_version = '1.0'
+module_pathname = '$libdir/auth_delay'
+relocatable = false
=?UTF-8?B?5oiQ5LmL54SV?= <zhcheng@ceresdata.com> writes:
The attached patch is a contrib module to set login restrictions on users with
too many authentication failure. The administrator could manage several GUC
parameters to control the login restrictions which are listed below.
- set the wait time when password authentication fails.
- allow the wait time grows when users of the same IP consecutively logon failed.
- set the maximum authentication failure number from the same user. The system
will prevent a user who gets too many authentication failures from entering the
database.
I'm not yet forming an opinion on whether this is useful enough
to accept. However, I wonder why you chose to add this functionality
to auth_delay instead of making a new, independent module.
It seems fairly unrelated to what auth_delay does, and the
newly-created requirement that the module be preloaded might
possibly break some existing use-case for auth_delay.
Also, a patch that lacks user documentation and has no code comments to
speak of seems unlikely to draw serious review.
regards, tom lane
On Thu, Nov 17, 2022 at 05:37:51PM -0500, Tom Lane wrote:
=?UTF-8?B?5oiQ5LmL54SV?= <zhcheng@ceresdata.com> writes:
The attached patch is a contrib module to set login restrictions on users with
too many authentication failure. The administrator could manage several GUC
parameters to control the login restrictions which are listed below.
- set the wait time when password authentication fails.
- allow the wait time grows when users of the same IP consecutively logon failed.
- set the maximum authentication failure number from the same user. The system
will prevent a user who gets too many authentication failures from entering the
database.I'm not yet forming an opinion on whether this is useful enough
to accept.
I'm not sure that doing that on the backend side is really a great idea, an
attacker will still be able to exhaust available connection slots.
If your instance is reachable from some untrusted network (which already sounds
scary), it's much easier to simply configure something like fail2ban to provide
the same feature in a more efficient way. You can even block access to other
services too while at it.
Note that there's also an extension to log failed connection attempts on an
alternate file with a fixed simple format if you're worried about your regular
logs are too verbose.