pg_sec_check#
pg_sec_check
pg_sec_check — Security audit tool for PostgreSQL
Introduction
pg_sec_check
(Postgres Security Check) — is a
utility designed to audit the security of PostgreSQL database
configurations. It automates the process of verifying various
aspects of security—from server settings to database-specific
parameters—providing detailed reports on identified issues and
recommendations for remediation.
Features
Flexible check system: define custom checks via configuration files.
Modular architecture: supports various types of executors (SQL, shell scripts) and validators (Lua scripts).
Versioned checks: checks can be tied to specific PostgreSQL versions (min/max).
Localization: multilingual support (currently Russian and English).
Report formats: generates reports in HTML and JSON formats.
Parallel execution: supports multithreading to speed up scanning.
Integrity protection: in release builds, verifies integrity of its components using checksums.
Architecture
Core: handles CLI parsing, config reading, execution flow, executor/validator coordination, and report generation.
Config file (config.json): defines the structure of the report, sections, and checks, including script paths.
Executors:
.sh
or.sql
scripts that gather data from the system or database.Validators: lua scripts that analyze executor results and produce check status, description, and recommendations.
Localization mechanism: enables multi-language output in reports.
Three core module types interact for running checks:
Executors: gather raw data (SQL queries or shell commands).
Validators: lua-based logic for interpreting gathered data.
Configuration: maps executors/validators to each check, including metadata like PostgreSQL version applicability.
This modular structure provides a high degree of customization and allows users to add their own environment-specific checks without having to modify the main program code.
Configuration file (config.json)
The checks are configured via a JSON file. Example, simple_config.json
:
{ "header": "Server and Database Information Report", "report_name": "Report name", "description": "Some description", "section" : { "system" : { "header": "Example bash test", "description": "Common info about current host", "items": [ { "name": "hello world check", "description" : "Compare hello world results", "executors": [ { "script": "tests/common/script/simple_script.sh", "min_ver": "none", "max_ver": "none" } ], "validators": [ { "script": "tests/common/lua/simple_bash_validate.lua", "min_ver": "none", "max_ver": "none" } ] } ] }, "sql" : { "header": "Example sql test", "description": "Test connection into database", "items": [ { "name" : "select pg_version", "description" : "select pg_version", "executors": [ { "script": "tests/common/script/simple_sql.sql", "min_ver": "none", "max_ver": "none" } ], "validators": [ { "script": "tests/common/lua/simple_sql_validate.lua", "min_ver": "none", "max_ver": "none" } ] } ] } } }
Global fields
header (String)
: report header.report_name (String)
: report name.description (String)
: general description.section (Object)
: object containing definitions of various sections of the report. Each section represents a logical group of checks. The keys of this object (for example, “system”, “sql”) are group identifiers that can be used to selectively run checks using the--groups
command line argument.
section
structure
header (String):
section header.description (String)
: section description.items (Array)
: array of objects, each of which defines a separate check.
items
structure
name (String)
: unique name displayed in the report.description (String)
: purpose of the check.executors (Array)
: array of executor objects. Several performers can be identified for a single check, however, it should be taken into account how their results will be processed by the validator. Usually, one contractor is used for verification.validators (Array)
: array of validator objects. Similarly to executors, there may be several validators. Most often, a single validator is used.
executors
fields
script (String)
: path to the executor’s script file. Can be a.sh
file for shell scripts or a.sql
file for SQL queries.min_ver (String)
: minimum version of PostgreSQL for which this check is applicable (for example, “10.0.0”). If “none” is specified, there is no limit on the minimum version. The check will not be run for PostgreSQL versions below the specified one.max_ver (String)
: maximum version of PostgreSQL for which this check is applicable. If “none” is specified, there is no limit on the maximum version. The check will not run for the above versions of PostgreSQL.
validators
fields
script (String)
: path to the validator’s Lua script file.min_ver (String)
/max_ver (String)
: same as for executors.
This schema ensures precise version targeting for each check, ideal for mixed-version environments.
Check Mechanism
Checks consist of two phases: executor runs → validator analyzes output.
Executors
The executors are designed to collect the data necessary for security analysis.
Shell scripts (
.sh
): executes system commands. Useful for inspecting PostgreSQL configs, access rights, env vars.When running the shell script, the program sets the following environment variables that can be used inside the script:
PG_HOST="database host"
PG_PORT="database port"
PG_USER:="username for connecting to the database"
PG_DB="database name"
PGDATA="path to the PostgreSQL data directory"
PG_VERSION="version of the PostgreSQL server to connect to"
PG_HBA_FILE="path to the pg_hba.conf file"
SQL scripts (
.sql
): used to make queries to the PostgreSQL database in order to obtain information about settings, database objects, user rights, etc.Currently, only one SELECT query is supported for SQL files. The result of this request is passed to the validator. This limitation should be taken into account when designing SQL checks; complex checks may require either a more complex SELECT query or splitting into several separate checks.
Validators
Validators analyze the data collected by the performers and give a verdict on the status of the parameter being checked. All validation logic is implemented in the Lua language.
Global data available in Lua validators:
For validators that process results of sh-scripts:
result.stdout (String):
standard output (stdout) of the executed shell script.
result.stderr (String):
standard error (stderr) of the executed shell script.
result.return_code (Number):
exit code of the executed shell script.Example Lua script for shell script validation
simple_bash_validate.lua
:-- GLOBAL DATA -- result.stdout the output of stdout from the executed bash script -- result.stderr the output of stderr from the executed bash script -- result.return_code the return code from the executed bash script function validate_result() local success = true local status_str = "Success" local short_desc = "The check was successful" local full_desc = "All parameters meet the requirements" local recommend = "No action is required" if not result.stdout == "hello world" then status_str = "Warning" short_desc = "Validation error" full_desc = "The X parameter does not match the Y format" recommend = "Fix the X parameter" end -- Important! The return value should look exactly like this. -- That is, the structure (table) should not change. -- { -- status: one of the values is (Warning, Info, Error, Success, Failed) -- short_description: String -- full_description: String -- recommendation: String -- } local result_table = { status = status_str, short_description = short_desc, full_description = full_desc, recommendation = recommend } return result_table end
For validators that process results of SQL scripts:
The Lua script receives a global table
sql_results
. This table is an array where each element is a table corresponding to a row of the SQL query result. The field names in these nested tables match the column names in theSELECT
query.Example Lua script for SQL validation
simple_sql_validate.lua
:-- GLOBAL DATA -- sql_results a table with the result of SQL execution function validate_result() local success = true local status_str = "Success" local short_desc = "Error: SQL data is not received or incorrect" local full_desc = "All parameters meet the requirements" local recommend = "No action is required" -- Checking for the presence of the first line if sql_results and sql_results[1] then -- Getting the first row (this is a table) local first_row = sql_results[1] -- an example for a table of the form: -- pg_version ------------------------ -- pg version data bla bla bla if first_row and type(first_row) == 'table' and first_row.pg_version then short_desc = first_row.pg_version status_str = "Success" full_desc = "Version PostgreSQL: " .. short_desc else short_desc = "Error: The 'pg_version' field was not found in the SQL result" status_str = "Failed" end else -- sql_results is empty status_str = "Failed" end -- Important! The return value should look exactly like this. -- That is, the structure (table) should not change. -- { -- status: one of the values is (Warning, Info, Error, Success, Failed) -- short_description: String -- full_description: String -- recommendation: String -- } local result_table = { status = status_str, short_description = short_desc, full_description = full_desc, recommendation = recommend } return result_table end
Structure of the return value for Lua validators:
Each Lua validator script must define a
validate_result()
function, which returns a
table with the following fixed structure:
{ status: "Warning" | "Info" | "Error" | "Success" | "Failed", -- Must be one of these values short_description: "Brief description of the result", -- String full_description: "Detailed explanation of the result", -- String recommendation: "Recommended remediation actions" -- String }
Adhering to this structure is critical, as the program core expects results in exactly this format to generate the report correctly. Deviations will result in errors during validation result processing.
locale_msg
function
Validator Lua scripts can use the locale_msg(key) function to retrieve localized strings.
See the Localization section for more details.
Localization
The program supports localization of messages displayed in reports, allowing the output to be adapted for users who speak different languages.
locale_msg
mechanism
Validator Lua scripts use the locale_msg(key)
function to retrieve translated strings, where
key
is a string identifier for the message.
For example, calling
locale_msg("first")
returns the
string corresponding to the "first"
key in the language specified via the
--locale
command-line argument. If
localization files for the selected language are not found, or
the key is missing, the fallback language is English.
Localization file format (.loc)
Localization files are plain text files with a
.loc
extension (e.g.,
en.loc
, ru.loc
). Each line
in the file represents a message and follows this format:
id,"message_key"
Examples:
localizations/ru.loc
# ru.loc # --- validate_anon_extension.lua --- 1,"<p>Проверка предзагрузки расширений анонимизации (<code>anon</code> или <code>pg_anonymize</code>) через параметр <code>session_preload_libraries</code>.</p>" 2,"<p>Анализ <code>session_preload_libraries</code> для расширений анонимизации не был успешно завершен или расширения не найдены.</p>" 3,"<ul><li>Проверьте SQL-запрос и конфигурацию PostgreSQL (параметр <code>session_preload_libraries</code>).</li></ul>" ... 1674,"использование SSL"
localizations/en.loc
# en.loc # --- validate_anon_extension.lua --- 1,"<p>Checking preload of anonymization extensions (<code>anon</code> or <code>pg_anonymize</code>) via the <code>session_preload_libraries</code> parameter.</p>" 2,"<p>Analysis of <code>session_preload_libraries</code> for anonymization extensions was not successfully completed or extensions were not found.</p>" 3,"<ul><li>Check the SQL query and PostgreSQL configuration (parameter <code>session_preload_libraries</code>).</li></ul>" ... 1674,"Ssl usage"
If the program is launched with the
--locale ru
argument, calling
locale_msg("first")
will return the
string "first"
in russian. The lookup
mechanism is based on matching the string key passed to
locale_msg
with the second field in the
localization file. The purpose of the numeric ID (e.g.,
1
) is currently not fully specified, but the
string key is considered the primary identifier.
Maintaining consistency and completeness of localization files for all supported languages is essential when adding new checks or updating existing messages. If a key is missing in the selected language’s localization file, the default (English) version of the message will be displayed.
Supported Languages
English (
en
)Russian (
ru
)
Report Formats
The program can generate check results in multiple output formats.
The desired format is specified using the
--format
(or -f
)
command-line argument.
Available formats:
Html: generates a report as an HTML page. This format is suitable for visual analysis, as it supports interactive elements, styling, and is easily readable in a web browser.
Json: generates the report in JSON format. This is a plain-text, machine-readable format commonly used for data exchange between systems. JSON reports are well-suited for automated processing, integration with monitoring or SIEM systems, or further analysis via scripts and other tools.
Full: when this option is selected (and it is the default), the program generates reports in all supported formats simultaneously (i.e., both HTML and JSON). This allows users to obtain both a human-readable version for immediate inspection and a machine-readable version for automation—without needing to re-run the utility. Output filenames are based on the value of the
--output
argument, with appropriate.html
and.json
extensions.
The choice of report format depends on the user’s goals: use HTML for quick review or management presentation, and JSON for automation or system integration.
Usage
Command-Line Arguments
The program is controlled via command-line arguments. The following table describes all available options:
Argument | Type | Default | Description |
---|---|---|---|
--format
|
String
| Full | Output report format. Options: Full, Html, Json |
--config
|
String
| config.json | Path to the configuration file |
--output
|
String
| output | Base name for report output files (output_web/ and output.json) |
--port
|
u16
| 5432 | Port to connect to the PostgreSQL database |
--host
|
String
| /tmp | Host to connect to the database (IP address, hostname, or Unix socket path) |
--user
|
String
| dev | Username for database connection |
--database
|
String
| postgres | Name of the target database |
--threads
|
u16
| CPU core count | Number of threads for task execution |
--debug
|
bool
| false | Enable debug mode (verbose output) |
--pgclients
|
u16
| 10 | PostgreSQL connection pool size |
--locale
|
String
| ru | Locale code for report output |
--groups
|
Vec<String>
| None | Comma-separated list of check groups to run (e.g., “system,sql”) |
--unsecure
|
bool
| false | Run without verifying checksums of bundled files |
The
--groups
option allows selective execution of specific check groups, defined as keys in thesection
object of the configuration file. This is useful for targeted testing or staged audits.The
--unsecure
option is intended for special use cases. In release builds, the program by default verifies the checksums of all its components—including bundled executor and validator scripts. This ensures tool integrity and protects against accidental or malicious modifications. If any of these files have been altered (e.g., during development or customization of standard checks), the program will refuse to run and report a checksum mismatch error. The--unsecure
flag bypasses this verification, allowing the tool to run with modified files.
Warning: Using the
--unsecure
flag disables a critical internal
security mechanism. Use with caution. It is not recommended to
use this flag in production or trusted environments unless you
fully understand the risks, as it may allow execution of
modified (potentially harmful) scripts.
Additional arguments:
RUST_LOG
: Environment variable that controls logging verbosity.BACKTRACE
: Environment variable that enables stack traces on panic or crash.
Development and Customization
The flexibility of Postgres Security Check is enabled by the ability to create custom checks and add new localization languages.
Creating Custom Checks
The process of creating a new check involves the following steps:
Define the goal of the check.
Clearly specify what security or configuration aspect should be validated.
Write an executor script.
For shell scripts (
.sh
): Create a script that collects the required data. Be aware of the available environment variables (PG_HOST
,PG_PORT
,PG_USER
,PG_DB
,PGDATA
,PG_VERSION
,PG_HBA_FILE
).For SQL scripts (
.sql
): Write a singleSELECT
query (only one per file) that extracts the necessary information from the database.
Write a Lua validator script (
.lua
).Develop a Lua script that analyzes the executor output (available via the global
result
table for shell orsql_results
for SQL). The script must define avalidate_result()
function that returns a table with the standard structure. Use thelocale_msg("key")
function for all translatable strings and add the corresponding keys and translations to the*.loc
files.Register the new check in
config.json
.In the appropriate section, add a new item to the
items
array. Specify thename
anddescription
of the check. In theexecutors
andvalidators
arrays, provide paths to the scripts. If needed, setmin_ver
andmax_ver
to restrict the check to specific PostgreSQL versions.
Debugging tips:
Test executor scripts independently to ensure they collect data correctly.
Validate the Lua logic by providing typical and edge-case input. Step-by-step debugging or printing intermediate values in Lua can help.
The ability to build custom checks is a major strength of this tool, allowing it to be tailored to the unique security and infrastructure needs of any organization.
Adding New Localization Languages
The localization system is based on plain-text files, which simplifies the process of adding support for additional languages.
To add a new localization:
Create a new
.loc
file. For example, for French, create a file namedfr.loc
. The file name must match the language code that will be passed using the--locale
argument.Copy existing keys. Use
en.loc
as a base and copy all key-value pairs.Translate values. Replace the text values with appropriate translations. For example, if
en.loc
contains:10,"connection failed"
, thenfr.loc
might contain:10,"échec de la connexion"
Place the file. Make sure the program can locate the new localization file. These files should be placed in the dedicated
localizations
directory.
Maintaining completeness and consistency of localization files (ensuring all keys exist in all language files) is essential for a high-quality user experience across different languages.