diff --git a/automation/deploy_pgcluster.yml b/automation/deploy_pgcluster.yml index cf2aed8b8..75bd8908f 100644 --- a/automation/deploy_pgcluster.yml +++ b/automation/deploy_pgcluster.yml @@ -83,18 +83,6 @@ - pgbackrest_auto_conf | default(true) | bool # to be able to disable auto backup settings tags: always - roles: - # (optional) if 'ssh_public_keys' is defined - - role: authorized-keys - tags: ssh_public_keys - - - role: pre-checks - vars: - minimal_ansible_version: 2.16.0 - timescale_minimal_pg_version: 12 # if enable_timescale is defined - tags: always - - tasks: - name: Clean dnf cache ansible.builtin.command: dnf clean all when: @@ -202,6 +190,22 @@ when: pre_deploy_command | default('') | length > 0 tags: pre_deploy, pre_deploy_command + roles: + # (optional) if 'ssh_public_keys' is defined + - role: authorized-keys + tags: ssh_public_keys + + - role: pre-checks + vars: + minimal_ansible_version: 2.16.0 + timescale_minimal_pg_version: 12 # if enable_timescale is defined + tags: always + + - role: hostname + + - role: tls_certificate/generate + when: tls_cert_generate|bool + - name: deploy_pgcluster.yml | Deploy etcd cluster ansible.builtin.import_playbook: etcd_cluster.yml when: not dcs_exists|bool and dcs_type == "etcd" @@ -261,7 +265,6 @@ when: firewall_enabled_at_boot|bool tags: firewall - - role: hostname - role: resolv_conf - role: etc_hosts - role: add-repository @@ -356,7 +359,7 @@ - role: cron - - role: tls_certificate + - role: tls_certificate/copy when: tls_cert_generate|bool - role: pgbouncer diff --git a/automation/molecule/tests/etcd/etcd.yml b/automation/molecule/tests/etcd/etcd.yml index c564cabc6..42991a5c4 100644 --- a/automation/molecule/tests/etcd/etcd.yml +++ b/automation/molecule/tests/etcd/etcd.yml @@ -1,8 +1,13 @@ --- - name: Check etcd health ansible.builtin.uri: - url: "http://{{ inventory_hostname }}:2379/health" + url: "{{ patroni_etcd_protocol | default('http', true) }}://{{ inventory_hostname }}:2379/health" + method: GET return_content: true + validate_certs: "{{ tls_cert_generate | default(false) | bool }}" + ca_path: "{{ tls_etcd_ca_cert_path if tls_cert_generate | default(false) | bool else omit }}" + client_cert: "{{ tls_etcd_cert_path if tls_cert_generate | default(false) | bool else omit }}" + client_key: "{{ tls_etcd_privatekey_path if tls_cert_generate | default(false) | bool else omit }}" register: etcd_health_status failed_when: "(etcd_health_status.content | from_json).health != 'true'" when: dcs_type == "etcd" diff --git a/automation/roles/confd/templates/confd.toml.j2 b/automation/roles/confd/templates/confd.toml.j2 index e27d66f9b..0e91e852f 100644 --- a/automation/roles/confd/templates/confd.toml.j2 +++ b/automation/roles/confd/templates/confd.toml.j2 @@ -1,22 +1,26 @@ backend = "etcdv3" -interval = 10 -watch = true nodes = [ -{% if not dcs_exists|bool and dcs_type == 'etcd' %} +{% if not dcs_exists|bool %} {% for host in groups['etcd_cluster'] %} - "http://{{ hostvars[host]['inventory_hostname'] }}:2379", + "{{ patroni_etcd_protocol | default('http', true) }}://{{ hostvars[host]['inventory_hostname'] }}:2379", {% endfor %} {% endif %} -{% if dcs_exists|bool and dcs_type == 'etcd' %} +{% if dcs_exists|bool %} {% for etcd_hosts in patroni_etcd_hosts %} "{{ patroni_etcd_protocol | default('http', true) }}://{{etcd_hosts.host}}:{{etcd_hosts.port}}", {% endfor %} {% endif %} ] -{% if dcs_exists|bool and dcs_type == 'etcd' %} +{% if tls_cert_generate | default(false) | bool %} +scheme = "https" +client_cakeys = "{{ tls_ca_cert_path | default('/etc/tls/ca.crt') }}" +client_cert = "{{ tls_cert_path | default('/etc/tls/server.crt') }}" +client_key = "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" +{% endif %} {% if patroni_etcd_username | default('') | length > 0 %} basic_auth = true username = "{{ patroni_etcd_username | default('') }}" password = "{{ patroni_etcd_password | default('') }}" {% endif %} -{% endif %} \ No newline at end of file +watch = true +interval = 10 diff --git a/automation/roles/etcd/tasks/main.yml b/automation/roles/etcd/tasks/main.yml index c18b35941..ddb0a7187 100644 --- a/automation/roles/etcd/tasks/main.yml +++ b/automation/roles/etcd/tasks/main.yml @@ -86,6 +86,17 @@ state: directory tags: etcd, etcd_conf +- name: Fetch etcd TLS certificate, key and CA from the master node + ansible.builtin.include_role: + name: ../roles/tls_certificate/copy + vars: + copy_tls_cert_path: "{{ tls_etcd_cert_path | default('/etc/etcd/tls/server.crt') }}" + copy_tls_ca_cert_path: "{{ tls_etcd_ca_cert_path | default('/etc/etcd/tls/ca.crt') }}" + copy_tls_privatekey_path: "{{ tls_etcd_privatekey_path | default('/etc/etcd/tls/server.key') }}" + copy_tls_owner: "etcd" + when: tls_cert_generate|bool + tags: etcd, etcd_conf + - name: Create etcd data directory ansible.builtin.file: path: "{{ etcd_data_dir }}" @@ -128,7 +139,12 @@ - name: Wait until the etcd cluster is healthy ansible.builtin.command: > /usr/local/bin/etcdctl endpoint health - --endpoints=http://{{ inventory_hostname }}:2379 + --endpoints={{ patroni_etcd_protocol | default('http', true) }}://{{ inventory_hostname }}:2379 + {% if tls_cert_generate | default(false) | bool %} + --cacert={{ tls_etcd_ca_cert_path | default('/etc/etcd/tls/ca.crt') }} + --cert={{ tls_etcd_cert_path | default('/etc/etcd/tls/server.crt') }} + --key={{ tls_etcd_privatekey_path | default('/etc/etcd/tls/server.key') }} + {% endif %} environment: ETCDCTL_API: "3" register: etcd_health_result diff --git a/automation/roles/etcd/templates/etcd.conf.j2 b/automation/roles/etcd/templates/etcd.conf.j2 index 0aa568c4a..b1f4e2bfa 100644 --- a/automation/roles/etcd/templates/etcd.conf.j2 +++ b/automation/roles/etcd/templates/etcd.conf.j2 @@ -1,13 +1,24 @@ ETCD_NAME="{{ ansible_hostname }}" -ETCD_LISTEN_CLIENT_URLS="http://{{ inventory_hostname }}:2379,http://127.0.0.1:2379" -ETCD_ADVERTISE_CLIENT_URLS="http://{{ inventory_hostname }}:2379" -ETCD_LISTEN_PEER_URLS="http://{{ inventory_hostname }}:2380" -ETCD_INITIAL_ADVERTISE_PEER_URLS="http://{{ inventory_hostname }}:2380" +ETCD_LISTEN_CLIENT_URLS="{{ patroni_etcd_protocol | default('http', true) }}://{{ inventory_hostname }}:2379,{{ patroni_etcd_protocol | default('http', true) }}://127.0.0.1:2379" +ETCD_ADVERTISE_CLIENT_URLS="{{ patroni_etcd_protocol | default('http', true) }}://{{ inventory_hostname }}:2379" +ETCD_LISTEN_PEER_URLS="{{ patroni_etcd_protocol | default('http', true) }}://{{ inventory_hostname }}:2380" +ETCD_INITIAL_ADVERTISE_PEER_URLS="{{ patroni_etcd_protocol | default('http', true) }}://{{ inventory_hostname }}:2380" ETCD_INITIAL_CLUSTER_TOKEN="{{ etcd_cluster_name }}" -ETCD_INITIAL_CLUSTER="{% for host in groups['etcd_cluster'] %}{{ hostvars[host]['ansible_hostname'] }}=http://{{ hostvars[host]['inventory_hostname'] }}:2380{% if not loop.last %},{% endif %}{% endfor %}" +ETCD_INITIAL_CLUSTER="{% for host in groups['etcd_cluster'] %}{{ hostvars[host]['ansible_hostname'] }}={{ patroni_etcd_protocol | default('http', true) }}://{{ hostvars[host]['inventory_hostname'] }}:2380{% if not loop.last %},{% endif %}{% endfor %}" ETCD_INITIAL_CLUSTER_STATE="new" ETCD_DATA_DIR="{{ etcd_data_dir }}" ETCD_ELECTION_TIMEOUT="5000" ETCD_HEARTBEAT_INTERVAL="1000" ETCD_INITIAL_ELECTION_TICK_ADVANCE="false" ETCD_AUTO_COMPACTION_RETENTION="1" +{% if tls_cert_generate | default(false) | bool %} +ETCD_CERT_FILE="{{ tls_etcd_cert_path | default('/etc/etcd/tls/server.crt') }}" +ETCD_KEY_FILE="{{ tls_etcd_privatekey_path | default('/etc/etcd/tls/server.key') }}" +ETCD_TRUSTED_CA_FILE="{{ tls_etcd_ca_cert_path | default('/etc/etcd/tls/ca.crt') }}" +ETCD_PEER_CERT_FILE="{{ tls_etcd_cert_path | default('/etc/etcd/tls/server.crt') }}" +ETCD_PEER_KEY_FILE="{{ tls_etcd_privatekey_path | default('/etc/etcd/tls/server.key') }}" +ETCD_PEER_TRUSTED_CA_FILE="{{ tls_etcd_ca_cert_path | default('/etc/etcd/tls/ca.crt') }}" +ETCD_PEER_CLIENT_CERT_AUTH="true" +ETCD_CLIENT_CERT_AUTH="true" +ETCD_TLS_MIN_VERSION="TLS1.2" +{% endif %} diff --git a/automation/roles/patroni/templates/patroni.yml.j2 b/automation/roles/patroni/templates/patroni.yml.j2 index 011165ac3..d519fc806 100644 --- a/automation/roles/patroni/templates/patroni.yml.j2 +++ b/automation/roles/patroni/templates/patroni.yml.j2 @@ -35,23 +35,27 @@ restapi: # password: password {% endif %} -{% if not dcs_exists|bool and dcs_type == 'etcd' %} +{% if dcs_type == 'etcd' %} etcd3: + {% if not dcs_exists|bool %} hosts: {% for host in groups['etcd_cluster'] %}{{ hostvars[host]['inventory_hostname'] }}:2379{% if not loop.last %},{% endif %}{% endfor %} -{% endif %} -{% if dcs_exists|bool and dcs_type == 'etcd' %} -etcd3: + {% endif %} + {% if dcs_exists|bool %} hosts: {% for etcd_hosts in patroni_etcd_hosts %}{{etcd_hosts.host}}:{{etcd_hosts.port}}{% if not loop.last %},{% endif %}{% endfor %} + {% endif %} + {% if tls_cert_generate | default(false) | bool %} + protocol: https + cacert: {{ tls_ca_cert_path | default('/etc/tls/ca.crt') }} + cert: {{ tls_cert_path | default('/etc/tls/server.crt') }} + key: {{ tls_privatekey_path | default('/etc/tls/server.key') }} + {% endif %} {% if patroni_etcd_username | default('') | length > 0 %} username: {{ patroni_etcd_username | default('') }} {% endif %} {% if patroni_etcd_password | default('') | length > 0 %} password: {{ patroni_etcd_password }} {% endif %} - {% if patroni_etcd_protocol | default('') | length > 0 %} - protocol: {{ patroni_etcd_protocol }} - {% endif %} {% endif %} {% if dcs_type == 'consul' %} diff --git a/automation/roles/patroni/templates/pg_hba.conf.j2 b/automation/roles/patroni/templates/pg_hba.conf.j2 index f3be1883a..53187ecb1 100644 --- a/automation/roles/patroni/templates/pg_hba.conf.j2 +++ b/automation/roles/patroni/templates/pg_hba.conf.j2 @@ -85,14 +85,14 @@ # TYPE DATABASE USER ADDRESS METHOD {% for client in postgresql_pg_hba %} - {{ client.type.ljust(10) |default('host') }}{{ client.database.ljust(25) |default('all') }}{{ client.user.ljust(25) |default('all') }}{{ client.address.ljust(25) |default('') }}{{ client.method |default('md5') }} {{ client.options |default(None) }} + {{ client.type.ljust(10) |default('{% if tls_cert_generate | default(false) | bool %}hostssl{% else %}host{% endif %}') }}{{ client.database.ljust(25) |default('all') }}{{ client.user.ljust(25) |default('all') }}{{ client.address.ljust(25) |default('') }}{{ client.method |default('md5') }} {{ client.options |default(None) }} {% endfor %} {% for patroni in groups['postgres_cluster'] %} - host all all {{ hostvars[patroni]['inventory_hostname'] }}/32 {{ postgresql_password_encryption_algorithm }} + {% if tls_cert_generate | default(false) | bool %}hostssl{% else %}host{% endif %} all all {{ hostvars[patroni]['inventory_hostname'] }}/32 {{ postgresql_password_encryption_algorithm }} {% endfor %} # Allow replication connections from localhost, by a user with the # replication privilege. - host replication {{ patroni_replication_username }} localhost trust + {% if tls_cert_generate | default(false) | bool %}hostssl{% else %}host{% endif %} replication {{ patroni_replication_username }} localhost trust {% for host in groups['postgres_cluster'] %} - host replication {{ patroni_replication_username }} {{ hostvars[host]['inventory_hostname'] }}/32 {{ postgresql_password_encryption_algorithm }} + {% if tls_cert_generate | default(false) | bool %}hostssl{% else %}host{% endif %} replication {{ patroni_replication_username }} {{ hostvars[host]['inventory_hostname'] }}/32 {{ postgresql_password_encryption_algorithm }} {% endfor %} diff --git a/automation/roles/tls_certificate/copy/tasks/main.yml b/automation/roles/tls_certificate/copy/tasks/main.yml index a0038ecc4..251431bfe 100644 --- a/automation/roles/tls_certificate/copy/tasks/main.yml +++ b/automation/roles/tls_certificate/copy/tasks/main.yml @@ -1,42 +1,34 @@ --- -# 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 +- name: Fetch TLS certificate, key and CA from the master node into memory run_once: true - ansible.builtin.fetch: + ansible.builtin.slurp: src: "{{ item }}" - dest: "files/tls/" - validate_checksum: true - flat: true delegate_to: "{{ groups.master[0] }}" + register: tls_files loop: - "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" - "{{ tls_cert_path | default('/etc/tls/server.crt') }}" + - "{{ tls_ca_cert_path | default('/etc/tls/ca.crt') }}" + tags: tls, tls_cert_copy + +- name: Create directory {{ copy_tls_privatekey_path | default(tls_privatekey_path | default('/etc/tls/server.key')) | dirname }} + ansible.builtin.file: + path: "{{ copy_tls_privatekey_path | default(tls_privatekey_path | default('/etc/tls/server.key')) | dirname }}" + state: directory + owner: "{{ copy_tls_owner | default(tls_owner | default('postgres')) }}" + group: "{{ copy_tls_owner | default(tls_owner | default('postgres')) }}" + mode: "0755" + tags: tls, tls_cert_copy -- name: Copy TLS certificate and key to replica +- name: Copy PostgreSQL TLS certificate, key and CA to all nodes ansible.builtin.copy: - src: "files/tls/{{ item.path | basename }}" + content: "{{ tls_files.results[item.index].content | b64decode }}" dest: "{{ item.path }}" - owner: "{{ tls_owner | default('postgres') }}" - group: "{{ tls_owner | default('postgres') }}" + owner: "{{ copy_tls_owner | default(tls_owner | default('postgres')) }}" + group: "{{ copy_tls_owner | default(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 + - { index: 1, path: "{{ copy_tls_cert_path | default(tls_cert_path | default('/etc/tls/server.crt')) }}", mode: "0644" } + - { index: 2, path: "{{ copy_tls_ca_cert_path | default(tls_ca_cert_path | default('/etc/tls/ca.crt')) }}", mode: "0644" } + - { index: 0, path: "{{ copy_tls_privatekey_path | default(tls_privatekey_path | default('/etc/tls/server.key')) }}", mode: "0400" } + tags: tls, tls_cert_copy diff --git a/automation/roles/tls_certificate/generate/tasks/main.yml b/automation/roles/tls_certificate/generate/tasks/main.yml new file mode 100644 index 000000000..d8b9037ca --- /dev/null +++ b/automation/roles/tls_certificate/generate/tasks/main.yml @@ -0,0 +1,120 @@ +--- +- name: Make sure that the python3-cryptography package is present + ansible.builtin.package: + name: python3-cryptography + state: present + register: pack_status + until: pack_status is success + delay: 5 + retries: 3 + tags: tls, tls_cert_generate + +- name: Clean up existing certificates (if any) + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + - "{{ tls_cert_path | default('/etc/tls/server.crt') }}" + - "{{ tls_ca_cert_path | default('/etc/tls/ca.crt') }}" + - "{{ tls_ca_key_path | default('/etc/tls/ca.key') }}" + - "{{ tls_etcd_cert_path | default('/etc/etcd/tls/server.crt') }}" + - "{{ tls_etcd_ca_cert_path | default('/etc/etcd/tls/ca.crt') }}" + - "{{ tls_etcd_privatekey_path | default('/etc/etcd/tls/server.key') }}" + tags: tls, tls_cert_generate + +- block: + - name: "Generate subjectAltName entries for all hosts" + ansible.builtin.set_fact: + subject_alt_name: >- + {{ + ( + ansible_play_hosts | map('extract', hostvars, 'ansible_hostname') | map('regex_replace', '^', 'DNS:') | list + + ansible_play_hosts | map('extract', hostvars, 'ansible_fqdn') | map('regex_replace', '^', 'DNS:') | list + + ansible_play_hosts | map('extract', hostvars, 'inventory_hostname') | map('regex_replace', '^', 'IP:') | list + + ['DNS:localhost', 'IP:127.0.0.1'] + ) | unique | join(',') + }} + + - name: "Display Certificate subjectAltName future value" + ansible.builtin.debug: + var: subject_alt_name + + ######## Generate CA ######## + - name: "Ensure TLS directory exist" + ansible.builtin.file: + path: "{{ tls_privatekey_path | default('/etc/tls/server.key') | dirname }}" + state: directory + owner: "root" + group: "root" + mode: "0755" + + - name: "Generate CA private key" + community.crypto.openssl_privatekey: + path: "{{ tls_ca_key_path | default('/etc/tls/ca.key') }}" + size: "{{ tls_privatekey_size | default(4096) }}" + type: "{{ tls_privatekey_type | default('RSA') }}" + + - name: "Create CSR for CA certificate" + community.crypto.openssl_csr_pipe: + privatekey_path: "{{ tls_ca_key_path | default('/etc/tls/ca.key') }}" + common_name: PostgreSQL CA + use_common_name_for_san: false + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + key_usage: + - keyCertSign + key_usage_critical: true + register: ca_csr + + - name: "Create self-signed CA certificate from CSR" + community.crypto.x509_certificate: + path: "{{ tls_ca_cert_path | default('/etc/tls/ca.crt') }}" + csr_content: "{{ ca_csr.csr }}" + privatekey_path: "{{ tls_ca_key_path | default('/etc/tls/ca.key') }}" + provider: "{{ tls_cert_provider | default('selfsigned') }}" + entrust_not_after: "+{{ tls_cert_valid_days | default(3650) }}d" + + ######## Generate Server cert/key ######## + - name: "Create server private key" + community.crypto.openssl_privatekey: + path: "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + size: "{{ tls_privatekey_size | default(4096) }}" + type: "{{ tls_privatekey_type | default('RSA') }}" + + - name: "Create server CSR" + community.crypto.openssl_csr_pipe: + privatekey_path: "{{ tls_privatekey_path | default('/etc/tls/server.key') }}" + common_name: "{{ patroni_cluster_name }}" + key_usage: + - digitalSignature + - keyEncipherment + - dataEncipherment + extended_key_usage: + - clientAuth + - serverAuth + subject: + C: "AL" + O: "autobase" + CN: "{{ patroni_cluster_name }}" + subject_alt_name: "{{ subject_alt_name }}" + register: csr + + - name: "Sign server certificate with the CA" + community.crypto.x509_certificate_pipe: + csr_content: "{{ csr.csr }}" + provider: ownca + ownca_path: "{{ tls_ca_cert_path | default('/etc/tls/ca.crt') }}" + ownca_privatekey_path: "{{ tls_ca_key_path | default('/etc/tls/ca.key') }}" + ownca_not_after: +{{ tls_cert_valid_days | default(3650) }}d + ownca_not_before: "-1d" + register: certificate + + - name: "Write server certificate" + ansible.builtin.copy: + dest: "{{ tls_cert_path | default('/etc/tls/server.crt') }}" + content: "{{ certificate.certificate }}" + delegate_to: "{{ groups.master[0] }}" + run_once: true + tags: tls, tls_cert_generate diff --git a/automation/roles/tls_certificate/tasks/main.yml b/automation/roles/tls_certificate/tasks/main.yml deleted file mode 100644 index 07ee54dd1..000000000 --- a/automation/roles/tls_certificate/tasks/main.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -- 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/roles/vip-manager/templates/vip-manager.yml.j2 b/automation/roles/vip-manager/templates/vip-manager.yml.j2 index ded0bd395..0cc1929cc 100644 --- a/automation/roles/vip-manager/templates/vip-manager.yml.j2 +++ b/automation/roles/vip-manager/templates/vip-manager.yml.j2 @@ -27,7 +27,7 @@ dcs-type: {{ vip_manager_dcs_type | default(dcs_type) }} # etcd, consul or patro {% if not dcs_exists | bool %} dcs-endpoints: {% for host in groups['etcd_cluster'] %} - - http://{{ hostvars[host]['inventory_hostname'] }}:2379 + - {{ patroni_etcd_protocol | default('http', true) }}://{{ hostvars[host]['inventory_hostname'] }}:2379 {% endfor %} {% else %} dcs-endpoints: @@ -64,16 +64,12 @@ etcd-user: {{ patroni_etcd_username | default("") }} {% if patroni_etcd_password | default("") | length > 0 %} etcd-password: {{ patroni_etcd_password | default("") }} {% endif %} -{% if patroni_etcd_ca_file | default("") | length > 0 %} +{% if tls_cert_generate | default(false) | bool %} # when etcd-ca-file is specified, TLS connections to the etcd endpoints will be used. -etcd-ca-file: {{ patroni_etcd_ca_file | default("") }} -{% endif %} -{% if patroni_etcd_cert_file | default("") | length > 0 %} +etcd-ca-file: {{ tls_ca_cert_path | default('/etc/tls/ca.crt') }} # when etcd-cert-file and etcd-key-file are specified, we will authenticate at the etcd endpoints using this certificate and key. -etcd-cert-file: {{ patroni_etcd_cert_file | default("") }} -{% endif %} -{% if patroni_etcd_key_file | default("") | length > 0 %} -etcd-key-file: {{ patroni_etcd_key_file | default("") }} +etcd-cert-file: {{ tls_cert_path | default('/etc/tls/server.crt') }} +etcd-key-file: {{ tls_privatekey_path | default('/etc/tls/server.key') }} {% endif %} {% endif %} diff --git a/automation/tags.md b/automation/tags.md index fabf2cfec..ed65267ab 100644 --- a/automation/tags.md +++ b/automation/tags.md @@ -108,3 +108,6 @@ - ssh_public_keys - mount, zpool - perf, flamegraph +- tls + - tls_cert_generate + - tls_cert_copy diff --git a/automation/vars/Debian.yml b/automation/vars/Debian.yml index 4f1ea629b..5e2d99900 100644 --- a/automation/vars/Debian.yml +++ b/automation/vars/Debian.yml @@ -40,7 +40,6 @@ 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 86fb6be98..46e158bcc 100644 --- a/automation/vars/RedHat.yml +++ b/automation/vars/RedHat.yml @@ -60,7 +60,6 @@ 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 e8fa0c4fc..ad1b812b7 100644 --- a/automation/vars/main.yml +++ b/automation/vars/main.yml @@ -104,7 +104,7 @@ patroni_etcd_hosts: [] # list of servers of an existing etcd cluster patroni_etcd_namespace: "service" # (optional) etcd namespace (prefix) patroni_etcd_username: "" # (optional) username for etcd authentication patroni_etcd_password: "" # (optional) password for etcd authentication -patroni_etcd_protocol: "" # (optional) http or https, if not specified http is used +patroni_etcd_protocol: "{{ 'https' if tls_cert_generate | bool else 'http' }}" # more options you can specify in the roles/patroni/templates/patroni.yml.j2 # https://patroni.readthedocs.io/en/latest/yaml_configuration.html#etcd @@ -174,12 +174,16 @@ 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 certificate (for PostgreSQL, PgBouncer and etcd) 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_cert_path: "/etc/tls/server.crt" +tls_privatekey_path: "/etc/tls/server.key" +tls_ca_cert_path: "/etc/tls/ca.crt" tls_owner: "postgres" +tls_etcd_cert_path: "/etc/etcd/tls/server.crt" +tls_etcd_ca_cert_path: "/etc/etcd/tls/ca.crt" +tls_etcd_privatekey_path: "/etc/etcd/tls/server.key" # PostgreSQL variables postgresql_version: 17 @@ -241,9 +245,11 @@ 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", value: "{{ 'on' if tls_cert_generate | bool else 'off' }}" } + - { option: "ssl_prefer_server_ciphers", value: "{{ 'on' if tls_cert_generate | bool else 'off' }}" } + - { option: "ssl_cert_file", value: "{{ tls_cert_path | default('') }}"} + - { option: "ssl_key_file", value: "{{ tls_privatekey_path | default('') }}"} + - { option: "ssl_ca_file", value: "{{ tls_ca_cert_path | default('') }}"} - { option: "ssl_min_protocol_version", value: "TLSv1.2"} - { option: "max_locks_per_transaction", value: "512" } - { option: "max_prepared_transactions", value: "0" } @@ -338,9 +344,11 @@ postgresql_pg_hba: - { type: "local", database: "all", user: "all", address: "", method: "{{ postgresql_password_encryption_algorithm }}" } - { type: "host", database: "all", user: "all", address: "127.0.0.1/32", method: "{{ postgresql_password_encryption_algorithm }}" } - { type: "host", database: "all", user: "all", address: "::1/128", method: "{{ postgresql_password_encryption_algorithm }}" } - - { type: "host", database: "all", user: "all", address: "0.0.0.0/0", method: "{{ postgresql_password_encryption_algorithm }}" } -# - { type: "host", database: "mydatabase", user: "mydb-user", address: "192.168.0.0/24", method: "{{ postgresql_password_encryption_algorithm }}" } -# - { type: "host", database: "all", user: "all", address: "192.168.0.0/24", method: "ident", options: "map=main" } # use pg_ident + - { type: "{{ host_type }}", database: "all", user: "all", address: "0.0.0.0/0", method: "{{ postgresql_password_encryption_algorithm }}" } +# - { type: "{{ host_type }}", database: "mydatabase", user: "mydb-user", address: "192.168.0.0/24", method: "{{ postgresql_password_encryption_algorithm }}" } +# - { type: "{{ host_type }}", database: "all", user: "all", address: "192.168.0.0/24", method: "ident", options: "map=main" } # use pg_ident + +host_type: "{{ 'hostssl' if tls_cert_generate | bool else 'host' }}" # list of lines that Patroni will use to generate pg_ident.conf postgresql_pg_ident: [] @@ -387,7 +395,7 @@ 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_server_tls_ca_file: "{{ tls_ca_cert_path }}" pgbouncer_pools: - { name: "postgres", dbname: "postgres", pool_parameters: "" }