diff --git a/automation/add_pgnode.yml b/automation/add_pgnode.yml index 08127325c..9dd7c15f1 100644 --- a/automation/add_pgnode.yml +++ b/automation/add_pgnode.yml @@ -235,6 +235,9 @@ - role: pg_probackup when: pg_probackup_install|bool + - role: tls_certificate/copy + when: tls_cert_generate|bool + - role: pgbouncer when: pgbouncer_install|bool diff --git a/automation/deploy_pgcluster.yml b/automation/deploy_pgcluster.yml index 0968f1fa6..cf2aed8b8 100644 --- a/automation/deploy_pgcluster.yml +++ b/automation/deploy_pgcluster.yml @@ -356,6 +356,9 @@ - role: cron + - role: tls_certificate + when: tls_cert_generate|bool + - role: pgbouncer when: pgbouncer_install|bool diff --git a/automation/roles/patroni/tasks/main.yml b/automation/roles/patroni/tasks/main.yml index 2d5d6f49c..d6ca5cf62 100644 --- a/automation/roles/patroni/tasks/main.yml +++ b/automation/roles/patroni/tasks/main.yml @@ -418,7 +418,7 @@ when: postgresql_wal_dir is defined and postgresql_wal_dir | length > 0 tags: patroni, custom_wal_dir -- block: # wheh postgresql NOT exists or PITR +- block: # when postgresql NOT exists or PITR - name: Prepare PostgreSQL | make sure PostgreSQL data directory "{{ postgresql_data_dir }}" exists ansible.builtin.file: path: "{{ postgresql_data_dir }}" diff --git a/automation/roles/pgbouncer/templates/pgbouncer.ini.j2 b/automation/roles/pgbouncer/templates/pgbouncer.ini.j2 index 8926d97bb..8bac8c6e3 100644 --- a/automation/roles/pgbouncer/templates/pgbouncer.ini.j2 +++ b/automation/roles/pgbouncer/templates/pgbouncer.ini.j2 @@ -39,10 +39,22 @@ so_reuseport = 1 client_tls_sslmode = {{ pgbouncer_client_tls_sslmode }} client_tls_key_file = {{ pgbouncer_client_tls_key_file }} client_tls_cert_file = {{ pgbouncer_client_tls_cert_file }} +{% if pgbouncer_client_tls_ca_file | default('') | length > 0 %} client_tls_ca_file = {{ pgbouncer_client_tls_ca_file }} +{% endif %} client_tls_protocols = {{ pgbouncer_client_tls_protocols }} client_tls_ciphers = {{ pgbouncer_client_tls_ciphers }} {% endif %} +{% if pgbouncer_server_tls_sslmode != 'disable' %} +server_tls_sslmode = {{ pgbouncer_server_tls_sslmode }} +server_tls_protocols = {{ pgbouncer_server_tls_protocols }} +server_tls_ciphers = {{ pgbouncer_server_tls_ciphers }} +server_tls_cert_file = {{ pgbouncer_server_tls_cert_file }} +server_tls_key_file = {{ pgbouncer_server_tls_key_file }} +{% if pgbouncer_server_tls_ca_file | default('') | length > 0 %} +server_tls_ca_file = {{ pgbouncer_server_tls_ca_file }} +{% endif %} +{% endif %} log_connections = 0 log_disconnections = 0 diff --git a/automation/roles/tls_certificate/copy/tasks/main.yml b/automation/roles/tls_certificate/copy/tasks/main.yml new file mode 100644 index 000000000..a0038ecc4 --- /dev/null +++ b/automation/roles/tls_certificate/copy/tasks/main.yml @@ -0,0 +1,42 @@ +--- +# for add_pgnode.yml + +- name: Ensure TLS directories exist + ansible.builtin.file: + path: "{{ item | dirname }}" + state: directory + owner: "{{ tls_owner | default('postgres') }}" + group: "{{ tls_owner | default('postgres') }}" + mode: "0750" + loop: + - "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + - "{{ tls_cert_path | default('/etc/tls/server.crt') }}" + +- name: Fetch TLS certificate and key from master + run_once: true + ansible.builtin.fetch: + src: "{{ item }}" + dest: "files/tls/" + validate_checksum: true + flat: true + delegate_to: "{{ groups.master[0] }}" + loop: + - "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + - "{{ tls_cert_path | default('/etc/tls/server.crt') }}" + +- name: Copy TLS certificate and key to replica + ansible.builtin.copy: + src: "files/tls/{{ item.path | basename }}" + dest: "{{ item.path }}" + owner: "{{ tls_owner | default('postgres') }}" + group: "{{ tls_owner | default('postgres') }}" + mode: "{{ item.mode }}" + loop: + - { path: "{{ tls_privatekey_path | default('/etc/tls/server.key') }}", mode: "{{ tls_privatekey_mode | default('0400') }}" } + - { path: "{{ tls_cert_path | default('/etc/tls/server.crt') }}", mode: "{{ tls_cert_mode | default('0644') }}" } + +- name: Delete TLS certificate and key from the ansible controller + ansible.builtin.file: + path: "files/tls/" + state: absent + delegate_to: localhost diff --git a/automation/roles/tls_certificate/tasks/main.yml b/automation/roles/tls_certificate/tasks/main.yml new file mode 100644 index 000000000..07ee54dd1 --- /dev/null +++ b/automation/roles/tls_certificate/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Ensure TLS directories exist + ansible.builtin.file: + path: "{{ item | dirname }}" + state: directory + owner: "{{ tls_owner | default('postgres') }}" + group: "{{ tls_owner | default('postgres') }}" + mode: "0750" + loop: + - "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + - "{{ tls_cert_path | default('/etc/tls/server.crt') }}" + +- name: "Generate private TLS key {{ tls_privatekey_path | default('/etc/tls/server.key') }}" + community.crypto.openssl_privatekey: + path: "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + owner: "{{ tls_owner | default('postgres') }}" + group: "{{ tls_owner | default('postgres') }}" + mode: "{{ tls_privatekey_mode | default('0400') }}" + size: "{{ tls_privatekey_size | default(4096) }}" + type: "{{ tls_privatekey_type | default('RSA') }}" + +- name: "Generate self-signed TLS certificate {{ tls_cert_path | default('/etc/tls/server.crt') }}" + community.crypto.x509_certificate: + path: "{{ tls_cert_path | default('/etc/tls/server.crt') }}" + privatekey_path: "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + owner: "{{ tls_owner | default('postgres') }}" + group: "{{ tls_owner | default('postgres') }}" + mode: "{{ tls_cert_mode | default('0644') }}" + provider: "{{ tls_cert_provider | default('selfsigned') }}" + entrust_not_after: "+{{ tls_cert_valid_days | default(3650) }}d" diff --git a/automation/vars/Debian.yml b/automation/vars/Debian.yml index 5e2d99900..4f1ea629b 100644 --- a/automation/vars/Debian.yml +++ b/automation/vars/Debian.yml @@ -40,6 +40,7 @@ system_packages: - python3-psycopg2 - python3-setuptools - python3-pip + - python3-cryptography - curl - less - sudo diff --git a/automation/vars/RedHat.yml b/automation/vars/RedHat.yml index 46e158bcc..86fb6be98 100644 --- a/automation/vars/RedHat.yml +++ b/automation/vars/RedHat.yml @@ -60,6 +60,7 @@ system_packages: - python{{ python_version }}-setuptools - python{{ python_version }}-pip - python{{ python_version }}-urllib3 + - python3-cryptography - less - sudo - vim diff --git a/automation/vars/main.yml b/automation/vars/main.yml index bf7e55cc2..7ef57c851 100644 --- a/automation/vars/main.yml +++ b/automation/vars/main.yml @@ -174,6 +174,12 @@ consul_services: # - { http: "http://{{ inventory_hostname }}:{{ patroni_restapi_port }}/async?lag={{ patroni_maximum_lag_on_replica }}", interval: "2s" } # - { args: ["systemctl", "status", "pgbouncer"], interval: "5s" } +# TLS certificate (for PostgreSQL & PgBouncer) +tls_cert_generate: true +tls_cert_valid_days: 3650 +tls_cert_path: "{{ postgresql_home_dir }}/tls/server.crt" +tls_privatekey_path: "{{ postgresql_home_dir }}/tls/server.key" +tls_owner: "postgres" # PostgreSQL variables postgresql_version: 17 @@ -235,6 +241,10 @@ postgresql_parameters: - { option: "max_connections", value: "1000" } - { option: "superuser_reserved_connections", value: "5" } - { option: "password_encryption", value: "{{ postgresql_password_encryption_algorithm }}" } + - { option: "ssl", value: "on"} + - { option: "ssl_cert_file", value: "{{ tls_cert_path }}"} + - { option: "ssl_key_file", value: "{{ tls_privatekey_path }}"} + - { option: "ssl_min_protocol_version", value: "TLSv1.2"} - { option: "max_locks_per_transaction", value: "512" } - { option: "max_prepared_transactions", value: "0" } - { option: "huge_pages", value: "try" } # "vm.nr_hugepages" is auto-configured for shared_buffers >= 8GB (if huge_pages_auto_conf is true) @@ -366,12 +376,18 @@ pgbouncer_auth_user: true # or 'false' if you want to manage the list of users f pgbouncer_auth_username: pgbouncer # user who can query the database via the user_search function pgbouncer_auth_password: "" # If not defined, a password will be generated automatically during deployment pgbouncer_auth_dbname: "postgres" -pgbouncer_client_tls_sslmode: "disable" -pgbouncer_client_tls_key_file: "" -pgbouncer_client_tls_cert_file: "" +pgbouncer_client_tls_sslmode: "require" +pgbouncer_client_tls_key_file: "{{ tls_privatekey_path }}" +pgbouncer_client_tls_cert_file: "{{ tls_cert_path }}" pgbouncer_client_tls_ca_file: "" pgbouncer_client_tls_protocols: "secure" # allowed values: tlsv1.0, tlsv1.1, tlsv1.2, tlsv1.3, all, secure (tlsv1.2,tlsv1.3) -pgbouncer_client_tls_ciphers: "default" # allowed values: default, secure, fast, normal, all (not recommended) +pgbouncer_client_tls_ciphers: "secure" # allowed values: default, secure, fast, normal, all (not recommended) +pgbouncer_server_tls_sslmode: "require" +pgbouncer_server_tls_protocols: "secure" +pgbouncer_server_tls_ciphers: "secure" +pgbouncer_server_tls_cert_file: "{{ tls_cert_path }}" +pgbouncer_server_tls_key_file: "{{ tls_privatekey_path }}" +pgbouncer_server_tls_ca_file: "" pgbouncer_pools: - { name: "postgres", dbname: "postgres", pool_parameters: "" }