Thread: RFC: Command Restrictions by INI file with Audit Logging (DROP/TRUNCATE/DELETE)
RFC: Command Restrictions by INI file with Audit Logging (DROP/TRUNCATE/DELETE)
Claro! Aqui está uma sugestão de mensagem em inglês, pronta para ser enviada à lista pgsql-hackers para propor sua modificação:
Subject: RFC: INI-based Command Restriction Layer for DROP/TRUNCATE/DELETE with Audit Logging
Dear PostgreSQL Hackers,
I'd like to propose a lightweight security enhancement feature to PostgreSQL, focused on environments where additional command restrictions are required — such as production databases with compliance constraints.
### Proposal Summary
The idea is to introduce a simple **INI-based permission layer** to restrict execution of potentially destructive commands (`DROP`, `TRUNCATE`, `DELETE`) on specific databases, without modifying core roles/permissions.
This is enforced via a configuration file:
`C:/Program Files/PostgreSQL/15/bin/restrict_commands.ini` (on Windows)
or
`/usr/local/pgsql/bin/restrict_commands.ini` (on Linux)
#### Sample INI format:
[db_financeiro]
drop=false
truncate=false
delete=false
[db_erp]
drop=false
truncate=true
delete=true
[db_analytics]
drop=true
truncate=false
delete=false
If a user attempts a restricted operation, the system blocks it and logs the attempt (including user, timestamp, and operation) to the data directory, e.g.:
data/db_financeiro_command_blocked.log
This layer does not interfere with PostgreSQL’s core access control, but acts as a policy wrapper for deployments that want an external, easy-to-manage safety valve.
Benefits
- External control (no role alteration needed)
- Auditable actions (even failed attempts)
- Does not require recompilation per policy change
- Protects against accidental or unauthorized destructive queries
- Can be added as an optional GUC in future (e.g., restrict_commands_policy = 'ini')
Patch Implementation
The change consists of:
- Modification to src/backend/commands/dbcommands.c
- A helper check_permission() to parse the INI
- log_violation() to append policy violation attempts
- Support for both Windows and Linux paths
A working prototype has already been built and tested on Windows. I’d like to know if this concept is welcome for further refinement and submission as an official patch.
Questions for the community:
- Would this be accepted as a contrib module or possibly a core feature under optional compilation?
- Are there similar mechanisms planned under RLS or privilege domains?
- Should this be extended to table-level control as well?
I’d be happy to prepare a full patch with unit tests and documentation for PostgreSQL 16+ if the concept is acceptable.
Thank you for your time and consideration.
Best regards,
Adriano Boller
LinkedIn: https://www.linkedin.com/in/adrianoboller
Location: Brazil
---
============================================================
Código Fonte Completo para Bloqueio de Comandos no PostgreSQL
============================================================
Este arquivo contém o código-fonte completo e as instruções para implementar o bloqueio de comandos DROP, TRUNCATE e DELETE no PostgreSQL, com base em um arquivo de configuração .ini e registro de tentativas bloqueadas em logs.
Inclui:
1. restrict_commands.c (código principal)
2. restrict_commands.h (cabeçalho)
3. Instruções de modificação para arquivos do PostgreSQL
4. Exemplo de restrict_commands.ini
Versão: Portátil (funciona em Windows e Linux)
============================================================
1. restrict_commands.c
============================================================
#include "postgres.h"
#include "utils/guc.h"
#include "storage/fd.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static char INI_PATH[1024];
static char LOG_PATH_BASE[1024];
// Inicializa caminhos dinamicamente na inicialização do módulo
void _PG_init(void) {
char share_path[MAXPGPATH];
get_share_path(my_exec_path, share_path);
snprintf(INI_PATH, sizeof(INI_PATH), "%s/restrict_commands.ini", share_path);
snprintf(LOG_PATH_BASE, sizeof(LOG_PATH_BASE), "%s/", DataDir);
}
bool check_command_permission(const char *dbname, const char *operation) {
FILE *fp = AllocateFile(INI_PATH, "r");
if (!fp) {
ereport(WARNING, (errmsg("Cannot open INI file: %s", INI_PATH)));
return true; // Permite por padrão se não conseguir ler
}
char line[256];
char section[128];
bool in_section = false;
snprintf(section, sizeof(section), "[%s]", dbname);
while (fgets(line, sizeof(line), fp)) {
if (strncmp(line, section, strlen(section)) == 0) {
in_section = true;
continue;
}
if (in_section && line[0] == '[')
break;
if (in_section && strstr(line, operation)) {
if (strstr(line, "false")) {
FreeFile(fp);
log_blocked_command(dbname, operation);
return false;
}
}
}
FreeFile(fp);
return true;
}
void log_blocked_command(const char *dbname, const char *operation) {
char logpath[512];
snprintf(logpath, sizeof(logpath), "%s%s_blocked.log", LOG_PATH_BASE, dbname);
// Verifica permissões de escrita
if (access(LOG_PATH_BASE, W_OK) != 0) {
ereport(WARNING, (errmsg("Cannot write to log directory: %s", LOG_PATH_BASE)));
return;
}
FILE *logf = AllocateFile(logpath, "a");
if (!logf) {
ereport(WARNING, (errmsg("Cannot open log file: %s", logpath)));
return;
}
time_t now = time(NULL);
char *timestamp = ctime(&now);
timestamp[strlen(timestamp)-1] = '\0';
char *user = GetUserNameOrNull();
if (!user) user = "unknown";
fprintf(logf, "[%s] USER: %s tried: %s (BLOCKED)\n", timestamp, user, operation);
FreeFile(logf);
}
============================================================
2. restrict_commands.h
============================================================
#ifndef RESTRICT_COMMANDS_H
#define RESTRICT_COMMANDS_H
bool check_command_permission(const char *dbname, const char *operation);
void log_blocked_command(const char *dbname, const char *operation);
#endif /* RESTRICT_COMMANDS_H */
============================================================
3. Instruções de Modificação nos Arquivos do PostgreSQL
============================================================
A seguir, as modificações que devem ser aplicadas nos arquivos do código-fonte do PostgreSQL para integrar a funcionalidade de bloqueio:
---
3.1. Para bloquear DROP DATABASE
Arquivo: src/backend/commands/dbcommands.c
Função: DropDatabase()
Adicione no início da função:
#include "utils/restrict_commands.h"
if (!check_command_permission(dbname, "drop")) {
ereport(ERROR, (errmsg("DROP DATABASE is blocked for this database by policy")));
}
---
3.2. Para bloquear TRUNCATE
Arquivo: src/backend/commands/tablecmds.c
Função: ExecuteTruncateGuts()
Adicione no início da função:
#include "utils/restrict_commands.h"
if (!check_command_permission(get_database_name(MyDatabaseId), "truncate")) {
ereport(ERROR, (errmsg("TRUNCATE is blocked for this database by policy")));
}
---
3.3. Para bloquear DELETE
Arquivo: src/backend/commands/delete.c
Função: ExecDelete()
Adicione no início da função:
#include "utils/restrict_commands.h"
if (!check_command_permission(get_database_name(MyDatabaseId), "delete")) {
ereport(ERROR, (errmsg("DELETE is blocked for this database by policy")));
}
---
Além disso, certifique-se de incluir "restrict_commands.o" no Makefile de src/backend/utils/misc:
OBJS = ... restrict_commands.o ...
============================================================
4. Exemplo de restrict_commands.ini
============================================================
Coloque este arquivo no diretório "share" do PostgreSQL (ex.: /usr/local/pgsql/share/ no Linux ou C:\Program Files\PostgreSQL\15\share no Windows).
[db_financeiro]
drop=false
truncate=false
delete=false
[db_erp]
drop=false
truncate=true
delete=true
[db_analytics]
drop=true
truncate=false
delete=false
============================================================
Observações Finais
============================================================
- O código é portátil e funciona tanto no Windows quanto no Linux, utilizando funções do PostgreSQL para garantir compatibilidade.
- Certifique-se de que o arquivo restrict_commands.ini tenha permissões adequadas para ser lido pelo usuário do PostgreSQL.
- Os logs de tentativas bloqueadas serão gravados em <data_dir>/<dbname>_blocked.log.
- Para aplicar as mudanças, compile o PostgreSQL com as modificações e reinicie o servidor.
Or English:
Here is the full English translation of your proposed solution for submission or documentation purposes:
============================================================
Full Source Code for Blocking Commands in PostgreSQL
============================================================
This file contains the complete source code and instructions to implement command blocking for DROP, TRUNCATE, and DELETE in PostgreSQL, based on an external .ini configuration file and logging of blocked attempts.
Includes:
- restrict_commands.c (main logic)
- restrict_commands.h (header)
- PostgreSQL source modification instructions
- Example restrict_commands.ini
Version: Portable (works on both Windows and Linux)
============================================================
1. restrict_commands.c
============================================================
#include "postgres.h"
#include "utils/guc.h"
#include "storage/fd.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static char INI_PATH[1024];
static char LOG_PATH_BASE[1024];
// Initialize paths dynamically at module load
void _PG_init(void) {
char share_path[MAXPGPATH];
get_share_path(my_exec_path, share_path);
snprintf(INI_PATH, sizeof(INI_PATH), "%s/restrict_commands.ini", share_path);
snprintf(LOG_PATH_BASE, sizeof(LOG_PATH_BASE), "%s/", DataDir);
}
bool check_command_permission(const char *dbname, const char *operation) {
FILE *fp = AllocateFile(INI_PATH, "r");
if (!fp) {
ereport(WARNING, (errmsg("Cannot open INI file: %s", INI_PATH)));
return true; // Allow by default if unreadable
}
char line[256];
char section[128];
bool in_section = false;
snprintf(section, sizeof(section), "[%s]", dbname);
while (fgets(line, sizeof(line), fp)) {
if (strncmp(line, section, strlen(section)) == 0) {
in_section = true;
continue;
}
if (in_section && line[0] == '[')
break;
if (in_section && strstr(line, operation)) {
if (strstr(line, "false")) {
FreeFile(fp);
log_blocked_command(dbname, operation);
return false;
}
}
}
FreeFile(fp);
return true;
}
void log_blocked_command(const char *dbname, const char *operation) {
char logpath[512];
snprintf(logpath, sizeof(logpath), "%s%s_blocked.log", LOG_PATH_BASE, dbname);
if (access(LOG_PATH_BASE, W_OK) != 0) {
ereport(WARNING, (errmsg("Cannot write to log directory: %s", LOG_PATH_BASE)));
return;
}
FILE *logf = AllocateFile(logpath, "a");
if (!logf) {
ereport(WARNING, (errmsg("Cannot open log file: %s", logpath)));
return;
}
time_t now = time(NULL);
char *timestamp = ctime(&now);
timestamp[strlen(timestamp)-1] = '\0';
char *user = GetUserNameOrNull();
if (!user) user = "unknown";
fprintf(logf, "[%s] USER: %s tried: %s (BLOCKED)\n", timestamp, user, operation);
FreeFile(logf);
}
============================================================
2. restrict_commands.h
============================================================
#ifndef RESTRICT_COMMANDS_H
#define RESTRICT_COMMANDS_H
bool check_command_permission(const char *dbname, const char *operation);
void log_blocked_command(const char *dbname, const char *operation);
#endif /* RESTRICT_COMMANDS_H */
============================================================
3. PostgreSQL Source Modifications
============================================================
Apply the following changes to integrate the command-blocking functionality:
3.1 Block
DROP DATABASE
File: src/backend/commands/dbcommands.c
Function: DropDatabase()
Add at the top of the function:
#include "utils/restrict_commands.h"
if (!check_command_permission(dbname, "drop")) {
ereport(ERROR, (errmsg("DROP DATABASE is blocked for this database by policy")));
}
3.2 Block
TRUNCATE
File: src/backend/commands/tablecmds.c
Function: ExecuteTruncateGuts()
Add:
#include "utils/restrict_commands.h"
if (!check_command_permission(get_database_name(MyDatabaseId), "truncate")) {
ereport(ERROR, (errmsg("TRUNCATE is blocked for this database by policy")));
}
3.3 Block
DELETE
File: src/backend/commands/delete.c
Function: ExecDelete()
Add:
#include "utils/restrict_commands.h"
if (!check_command_permission(get_database_name(MyDatabaseId), "delete")) {
ereport(ERROR, (errmsg("DELETE is blocked for this database by policy")));
}
Makefile
Add restrict_commands.o to the appropriate Makefile (e.g. src/backend/utils/misc/Makefile):
OBJS = ... restrict_commands.o ...
============================================================
4. Example restrict_commands.ini
============================================================
Place this file in PostgreSQL’s share directory (e.g., /usr/local/pgsql/share/ or C:\Program Files\PostgreSQL\15\share):
[db_financeiro]
drop=false
truncate=false
delete=false
[db_erp]
drop=false
truncate=true
delete=true
[db_analytics]
drop=true
truncate=false
delete=false
============================================================
Final Notes
============================================================
- The code is portable and uses PostgreSQL internal utilities for cross-platform compatibility (Windows and Linux).
- Ensure the restrict_commands.ini file is readable by the PostgreSQL user.
- Logs of blocked attempts are written to: <data_dir>/<dbname>_blocked.log
- To apply the changes: rebuild PostgreSQL with the new files, recompile, and restart the server
|
Attachment
Re: RFC: Command Restrictions by INI file with Audit Logging (DROP/TRUNCATE/DELETE)
Subject: RFC: INI-based Command Restriction Layer for DROP/TRUNCATE/DELETE with Audit Logging
I’d be happy to prepare a full patch with unit tests and documentation for PostgreSQL 16+ if the concept is acceptable.
Re: RFC: Command Restrictions by INI file with Audit Logging (DROP/TRUNCATE/DELETE)
"David G. Johnston" <david.g.johnston@gmail.com> writes: > On Sunday, May 4, 2025, ADRIANO BOLLER <adriano@wxsolucoes.com.br> wrote: >> Subject: RFC: INI-based Command Restriction Layer for DROP/TRUNCATE/DELETE >> with Audit Logging > I seem to recall similar features being discussed previously. Finding > those discussion would be nice. I don't have an opinion on whether this is actually a feature we want, but a couple of thoughts: 1. This seems to overlap a whole lot with one intended use-case for event triggers. You could likely build it as an extension, relying on an event trigger to get control at the right places. 2. The choice of .INI format for the config file seems a bit non-simpatico for Postgres. We have nothing else that uses that, which for starters means you'd have to write your own parser. Something that might integrate better is JSON; we already have code for that, and use-cases such as backup manifest files. regards, tom lane
Re: RFC: Command Restrictions by INI file with Audit Logging (DROP/TRUNCATE/DELETE)
|
"David G. Johnston" <david.g.johnston@gmail.com> writes:
> On Sunday, May 4, 2025, ADRIANO BOLLER <adriano@wxsolucoes.com.br> wrote:
>> Subject: RFC: INI-based Command Restriction Layer for DROP/TRUNCATE/DELETE
>> with Audit Logging
> I seem to recall similar features being discussed previously. Finding
> those discussion would be nice.
I don't have an opinion on whether this is actually a feature we want,
but a couple of thoughts:
1. This seems to overlap a whole lot with one intended use-case for
event triggers. You could likely build it as an extension, relying
on an event trigger to get control at the right places.
2. The choice of .INI format for the config file seems a bit
non-simpatico for Postgres. We have nothing else that uses that,
which for starters means you'd have to write your own parser.
Something that might integrate better is JSON; we already have
code for that, and use-cases such as backup manifest files.
regards, tom lane
Attachment
Re: RFC: Command Restrictions by INI file with Audit Logging (DROP/TRUNCATE/DELETE)
Tom Lane <tgl@sss.pgh.pa.us> writes: > 2. The choice of .INI format for the config file seems a bit > non-simpatico for Postgres. We have nothing else that uses that, The pg_service.conf format is pretty .INI-like, it even calls itself that in the docs: Either service file uses an “INI file” format where the section name is the service name and the parameters are connection parameters; - https://www.postgresql.org/docs/current/libpq-pgservice.html > which for starters means you'd have to write your own parser. > Something that might integrate better is JSON; we already have > code for that, and use-cases such as backup manifest files. JSON isn't very human-friendly for writing, it's more of a data interchange format that happens to be human-readable. > regards, tom lane - ilmari