diff --git a/lib/charms/postgresql_k8s/v0/postgresql_tls.py b/lib/charms/postgresql_k8s/v0/postgresql_tls.py index 800a99ea5e..7b00bf8283 100644 --- a/lib/charms/postgresql_k8s/v0/postgresql_tls.py +++ b/lib/charms/postgresql_k8s/v0/postgresql_tls.py @@ -18,6 +18,7 @@ """ import base64 +import ipaddress import logging import re import socket @@ -44,7 +45,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version. -LIBPATCH = 3 +LIBPATCH = 4 logger = logging.getLogger(__name__) SCOPE = "unit" @@ -54,11 +55,14 @@ class PostgreSQLTLS(Object): """In this class we manage certificates relation.""" - def __init__(self, charm, peer_relation): + def __init__( + self, charm, peer_relation: str, additional_dns_names: Optional[List[str]] = None + ): """Manager of PostgreSQL relation with TLS Certificates Operator.""" super().__init__(charm, "client-relations") self.charm = charm self.peer_relation = peer_relation + self.additional_dns_names = additional_dns_names or [] self.certs = TLSCertificatesRequiresV1(self.charm, TLS_RELATION) self.framework.observe( self.charm.on.set_tls_private_key_action, self._on_set_tls_private_key @@ -86,8 +90,8 @@ def _request_certificate(self, param: Optional[str]): csr = generate_csr( private_key=key, subject=self.charm.get_hostname_by_unit(self.charm.unit.name), - sans=self._get_sans(), additional_critical_extensions=self._get_tls_extensions(), + **self._get_sans(), ) self.charm.set_secret(SCOPE, "key", key.decode("utf-8")) @@ -149,8 +153,8 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: new_csr = generate_csr( private_key=key, subject=self.charm.get_hostname_by_unit(self.charm.unit.name), - sans=self._get_sans(), additional_critical_extensions=self._get_tls_extensions(), + **self._get_sans(), ) self.certs.request_certificate_renewal( old_certificate_signing_request=old_csr, @@ -158,19 +162,40 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: ) self.charm.set_secret(SCOPE, "csr", new_csr.decode("utf-8")) - def _get_sans(self) -> List[str]: - """Create a list of DNS names for a PostgreSQL unit. + def _get_sans(self) -> dict: + """Create a list of Subject Alternative Names for a PostgreSQL unit. Returns: - A list representing the hostnames of the PostgreSQL unit. + A list representing the IP and hostnames of the PostgreSQL unit. """ + + def is_ip_address(address: str) -> bool: + """Returns whether and address is an IP address.""" + try: + ipaddress.ip_address(address) + return True + except (ipaddress.AddressValueError, ValueError): + return False + unit_id = self.charm.unit.name.split("/")[1] - return [ + + # Create a list of all the Subject Alternative Names. + sans = [ f"{self.charm.app.name}-{unit_id}", self.charm.get_hostname_by_unit(self.charm.unit.name), socket.getfqdn(), str(self.charm.model.get_binding(self.peer_relation).network.bind_address), ] + sans.extend(self.additional_dns_names) + + # Separate IP addresses and DNS names. + sans_ip = [san for san in sans if is_ip_address(san)] + sans_dns = [san for san in sans if not is_ip_address(san)] + + return { + "sans_ip": sans_ip, + "sans_dns": sans_dns, + } @staticmethod def _get_tls_extensions() -> Optional[List[ExtensionType]]: diff --git a/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/lib/charms/tls_certificates_interface/v1/tls_certificates.py index 7893ff9398..e98d7866c9 100644 --- a/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ b/lib/charms/tls_certificates_interface/v1/tls_certificates.py @@ -222,6 +222,7 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: import logging import uuid from datetime import datetime, timedelta +from ipaddress import IPv4Address from typing import Dict, List, Optional from cryptography import x509 @@ -657,7 +658,9 @@ def generate_csr( email_address: str = None, country_name: str = None, private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, + sans_oid: Optional[str] = None, + sans_ip: Optional[List[str]] = None, + sans_dns: Optional[List[str]] = None, additional_critical_extensions: Optional[List] = None, ) -> bytes: """Generates a CSR using private key and subject. @@ -672,7 +675,9 @@ def generate_csr( email_address (str): Email address. country_name (str): Country Name. private_key_password (bytes): Private key password - sans (list): List of subject alternative names + sans_dns (list): List of DNS subject alternative names + sans_ip (list): List of IP subject alternative names + sans_oid (str): Additional OID additional_critical_extensions (list): List if critical additional extension objects. Object must be a x509 ExtensionType. @@ -693,10 +698,17 @@ def generate_csr( if country_name: subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - if sans: - csr = csr.add_extension( - x509.SubjectAlternativeName([x509.DNSName(san) for san in sans]), critical=False - ) + + _sans = [] + if sans_oid: + _sans.append(x509.RegisteredID(x509.ObjectIdentifier(sans_oid))) + if sans_ip: + _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) + if sans_dns: + _sans.extend([x509.DNSName(san) for san in sans_dns]) + if _sans: + csr = csr.add_extension(x509.SubjectAlternativeName(_sans), critical=False) + if additional_critical_extensions: for extension in additional_critical_extensions: csr = csr.add_extension(extension, critical=True) diff --git a/src/charm.py b/src/charm.py index a2e75bad59..e463f2b917 100755 --- a/src/charm.py +++ b/src/charm.py @@ -87,7 +87,7 @@ def __init__(self, *args): self.postgresql_client_relation = PostgreSQLProvider(self) self.legacy_db_relation = DbProvides(self, admin=False) self.legacy_db_admin_relation = DbProvides(self, admin=True) - self.tls = PostgreSQLTLS(self, PEER) + self.tls = PostgreSQLTLS(self, PEER, [self.primary_endpoint, self.replicas_endpoint]) self.restart_manager = RollingOpsManager( charm=self, relation="restart", callback=self._restart ) diff --git a/tests/unit/test_postgresql_tls.py b/tests/unit/test_postgresql_tls.py index 97a7e6dcc9..b4cf79ae1e 100644 --- a/tests/unit/test_postgresql_tls.py +++ b/tests/unit/test_postgresql_tls.py @@ -187,12 +187,16 @@ def test_get_sans(self): sans = self.charm.tls._get_sans() self.assertEqual( sans, - [ - "postgresql-k8s-0", - "postgresql-k8s-0.postgresql-k8s-endpoints", - socket.getfqdn(), - "1.1.1.1", - ], + { + "sans_ip": ["1.1.1.1"], + "sans_dns": [ + "postgresql-k8s-0", + "postgresql-k8s-0.postgresql-k8s-endpoints", + socket.getfqdn(), + "postgresql-k8s-primary.None.svc.cluster.local", + "postgresql-k8s-replicas.None.svc.cluster.local", + ], + }, ) def test_get_tls_extensions(self): diff --git a/tox.ini b/tox.ini index 87f8b693e5..f83721f6db 100644 --- a/tox.ini +++ b/tox.ini @@ -68,7 +68,7 @@ commands = [testenv:charm-integration] description = Run charm integration tests deps = - juju + juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms pytest pytest-operator psycopg2-binary @@ -79,7 +79,7 @@ commands = [testenv:database-relation-integration] description = Run database relation integration tests deps = - juju + juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms pytest pytest-operator psycopg2-binary @@ -90,7 +90,7 @@ commands = [testenv:db-relation-integration] description = Run db relation integration tests deps = - juju + juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms pytest pytest-operator psycopg2-binary @@ -101,7 +101,7 @@ commands = [testenv:db-admin-relation-integration] description = Run db-admin relation integration tests deps = - juju + juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms pytest pytest-operator psycopg2-binary @@ -112,7 +112,7 @@ commands = [testenv:password-rotation-integration] description = Run password rotation integration tests deps = - juju + juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms pytest pytest-operator psycopg2-binary @@ -123,7 +123,7 @@ commands = [testenv:tls-integration] description = Run TLS integration tests deps = - juju + juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms pytest pytest-operator psycopg2-binary @@ -134,7 +134,7 @@ commands = [testenv:integration] description = Run all integration tests deps = - juju + juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms pytest pytest-operator psycopg2-binary