pg_sec_check#

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 the SELECT 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 the section 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:

  1. Define the goal of the check.

    Clearly specify what security or configuration aspect should be validated.

  2. 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 single SELECT query (only one per file) that extracts the necessary information from the database.

  3. Write a Lua validator script (.lua).

    Develop a Lua script that analyzes the executor output (available via the global result table for shell or sql_results for SQL). The script must define a validate_result() function that returns a table with the standard structure. Use the locale_msg("key") function for all translatable strings and add the corresponding keys and translations to the *.loc files.

  4. Register the new check in config.json.

    In the appropriate section, add a new item to the items array. Specify the name and description of the check. In the executors and validators arrays, provide paths to the scripts. If needed, set min_ver and max_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:

  1. Create a new .loc file. For example, for French, create a file named fr.loc. The file name must match the language code that will be passed using the --locale argument.

  2. Copy existing keys. Use en.loc as a base and copy all key-value pairs.

  3. Translate values. Replace the text values with appropriate translations. For example, if en.loc contains: 10,"connection failed", then fr.loc might contain: 10,"échec de la connexion"

  4. 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.