Encrypting the PostgreSQL data directory at rest reduces exposure from stolen disks, leaked block snapshots, or retired storage, because the database files remain unreadable without the encryption key.

PostgreSQL stores tables, indexes, and write-ahead log (WAL) as regular files under its data_directory, so at-rest encryption is commonly applied below the database by encrypting the underlying block device with dm-crypt and LUKS.

At-rest encryption only protects powered-off storage, not a running server or accounts with filesystem access, so key management, boot-time unlocking, planned downtime, and backup encryption need deliberate planning before migrating a production cluster.

Steps to encrypt PostgreSQL data at rest using LUKS:

  1. Identify the current PostgreSQL data directory path.
    $ sudo -u postgres psql -tAc "SHOW data_directory;"
    /var/lib/postgresql/16/main

  2. Identify the target block device or partition for the encrypted volume.
    $ lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS
    NAME   SIZE TYPE MOUNTPOINTS
    sda    100G disk
    ├─sda1   1G part /boot
    └─sda2  99G part /
    sdb    200G disk
    └─sdb1  200G part

    Use a dedicated partition or logical volume for the database volume.

  3. Create a LUKS2 header on the target device.
    $ sudo cryptsetup luksFormat --type luks2 /dev/sdb1
    WARNING!
    ========
    This will overwrite data on /dev/sdb1 irrevocably.
    Are you sure? (Type 'YES' in capital letters): YES
    Enter passphrase for /dev/sdb1:
    Verify passphrase:

    Selecting the wrong device permanently erases data.

  4. Open the encrypted device with a stable mapper name.
    $ sudo cryptsetup open /dev/sdb1 pgdata_crypt
    Enter passphrase for /dev/sdb1:

    The mapper name pgdata_crypt is referenced by /etc/crypttab and /etc/fstab.

  5. Confirm the mapper device is active.
    $ sudo cryptsetup status pgdata_crypt
    /dev/mapper/pgdata_crypt is active.
      type:    LUKS2
      cipher:  aes-xts-plain64
      keysize: 512 bits
      device:  /dev/sdb1
      mode:    read/write
  6. Create a filesystem on the unlocked mapper device.
    $ sudo mkfs.ext4 -L pgdata /dev/mapper/pgdata_crypt
    mke2fs 1.46.5 (30-Dec-2021)
    Creating filesystem with 52424832 4k blocks and 13107200 inodes
    Filesystem UUID: 2a3b4c5d-6e7f-8901-2345-6789abcdef01
    ##### snipped #####
  7. Stop the PostgreSQL service to prevent writes during migration.
    $ sudo systemctl stop postgresql
  8. Verify the PostgreSQL service is stopped.
    $ sudo systemctl status postgresql
    ● postgresql.service - PostgreSQL RDBMS
         Loaded: loaded (/lib/systemd/system/postgresql.service; enabled)
         Active: inactive (dead)
    ##### snipped #####
  9. Move the existing data directory to a temporary backup path.
    $ sudo mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.unencrypted

    Replace /var/lib/postgresql/16/main with the data_directory value from the first step.

  10. Create the original data directory path as an empty mount point.
    $ sudo mkdir -p /var/lib/postgresql/16/main
  11. Mount the encrypted filesystem on the PostgreSQL data directory path.
    $ sudo mount /dev/mapper/pgdata_crypt /var/lib/postgresql/16/main
  12. Set the encrypted data directory ownership to the postgres user.
    $ sudo chown postgres:postgres /var/lib/postgresql/16/main
  13. Set restrictive permissions on the encrypted data directory.
    $ sudo chmod 0700 /var/lib/postgresql/16/main
  14. Copy the database files into the encrypted data directory.
    $ sudo rsync -aHAX --numeric-ids --info=progress2 /var/lib/postgresql/16/main.unencrypted/ /var/lib/postgresql/16/main/
          2,188,767,232  35%   92.41MB/s    0:00:15 (xfr#124, to-chk=0/980)
    ##### snipped #####

    SELinux systems may require restorecon -RF on the data directory after copying.

  15. Record the LUKS UUID for the crypttab entry.
    $ sudo blkid /dev/sdb1
    /dev/sdb1: UUID="11111111-2222-3333-4444-555555555555" TYPE="crypto_LUKS" PARTUUID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
  16. Add a boot-time unlock entry to /etc/crypttab.
    /etc/crypttab
    pgdata_crypt UUID=11111111-2222-3333-4444-555555555555 none luks

    A passphrase prompt at boot uses none, while unattended boot typically uses a protected key file or hardware-backed unlock.

  17. Record the filesystem UUID for the fstab mount entry.
    $ sudo blkid /dev/mapper/pgdata_crypt
    /dev/mapper/pgdata_crypt: LABEL="pgdata" UUID="2a3b4c5d-6e7f-8901-2345-6789abcdef01" TYPE="ext4"
  18. Add the encrypted filesystem mount to /etc/fstab using the original data directory path.
    /etc/fstab
    UUID=2a3b4c5d-6e7f-8901-2345-6789abcdef01 /var/lib/postgresql/16/main ext4 defaults 0 2

    An invalid /etc/fstab entry can drop the system into emergency mode during boot.

  19. Reload systemd to apply unit generation for the updated /etc/crypttab and /etc/fstab files.
    $ sudo systemctl daemon-reload
  20. Start the PostgreSQL service on the encrypted data directory.
    $ sudo systemctl start postgresql
  21. Verify the data directory is mounted from the mapper device.
    $ findmnt -no SOURCE,TARGET /var/lib/postgresql/16/main
    /dev/mapper/pgdata_crypt /var/lib/postgresql/16/main

  22. Verify database access using a simple query.
    $ sudo -u postgres psql -c "SELECT now();"
                  now
    -------------------------------
     2025-12-15 11:12:34.12345+00
    (1 row)
  23. Remove the unencrypted data directory backup after validation.
    $ sudo rm -rf /var/lib/postgresql/16/main.unencrypted

    Removing the backup is irreversible, but keeping it leaves an unencrypted copy of the database on disk.

  24. Reboot to validate boot-time unlock with automatic mounting.
    $ sudo reboot

    A passphrase-based crypttab entry prompts on the system console during startup.