Files
notes/postgresql/postgres_TLS_01.md

306 lines
8.4 KiB
Markdown
Raw Normal View History

2026-03-12 22:01:38 +01:00
## Generate PostgreSQL server certificate
PostgreSQL server generate **certificate private key** and a **certificate request** for the `CN = PostgreSQL server`
```bash
openssl genrsa -out postgres_server.key 4096
openssl req -new -key postgres_server.key -out postgres_server.csr
```
The CA root put the **certificate request** in a temporary location (ex. `generated` directory), generate a **signed certificate** and optionally create a **certificate full chain**.
Configuration file used by root CA:
```
[ req ]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[ dn ]
CN = PostgreSQL server
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = raxus.swgalaxy
DNS.2 = mobus.swgalaxy
DNS.3 = pgsql.swgalaxy
```
> **_NOTE:_** `CN` in CA configuration file can be diffrent than the `CN` of CSR. The CA does not replace it unless explicitly configured to do so.
```bash
openssl x509 -req \
-in generated/postgres_server.csr \
-CA rootCA.pem -CAkey rootCA.key -CAserial rootCA.srl \
-out generated/postgres_server.crt \
-days 3650 \
-sha256 \
-extensions req_ext -extfile generated/postgres_server.cnf
cat generated/postgres_server.crt rootCA.pem > generated/postgres_server.fullchain.crt
```
To inspect a certificate in Linux command line:
```bash
openssl x509 -in rootCA.pem -text -noout
openssl x509 -in generated/postgres_server.crt -text -noout
```
## Generate PostgreSQL client certificate(s)
We will generate 2 certificates for:
- `CN=PostgreSQL client1`
- `CN=PostgreSQL client2`
```bash
openssl genrsa -out postgres_client1.key 4096
openssl req -new -key postgres_client1.key -out postgres_client1.csr
openssl genrsa -out postgres_client2.key 4096
openssl req -new -key postgres_client2.key -out postgres_client2.csr
```
Configuration file used by root CA will be the same for both certificates:
```
[ req ]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[ dn ]
CN = Generic Client PostgreSQL
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = anyhost.anydomain
```
```bash
openssl x509 -req \
-in generated/postgres_client1.csr \
-CA rootCA.pem -CAkey rootCA.key -CAserial rootCA.srl \
-out generated/postgres_client1.crt \
-days 3650 \
-sha256 \
-extensions req_ext -extfile generated/postgres_client.cnf
cat generated/postgres_client1.crt rootCA.pem > generated/postgres_client1.fullchain.crt
openssl x509 -req \
-in generated/postgres_client2.csr \
-CA rootCA.pem -CAkey rootCA.key -CAserial rootCA.srl \
-out generated/postgres_client2.crt \
-days 3650 \
-sha256 \
-extensions req_ext -extfile generated/postgres_client.cnf
cat generated/postgres_client2.crt rootCA.pem > generated/postgres_client2.fullchain.crt
```
## On PostgreSQL server, add the root CA certificate as trusted certificate.
Put root CA certificate (`.crt` ou `.pem`)under `/etc/pki/ca-trust/source/anchors`, then update the system trust store.
```bash
cd /etc/pki/ca-trust/source/anchors
chmod 644 rootCA.pem
update-ca-trust extract
```
```bash
openssl verify -CAfile /etc/pki/tls/certs/ca-bundle.crt <path_to>/postgres_server.crt
# Inspect the root CA certificate to get the exact CN
grep -R "<CN_name>" /etc/pki/ca-trust/extracted/
```
## Set up SSL on PostgreSQL server
Place server certificate, server private key and root CA certificate (optional but recommended) in the PostgreSQL data directory.
Default paths:
- `$PGDATA/server.key`
- `$PGDATA/server.crt`
Or override with:
```
ssl_cert_file = '/path/to/server.crt'
ssl_key_file = '/path/to/server.key'
ssl_ca_file = '/path/to/root.crt'
```
Set file pemisions:
```bash
chmod 640 $PGDATA/postgres_server.crt
chmod 600 $PGDATA/postgres_server.key
chmod 640 $PGDATA/rootCA.pem
```
> root CA certificate is required **only for mTLS** mode when the server will validate the client certificate authenticity
> Add (concatebate) all CA intermediate (if any) in `ssl_cert_file`
Enable TLS in `$PGDATA/postgresql.conf`.
```
ssl_cert_file = 'postgres_server.crt'
ssl_key_file = 'postgres_server.key'
ssl_ca_file = 'rootCA.pem'
ssl = on
ssl_ciphers = 'HIGH:!aNULL:!MD5'
ssl_prefer_server_ciphers = on
ssl_min_protocol_version = 'TLSv1.2'
ssl_max_protocol_version = 'TLSv1.3'
```
PostgreSQL will now listen for both encrypted and unencrypted connections on the **same port**.
## Server side SSL modes
### Request TLS mode
Config in `$PGDATA/pg_hba.conf`:
```
host all all all md5
```
### Require TLS mode
Config in `$PGDATA/pg_hba.conf`:
```
hostssl all all 0.0.0.0/0 md5
```
### Require TLS mode + client certificate
Config in `$PGDATA/pg_hba.conf`:
```
hostssl all all 0.0.0.0/0 cert
```
## Client side SSL modes
| Mode | Encrypts | Validates CA | Validates Hostname | Typical Use |
| --- | --- | --- | --- | --- |
| `require` | Yes | No | No | Basic encryption |
| `verify-ca` | Yes | Yes | No | Internal/IP-based |
| `verify-full` | Yes | Yes | Yes | Production |
Examples:
```bash
# mode: require
psql "host=raxus.swgalaxy port=5501 user=vplesnila dbname=postgres sslmode=require"
# mode: verify-ca
psql "host=raxus.swgalaxy port=5501 user=vplesnila dbname=postgres sslmode=verify-ca sslrootcert=rootCA.pem"
```
For `verify-full` mode the client need client certificate, client private key and root CA certificate on client side.
In our example we will use previously generated certificats for:
- `CN=PostgreSQL client1`
- `CN=PostgreSQL client2`
Set file pemisions:
```bash
chmod 640 postgres_client1.crt
chmod 600 postgres_client1.key
chmod 640 postgres_client2.crt
chmod 600 postgres_client2.key
chmod 640 rootCA.pem
```
In `verify-full` mode we can get ride of client password by mapping the `CN` in the certificate to a **local PostgreSQL role** (aka **local user**).
Create local roles (with no password) in PostgreSQL instance:
```sql
CREATE ROLE app1 LOGIN;
CREATE ROLE app2 LOGIN;
```
Add in `$PGDATA/pg_ident.conf`:
```
# MAPNAME SYSTEM-IDENTITY PG-USERNAME
certmap_app1 "PostgreSQL client1" app1
certmap_app2 "PostgreSQL client2" app2
```
Add in `$PGDATA/pg_hba.conf`:
```
hostssl all app1 0.0.0.0/0 cert map=certmap_app1
hostssl all app2 0.0.0.0/0 cert map=certmap_app2
```
> Restart PostgreSQL instance after modifying `$PGDATA/pg_ident.conf` and `$PGDATA/pg_hba.conf`.
Connect in `verify-full` mode using certificate for authentification:
```bash
# mode: verify-full
psql "host=raxus.swgalaxy port=5501 user=app1 dbname=postgres sslmode=verify-full sslrootcert=rootCA.pem sslcert=postgres_client1.crt sslkey=postgres_client1.key"
psql "host=raxus.swgalaxy port=5501 user=app2 dbname=postgres sslmode=verify-full sslrootcert=rootCA.pem sslcert=postgres_client2.crt sslkey=postgres_client2.key"
```
As `SAN` **(Subject Alternative Name)** of the **server** certificate match to:
- `raxus.swgalaxy`
- `mobus.swgalaxy`
- `pgsql.swgalaxy`
it is possible to connect to all theses DNS enteries using the same **server** certificate.
Example:
```bash
psql "host=pgsql.swgalaxy port=5501 user=app1 dbname=postgres sslmode=verify-full sslrootcert=rootCA.pem sslcert=postgres_client1.crt sslkey=postgres_client1.key"
```
> ⭐ Server certificates → client checks SAN/CN for hostname
> ⭐ Client certificates → server checks only CN for user identity
> **IMPORTANT**: we can mix TLS authentification by certificate and password.
PostgreSQL processes pg_hba.conf toptobottom, and the first matching rule wins.
Once a connection matches a line (based on type, database, user, address, etc.), PostgreSQL stops and applies that rules authentication method.
The folowing `$PGDATA/pg_hba.conf` allows authentification using certificates for PostgreSQL users app1 and app2 and authentification using password for all other users.
```
hostssl all app1 0.0.0.0/0 cert map=certmap_app1
hostssl all app2 0.0.0.0/0 cert map=certmap_app2
hostssl all all 0.0.0.0/0 md5
```
Check if connections are using TLS:
```sql
SELECT * FROM pg_stat_ssl;
```
Check for connection username:
```sql
select pid as process_id,
usename as username,
datname as database_name,
client_addr as client_address,
application_name,
backend_start,
state,
state_change
from pg_stat_activity;
```