Monday, August 7, 2023

Installing pgbadger on Linux for PostgreSQL log analysis

As a DBA, we often encounter scenarios when we need to track PostgreSQL database performance by analyzing workloads and identifying bottlenecks if any. There comes pgbadger – an open-source log analyzer for PostgreSQL. It parses the PostgreSQL log files and gives you a nice overview of the PostgreSQL cluster including various query metrics like the number of query executions, their types and duration, session details, Locks statistics, warnings, errors and other event patterns etc. All of these details will be available in a nice HTML format on your web browser.

Installation:

Prerequisite:

The pgbadger is written in pure Perl and uses a JavaScript library (flotr2) to draw graphs. Hence, you need to ensure that a modern Perl distribution is available in your system. Charts are rendered using a JavaScript library and Your web browser will do all the work. Nothing additional is required here.

Use the below command to install perl if it is not already installed in your system.

$ yum install -y perl perl-devel

Either of the below two methods can be followed to install pgbadger.

Method 1:

The first step is to download the latest pgbadger installation package. The Official releases are published on the GitHub Release page of pgBadger. While I am writing this blog post, the latest available version is 11.4.

Let’s run the below command on the Linux command line interface to download the pgbadger version 11.4.

$ wget https://github.com/darold/pgbadger/archive/v11.4.tar.gz

Alternatively, you can clone the pgbadger GitHub repo as well if you have Git installed in your system.

$ git clone https://github.com/dalibo/pgbadger.git

Once the download is complete, you have to extract the archive from the tarball.

$ tar xzvf v11.4.tar.gz
[user@server pgbadger_demo]$ tar xzvf v11.4.tar.gz
pgbadger-11.4/
pgbadger-11.4/.editorconfig
pgbadger-11.4/.gitignore
pgbadger-11.4/CONTRIBUTING.md
pgbadger-11.4/ChangeLog
pgbadger-11.4/HACKING.md
pgbadger-11.4/LICENSE
pgbadger-11.4/MANIFEST
pgbadger-11.4/META.yml
pgbadger-11.4/Makefile.PL
pgbadger-11.4/README
pgbadger-11.4/README.md
pgbadger-11.4/doc/
pgbadger-11.4/doc/pgBadger.pod
pgbadger-11.4/pgbadger
pgbadger-11.4/resources/
pgbadger-11.4/resources/.gitignore
pgbadger-11.4/resources/LICENSE
pgbadger-11.4/resources/README
pgbadger-11.4/resources/bean.js
pgbadger-11.4/resources/bootstrap.css
pgbadger-11.4/resources/bootstrap.js
pgbadger-11.4/resources/font/
pgbadger-11.4/resources/font/FontAwesome.otf
pgbadger-11.4/resources/font/fontawesome-webfont.eot
pgbadger-11.4/resources/fontawesome.css
pgbadger-11.4/resources/jqplot.barRenderer.js
pgbadger-11.4/resources/jqplot.canvasAxisTickRenderer.js
pgbadger-11.4/resources/jqplot.canvasTextRenderer.js
pgbadger-11.4/resources/jqplot.categoryAxisRenderer.js
pgbadger-11.4/resources/jqplot.cursor.js
pgbadger-11.4/resources/jqplot.dateAxisRenderer.js
pgbadger-11.4/resources/jqplot.highlighter.js
pgbadger-11.4/resources/jqplot.pieRenderer.js
pgbadger-11.4/resources/jqplot.pointLabels.js
pgbadger-11.4/resources/jquery.jqplot.css
pgbadger-11.4/resources/jquery.jqplot.js
pgbadger-11.4/resources/jquery.js
pgbadger-11.4/resources/patch-jquery.jqplot.js
pgbadger-11.4/resources/pgbadger.css
pgbadger-11.4/resources/pgbadger.js
pgbadger-11.4/resources/pgbadger_slide.js
pgbadger-11.4/resources/underscore.js
pgbadger-11.4/t/
pgbadger-11.4/t/01_lint.t
pgbadger-11.4/t/02_basics.t
pgbadger-11.4/t/03_consistency.t
pgbadger-11.4/t/exp/
pgbadger-11.4/t/exp/stmt_type.out
pgbadger-11.4/t/fixtures/
pgbadger-11.4/t/fixtures/cloudsql.log.gz
pgbadger-11.4/t/fixtures/light.postgres.log.bz2
pgbadger-11.4/t/fixtures/logplex.gz
pgbadger-11.4/t/fixtures/pg-syslog.1.bz2
pgbadger-11.4/t/fixtures/pgbouncer.log.gz
pgbadger-11.4/t/fixtures/rds.log.bz2
pgbadger-11.4/t/fixtures/stmt_type.log
pgbadger-11.4/tools/
pgbadger-11.4/tools/README.pgbadger_tools
pgbadger-11.4/tools/README.updt_embedded_rsc
pgbadger-11.4/tools/pgbadger_tools
pgbadger-11.4/tools/updt_embedded_rsc.pl
[user@server pgbadger_demo]$

Now go to the extracted directory.

$ cd pgbadger-11.4

Compile the makefile.

$ perl Makefile.PL
[user@server pgbadger-11.4]$ perl Makefile.PLChecking if your kit is complete...
Looks good
Writing Makefile for pgBadger
[user@server pgbadger-11.4]$

Now install the pgbadger using the below command.

$ make && sudo make install
[user@server pgbadger-11.4]$ make && sudo make install
which: no pod2markdown in (/usr/local/rvm/gems/ruby-2.4.1/bin:/usr/local/rvm/gems/ruby-2.4.1@global/bin:/usr/local/rvm/rubies/ruby-2.4.1/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/usr/local/rvm/bin:/home/cloud_user/.local/bin:/home/user/bin)
cp pgbadger blib/script/pgbadger
/usr/bin/perl -MExtUtils::MY -e 'MY->fixin(shift)' -- blib/script/pgbadger
echo "=head1 SYNOPSIS" > doc/synopsis.pod
./pgbadger --help >> doc/synopsis.pod
echo "=head1 DESCRIPTION" >> doc/synopsis.pod
sed -i.bak 's/ +$//g' doc/synopsis.pod
rm doc/synopsis.pod.bak
sed -i.bak '/^=head1 SYNOPSIS/,/^=head1 DESCRIPTION/d' doc/pgBadger.pod
sed -i.bak '4r doc/synopsis.pod' doc/pgBadger.pod
rm doc/pgBadger.pod.bak
Manifying blib/man1/pgbadger.1p
rm doc/synopsis.pod
[sudo] password for user: 
which: no pod2markdown in (/sbin:/bin:/usr/sbin:/usr/bin)
echo "=head1 SYNOPSIS" > doc/synopsis.pod
./pgbadger --help >> doc/synopsis.pod
echo "=head1 DESCRIPTION" >> doc/synopsis.pod
sed -i.bak 's/ +$//g' doc/synopsis.pod
rm doc/synopsis.pod.bak
sed -i.bak '/^=head1 SYNOPSIS/,/^=head1 DESCRIPTION/d' doc/pgBadger.pod
sed -i.bak '4r doc/synopsis.pod' doc/pgBadger.pod
rm doc/pgBadger.pod.bak
Manifying blib/man1/pgbadger.1p
Installing /usr/local/share/man/man1/pgbadger.1p
Installing /usr/local/bin/pgbadger
Appending installation info to /usr/lib64/perl5/perllocal.pod
rm doc/synopsis.pod
[user@server pgbadger-11.4]$

Finally, verify the installation by running the below command which will display the pgbadger installed version 🎉

$ pgbadger -V
[user@server pgbadger-11.4]$ pgbadger -V
pgBadger version 11.4
[user@server pgbadger-11.4]$

Method 2:

The pgbadger package is also available at the PostgreSQL yum repository. If you have already installed the repository configuration package from the official PostgreSQL repository, then pgbadger can be installed by running the below command.

$ sudo yum install pgbadger
[user@server ~]$ sudo yum install pgbadger
Loaded plugins: amazon-id, search-disabled-repos
Resolving Dependencies
--> Running transaction check
---> Package pgbadger.noarch 0:11.4-1.rhel7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

==============================================================================================================================================================================================================================================
 Package                                                 Arch                                                  Version                                                       Repository                                                  Size
==============================================================================================================================================================================================================================================
Installing:
 pgbadger                                                noarch                                                11.4-1.rhel7                                                  pgdg-common                                                346 k

Transaction Summary
==============================================================================================================================================================================================================================================
Install  1 Package

Total download size: 346 k
Installed size: 1.5 M
Is this ok [y/d/N]: y
Downloading packages:
warning: /var/cache/yum/x86_64/7Server/pgdg-common/packages/pgbadger-11.4-1.rhel7.noarch.rpm: Header V4 DSA/SHA1 Signature, key ID 442df0f8: NOKEY========================-                                 ]  0.0 B/s | 224 kB  --:--:-- ETA 
Public key for pgbadger-11.4-1.rhel7.noarch.rpm is not installed
pgbadger-11.4-1.rhel7.noarch.rpm                                                                                                                                                                                       | 346 kB  00:00:01     
Retrieving key from file:///etc/pki/rpm-gpg/RPM-GPG-KEY-PGDG
Importing GPG key 0x442DF0F8:
 Userid     : "PostgreSQL RPM Building Project <pgsqlrpms-hackers@pgfoundry.org>"
 Fingerprint: 68c9 e2b9 1a37 d136 fe74 d176 1f16 d2e1 442d f0f8
 Package    : pgdg-redhat-repo-42.0-14.noarch (@/pgdg-redhat-repo-latest.noarch)
 From       : /etc/pki/rpm-gpg/RPM-GPG-KEY-PGDG
Is this ok [y/N]: y
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : pgbadger-11.4-1.rhel7.noarch                                                                                                                                                                                               1/1
  Verifying  : pgbadger-11.4-1.rhel7.noarch                                                                                                                                                                                               1/1

Installed:
  pgbadger.noarch 0:11.4-1.rhel7                                                                                                                                                                                                              

Complete!
[user@server ~]$

PostgreSQL Configuration:

Since pgbadger reads the PostgreSQL logs and picks up the information, it is essential to make some changes on Postgresql.Conf file so that necessary information is available in the PostgreSQL log file for pgbadger to read.

The first and foremost thing is to enable query logging. Change the log_min_duration_statement parameter value accordingly on Postgresql.Conf file. The value ‘0’ signifies that every statement will be logged which may not be ideal for a very busy server. You can change the value accordingly based on your requirement. Suppose, you want to log queries running for more than 2 seconds, then set the parameter value(in milliseconds) as 2000.

** Do not enable both log_min_duration_statement, log_duration and log_statement altogether, this will result in wrong counter values and excessive log size.

** Do not enable log_statement as its log format will not be parsed by pgBadger. If you have log_statement set to 'all' nothing will be logged through the log_min_duration_statement directive and pgbadger will not be able to parse the log.

** If you don’t want all details about queries, set log_min_duration_statement to -1 to disable it and enable log_duration in your postgresql.conf file. It will only report the duration and number of queries.

The log_min_duration_statement should always be preferred (recommended).

log_min_duration_statement = 0

The next important parameter to look for is log_line_prefix whose value can be specified based on the value of another parameter log_destination . The log_destination value tells PostgreSQL to generate logs in the specified format. By default, PostgreSQL generates logs in standard error (stderr) format. In this format, each log message is prefixed with the information specified by the parameter log_line_prefix.

With stderr log format, log_line_prefix must be having at least a time escape sequence %t, %m or %n and the process-related escape sequence %p or %c as shown below.

log_line_prefix = '%t [%p]: '

It also lets you add the user, database name, application name and client IP address as well as shown below.

log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a,client=%h '

Another thing to remember is that your log messages should be in English with or without locale support. The below settings ensure that:

 lc_messages='en_US.UTF-8'
 lc_messages='C'

The usual postgresql.conf configuration for pgBadger looks like this:

log_min_duration_statement = 0
log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a,client=%h '
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
log_temp_files = 0
log_autovacuum_min_duration = 0
log_error_verbosity = default
lc_messages='en_US.UTF-8'
lc_messages='C'

The above changes require you to perform a PostgreSQL restart.

systemctl restart postgresql

Generating pgbadger report:

Now that everything is in place, let’s proceed with generating a pgbadger report.

The simplest way to analyze a particular log and generate the pgbadger report is to specify the log file path to pgbadger and it will generate the report to your present working directory.

$ pgbadger <log file>

For Example:

[user@server ~]$ sudo pgbadger /var/lib/pgsql/12/data/log/postgresql-Fri.log
[========================>] Parsed 1521 bytes of 1521 (100.00%), queries: 0, events: 1
LOG: Ok, generating html report...
[user@server ~]$

You can also analyze all your log files at once by using the wildcard * while specifying log names. Also, you can specify the output directory and file name as well by specifying the appropriate switch as shown below.

[user@server]$ sudo pgbadger /var/lib/pgsql/12/data/log/postgresql-* -O /var/www/pgbadger -o pgbadger.html
[========================>] Parsed 15914 bytes of 15914 (100.00%), queries: 0, events: 20
LOG: Ok, generating html report...
[user@server]$

The pgbadger report can be generated incrementally at a certain interval and you can even specify the retention period of the report files as well. This can be accomplished by setting up a cron job. For example, the below cron job will generate pgbadger report daily at 01:00 AM with the retention set as one week.

$ crontab -e
0 1 * * * /usr/bin/pgbadger --R 1 -I -q /var/lib/pgsql/12/data/log/postgresql-* -O /var/www/pgbadger/

You can use the help command as mentioned below to get the complete list of options and their usage in pgbadger.

$ pgbadger --help
[user@server ~]$ pgbadger --help

Usage: pgbadger [options] logfile [...]

    PostgreSQL log analyzer with fully detailed reports and graphs.

Arguments:

    logfile can be a single log file, a list of files, or a shell command
    returning a list of files. If you want to pass log content from stdin
    use - as filename. Note that input from stdin will not work with csvlog.

Options:

    -a | --average minutes : number of minutes to build the average graphs of
                             queries and connections. Default 5 minutes.
    -A | --histo-average min: number of minutes to build the histogram graphs
                             of queries. Default 60 minutes.
    -b | --begin datetime  : start date/time for the data to be parsed in log
                             (either a timestamp or a time)
    -c | --dbclient host   : only report on entries for the given client host.
    -C | --nocomment       : remove comments like /* ... */ from queries.
    -d | --dbname database : only report on entries for the given database.
    -D | --dns-resolv      : client ip addresses are replaced by their DNS name.
                             Be warned that this can really slow down pgBadger.
    -e | --end datetime    : end date/time for the data to be parsed in log
                             (either a timestamp or a time)
    -E | --explode         : explode the main report by generating one report
                             per database. Global information not related to a
                             database are added to the postgres database report.
    -f | --format logtype  : possible values: syslog, syslog2, stderr, jsonlog,
                             cvs, pgbouncer, logplex, rds and redshift. Use this
                             option when pgBadger is not able to detect the log
                             format.
    -G | --nograph         : disable graphs on HTML output. Enabled by default.
    -h | --help            : show this message and exit.
    -H | --html-outdir path: path to directory where HTML report must be written
                             in incremental mode, binary files stay on directory
                             defined with -O, --outdir option.
    -i | --ident name      : programname used as syslog ident. Default: postgres
    -I | --incremental     : use incremental mode, reports will be generated by
                             days in a separate directory, --outdir must be set.
    -j | --jobs number     : number of jobs to run at same time. Run as single
                             by default or when working with csvlog.
    -J | --Jobs number     : number of log file to parse in parallel. Process one
                             file at a time by default or when csvlog is used.
    -l | --last-parsed file: allow incremental log parsing by registering the
                             last datetime and line parsed. Useful if you want
                             to watch errors since last run or if you want one
                             report per day with a log rotated each week.
    -L | --logfile-list file:file containing a list of log file to parse.
    -m | --maxlength size  : maximum length of a query, it will be restricted to
                             the given size. Default truncate size is 100000.
    -M | --no-multiline    : do not collect multiline statement to avoid garbage
                             especially on errors that generate a huge report.
    -n | --nohighlight     : disable SQL code highlighting.
    -N | --appname name    : only report on entries for given application name
    -o | --outfile filename: define the filename for the output. Default depends
                             on the output format: out.html, out.txt, out.bin,
                             out.json or out.tsung. This option can be used
                             multiple time to output several format. To use json
                             output the Perl module JSON::XS must be installed,
                             To dump output to stdout use - as filename.
    -O | --outdir path     : directory where out file must be saved.
    -p | --prefix string   : the value of your custom log_line_prefix as
                             defined in your postgresql.conf. Only use it if you
                             aren't using one of the standard prefixes specified
                             in the pgBadger documentation, such as if your
                             prefix includes additional variables like client ip
                             or application name. See examples below.
    -P | --no-prettify     : disable SQL queries prettify formatter.
    -q | --quiet           : don't print anything to stdout, not even a progress
                             bar.
    -Q | --query-numbering : add numbering of queries to the output when using
                             options --dump-all-queries or --normalized-only.
    -r | --remote-host ip  : set the host where to execute the cat command on
                             remote logfile to parse locally the file.
    -R | --retention N     : number of weeks to keep in incremental mode. Default
                             to 0, disabled. Used to set the number of weeks to
                             keep in output directory. Older weeks and days
                             directory are automatically removed.
    -s | --sample number   : number of query samples to store. Default: 3.
    -S | --select-only     : only report SELECT queries.
    -t | --top number      : number of queries to store/display. Default: 20.
    -T | --title string    : change title of the HTML page report.
    -u | --dbuser username : only report on entries for the given user.
    -U | --exclude-user username : exclude entries for the specified user from
                             report. Can be used multiple time.
    -v | --verbose         : enable verbose or debug mode. Disabled by default.
    -V | --version         : show pgBadger version and exit.
    -w | --watch-mode      : only report errors just like logwatch could do.
    -W | --wide-char       : encode html output of queries into UTF8 to avoid
                             Perl message "Wide character in print".
    -x | --extension       : output format. Values: text, html, bin, json or
                             tsung. Default: html
    -X | --extra-files     : in incremental mode allow pgBadger to write CSS and
                             JS files in the output directory as separate files.
    -z | --zcat exec_path  : set the full path to the zcat program. Use it if
                             zcat or bzcat or unzip is not in your path.
    -Z | --timezone +/-XX  : Set the number of hours from GMT of the timezone.
                             Use this to adjust date/time in JavaScript graphs.
    --pie-limit num        : pie data lower than num% will show a sum instead.
    --exclude-query regex  : any query matching the given regex will be excluded
                             from the report. For example: "^(VACUUM|COMMIT)"
                             You can use this option multiple times.
    --exclude-file filename: path of the file which contains all the regex to
                             use to exclude queries from the report. One regex
                             per line.
    --include-query regex  : any query that does not match the given regex will
                             be excluded from the report. You can use this
                             option multiple times. For example: "(tbl1|tbl2)".
    --include-file filename: path of the file which contains all the regex of
                             the queries to include from the report. One regex
                             per line.
    --disable-error        : do not generate error report.
    --disable-hourly       : do not generate hourly report.
    --disable-type         : do not generate report of queries by type, database
                             or user.
    --disable-query        : do not generate query reports (slowest, most
                             frequent, queries by users, by database, ...).
    --disable-session      : do not generate session report.
    --disable-connection   : do not generate connection report.
    --disable-lock         : do not generate lock report.
    --disable-temporary    : do not generate temporary report.
    --disable-checkpoint   : do not generate checkpoint/restartpoint report.
    --disable-autovacuum   : do not generate autovacuum report.
    --charset              : used to set the HTML charset to be used.
                             Default: utf-8.
    --csv-separator        : used to set the CSV field separator, default: ,
    --exclude-time  regex  : any timestamp matching the given regex will be
                             excluded from the report. Example: "2013-04-12 .*"
                             You can use this option multiple times.
    --include-time  regex  : only timestamps matching the given regex will be
                             included in the report. Example: "2013-04-12 .*"
                             You can use this option multiple times.
    --exclude-db name      : exclude entries for the specified database from
                             report. Example: "pg_dump". Can be used multiple
                             time.
    --exclude-appname name : exclude entries for the specified application name
                             from report.  Example: "pg_dump".  Can be used
                             multiple time.
    --exclude-line regex   : pgBadger will start to exclude any log entry that
                             will match the given regex. Can be used multiple
                             time.
    --exclude-client name  : exclude log entries for the specified client ip.
                             Can be used multiple time.
    --anonymize            : obscure all literals in queries, useful to hide
                             confidential data.
    --noreport             : prevent pgBadger to create reports in incremental
                             mode.
    --log-duration         : force pgBadger to associate log entries generated
                             by both log_duration = on and log_statement = 'all'
    --enable-checksum      : used to add a md5 sum under each query report.
    --journalctl command   : command to use to replace PostgreSQL logfile by
                             a call to journalctl. Basically it might be:
                                journalctl -u postgresql-9.5
    --pid-dir path         : set the path where the pid file must be stored.
                             Default /tmp
    --pid-file file        : set the name of the pid file to manage concurrent
                             execution of pgBadger. Default: pgbadger.pid
    --rebuild              : used to rebuild all html reports in incremental
                             output directories where there's binary data files.
    --pgbouncer-only       : only show PgBouncer related menu in the header.
    --start-monday         : in incremental mode, calendar's weeks start on
                             sunday. Use this option to start on monday.
    --normalized-only      : only dump all normalized query to out.txt
    --log-timezone +/-XX   : Set the number of hours from GMT of the timezone
                             that must be used to adjust date/time read from
                             log file before beeing parsed. Using this option
                             make more difficult log search with a date/time.
    --prettify-json        : use it if you want json output to be prettified.
    --month-report YYYY-MM : create a cumulative HTML report over the specified
                             month. Requires incremental output directories and
                             the presence of all necessary binary data files
    --noexplain            : do not process lines generated by auto_explain.
    --command CMD          : command to execute to retrieve log entries on
                             stdin. pgBadger will open a pipe to the command
                             and parse log entries generated by the command.
    --no-week              : inform pgbadger to not build weekly reports in
                             incremental mode. Useful if it takes too much time.
    --explain-url URL      : use it to override the url of the graphical explain
                             tool. Default: http://explain.depesz.com/?is_public=0&is_anon=0&plan=
    --tempdir DIR          : set directory where temporary files will be written
                             Default: File::Spec->tmpdir() || '/tmp'
    --no-process-info      : disable changing process title to help identify
                             pgbadger process, some system do not support it.
    --dump-all-queries     : dump all queries found in the log file replacing
                             bind parameters are included in the queries at
                             their respective placeholders position.

pgBadger is able to parse a remote log file using a passwordless ssh connection.
Use the -r or --remote-host to set the host ip address or hostname. There's also
some additional options to fully control the ssh connection.

    --ssh-program ssh        path to the ssh program to use. Default: ssh.
    --ssh-port port          ssh port to use for the connection. Default: 22.
    --ssh-user username      connection login name. Default to running user.
    --ssh-identity file      path to the identity file to use.
    --ssh-timeout second     timeout to ssh connection failure. Default 10 secs.
    --ssh-option  options    list of -o options to use for the ssh connection.
                             Options always used:
                                 -o ConnectTimeout=$ssh_timeout
                                 -o PreferredAuthentications=hostbased,publickey

Log file to parse can also be specified using an URI, supported protocol are
http[s] and [s]ftp. The curl command will be used to download the file and the
file will be parsed during download. The ssh protocol is also supported and will
use the ssh command like with the remote host use. See examples bellow.

Examples:

    pgbadger /var/log/postgresql.log
    pgbadger /var/log/postgres.log.2.gz /var/log/postgres.log.1.gz /var/log/postgres.log
    pgbadger /var/log/postgresql/postgresql-2012-05-*
    pgbadger --exclude-query="^(COPY|COMMIT)" /var/log/postgresql.log
    pgbadger -b "2012-06-25 10:56:11" -e "2012-06-25 10:59:11" /var/log/postgresql.log
    cat /var/log/postgres.log | pgbadger -
    # Log prefix with stderr log output
    pgbadger --prefix '%t [%p]: user=%u,db=%d,client=%h' /pglog/postgresql-2012-08-21*
    pgbadger --prefix '%m %u@%d %p %r %a : ' /pglog/postgresql.log
    # Log line prefix with syslog log output
    pgbadger --prefix 'user=%u,db=%d,client=%h,appname=%a' /pglog/postgresql-2012-08-21*
    # Use my 8 CPUs to parse my 10GB file faster, much faster
    pgbadger -j 8 /pglog/postgresql-10.1-main.log

Use URI notation for remote log file:

    pgbadger http://172.12.110.1//var/log/postgresql/postgresql-10.1-main.log
    pgbadger ftp://username@172.12.110.14/postgresql-10.1-main.log
    pgbadger ssh://username@172.12.110.14:2222//var/log/postgresql/postgresql-10.1-main.log*

You can use together a local PostgreSQL log and a remote pgbouncer log file to parse:

    pgbadger /var/log/postgresql/postgresql-10.1-main.log ssh://username@172.12.110.14/pgbouncer.log

Generate Tsung sessions XML file with select queries only:

  pgbadger -S -o sessions.tsung --prefix '%t [%p]: user=%u,db=%d ' /pglog/postgresql-10.1.log

Reporting errors every week by cron job:

    30 23 * * 1 /usr/bin/pgbadger -q -w /var/log/postgresql.log -o /var/reports/pg_errors.html

Generate report every week using incremental behavior:

    0 4 * * 1 /usr/bin/pgbadger -q `find /var/log/ -mtime -7 -name "postgresql.log*"` -o /var/reports/pg_errors-`date +\%F`.html -l /var/reports/pgbadger_incremental_file.dat

This supposes that your log file and HTML report are also rotated every week.

Or better, use the auto-generated incremental reports:

    0 4 * * * /usr/bin/pgbadger -I -q /var/log/postgresql/postgresql.log.1 -O /var/www/pg_reports/

will generate a report per day and per week.

In incremental mode, you can also specify the number of week to keep in the
reports:

    /usr/bin/pgbadger --retention 2 -I -q /var/log/postgresql/postgresql.log.1 -O /var/www/pg_reports/

If you have a pg_dump at 23:00 and 13:00 each day during half an hour, you can
use pgBadger as follow to exclude these period from the report:

    pgbadger --exclude-time "2013-09-.* (23|13):.*" postgresql.log

This will help avoid having COPY statements, as generated by pg_dump, on top of
the list of slowest queries. You can also use --exclude-appname "pg_dump" to
solve this problem in a simpler way.

You can also parse journalctl output just as if it was a log file:

    pgbadger --journalctl 'journalctl -u postgresql-9.5'

or worst, call it from a remote host:

    pgbadger -r 192.168.1.159 --journalctl 'journalctl -u postgresql-9.5'

you don't need to specify any log file at command line, but if you have other
PostgreSQL log file to parse, you can add them as usual.

To rebuild all incremental html reports after, proceed as follow:

    rm /path/to/reports/*.js
    rm /path/to/reports/*.css
    pgbadger -X -I -O /path/to/reports/ --rebuild

it will also update all resource files (JS and CSS). Use -E or --explode
if the reports were built using this option.

pgBadger also support Heroku PostgreSQL logs using logplex format:

    heroku logs -p postgres | pgbadger -f logplex -o heroku.html -

this will stream Heroku PostgreSQL log to pgbadger through stdin.

pgBadger can auto detect RDS and cloudwatch PostgreSQL logs using
rds format:

    pgbadger -f rds -o rds_out.html rds.log

CloudSQL Postgresql logs it's fairly normal PostgreSQL log but encapsulated in
JSON format. It is auto detected too by pgBagder but in case you need to force
the log format, use `jsonlog`

    pgbadger -f jsonlog -o cloudsql_out.html cloudsql.log

This is the same than with the jsonlog extension, the json format is different
but pgBadger can parse both format.

To create a cumulative report over a month use command:

    pgbadger --month-report 2919-05 /path/to/incremantal/reports/

this will add a link to the month name into the calendar view in
incremental reports to look at report for month 2019 May.
Use -E or --explode if the reports were built using this option.

[user@server ~]$

Finally, I would say it’s one of the best freely available tools for PostgreSQL log analysis and identifying performance issues. The shareable and feature-rich HTML reports & graphs have made it the go-to tool when it comes to capturing and sharing the database performance report with stakeholders 📊


Tuesday, July 25, 2023

Memory inspection through pg_buffercache

 pg_buffercache is a very useful extension that allows for the inspection of the memory as used by a live PostgreSQL instance. The extension is available by means of the contrib module and is very useful to take a look at the memory usage, in other words the usage of the shared_buffers.

Thanks to this module it is possible to clearly understand the memory consumption and, therefore, the correct tuning of the shared_buffers parameter.
A few years ago I wrote a set of example queries to interact with the module and get a glance at the memory usage. While those queries were a starting point, they had some issues especially when a table was not consuming memory (disibion by zero, and so on).

I finally found the time to produce a cleaner approach to those queries, so I re-implemented all the queries by means of functions. The script is a psql script, and uses some special backslash commands, but you can extract the SQL pure part and execute it by means of another client.
The script creates a memory schema and places all the functions into such schema; the functions have a name that starts with f_memory, so that they should not clash with existing functions.
In the following I describe every function.
Please note that the idea here is to provide a background about memory inspection, there is still room for improvements and fixes!

Installing the functions

It does suffice to execute the memory.sql psql script to get the creation of the schema memory and all the functions into such schema. The script provides some information about the objects created:

tfdb=# \i memory.sql 
Creating a schema named memory...
All objects created!
Try one of the following functions:
 - memory.f_memory() to get very basic information
 - memory.f_memory_usage() to get information about the whole memory
 - memory.f_memory_usage_by_database() to get information about single databases
 - memory.f_memory_usage_by_table() to get information about tables in the current database
 - memory.f_memory_usage_by_table_cumulative() to get cumulative information for tables

You can add the memory schema to the search path.
Try running the following query while testing the database (e.g., via pgbench):

select memory.f_memory_usage();
\watch 5


The output of the functions

All the function accept a boolean human flag, that by default is set to true. If the flag is set the output of the memory dimensions will be formatted using pg_size_pretty(), therefore will be in a human readable format. Otherwise the output will be formatted as plain number of bytes.

tfdb=# select * from memory.f_memory();
 total  |  used  |  free  
--------|--------|--------
 800 MB | 101 MB | 699 MB
(1 row)

tfdb=# select * from memory.f_memory( false );
   total   |   used    |   free    
-----------|-----------|-----------
 838860800 | 106168320 | 732692480
(1 row)


Utility functions

There are a few utility functions that are used as a backbone to build the others. In particular:
  • memory.f_check_pg_buffercache() it checks that the extension pg_buffercache is installed into the database;
  • memory.f_check_user() checks that the user is either an administrator or has the privileges to run pg_buffercache functions;
  • memory.f_check() calls the previous two functions and raises an exception if the check fails. This function is invoked by all the other memory related functions, so that before the function is run the user can get an alert about missing pieces;
  • memory.f_usagecounter_to_string() provides a textual description of the pg_buffercache.usagecount value;
  • memory.f_tablename() provides the name of a table, index or view os anything that will appear in the output of other functions;
  • memory.f_print_bytes() prints the amount of bytes as text, using either pg_size_pretty() or plain text conversion. This is used in every function to support the above mentioned human flag.

Available functions

The available functions to inspect the memory usage are described in the following.

f_memory()

The function memory.f_memory() provides a glance at free and used memory in the cluster.

tfdb=# select * from memory.f_memory();
 total  |  used  |  free  
--------|--------|--------
 800 MB | 163 MB | 637 MB
(1 row)



f_memory_usage()

The function memory.f_memory_usage() provides a more detailed view about the usage of the memory. In particular it provides the amount of memory used by level of usagecount.

tfdb=# select * from memory.f_memory_usage();
 total_memory | memory  | percent | cumulative |  description   
--------------|---------|---------|------------|----------------
 800 MB       | 22 MB   | 2.71 %  | 2.71%      | VERY HIGH (5)
 800 MB       | 2536 kB | 0.31 %  | 3.02%      | HIGH (4)
 800 MB       | 1936 kB | 0.24 %  | 3.26%      | MID (3)
 800 MB       | 1888 kB | 0.23 %  | 3.49%      | LOW (2)
 800 MB       | 135 MB  | 16.85 % | 20.34%     | VERY LOW (1)
 800 MB       | 637 MB  | 79.66 % | 100.00%    | == FREE == (0)
(6 rows)


The memory column provides the amount of memory used for a specific region, and the percent columns provide the ratio of memory usage with regard to the total memory. The cumulative column provides the amount ratio of the usage level greater than the current one.
As an example, in the above there are 135 MB used not frequently, and thus the 20.34 % of memory is used from very high to very low.

f_memory_usage_by_database()

The function memory.f_memory_usage_by_database() provides information about the usage of memory by each database in the cluster, and provides also the caching amount of every database.

pgbench=# select * from memory.f_memory_usage_by_database();
 total_memory |  database   | size_in_memory | size_on_disk | percent_cached | percent_of_memory 
--------------|-------------|----------------|--------------|----------------|-------------------
 256 MB       | pgbench     | 182 MB         | 1505 MB      | 12.11%         | 71.15%
 256 MB       | ltdb        | 608 kB         | 171 MB       | 0.35%          | 0.23%
 256 MB       | postgres    | 544 kB         | 104 MB       | 0.51%          | 0.21%
 256 MB       | restore     | 544 kB         | 104 MB       | 0.51%          | 0.21%
 256 MB       | restore2    | 544 kB         | 104 MB       | 0.51%          | 0.21%
 256 MB       | restore3    | 544 kB         | 104 MB       | 0.51%          | 0.21%
 256 MB       | restore4    | 544 kB         | 8269 kB      | 6.58%          | 0.21%
 256 MB       | template1   | 544 kB         | 8245 kB      | 6.60%          | 0.21%
(8 rows)
         


f_memory_usage_by_table()

The function memory.f_memory_usage_by_table() provides information about the usage of all tabular like stuff, in other words about relations.

tfdb=# select * from memory.f_memory_usage_by_table();
...

 800 MB       | tfdb | (table) respi.y2019m12                         | 8192 bytes | 0.00 %  | VERY HIGH (5)
 800 MB       | tfdb | (table) respi.y2019m12                         | 22 MB      | 2.70 %  | VERY VERY LOW (0)
 800 MB       | tfdb | (index) respi.y2019m12_ts_idx                  | 32 kB      | 0.00 %  | VERY HIGH (5)
 800 MB       | tfdb | (index) respi.y2019m12_ts_idx1                 | 8192 bytes | 0.00 %  | VERY HIGH (5)



f_memory_usage_by_table_cumulative()

The function f_memory_usage_by_table_cumulative() provides an overview of how much memory a single table is “consuming”, without any regard to the usage level counter.

tfdb=# select * from memory.f_memory_usage_by_table_cumulative();
-[ RECORD 1 ]-----|-----------------------------------------------
total_memory      | 800 MB
database          | tfdb
relation          | (table) respi.y2019m07
memory            | 10 MB
on_disk           | 1159 MB
percent_of_memory | 1.27 %
percent_of_disk   | 0.88%
usagedescription  | any
-[ RECORD 2 ]-----|-----------------------------------------------
total_memory      | 800 MB
database          | tfdb
relation          | (table) respi.y2019m06
memory            | 10 MB
on_disk           | 1156 MB
percent_of_memory | 1.26 %
percent_of_disk   | 0.87%
usagedescription  | any
...


The function accepts the usual human argument, but also an integer optional argument that represents the usage counter you are interested in. When specified, the function will show only the amount of memory used with a greater or equal usage counter.

tfdb=# select * from memory.f_memory_usage_by_table_cumulative( 5 );
-[ RECORD 1 ]-----|-----------------------------------------------
total_memory      | 800 MB
database          | tfdb
relation          | (table) respi.y2019m07
memory            | 8192 bytes
on_disk           | 1159 MB
percent_of_memory | 0.00 %
percent_of_disk   | 0.00%
usagedescription  | >= VERY HIGH (5)
-[ RECORD 2 ]-----|-----------------------------------------------
total_memory      | 800 MB
database          | tfdb
relation          | (table) respi.y2019m06
memory            | 8192 bytes
on_disk           | 1156 MB
percent_of_memory | 0.00 %
percent_of_disk   | 0.00%
usagedescription  | >= VERY HIGH (5)
...


Conclusions

The above set of functions can be used as a starting point to build your own set of queries to inspect the memory usage of a live PostgreSQL cluster. There is still room for improvements and reduce the code duplication, so stay tuned for other versions!

Thursday, April 27, 2023

Create and Install Client and Server SSL Certificates for PostgreSQL Database

 

When a database client establishes a TCP/IP connection to a database server, it typically creates an unencrypted communication channel unless explicitly stated. Most database engines have native support for encryption and authentication using SSL/TLS.

In typical secure setups, the database server defines the minimum level of security required for TCP/IP connections. It is possible to force all TCP/IP connections to be encrypted – for example, PostgreSQL uses “hostssl” in the pg_hba.conf file, while MySQL uses “require_secure_transport=ON” in the my.cnf file. In addition to encryption, authentication can be enabled by installing client/server certificates on the respective component.

Depending on the environment (e.g. dev/staging/uat/production), databases can have varying security requirements. Development databases may allow unencrypted TCP/IP connections while production databases would be hardened to only allow encrypted ones.

However, it is possible for the application/client to be the one that enforces this requirement. For example, an application might append a “sslmode=verify-ca” to the JDBC connection string, forcing all outgoing connections to use SSL/TLS encryption and authentication. This might be fine in the production environment, but it would also require that a development database be able to respond to SSL/TLS encryption and authentication connection requests.

This post will look at how we can enable SSL/TLS encryption and authentication on a self-managed PostgreSQL database and client so as to establish encrypted and secure connections between them.

Understanding What SSL/TLS Protects against

Without going too deep down the rabbit hole, SSL/TLS provides protection against the following threats:

  • Eavesdropping – malicious third party examines the network traffic between the client and the server
  • Man in the middle (MITM) – malicious third party pretends to be your database server and accepts your traffic
  • Impersonation – malicious third party pretends to be an authorized client and accesses the database server

Because security often has computational overheads that impact application performance, most database servers offer several levels of protection for their connections. PostgreSQL has several SSL Modes:

  • disable
  • allow
  • prefer
  • require
  • verify-ca
  • verify-full

Except for verify-ca and verify-full, the remaining modes are supported by PostgreSQL out of the box. A full description of the threats and the protection levels can be found at https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION

Setup

I’ll be using the following for this guide:

  • Database: PostgreSQL 12.7 (running locally)
  • Cryptography Library: OpenSSL 1.1.1f
  • Host: Virtual Machine running Ubuntu 20.04.2 LTS (fresh install)
  • Hypervisor: Oracle VM VirtualBox 6.1

Step 1: Verify/Install OpenSSL Cryptography Library

OpenSSL is a cryptography software library used by PostgreSQL to secure TCP/IP connections via SSL/TLS (docs). Verify that OpenSSL is installed:

$ openssl version
OpenSSL 1.1.1f  31 Mar 2020

Or install it if necessary:

$ sudo apt-get install openssl

Step 2: Install, Configure and Start PostgreSQL

Check the version of PostgreSQL that is in the standard Ubuntu package repository either via the PostgreSQL Apt Repository page, or using apt-cache madison:

$ apt-cache madison postgresql
postgresql | 12+214ubuntu0.1 | http://sg.archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages
postgresql | 12+214ubuntu0.1 | http://sg.archive.ubuntu.com/ubuntu focal-security/main amd64 Packages
postgresql |     12+214 | http://sg.archive.ubuntu.com/ubuntu focal/main amd64 Packages

Install PostgreSQL database:

$ sudo apt-get install postgresql
... 
The following additional packages will be installed:
  libllvm10 libpq5 libsensors-config libsensors5 postgresql-12 postgresql-client-12 postgresql-client-common postgresql-common
 ssl-cert sysstat
Suggested packages:
  lm-sensors postgresql-doc postgresql-doc-12 libjson-perl openssl-blacklist isag
The following NEW packages will be installed:
  libllvm10 libpq5 libsensors-config libsensors5 postgresql postgresql-12 postgresql-client-12 postgresql-client-common postgresql-common ssl-cert sysstat
0 upgraded, 11 newly installed, 0 to remove and 66 not upgraded.
Need to get 30.6 MB of archives.
After this operation, 122 MB of additional disk space will be used.
Do you want to continue? [Y/n] y

Verify that PostgreSQL has been installed successfully:

$ sudo systemctl status postgresql
● postgresql.service - PostgreSQL RDBMS
     Loaded: loaded (/lib/systemd/system/postgresql.service; enabled; vendor preset: enabled)
     Active: active (exited) since Thu 2021-08-05 12:41:01 UTC; 8min ago
   Main PID: 3410 (code=exited, status=0/SUCCESS)
      Tasks: 0 (limit: 2279)
     Memory: 0B
     CGroup: /system.slice/postgresql.service

Aug 05 12:41:01 user systemd[1]: Starting PostgreSQL RDBMS...
Aug 05 12:41:01 user systemd[1]: Finished PostgreSQL RDBMS.


$ tail /var/log/postgresql/postgresql-12-main.log
...
2021-08-05 17:50:50.910 UTC [7919] LOG:  database system is ready to accept connections

Step 3: Establish Encrypted/Unencrypted Connection (Without Certificate Verification) via psql Client

Out of the box, PostgreSQL will accept both encrypted and unencrypted TCP/IP connections. Before testing, I will make a quick change to pg_hba.conf to allow passwordless logins via localhost. For a non-POC database server, you should use the ALTER USER command to set a password.

$ find / -name pg_hba.conf 2>/dev/null
/etc/postgresql/12/main/pg_hba.conf

$ sudo vi /etc/postgresql/12/main/pg_hba.conf
host    all             all             127.0.0.1/32            trust     # I modified this line

## the above modification to pg_hba.conf will allow passwordless logins for all users connecting via TCP/IP to 127.0.0.1 (localhost)

Reload the PostgreSQL process to pick up the new configs.

$ sudo systemctl restart postgresql

Now lets connect to PostgreSQL with and without SSL/TLS encryption (refer to absence/presence of line that begins with “SSL connection”):

$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=disable
psql (12.7 (Ubuntu 12.7-0ubuntu0.20.04.1))
Type "help" for help.

postgres=# exit

$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=require
psql (12.7 (Ubuntu 12.7-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# exit

However, when we try to use the verify-ca or verify-full modes, we get an error as we have not set up the server certificates yet.

$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-ca
psql: error: root certificate file "/home/user/.postgresql/root.crt" does not exist
Either provide the file or change sslmode to disable server certificate verification.

$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-full
psql: error: root certificate file "/home/user/.postgresql/root.crt" does not exist
Either provide the file or change sslmode to disable server certificate verification.

Step 4: SSL Certificate for PostgreSQL Server

There are two possible options when it comes to creating server certificates for PostgreSQL – self-signed and CA-signed.

Step 4a: Create and Install Self-Signed Server Certificate for PostgreSQL

For this section, I am following the commands from the PostgreSQL documentation at https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CERTIFICATE-CREATION.

Use OpenSSL to create a self-signed server certificate and private key for the PostgreSQL server:

$ openssl req -new -x509 -days 365 -nodes -text -out server.crt \
>  -keyout server.key -subj "/CN=127.0.0.1"
Generating a RSA private key
.......................+++++
...........................................................+++++
writing new private key to 'server.key'
-----

$ chmod og-rwx server.key server.crt

Note that the common name of the certificate is set to 127.0.0.1.

As this is a non-production environment, I decided to locate all my certificates within ~/.postgresql for convenience (recall the error message in Step 3). In a production environment, you would usually put it in /etc/ssl/certs/ and /etc/ssl/private/ (or similar).

$ mkdir ~/.postgresql
$ mv server.* ~/.postgresql/

Since we are self-signed, we will use the server certificate (server.crt) as the trusted root certificate (root.crt) for the client (source). As root.crt is the same as server.crt, this will tell the psql client to trust certificates that are signed by itself (i.e. self-signed). Note that the “~/.postgresql/root.crt” location used by psql is the default location and is configurable.

$ cp ~/.postgresql/server.crt ~/.postgresql/root.crt

As the postgres service account will be accessing the certificate and private key, change ownership of server.crt and server.key to the postgres user:

$ sudo chown postgres:postgres ~/.postgresql/server*

If you do not do this, the database will fail to start with the error:

FATAL:  could not load server certificate file "/home/user/.postgresql/server.crt": Permission denied

Right now, our ~/.postgresql folder should look like this:

$ ls -al ~/.postgresql/
-rw------- 1 user     user     4141 Aug  5 17:28 root.crt
-rw------- 1 postgres postgres 4141 Aug  5 17:02 server.crt
-rw------- 1 postgres postgres 1704 Aug  5 17:02 server.key

Modify ssl_cert_file and ssl_key_file settings in the postgresql.conf file to point the our ~/.postgresql folder:

$ find / -name postgresql.conf 2>/dev/null
/etc/postgresql/12/main/postgresql.conf

$ sudo vi /etc/postgresql/12/main/postgresql.conf
ssl = on                                              # verify this is set to 'on'
ssl_cert_file = '/home/user/.postgresql/server.crt'   # update this line
ssl_key_file = '/home/user/.postgresql/server.key'    # update this line

Restart PostgreSQL server and verify that it is ready:

$ sudo systemctl restart postgresql

$ tail /var/log/postgresql/postgresql-12-main.log
...
2021-08-05 17:50:50.910 UTC [7919] LOG:  database system is ready to accept connections

Verify that we are now able to connect to the PostgreSQL database server using the verify-ca and verify-full SSL modes:

$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-ca
psql (12.7 (Ubuntu 12.7-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# exit 


$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-full
psql (12.7 (Ubuntu 12.7-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# exit

Verify that without the right “CA” certificate (i.e. the server certificate), the psql client will not be able to verify that the server certificate is signed by a “trusted” entity:

# verify missing CA certificate
$ rm ~/.postgresql/root.crt
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-ca
psql: error: root certificate file "/home/user/.postgresql/root.crt" does not exist
Either provide the file or change sslmode to disable server certificate verification.

# verify different CA certificate does not work
$ openssl req -new -x509 -days 365 -nodes -text -out server1.crt \
>  -keyout server1.key -subj "/CN=127.0.0.1"
$ cp server1.crt ~/.postgresql/root.crt
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-ca
psql: error: SSL error: certificate verify failed

Step 4b: Create and Install a CA-Signed Server Certificate for PostgreSQL

This step continues from Step 3 and does not depend on Step 4a.

In this approach, the steps are:

  1. For self-managed CA: create the private key and certificate
  2. For PostgreSQL Server: create the private key and a Certificate Signing Request (CSR)
  3. Create a signed certificate for PostgreSQL server by using CA key and certificate to sign CSR

For this step, I am following the blog post from https://blog.devolutions.net/2020/07/tutorial-how-to-generate-secure-self-signed-server-and-client-certificates-with-openssl as I found it easier to understand than PostgreSQL’s documentation.

First, create the private key and certificate belonging to our self-managed CA:

$ openssl ecparam -name prime256v1 -genkey -noout -out ca.key
$ openssl req -new -x509 -sha256 -key ca.key -out ca.crt -subj "/CN=ca.luppeng.com"

$ ls 
ca.crt  ca.key

Next, create the private key and a Certificate Signing Request (CSR) for our PostgreSQL server:

$ openssl ecparam -name prime256v1 -genkey -noout -out server.key
$ openssl req -new -sha256 -key server.key -out server.csr -subj "/CN=127.0.0.1"

$ ls
ca.crt  ca.key  server.csr  server.key

Now use the self-managed CA private key and certificate to sign the CSR to generate the server certificate:

$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256
Signature ok
subject=CN = 127.0.0.1
Getting CA Private Key

$ ls 
ca.crt  ca.key  ca.srl  server.crt  server.csr  server.key

With the signed server certificate, we can install the certificates on the client and server: 1) server.crt and server.key will be used by the PostgreSQL database server, 2) while the CA’s certificate (ca.crt) will be used by the psql client to validate that the server certificate returned by the server was signed by it. The private key of the CA will not be required for this step (and should be kept securely by the CA)

As this is a non-production environment, I decided to locate all my certificates within ~/.postgresql for convenience (recall the error message in Step 3). In a production environment, you would usually put it in /etc/ssl/certs/ and /etc/ssl/private/ (or similar).

$ mkdir ~/.postgresql
$ mv server.crt ~/.postgresql/
$ mv server.key ~/.postgresql/
$ mv ca.crt ~/.postgresql/

As the postgres service account will be accessing the certificate and key, change ownership of server.crt and server.key to the postgres user.

$ chmod og-rwx ~/.postgresql/server.*
$ sudo chown postgres:postgres ~/.postgresql/server.crt ~/.postgresql/server.key

If you do not do this, the database will fail to start with the error:

FATAL:  could not load server certificate file "/home/user/.postgresql/server.crt": Permission denied

Right now, our ~/.postgresql folder should look like this:

$ ls -al ~/.postgresql/
-rw-rw-r-- 1 user     user      591 Aug  6 04:32 ca.crt
-rw------- 1 postgres postgres  461 Aug  6 04:41 server.crt
-rw------- 1 postgres postgres  227 Aug  6 04:36 server.key

Modify ssl_cert_file and ssl_key_file settings in the postgresql.conf file to point the our ~/.postgresql folder:

$ find / -name postgresql.conf 2>/dev/null
/etc/postgresql/12/main/postgresql.conf

$ sudo vi /etc/postgresql/12/main/postgresql.conf
ssl = on                                              # verify this is set to 'on'
ssl_cert_file = '/home/user/.postgresql/server.crt'   # update this line
ssl_key_file = '/home/user/.postgresql/server.key'    # update this line

Restart PostgreSQL server and verify that it is ready:

$ sudo systemctl restart postgresql

$ tail /var/log/postgresql/postgresql-12-main.log
...
2021-08-06 06:14:36.231 UTC [9105] LOG:  database system is ready to accept connections

As per this PostgreSQL documentation, the ~/.postgresql/root.crt file stores a list of trusted certificate authorities that is used to verify that the server certificate is signed by an entity on the list. We have set up the CA certificate as ca.crt, so we’ll copy and rename it to root.crt. (note: it is possible to change the default location for this file)

$ cp ~/.postgresql/ca.crt ~/.postgresql/root.crt

Verify that we are able to connect to the PostgreSQL database server using the verify-ca and verify-full SSL modes:

$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-ca
psql (12.7 (Ubuntu 12.7-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# exit


$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-full
psql (12.7 (Ubuntu 12.7-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# exit

Verify that without the right CA certificate (which we generated in at the beginning of this step), the psql client will not be able to verify that the server certificate is signed by a trusted entity:

# verify missing CA certificate
$ mv ~/.postgresql/root.crt ~/.postgresql/root.crt.bk
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-ca
psql: error: root certificate file "/home/user/.postgresql/root.crt" does not exist
Either provide the file or change sslmode to disable server certificate verification.

# verify different CA certificate does not work
$ openssl ecparam -name prime256v1 -genkey -noout -out ca1.key
$ openssl req -new -x509 -sha256 -key ca1.key -out ca1.crt -subj "/CN=ca1.luppeng.com"
$ cp ca1.crt ~/.postgresql/root.crt
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=verify-ca
psql: error: SSL error: certificate verify failed

To summarize, in this approach we are saying that we trust the CA, and are using its certificate to verify that PostgreSQL server’s certificate was indeed signed by it. If we trust the CA, and the CA trusts the PostgreSQL server, then by associative property we can trust the PostgreSQL server.

Step 5: SSL Certificate for PostgreSQL Client

What we have done so far is to provide a way for the client to validate the PostgreSQL database server’s identity. However, in some scenarios, the PostgreSQL database server should only allow connections from trusted clients. In this section, we see how we can use client certificates to enable the database server to validate the client’s identity.

To understand how this works, we can reference the following PostgreSQL documentations:

This is the requirement for the database server configuration (i.e. postgresql.conf):

To require the client to supply a trusted certificate, place certificates of the root certificate authorities (CAs) you trust in a file in the data directory, set the parameter ssl_ca_file in postgresql.conf to the new file name, and add the authentication option clientcert=verify-ca or clientcert=verify-full to the appropriate hostssl line(s) in pg_hba.conf. A certificate will then be requested from the client during SSL connection startup.

https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CLIENT-CERTIFICATES

This is the requirement of the pg_hba.conf:

… there is one method-independent authentication option clientcert, which can be specified in any hostssl record. This option can be set to verify-ca or verify-full. Both options require the client to present a valid (trusted) SSL certificate, while verify-full additionally enforces that the cn (Common Name) in the certificate matches the username or an applicable mapping.

https://www.postgresql.org/docs/current/auth-pg-hba-conf.html

This is the requirement for the psql client:

If the server attempts to verify the identity of the client by requesting the client’s leaf certificate, libpq will send the certificates stored in file ~/.postgresql/postgresql.crt in the user’s home directory. The certificates must chain to the root certificate trusted by the server. A matching private key file ~/.postgresql/postgresql.key must also be present. The private key file must not allow any access to world or group; achieve this by the command chmod 0600 ~/.postgresql/postgresql.key

https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-CLIENTCERT

Let’s begin. If you have not already generated the private key and certificate for the self-managed CA (previously done in Step 4b), you can do so by:

$ openssl ecparam -name prime256v1 -genkey -noout -out ca.key
$ openssl req -new -x509 -sha256 -key ca.key -out ca.crt -subj "/CN=ca.luppeng.com"

Next, create the private key and a Certificate Signing Request (CSR) for our PostgreSQL client:

$ openssl ecparam -name prime256v1 -genkey -noout -out client.key
$ openssl req -new -sha256 -key client.key -out client.csr -subj "/CN=postgres"

## note that the CN in the client cert must be the is the name of your postgresql user (e.g. alice, app_db_user) when using "verify-full" mode. Here, I am using the default postgresql user called "postgres"

Now, use the self-managed CA private key and certificate to sign the client’s CSR and generate the client certificate:

$ openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256
Signature ok
subject=CN = postgres
Getting CA Private Key

$ ls 
ca.crt  ca.key  ca.srl  client.crt  client.csr  client.key

As per PostgreSQL documentation above, the client certificates should be stored at:

  • Client certificate at ~/.postgresql/postgresql.crt
  • Client private key at ~/.postgresql/postgresql.key

So let’s do that:

$ mkdir ~/.postgresql
$ cp client.crt ~/.postgresql/postgresql.crt
$ cp client.key ~/.postgresql/postgresql.key
$ chmod 0600 ~/.postgresql/postgresql.crt ~/.postgresql/postgresql.key

The PostgreSQL server would need the CA’s certificate to validate that it was indeed signed by the CA (and can be trusted). As the postgres service account will be accessing the certificate and key, change ownership of ca.crt to the postgres user:

$ cp ca.crt ~/.postgresql/ca.crt
$ sudo chown postgres:postgres ~/.postgresql/ca.crt

$ ls -al ~/.postgresql
-rw-rw-r-- 1 postgres postgres  591 Aug  6 19:15 ca.crt
-rw------- 1 user     user     4135 Aug  6 19:15 postgresql.crt
-rw------- 1 user     user     1704 Aug  6 19:15 postgresql.key

Modify ssl_ca_file setting in the postgresql.conf file to point to ~/.postgresql/ca.crt folder:

$ find / -name postgresql.conf 2>/dev/null
/etc/postgresql/12/main/postgresql.conf

$ sudo vi /etc/postgresql/12/main/postgresql.conf
ssl = on                                         # verify this is set to 'on'
ssl_ca_file = '/home/user/.postgresql/ca.crt'    # update this line

Modify pg_hba.conf to use clientcert as the auth-option:

$ sudo vi /etc/postgresql/12/main/pg_hba.conf
hostssl    all    all    127.0.0.1/32    cert clientcert=verify-full    # I modified this line 

Restart PostgreSQL server and verify that it is ready:

$ sudo systemctl restart postgresql

$ tail /var/log/postgresql/postgresql-12-main.log
...
2021-08-06 15:07:35.469 UTC [11227] LOG:  database system is ready to accept connections

Verify that we are able to connect to the Postgresql server using the client certificate and private key. Here, I do not assume that the server has certificates and that the CA used to sign them is the same as the client, so the highest SSL mode we will go is “require” to establish encrypted communication channel only. If you have set things up correctly from previous steps, you can try the SSL modes that perform authentication.

# Verify that encrypted connection with client certificate works 
# (PostgreSQL server could use CA certificate to verify that we are a trusted client)
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=require
psql (12.7 (Ubuntu 12.7-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# exit

Verify that missing/invalid client certificates will result in failure to connect:

# Verify that connection fails when client certificate is missing 
# (enforced by "cert clientcert=verify-full" part of pg_hba.conf entry)
$ mv ~/.postgresql/postgresql.crt ~/.postgresql/postgresql.crt.bk
$ mv ~/.postgresql/postgresql.key ~/.postgresql/postgresql.key.bk
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=require
psql: error: FATAL:  connection requires a valid client certificate


# Verify that unencrypted connections are not allowed 
#(enforced by "hostssl" part of pg_hba.conf entry) 
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=disable
psql: error: FATAL:  no pg_hba.conf entry for host "127.0.0.1", user "postgres", database "postgres", SSL off


# Verify that connection fails when we use an untrusted certificate (i.e. not signed by CA)
# reused command from self-signed certificate to generate a certificate not signed by CA
$ openssl req -new -x509 -days 365 -nodes -text -out client-untrusted.crt \
>  -keyout client-untrusted.key -subj "/CN=postgres"
$ cp client-untrusted.crt ~/.postgresql/postgresql.crt
$ cp client-untrusted.key ~/.postgresql/postgresql.key
$ psql postgresql://postgres@127.0.0.1:5432/postgres?sslmode=require
psql: error: SSL error: tlsv1 alert unknown ca

Summary

To secure your database connection, PostgreSQL has six different SSL modes – disable, allow, prefer, require, verify-ca, and verify-full. The first four modes are for encryption and supported out of the box (pre-requisite: OpenSSL library installed) by PostgreSQL, while the last two modes need some setting up and configuration of certificates and private keys.

To verify a PostgreSQL server’s identity, we can install either a self-signed or CA-signed certificate and private key on the server. The client would then use a copy of the CA certificate (or the self-signed cert itself) to verify that the server can be trusted.

To verify a PostgreSQL client’s identity, we can use a CA-signed certificate and private key when establishing the connection. The server would then use a copy of the CA certificate to verify that the client can be trusted.

It is possible to perform both client and server certificate validation for the highest level of encryption and authentication. However, there will be performance overheads from the encryption and certificate validation process – one needs to carefully analyze the tradeoff before selecting that route.

Hope the above guide is helpful to understand how the PostgreSQL server and client can improve their security via SSL encryption and authentication. Thanks for reading and appreciate any support you can give! 🙂

Wednesday, November 3, 2021

oraenv++: an easy and powerful tool to set up your Oracle environment

 

I have never found a really good and portable way of setting up an Oracle/ASM environment. For sure, there is oraenv but it is based on oratab which is a static hardcoded text file and oraenv does not work well (at all ?) with RAC. Some tries to set up aliases to set up their environment but same, it is hardcoded, Oracle uses hardcoded env files in OCI, etc ... All of this works more and less well but it has always seemed a bit clumsy to me and during a recent email discussion I had with Osman, I decided to jump into that challenge to create something better, easier and more efficient than any of these tools and a fews days later was born oraenv++!

Key features

  • oraenv++ reads the environmental information from GI/CRS/Oracle Restart so no hardcoded configuration file is needed; sorry I cannot do anything if you do not have GI/CRS/Oracle Restart
  • oraenv++ sets the ORACLE_SIDORACLE_HOMEORACLE_BASEPATH and can also set the ORACLE_PDB_SID environment variable to be able to directly connect to a PDB (from 18.8)
  • if you do not know the name of the database you want to set the env up, oraenv++ will show you a menu with a list of databases from your configuration to choose from
  • you can also grep and grep -v in the database list of your environment (regexp are supported)
  • GI storing the database names in lowercase regardless of their case, oraenv++ is not key sensitive regarding the database name nor the pattern(s) to grep/ungrep
  • oraenv++ has to be sourced (. oraenv++) and not executed (./oraenv++); I'll eventually write about the differences in my bash tips
  • I have tested it with GI 19c and I see no reason why it would not work from any GI 12c+ (including 21c+); not sure for GI 11g which should not be much around any more these days
  • I have been using oraenv++ for a little while now and I honestly find it











Examples

A first example to simply set up a database environment:
$ . oraenv++ prod12a
Database            : prod12a
ORACLE_HOME         : /u01/app/oracle/product/12.1.0.2/db_1
ORACLE_BASE         : /u01/app/oracle
ORACLE_SID          : PROD12A1
ORACLE_PDB_SID      :
sqlplus is          : /u01/app/oracle/product/12.1.0.2/db_1/bin/sqlplus
$
Here, we have set up the prod12a environememnt with the correct instance name running on the host you are connected to. If you do not want to see this output, you can make it silent:
$ . oraenv++ prod12a --silent
$
or
$ OPP_SILENT=True
$ . oraenv++ prod12a
$
If you do not specify a database name, oraenv++ will list all the databases which have an instance on this host from your GI configuration and show you a menu to choose from (let's go with 6 below):
$ . oraenv++
    Database    SID          ORACLE_HOME
-----------------------------------------------------------------
  1/ asm        +ASM1      /u01/app/19.11.0.0/grid
  2/ dev12a     DEV12A1    /u01/app/oracle/product/12.1.0.2/db_1
  3/ dev12b     DEV12B1    /u01/app/oracle/product/12.1.0.2/db_1
  4/ dev19a     DEV19A1    /u01/app/oracle/product/19.0.0.0/db_1
  5/ dev19b     DEV19B1    /u01/app/oracle/product/19.0.0.0/db_1
  6/ misc1      MISC11     /u01/app/oracle/product/12.1.0.2/db_1
  7/ misc2      MISC21     /u01/app/oracle/product/19.0.0.0/db_1
  8/ test       TEST1      /u01/app/oracle/product/11.2.0.4/db_1
  9/ prod12a    PROD12A1   /u01/app/oracle/product/12.1.0.2/db_1
 10/ prod12b    PROD12B1   /u01/app/oracle/product/12.1.0.2/db_1
 11/ prod19a    PROD19A1   /u01/app/oracle/product/19.0.0.0/db_1
 12/ prod19b    PROD19B1   /u01/app/oracle/product/19.0.0.0/db_1
-----------------------------------------------------------------
Which environment you want to set up ? (CTRL+C for exit)
6
Database            : misc1
ORACLE_HOME         : /u01/app/oracle/product/12.1.0.2/db_1
ORACLE_BASE         : /u01/app/oracle
ORACLE_SID          : MISC11
ORACLE_PDB_SID      :
sqlplus is          : /u01/app/oracle/product/12.1.0.2/db_1/bin/sqlplus
$
If you have many databases, you can narrow the selection down using --grep or --ungrep (acts as a grep -v); note that these 2 options allow mutiple comma separated pattern to be grepped/ungrepped; they also supports regexp -- this is far overkill but it is fun :)
$. oraenv++ --grep prod,dev --ungrep 12
     Database    SID          ORACLE_HOME
-----------------------------------------------------------------
  1/ dev19a     DEV19A1    /u01/app/oracle/product/19.0.0.0/db_1
  2/ dev19b     DEV19B1    /u01/app/oracle/product/19.0.0.0/db_1
  3/ prod19a    PROD19A1   /u01/app/oracle/product/19.0.0.0/db_1
  4/ prod19b    PROD19B1   /u01/app/oracle/product/19.0.0.0/db_1
-----------------------------------------------------------------
Which environment you want to set up ? (CTRL+C for exit)
This could also have been written (just to showcase a regexp :)):
$. oraenv++ --grep 19[ab]$
     Database    SID          ORACLE_HOME
-----------------------------------------------------------------
  1/ dev19a     DEV19A1    /u01/app/oracle/product/19.0.0.0/db_1
  2/ dev19b     DEV19B1    /u01/app/oracle/product/19.0.0.0/db_1
  3/ prod19a    PROD19A1   /u01/app/oracle/product/19.0.0.0/db_1
  4/ prod19b    PROD19B1   /u01/app/oracle/product/19.0.0.0/db_1
-----------------------------------------------------------------
Which environment you want to set up ? (CTRL+C for exit)
It indeed also works with ASM:
# . oraenv++ asm
Database            : asm
ORACLE_HOME         : /u01/app/19.11.0.0/grid
ORACLE_BASE         : /u01/app/oracle
ORACLE_SID          : +ASM1
ORACLE_PDB_SID      :
sqlplus is          : /u01/app/19.11.0.0/grid/bin/sqlplus
#
If you want to direct connect to a PDB, you can also specify one with the --pdb option which will set the ORACLE_PDB_SID variable and you could then directly connect to this PDB (this works from 18.8):
$ . oraenv++ dev19a --pdb MY_PDB
Database            : dev19a
ORACLE_HOME         : /u01/app/oracle/product/19.0.0.0/db_1
ORACLE_BASE         : /u01/app/oracle
ORACLE_SID          : DEV19A1
ORACLE_PDB_SID      : MYPDB
sqlplus is          : /u01/app/oracle/product/19.0.0.0/db_1/bin/sqlplus
$ sqlplus / as sysdba
SQL> show con_name
CON_NAME
----------------
MY_PDB

TODO / other features / Ideas

First of all, feel free to post down below or contact me if you have ideas or remarks.

On my end, one cool feature to implement in a next future would be to list the PDBs per CDB showing a menu to be able to choose from as we have hundreds of PDBs per CDB and we cannot really remember of all them then choosing from a list would be far easier. The thing here is that the PDBs are real GI resource from 21c only, they are listed in the services before 21c which I already use to show them in rac-status.sh. The thing is that crsctl is slow to query all the services when there are a lot of them so this would slow oraenv++
. Having said that, the direct connection to a PDB feature takes few seconds to get connected and ssh in any Cloud I have experienced are also slow verifying your credentials, billing you, etc ... and no one complains about it :)

Where to download ?

You can download oraenv++ from my public git repo or using the direct link to the source code from the Scripts menu on top of this page.
In case of issue/question, as usual, feel free to post a comment down below, contact me by email or linkeding chat.

Enjoy !