In an ideal world Postgresql ports would not be open to the internet however it is never a bad idea to add a layer of security. If you do have public facing postgresql ports then it is essential.
For this tutorial I have written a couple of script to help with creating a Certificate authority so that you can better manage your ssl certificates.
As with all the tutorials on this site it assumes that you have your postgresql database in /pgsql rather than the standard location. You will be creating the certificate authority in /pgsql/CA.
Theoretically this method can be used to create a CA for any purpose you like, albeit you may wish to amend the paths.
Setting up the certificate authority
The initial setup has been taken from these instructions: https://help.ubuntu.com/community/OpenSSL
If you create your certificate authority in “/pgsql/postgresCA” then you should not have to modify these files however if you wish to locate it elsewhere, for branding reasons etc. then modification of the process will be required. These instructions are assuming that you wish to create a certificate authority called “inplicareCA” in the afore mentioned path.
First and foremost create some directories:
cd /pgsql && mkdir -p /pgsql/CA/signedcerts && mkdir /pgsql/CA/private && cd /pgsql/CA
The previous command will create a folder in /pgsql called “CA” containing 2 subfolders “signedcerts” and “private“.
Within this initial working environment, the significance of the subdirectories, and their contents is as follows:
- /pgsql/CA : contains CA certificate, certificates database, generated certificates, keys, and requests
- /pgsql/CA/signedcerts : contains copies of each signed certificate
- /pgsql/CA/private : contains the private key
In order to ensure that the permissions are mostly automatically set, for the rest of this process you need to su to the postgres user.
su postgres
Next, create an initial certificate database in the /pgsql/CA subdirectory with the following command at a terminal prompt:
echo '01' > serial && touch index.txt
The certificate database is literally a handful of text files, the serial file initially contains 01 which the CA uses as the serial number for the first signed certificate. Once it has singed that certificate it increments the value, but once you have done the preliminary setup the rest is done automatically for you.
Now create an initial postgresCA.cnf (you can name it anything you like, just as long as you remember) file suitable for the creation of CA certificates. Using your favorite editor, edit the file /pgsql/CA/postgresCA.cnf
This file content is in essence a configuration file for your new certificate authority that contains information about the directories and the files that it will rely on in order to operate. If you are giving your certificate authority a different name then you are likely going to have to change directives in the “[ local_ca ]” stanza and in nearly all instances you are going to need to change some directives in the “[ req ]” and “[ root_ca_distinguished_name ]” stanza’s. The directives to change are all fairly obvious.
# My sample caconfig.cnf file. # # Default configuration to use when one is not provided on the command line. # [ ca ] default_ca = local_ca # # # Default location of directories and files needed to generate certificates. # [ local_ca ] dir = /pgsql/CA certificate = $dir/postgresCA.pem database = $dir/index.txt new_certs_dir = $dir/signedcerts private_key = $dir/private/postgresCA.pem serial = $dir/serial # # # Default expiration and encryption policies for certificates. # default_crl_days = 365 default_days = 1825 default_md = sha256 # policy = local_ca_policy x509_extensions = local_ca_extensions # # # Copy extensions specified in the certificate request # copy_extensions = copy # # # Default policy to use when generating server certificates. The following # fields must be defined in the server certificate. # [ local_ca_policy ] commonName = supplied stateOrProvinceName = supplied countryName = supplied emailAddress = supplied organizationName = supplied organizationalUnitName = supplied # # # x509 extensions to use when generating server certificates. # [ local_ca_extensions ] basicConstraints = CA:false # # # The default root certificate generation policy. # [ req ] default_bits = 4096 default_keyfile = /pgsql/CA/private/postgresCA.pem default_md = sha256 # prompt = no distinguished_name = root_ca_distinguished_name x509_extensions = root_ca_extensions # # # Root Certificate Authority distinguished name. Change these fields to match # your local environment! # [ root_ca_distinguished_name ] commonName = CHANGE ME stateOrProvinceName = CHANGE ME countryName = CHANGE ME emailAddress = CHANGE_MY@EMAIL.ADDRESS organizationName = CHANGE ME organizationalUnitName = CHANGE ME # [ root_ca_extensions ] basicConstraints = CA:true
Directives marked “CHANGE” should be amended to reflect your organisation then save the file as postgresCA.cnf in /pgsql/CA.
Once you have the configuration file you need to tell the terminal where to look for it; Alternatively it can form part of the command line to generate the certificate using the “-config” directive, but the instructions that I initially used did it this way so I am sticking to it.
export OPENSSL_CONF=~/inplicareCA/inplicareCA.cnf
Next create an empty text file for the revocation list.
touch postgresCRL.pem
Now you can generate your certificate authority’s own certrificate.
IMPORTANT!!! The name of the certificate **MUST** match that given in the “certificate” directive of the previously created configuration file (in our example “postgresCA.cnf”).
openssl req -x509 -newkey rsa:4096 -out postgresCA.pem -outform PEM -days 3650
The above commend will generate a certificate that will be valid for around 10 years. During the generation process openssl will ask you to provide a password. You will need this every time you wish to sign a certificate with your certificate authority so do not forget it.
When complete you should have two files, one in /pgsql/CA called postgresCA.pem and another in /pgsql/CA/private named in accordance with the name provided under the the “private_key” directive in the postgresCA.cnf configuration file (in our example postgresCA.pem).
Next, while not strictly necessary, strip the text from the certificate by running:
openssl x509 -in postgresCA.pem -out postgresCA.crt
NOTE: the suffix has changed to .crt When I did this I ended up with 2 identical files so I am not sure what benefit this might be, but you will be using the postgresCA.crt file on the client in later steps.
Congratulations, you now have a working certificate authority.
Client Certificates
Getting this right annoyed me off so much that I wrote a script to do it for me. If you wish to know how to do it the manual way then read the wiki here: https://help.ubuntu.com/community/OpenSSL, or read the script. We are only going to deal with what we need to use the script here.
The script is as follows; just save it to a file named /pgsql/CA/newcert.sh and make it executable.
#!/bin/bash # prompt_confirm() { while true; do read -r -n 1 -p "${1:-Continue?} [y/n]: " REPLY case $REPLY in [yY]) echo ; return 0 ;; [nN]) echo ; return 1 ;; *) printf " \033[31m %s \n\033[0m" "invalid input" esac done } if [[ $# -eq 0 ]] ; then echo 'USAGE: You need to pass a valid configuration (.cnf) file to the script. e.g. "./newcert -c mycert.cnf" Use -h flag for more help. ' exit 1 fi CONF_FILE=$1 TMP_KEYFILE='ictempkey.pem' TMP_REQFILE='ictempreq.pem' BUFFER_FILE='buffer.pem' CA_CNF='postgresCA.cnf' CRL='postgresCRL.pem' PASSWORD='' while getopts c:hp option do case "${option}" in p) PASSWORD="FAKEPASS";; c) CONF_FILE=${OPTARG};; h) echo "*----------------------------------------------------------------------------------------*" echo "* *" echo "* USAGE *" echo "* ./newcert.sh -c filename.cnf -p *" echo "* *" echo "* -c (required) The openssl configuration file for the certificate you wish to *" echo "* create. *" echo "* *" echo "* NOTE: The name of this file will determine the name of the certificate *" echo "* *" echo "* *" echo "* -p (optional) Do not prompt for a password the script will use a fake password *" echo "* to create the certificate and then remove it automatically without prompting. *" echo "* *" echo "*----------------------------------------------------------------------------------------*" exit 0 esac done if [[ $CONF_FILE == *.cnf ]]; then if [[ -f $CONF_FILE ]]; then echo 'Building certificate using ' $CONF_FILE export OPENSSL_CONF=./$CONF_FILE if [[ -z "$PASSWORD" ]]; then openssl req -newkey rsa:4096 -keyout $TMP_KEYFILE -keyform PEM -out $TMP_REQFILE -outform PEM else openssl req -newkey rsa:4096 -passout pass:$PASSWORD -keyout $TMP_KEYFILE -keyform PEM -out $TMP_REQFILE -outform PEM fi status=$? if [[ status -eq 0 ]]; then if [[ -z "$PASSWORD" ]]; then if prompt_confirm "Would you like to remove the password from the certificate (You will be prompted for a password every time you use the certificate if you don't)"; then echo 'Removing password' openssl rsa < $TMP_KEYFILE > $BUFFER_FILE fi else openssl rsa -passin pass:$PASSWORD < $TMP_KEYFILE > $BUFFER_FILE fi status=$? if [[ status -eq 0 ]]; then mv $BUFFER_FILE $TMP_KEYFILE if prompt_confirm "Would you like to sign the certificate"; then export OPENSSL_CONF=./$CA_CNF CERTNAME=${CONF_FILE%.*} echo 'Signing Certificate "'$CERTNAME'"' openssl ca -in $TMP_REQFILE -out $CERTNAME.crt status=$? if [[ status -eq 0 ]]; then mv $TMP_KEYFILE $CERTNAME.key echo 'SUCCESS CERTIFICATE SIGNED' echo 'Congratulations, the following files have now been created: "' $CERTNAME'.crt" and "'$CERTNAME'.key"' if prompt_confirm "Would you like to test the certificate?"; then openssl verify -CRLfile ./$CRL -crl_check_all -CAfile ./${CA_CNF%.*}.crt $CERTNAME.crt fi else echo 'ERROR or SIGNING CERTIFICATE ABORTED' echo 'If you wish to create a certificate later please re run this script, the current unsigned and invalid certificates have been deleted' rm $TMP_REQFILE rm $TMP_KEYFILE fi else echo 'CERTIFICATE SIGNING ABORTED' echo 'If you wish to create a certificate later please re run this script, the current unsigned and invalid certificates have been deleted' rm $TMP_REQFILE rm $TMP_KEYFILE fi else echo 'ERROR: An error occurred while removeing the password, exiting!' exit 1 fi else echo 'ERROR: Building the certificate failed, exiting!' exit 1 fi else echo 'ERROR: ' $CONF_FILE ' does not exist, exiting!' exit 1 fi else echo 'ERROR: Invalid file format, you need to pass a .cnf file to the script, exiting!' exit 1 fi exit 1
Now we need a configuration file. The name of this file will determine the name of the certificate so think about it carefully. In this example I am simply going to call it server.
In /pgsql/CA create a file called server.cnf.
touch server.cnf
NOTE: The .cnf suffix is important, the script will not work without it.
Now edit this file with the appropriate information. The only things that are of importance are the “commonName” and any “alt_names“. The reason these are important is that they are not reusable so in order to issue a further certificate using the same commonName or alt_name you will first need to revoke this one.
The file should look something like:
# # server.cnf # [ req ] prompt = no distinguished_name = server_distinguished_name req_extensions = v3_req [ server_distinguished_name ] commonName = CHANGE.ME.FQDN stateOrProvinceName = CHANGE ME countryName = CHANGE ME emailAddress = CHANGE.ME.EMAIL organizationName = CHANGE ME organizationalUnitName = CHANGE ME [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names [ alt_names ] DNS.1 = CHANGE ME
When it is finished; you can have as many alt_names as you like, you simply increment the DNS. directive on a new line. i.e. DNS.1, DNS.2 etc..
Save the file as /pgsql/CA/server.cnf and then run the script to generate the the certificate and sign it.
NOTE: this script is not bomb proof, but as long as you haven’t done anything too daft it should work ok.
Method 1
./newcert.sh server.cnf
and follow the instructions in the terminal.
You will be prompted for a password for the client certificate, then you will be asked if you want to remove it; it is all part of the process, just provide any password of 5 characters or more, but make sure you remember it as you will be prompted for it one more time in order to remove it. THE SCRIPT HAS BEEN UPDATED NOW SO THAT YOU DO NOT HAVE TO DO THIS IF YOU PASS IT THE -p FLAG; IF YOU CHOOSE THIS METHOD THEN YOU WILL NEED TO PASS THE -c FLAG ALONG WITH THE FILENAME.
Method 2
./newcert.sh -p -c server.cnf
You will still be prompted for the CA password.
NOTE: If you are having trouble running this script then you may need to edit it; This will most likely be the case if your configuration file for your CA is something other than postgresCA.cnf, you will have to change the CA_CNF= directive in the script to match.
In our example the script should have generated 2 files: “server.crt” and “server.key” you will need these along with the public key of your certificate authority “postgresCA.crt to use them with your postgresql servers.
The last thing you need to do is change the permissions on your .key file.
WARNING: Failure to do this will prevent postgresql from starting correctly when you assign the certificates.
I recommend creating a certificate for each individual client.
chmod 400 ./server.key
Setting up postgresql for SSL authentication
You will need your certificates somewhere where postgresql can access them; if you are using them on the server that you set the CA up on then you can leave them where they are. If it is on a replication server then I usually put them directly in the /pgsql directory.
If you are still logged in as the postgres user then exit and change the user to root or use sudo as is your desire.
The first file you need to edit is /etc/postgresql/15/main/postgresql.conf.(remember this path is version specific).
Look for the ssl directives and change/uncomment them so it looks like
# - SSL - ssl = on ssl_ca_file = '/pgsql/CA/postgresCA.crt' ssl_cert_file = '/pgsql/CA/server.crt' ssl_crl_file = '' ssl_key_file = '/pgsql/CA/server.key' ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers ssl_prefer_server_ciphers = on #ssl_ecdh_curve = 'prime256v1' #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = off
then save the file.
The next bit is somewhat specific to your user setup as you are amending pg_hba.conf to tell your configuration to use ssl when negotiating connections. This is on a per user basis so if you have user “foo” and user “bar” you will have to turn on ssl for each individually.
To do so simply change host to hostssl
hostssl mydatabase foo XXX.XXX.XXX.XXX/XX scram-sha-256 hostssl mydatabase bar XXX.XXX.XXX.XXX/XX scram-sha-256
Now if you restart postgresql when you log in to psql as one of the hostssl users you should see something like:
psql (15.7 (Debian 15.7-0+deb12u1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off) Type "help" for help.
This means that your connection is secure.
Revoking client certificates
Every certificate generated will authenticate against the server until either it is revoked or it runs out. The server is reliant on a certificate revocation list in order to determine which certificates have been revoked by the CA so we need to generate a certificate revocation list every time we revoke a certificate.
Again I have wrote a small script to do this as it is so easy to miss a step if you do it manually.
#!/bin/bash # # REVOKE A CERTIFICATE prompt_confirm() { while true; do read -r -n 1 -p "${1:-Continue?} [y/n]: " REPLY case $REPLY in [yY]) echo ; return 0 ;; [nN]) echo ; return 1 ;; *) printf " \033[31m %s \n\033[0m" "invalid input" esac done } if [[ $# -eq 0 ]] ; then echo 'USAGE: "./revoke -c mycert.crt -p capassword"' exit 1 fi CERTFILE=$1 while getopts c:p: option do case "${option}" in c) CERTFILE=${OPTARG};; p) PASSWORD=${OPTARG};; esac done echo $PASSWORD #You may need to change these directives to reflect your real CAServer name (not to be confused with the #actual server name) and the name that you have given to your certificate revocation list. CA='postgresCA' CRL='postgresCRL' if [[ $CERTFILE == *.crt ]]; then if [[ -f $CERTFILE ]]; then echo 'Revoking '$CERTFILE export OPENSSL_CONF=./$CA.cnf if [[ -z "$PASSWORD" ]]; then openssl ca -config $CA.cnf -revoke ./$CERTFILE else echo 'openssl ca -config '$CA.cnf' -passin pass:'$PASSWORD' -revoke ./'$CERTFILE openssl ca -config $CA.cnf -passin pass:$PASSWORD -revoke ./$CERTFILE fi status=$? if [[ status -eq 0 ]]; then if prompt_confirm "Would you like to update the CERTIFICATE REVOCATION LIST?"; then if [[ -z "$PASSWORD" ]]; then openssl ca -config $CA.cnf -gencrl -keyfile ./private/$CA.pem -cert $CA.pem -out $CRL.pem else openssl ca -config $CA.cnf -gencrl -keyfile ./private/$CA.pem -cert $CA.pem -out $CRL.pem -passin pass:$PASSWORD fi status=$? if [[ status -eq 0 ]]; then echo 'CERTIFCATE REVOKED: The certificate file ' $CRL'.pem has been updated.' echo 'Additional steps my be required in order to propogate the changes' if prompt_confirm "The certificate and private key can now be safely deleted, would you like to do this automatically now?"; then rm ./$CERTFILE rm ./${CERTFILE%.*}.key fi fi fi else echo 'ERROR: The certificate revocation failed, exiting.' exit 1 fi else echo 'The file "'$CERTFILE'" does not exist; exiting!' exit 1 fi else echo 'You need to provide the script with a certificate file with the extension ".crt" in order to proceed.' exit 1 fi
Copy the above and save it to a file called /pgsql/CA/revoke.sh and make it executable.
This script requires the name of the certificate to be revoked to passed as an argument.
./revoke.sh -c server.crt -p secretpassword
Follow the instructions on the screen and you will revoke the certificate created earlier.
NOTE: When a certificate expires you will need to revoke it before creating a new one.
Showing revoked certificates
You can do this from the command line quite simply by passing running the following:
openssl crl -in postgresCRL.pem -noout -text
This will display the certificates that have been revoked according to the certificate revocation list. If for some reason the CRL has not been updated then the crl will not display the revoked certificates. The serial numbers of the revoked certificates should match those marked with an “R” in the database (index.txt file).
NOTE: The CRL can be updated any time by running:
openssl ca -config inplicareCA.cnf -gencrl -keyfile ./private/postgresCA.pem -cert postgresCA.pem -out postgresCRL.pem
however this should not ordinarily be necessary as is done in the script.
Recent Comments