Deploy PostgreSQL With TLS in Kubernetes

Jonathan S. Katz
PostgreSQL Kubernetes Security PostgreSQL Operator DevOps

Ensuring data can be securely transmitted is a requirement of many production systems. PostgreSQL supports TLS as a means of encrypting network communication, verifying hosts, and allowing for certificate-based authentication.

The TLS functionality of PostgreSQL is extendable into Kubernetes deployments. The Crunchy Data Postgres Operator has provided support for TLS since version 4.3, using Kubernetes Secrets for mounting the TLS components safely to each Pod. The PostgreSQL Operator does not make an opinion about the PKI used to generate TLS certificate, but rather loads the TLS key pair and certificate authority (CA) for the PostgreSQL server.

Let's go through an example of creating a TLS-enabled PostgreSQL cluster.

Prerequisites

This example assumes that you have deployed the Crunchy Data Postgres Operator to Kubernetes using the quickstart.

There are three required items to enable TLS in your PostgreSQL clusters:

  • A CA certificate
  • A TLS private key
  • A TLS certificate

There are a variety of methods available to generate these items. In fact, Kubernetes comes with its own certificate management system! The PostgreSQL documentation also provides an example for how to generate a TLS certificate as well.

It is up to you to decide how you want to manage this for your cluster, but let's walk through an example below.

We first need to generate a CA. Use the command below to generate an ECDSA CA:

openssl req \
  -x509 \
  -nodes \
  -newkey ec \
  -pkeyopt ec_paramgen_curve:prime256v1 \
  -pkeyopt ec_param_enc:named_curve \
  -sha384 \
  -keyout ca.key \
  -out ca.crt \
  -days 3650 \
  -subj "/CN=*"

For a production system, you would likely generate an intermediate CA as well.

Now let's generate a TLS key and certificate that our PostgreSQL cluster will use. In the next section, we're going to create a PostgreSQL cluster named hippo in the namespace pgo (based on the Postgres Operator quickstart).  Knowing that, and in accordance of how DNS in Kubernetes works, let's generate a certificate with a CN of hippo.pgo:

openssl req \
  -new \
  -newkey ec \
  -nodes \
  -pkeyopt ec_paramgen_curve:prime256v1 \
  -pkeyopt ec_param_enc:named_curve \
  -sha384 \
  -keyout server.key \
  -out server.csr \
  -days 365 \
  -subj "/CN=hippo.pgo"

Finally, take the the certificate signing request (server.csr) generated by the previous command, and have the CA sign it:

openssl x509 \
  -req \
  -in server.csr \
  -days 365 \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -sha384 \
  -out server.crt

We can now move on to deploying a PostgreSQL cluster with TLS in Kubernetes!

Deploying a PostgreSQL Cluster with TLS 

To set up TLS for your PostgreSQL cluster, you have to create two Secrets. One that contains the CA certificate, and the other that contains the server TLS key pair.

First, create the Secret that contains your CA certificate. Create the Secret as a generic Secret with the following requirements:

  • The Secret must be in the same Namespace as where you are deploying your PostgreSQL cluster
  • The name of the key that is holding the CA must be ca.crt

For example, to create a CA Secret with the trusted CA to use for the PostgreSQL clusters in the pgo namespace, you can execute the following command:

kubectl create secret generic postgresql-ca -n pgo --from-file=ca.crt=ca.crt

Note that you can reuse this CA Secret for other PostgreSQL clusters deployed by the Postgres Operator.

Next, create the Secret that contains your TLS key and certificate. You can create this as a Kubernetes TLS Secret:

kubectl create secret tls hippo.tls -n pgo --cert=server.crt --key=server.key

With these Secrets presents, you can create a TLS-enabled PostgreSQL cluster named `hippo` with the following command:

pgo create cluster hippo \
  --server-ca-secret=postgresql-ca \
  --server-tls-secret=hippo.tls

The --server-ca-secret and --server-tls-secret will automatically enable TLS connections in the deployed PostgreSQL cluster. These flags should reference the CA Secret and the TLS key pair Secret, respectively. If you want to force all connections to be over TLS, you can add the `--tls-only flag`:

pgo create cluster hippo --tls-only \
  --server-ca-secret=postgresql-ca \
  --server-tls-secret=hippo.tls

For the rest of the examples, I will be using the Postgres cluster deployed with the "--tls-only" flag enabled.

SSL/TLS Modes & Connecting to a PostgreSQL Cluster with TLS

If deployed successfully, when you connect to the PostgreSQL cluster, assuming your PGSSLMODE is set to prefer or higher, you will see something like this in your psql terminal:

SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)

Let's see this in action and try out connecting to a PostgreSQL cluster and experiment with a few of the "TLS modes" that PostgreSQL provides. For simplicity, in a separate terminal window, create a Kubernetes port-forward from our host machine to the PostgreSQL cluster:

kubectl -n pgo port-forward svc/hippo 5432:5432

For the purposes of this exercise, we'll log in as the postgres superuser. We'll use a Kubernetes trick to dynamically grab the password from the Secret before we log in and populate it into the PGPASSWORD environment variable. For our `hippo` cluster, it looks similar to this:

PGPASSWORD=$(kubectl -n pgo get secrets hippo-postgres-secret -o jsonpath="{.data.password}" | base64 -d)

First, let's look at sslmode disable -- as the name suggests, it means that a PostgreSQL client will only connect to a server without TLS and reject any attempts to connect over TLS. If I attempt to connect to my "TLS only" cluster with this mode, the connection will be refused:

PGSSLMODE=disable PGPASSWORD=$(kubectl -n pgo get secrets hippo-postgres-secret -o jsonpath="{.data.password}" | base64 -d) psql -h localhost -U postgres hippo
psql: error: FATAL:  no pg_hba.conf entry for host "127.0.0.1", user "postgres", database "hippo", SSL off

The next sslmode to consider is prefer, which is the default mode that Postgres uses. prefer will first attempt to connect to a PostgreSQL cluster over TLS, but if TLS is unavailable, it will fall back to using an unencrypted connection.

Then, there's sslmode require, in which the client will refuse to connect to a PostgreSQL server unless the connection is over TLS:

PGSSLMODE=require PGPASSWORD=$(kubectl -n pgo get secrets hippo-postgres-secret -o jsonpath="{.data.password}" | base64 -d) psql -h localhost -U postgres hippo
psql (13.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)

While the require sslmode will protect your connections from eavesdropping, it will not protect against potential MITM attacks as it does not perform any identity verification: all it does is check that it can connect to a server over TLS. To protect against MITM, you will need to use the verify-full sslmode (though in some limited cases, verify-ca may be good enough).

Let's first see what it takes to use the verify-ca sslmode. Let's take the connection string above and use verify-ca option for sslmode:

PGSSLMODE=verify-ca PGPASSWORD=$(kubectl -n pgo get secrets hippo-postgres-secret -o jsonpath="{.data.password}" | base64 -d) psql -h localhost -U postgres hippo
psql: error: root certificate file "~/.postgresql/root.crt" does not exist

Either provide the file or change sslmode to disable server certificate verification.

Both verify-ca and verify-full need to be aware of a trusted certificate authority bundle so that they can verify the authenticity of the server. The prompt above suggests that the PostgreSQL client can use a trusted CA bundle by storing it in a particular location off of the $HOME directory, but we can also use the PGSSLROOTCERT environment variable to point at the CA certificate we had generated earlier:

PGSSLMODE=verify-ca PGSSLROOTCERT=ca.crt PGPASSWORD=$(kubectl -n pgo get secrets hippo-postgres-secret -o jsonpath="{.data.password}" | base64 -d) psql -h localhost -U postgres hippo
psql (13.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)

verify-full adds additional protection by performing identity verification of the server by checking against the Subject Alternate Names or Common Name of the certificate and ensuring that it matches the host (-h) value. Using the above example, if you were to switch the sslmode to verify-full you would see the following:

PGSSLMODE=verify-full PGSSLROOTCERT=ca.crt PGPASSWORD=$(kubectl -n pgo get secrets hippo-postgres-secret -o jsonpath="{.data.password}" | base64 -d) psql -h localhost -U postgres hippo
psql: error: server certificate for "hippo.pgo" does not match host name "localhost"

Given this is an example using the port-forward strategy above, we would not be able to connect over verify-full unless we add an alias to the hosts file. For example, purposes, add the following to your hosts file:

echo '127.0.0.1 hippo.pgo' | sudo tee -a /etc/hosts

and try connecting again:

PGSSLMODE=verify-full PGSSLROOTCERT=ca.crt PGPASSWORD=$(kubectl -n pgo get secrets hippo-postgres-secret -o jsonpath="{.data.password}" | base64 -d) psql -h localhost -U postgres hippo
psql (13.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

For production clusters where you want to ensure proper TLS verification, you will want to use sslmode verify-full.

Next Steps

When deploying PostgreSQL clusters to production environments or anywhere where you are operating in an untrusted network, it is paramount that you deploy them with TLS. The PostgreSQL Operator simplifies the process of creating TLS-enabled Postgres clusters, but you will have to decide which level of identity verification you want to provide for your applications connecting to your databases.

Newsletter