PostgreSQL Security Hardening Guide
PostgreSQL Security Hardening Guide
PostgreSQL security
made easy
Contents
Executive summary 3
Introduction 4
Minimum requirements 4
Sandboxing 4
Logging 16
Installation 17
Conclusion 18
Annexes 19
2
Executive summary
In this first of a series of PostgreSQL security whitepapers, we dive into the technical
measures you need to implement to harden your PostgreSQL databases. Hardening your
PostgreSQL deployments is essential for your enterprise to protect its data and achieve
compliance with security standards like ISO 27k, PCI-DSS, CIS, DISA-STIG, and FedRAMP.
In our “How to secure your database” blog, we provided an overview on the typical
measures to implement to protect your database deployments, the common cyber
attacks, and the unfortunate consequences of improperly protected databases.
In this whitepaper, we will dig deeper into database security and provide a detailed guide to
implementing hardening controls at the database level. We will cover the following areas:
• Sandboxing
• Securing your supply chain
• Storage layout
• Logging
• Installation
Let’s start our journey by detailing the minimum hardware and software requirements to
successfully implement all the commands and concepts explained in this document.
Minimum requirements
We will assume that you have a machine powered by Ubuntu 24.04 LTS or later, that you
have an internet connection, and that your system has the following usable resources:
• 9 GB of storage
• 4 GB of RAM
• 2 vCPUs
Sandboxing
What is sandboxing?
Sandboxing is about isolating a workload from the others running on
the same physical node. Isolation is achieved by limiting the:
4
What are we sandboxing?
Before digging further into sandboxing, we will first depict the stack
of dependencies for a classical application (think a deb packaged
application) installed on a modern OS such as Ubuntu:
Application 1 Application n
(App specific binaries) ... (App specific binaries)
Runtime dependencies
(+ libraries, language runtimes)
OS Distro
(+ kernel patches, init system, package manager, shell, utilities)
Kernel
Hardware
At the bottom of the software stack, we have the kernel that handles all the interactions
with the physical layer via device drivers. Next, we find the distro that contains a
number of utilities and systems that makes the kernel usable to its users and installed
applications. Finally, we find the application binaries with all its dependencies.
In an ideal scenario, we would prefer that every application is running on its own hardware,
its own kernel and using only its dedicated dependencies. Yet, doing this will lead to an
explosion in costs, wasted resources, and an operational nightmare. So all sandboxing
solutions try to find a balance between isolation and efficiency by making compromises on
the size of the sandbox, its overhead and convenience. Let’s now dig into the sandbox types.
Type of sandboxes
Virtual machines
As the name suggests, this kind of virtualization emulates the full stack of dependencies
including the hardware. A running virtual machine (VM) looks conceptually as follows:
Dependencies Dependencies
Guest distro ... Guest distro
Guest kernel Guest kernel
vm1 Vm2
VM Manager
Host distro
Host kernel
Hardware
5
The VM manager provides a hardware “emulation” layer, often assisted by kernel modules
(such as KVM) and native hardware virtualization, while providing facilities to manage
them. We note that every VM is shipped with its own kernel that can be completely
different from the host kernel (e.g. running Windows VM on top of Ubuntu).
Containers
The container manager will also typically use a “layered” filesystem, such as OverlayFS, to
avoid duplicating common dependencies between containers running on the same host.
Based on what you embed within a container, we can distinguish 2 types of containers:
• System containers
• Application containers
System containers
System containers are designed as lightweight VMs, so you can pack any number of applications
together in a system container image. OS containers look conceptually like the following:
OS container 1 OS container n
Host distro
Host kernel
Hardware
6
Application containers
Host distro
Host kernel
Hardware
Snaps
Snaps are similar to application containers. Yet, snaps add a number of features such as:
Dependencies Dependencies
...
Snap 1 Snap n
Host distro
Host kernel
Hardware
The core snaps are used to factor out the most common dependencies across snaps. They will
automatically be pulled by snapd when needed.
7
What type of sandboxing for a database?
Database management systems often need to change a few kernel
parameters (like disabling transparent huge pages).
Therefore, it is recommended to run your database servers on dedicated “kernels” that are
not shared with other applications. We can then choose among system containers, application
containers, and snaps in order to isolate database servers running on the same kernel.
Additionally, we often need to colocate several components along with the database server,
like backup tools and monitoring agents. Therefore, system containers or snaps are the most
suitable options. Both allow you to easily pack any number of components in a single snap or
container. The snap ecosystem also enables you to connect different snaps in a structured way.
Creating a sandbox
Let’s start by installing LXD, our Canonical tool to easily
manage virtual machines and containers.
Ideally the above command should not yield any result (so basically, all configured
sources should point to the official [Link] repositories). Otherwise, you can check
the non-ubuntu sources of your deb packages using the following command:
If the above does not yield any result then you can remove any line reported by
the grep command on /etc/apt/[Link].d/[Link].
8
Otherwise, you need to ensure that you trust the provider of those packages and that you are
fine with its policy and commitment regarding security fixes.
Please note that the previous instructions cover deb packages. You need to do the same kind
of research for all the other types of packages or images you are using (containers, snaps …).
If Expanded Security Maintenance is not enabled then you can subscribe to Ubuntu Pro (free
for personal use for up to 5 machines). Once you get your token, just type the command:
If it is not running then you just need to issue the following command:
You can also select the update types that you would like to automatically apply and the ones
to perform manually by editing the file /etc/apt/[Link].d/50unattended-upgrades.
In order to enable automatic security updates, you need to have the following section:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro
codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
9
You can exclude specific packages by adding them to the Package-Blacklist section:
Unattended-Upgrade::Package-Blacklist {
"postgres";
};
For example, this can be useful to orchestrate the upgrade flow of your PostgreSQL fleet by
testing your changes in a test system before applying them in a production environment.
You can also configure the time and frequency of your updates by editing the following files:
Having different storage pools can also help optimize your storage performance/cost
ratio as detailed in this guide to database cloud migration. The guide also details the
types and benefits of encryption which we will use throughout this white paper.
10
Creating storage pools
base_dir=/var/lib/luks/
keys_dir=/root/keys/
sudo install -d -m 700 "${base_dir}" "${keys_dir}"
mounts=(
"temp ,noexec"
"logs ,noexec"
"data"
"archive ,noexec"
)
Check the output of the following commands to ensure our new pools are correctly configured:
11
Let’s now check that our mounts are persistent across reboots:
exit
exit
sudo lxc restart pg
sudo lxc list
sudo lxc exec pg bash
su -l ubuntu
lsblk
We will provide in the Annexes section more details that might help
you implement some of the above techniques in your environment. For
now, we will focus on using the newly created storage pools.
Generic files
To make the transition as smooth as possible you can follow these generic steps:
12
Let’s start by moving the relevant temporary files to the new storage pool.
Configuring TEMP
tmp_dir="${base_dir}"/temp/tmp
sudo mkdir -p "${tmp_dir}"
# Ensuring permissions are properly setup
sudo chmod 1777 "${tmp_dir}"
sudo chown root:root "${tmp_dir}"
ll /tmp/
sudo lsof +D /tmp/ | awk '!/COMMAND/{print $1 | "sort -u"}'
# stop listed services using: sudo systemctl stop <>.service
sudo rsync -avz /tmp/ "${tmp_dir}"
# sudo umount /tmp # if it is mounted already elsewhere
sudo mount --rbind "${tmp_dir}" /tmp
# start previously stopped services using: sudo systemctl start
<>.service
echo "${tmp_dir}" /tmp none rbind 0 0 | sudo tee -a /etc/fstab > /dev/
null
var_tmp_dir="${base_dir}"/temp/var/tmp
sudo mkdir -p "${var_tmp_dir}"
sudo chmod 1777 "${var_tmp_dir}"
sudo chown root:root "${var_tmp_dir}"
ll /var/tmp/
sudo lsof +D /var/tmp/ | awk '!/COMMAND/{print $1 | "sort -u"}'
# stop listed services using: sudo systemctl stop <>.service
sudo rsync -avz /var/tmp/ "${var_tmp_dir}"
sudo mount --rbind "${var_tmp_dir}" /var/tmp
# start previously stopped services using: sudo systemctl start
<>.service
echo "${var_tmp_dir}" /var/tmp none rbind 0 0 | sudo tee -a /etc/fstab
> /dev/null
Configuring LOGS
logs_dir="${base_dir}"/logs/var/log
sudo mkdir -p "${logs_dir}"
sudo chmod 1777 "${logs_dir}"
sudo chown root:syslog "${logs_dir}"
ll /var/log/
sudo lsof +D /var/log/ | awk '!/COMMAND/{print $1 | "sort -u"}'
# stop listed services using: sudo systemctl stop <>.service
sudo rsync -avz /var/log/ "${logs_dir}"
sudo mount --rbind "${logs_dir}" /var/log
echo "${logs_dir}" /var/log none rbind 0 0 | sudo tee -a /etc/fstab >
/dev/null
13
Let’s now restart the VM again to check that everything went fine:
PostgreSQL files
Let’s create a template [Link] file (named [Link] below) that all newly
created databases will inherit from:
templ="/etc/postgresql-common/createcluster.d/[Link]"
sudo touch ${templ}
echo data_directory = "'/mnt/data/pg/%v/%c'" | sudo tee -a ${templ} > /
dev/null
echo log_directory = "'/mnt/logs/pg/%v/%c/'" | sudo tee -a ${templ} > /
dev/null
echo waldir = "'/mnt/logs/pg/%v/%c/pg_wal'" | sudo tee -a ${templ} > /
dev/null
14
Existing PostgreSQL installations
Second, we need to move existing installations to the new locations. Ubuntu makes
installing PostgreSQL so easy that you basically just need to install one package (e.g.
postgresql-<version>) to get a fully functional PostgreSQL instance. Using the latter
installation method will configure postgresql to use the default locations for the
various directories it needs. Let’s start by checking the location of your files.
• main is the default name for a newly created cluster installed using postgresql-
<version>. You might need to replace it with the existing cluster names
• running the following instructions will result in a downtime that you need to plan for
vi /etc/postgresql/16/main/[Link]
# Change the following lines as shown below
data_directory = '/mnt/data/pg/16/main'
Now that we have successfully configured our storage, let’s address logging and
auditing – both of which are an essential part of a hardening strategy.
15
Logging
Let’s create a template config file that we will place at:
templ="/etc/postgresql-common/conf.d/[Link]"
sudo touch ${templ}
sudo vi ${templ}
Let’s now make sure that the above settings will be picked by
the to-be created clusters by doing the following:
Please note that while the above settings should fit most organizations' needs,
you might want to customize some values to comply with your internal policies.
Now that all future PostgreSQL deployments will comply with our common
configuration, let’s proceed with the installation of a sample PostgreSQL server.
16
Installation
Hardening is all about making the task of a malicious user as difficult as possible.
One way to achieve this is to avoid predictable values (like default ones) even
if it might look harmless at a first look. We will apply this principle to:
Database port
We will use the following function to get a randomly chosen open port:
Database username
We can also customize the OS username by using the option -u when
adding a PostgreSQL cluster using the pg_createcluster utility as we
will do in the next section. Let’s create the OS user first:
After the above steps, we recommend restarting the node. Then we can
use pg_lsclusters to check that the installation went successfully:
exit
exit
sudo lxc restart pg
sudo lxc exec pg bash
su -l ubuntu
pg_lsclusters
more /var/log/postgresql/[Link] # Check startup logs
# sudo vi /mnt/logs/pg/16/pg1/<log_file_name>.log # Check the first log
file 17
Now that we have a running PostgreSQL cluster, we can try to connect to it as follows:
Conclusion
In this whitepaper, we covered the hardening measures you need to implement for your
PostgreSQL fleet before and during the installation. We established that hardening is about
making the task of a malicious user as difficult as possible. We demonstrated that hardening
requires discipline in many layers, from your supply chain to your database management system.
In the meantime, if you need professional help with your PostgreSQL deployments
then you can rely on our services at Canonical, the open source experts:
Security maintenance
We can provide you with regularly maintained PostgreSQL packages either
in deb, snap or container format for up to 10 years per release track. We
cover not only the PostgreSQL server but also the extensions and tools you
typically install with PostgreSQL, such as timescale and pgbackrest.
Firefighting support
With firefighting support, our dedicated staff will get on a video call
with you to help you diagnose and resolve your PostgreSQL issues
while you keep full control over your infrastructure.
Managed PostgreSQL
Our database team manages all your Day 2 operations, from observability to upgrades.
We handle all issues, and consistently reinforce the security of your environment.
Get in touch
Send us a message to discuss your specific use case.
Author
Mohamed Wadie Nsiri 18
Annexes
As promised earlier, you can find in the following sections some instructions
that can help you implement secure and compliant encryption at-rest.
exit
exit
sudo lxc stop pg
sudo lxc list
sudo lxc config device add pg vtpm tpm path=/dev/tpm0 pathrm=/dev/
tpmrm0
sudo lxc start pg
sudo lxc exec pg bash
ll /dev/tpm* # Check that you have a tpm0 and a tmprm0 added
First, we will create a master encryption key that we will use to encrypt the individual
volume keys. This is a typical pattern that is used in many Transparent Data Encryption
implementations such as Oracle database’s TDE. Let’s proceed with the key generation:
keys_dir="/root/keys/"
master_key_file="${keys_dir}luks-key-master"
openssl rand -out "${master_key_file}" 32
chmod 400 "${master_key_file}"
ll "${keys_dir}"
We will now encrypt the storage keys using the newly created master key.
We will now seal the master key using the TPM emulator we previously attached
to our VM. This operation involves several steps that are summarized below:
• First, we will create a policy that defines under which conditions the sealed-
key can be retrieved from the TPM. This is done by linking the state of some
registers to the sealed key. The key can then be unsealed only if the state of
chosen registers matches the one measured at the time of sealing.
19
• Second we will create a primary key under the TPM’s hierarchy. This can
be understood as an isolated vault within the TPM. Objects sealed using
this key can be unsealed only using the same primary keys’ identity.
• Create a sealed version of our master key
• Load the sealed version in TPM memory
• Persist the sealed version in the TPM
21
decrypted_key="${runtime_key_dir}/luks-key-${name}"
ln_target="${keys_dir}/luks-key-${name}"
if openssl enc -aes-256-cbc -d -pbkdf2 -in "${enc_key}" \
-out "${decrypted_key}" -pass file:"${master_key}"; then
chmod 400 "${decrypted_key}"
ln -sf "${decrypted_key}" "${ln_target}"
log "Key ${name} decrypted and linked at ${ln_target}"
else
log "Failed to decrypt key: ${enc_key}"
fi
done
# Remove unsealed master key from memory
rm -f "${master_key}"
log "Unseal process completed."
EOT
You will still need to integrate the script in your own boot process which depends
on your environment and is outside the scope of this document. Let’s now check
how we can implement encryption at-rest using tang and clevis components.
A secret manager
Let’s start by spawning a VM to host the Tang server:
exit
exit
sudo lxc launch ubuntu:24.04 tang --vm -c [Link]=2 -c limits.
memory=4GiB
sudo lxc exec tang bash
su -l ubuntu
sudo apt-get update
sudo apt install -y tang jose
sudo systemctl enable [Link]
sudo systemctl start [Link]
tang-show-keys # Note the provided signing key
22
Let’s now deploy some prerequisites in our postgresql VM:
exit
exit
# Check the ipv4 of the tang server using
sudo lxc list tang -c 4 | awk '/\./ {print $2}'
sudo lxc exec pg bash
su -l ubuntu
tang_ip=<ip noted previously>
# Check we have connectivity with tang server
curl -sfg [Link] -o /tmp/[Link]
# Check that the following corresponds to the signature of the tang
server
more /tmp/[Link]
sudo apt install -y clevis clevis-luks
Again, you will still need to integrate the script in your own boot process which depends
on your environment and is therefore outside the scope of this document.
23
© 2025 Canonical Limited. Ubuntu, Kubuntu, Canonical and Canonical Limited
their associated logos are the registered trademarks of
Canonical Ltd. All other trademarks are the properties of Registered in Isle of Man,
their respective owners. Any information referred to in this Company number 110334C
document may change without notice and Canonical will not
be held responsible for any such changes. Registered office
2nd Floor
Clarendon House
Victoria Street
Douglas
IM1 2LN
Isle of Man